From c555d1274b354e0731db9b488e144bfd3ac2b6a0 Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Thu, 28 Jun 2018 08:22:50 +0000 Subject: [PATCH 01/27] Creating SVN branch for iTop 2.5 SVN:2.5[5920] From 774ecb4003ec1c9ed5d57fe34b80a3e7c6e5388f Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Thu, 5 Jul 2018 13:02:10 +0000 Subject: [PATCH 02/27] (retrofit from trunk) Do not check if the organizations are allowed if there is no user logged in (use case: automatic synchro of users at connection time) SVN:2.5[5931] --- addons/userrights/userrightsprofile.class.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php index ea2ac63a8..cf943fb5d 100644 --- a/addons/userrights/userrightsprofile.class.inc.php +++ b/addons/userrights/userrightsprofile.class.inc.php @@ -402,9 +402,9 @@ class URP_UserOrg extends UserRightsBaseClassGUI */ protected function CheckIfOrgIsAllowed() { - if (UserRights::IsAdministrator()) { return; } + if (!UserRights::IsLoggedIn() || UserRights::IsAdministrator()) { return; } - $oUser = UserRights::GetUserObject(); + $oUser = UserRights::GetUserObject(); $oAddon = UserRights::GetModuleInstance(); $aOrgs = $oAddon->GetUserOrgs($oUser, ''); if (count($aOrgs) > 0) From 6e9fcb81f0a7b12ce9852f59a8369f9085051e72 Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Thu, 12 Jul 2018 07:46:36 +0000 Subject: [PATCH 03/27] (Retrofit from trunk) Fix setup for PHP 5.5 (cannot use expression as default field value) (r5933) SVN:2.5[5934] --- core/attributedef.class.inc.php | 30 +++++++++++++++--------------- core/cmdbsource.class.inc.php | 28 ++++++++++++++++------------ 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 16bf152a5..b4ac8205b 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -1578,7 +1578,7 @@ class AttributeDBFieldVoid extends AttributeDefinition protected function GetSQLCol($bFullSpec = false) { return 'VARCHAR(255)' - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? $this->GetSQLColSpec() : ''); } protected function GetSQLColSpec() @@ -2205,7 +2205,7 @@ class AttributeString extends AttributeDBField protected function GetSQLCol($bFullSpec = false) { return 'VARCHAR(255)' - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? $this->GetSQLColSpec() : ''); } @@ -2606,7 +2606,7 @@ class AttributePassword extends AttributeString protected function GetSQLCol($bFullSpec = false) { return "VARCHAR(64)" - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? $this->GetSQLColSpec() : ''); } @@ -2739,7 +2739,7 @@ class AttributeText extends AttributeString protected function GetSQLCol($bFullSpec = false) { - return "TEXT".CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + return "TEXT".CMDBSource::GetSqlStringColumnDefinition(); } public function GetSQLColumns($bFullSpec = false) @@ -2749,7 +2749,7 @@ class AttributeText extends AttributeString if ($this->GetOptional('format', null) != null ) { // Add the extra column only if the property 'format' is specified for the attribute - $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')".CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')".CMDBSource::GetSqlStringColumnDefinition(); if ($bFullSpec) { $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'text'"; // default 'text' is for migrating old records @@ -3072,7 +3072,7 @@ class AttributeLongText extends AttributeText { protected function GetSQLCol($bFullSpec = false) { - return "LONGTEXT".CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); } public function GetMaxSize() @@ -3254,7 +3254,7 @@ class AttributeCaseLog extends AttributeLongText { $aColumns = array(); $aColumns[$this->GetCode()] = 'LONGTEXT' // 2^32 (4 Gb) - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + .CMDBSource::GetSqlStringColumnDefinition(); $aColumns[$this->GetCode().'_index'] = 'BLOB'; return $aColumns; } @@ -3642,13 +3642,13 @@ class AttributeEnum extends AttributeString // make sure that this string will match the field type returned by the DB // (used to perform a comparison between the current DB format and the data model) return "ENUM(".implode(",", $aValues).")" - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? $this->GetSQLColSpec() : ''); } else { return "VARCHAR(255)" - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? " DEFAULT ''" : ""); // ENUM() is not an allowed syntax! } } @@ -5463,7 +5463,7 @@ class AttributeURL extends AttributeString protected function GetSQLCol($bFullSpec = false) { return "VARCHAR(2048)" - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? $this->GetSQLColSpec() : ''); } @@ -5640,8 +5640,8 @@ class AttributeBlob extends AttributeDefinition { $aColumns = array(); $aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb) - $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; - $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); + $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); return $aColumns; } @@ -6779,7 +6779,7 @@ class AttributeOneWayPassword extends AttributeDefinition public function GetImportColumns() { $aColumns = array(); - $aColumns[$this->GetCode()] = 'TINYTEXT'.CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + $aColumns[$this->GetCode()] = 'TINYTEXT'.CMDBSource::GetSqlStringColumnDefinition(); return $aColumns; } @@ -6853,7 +6853,7 @@ class AttributeTable extends AttributeDBField protected function GetSQLCol($bFullSpec = false) { - return "LONGTEXT".CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION; + return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); } public function GetMaxSize() @@ -7261,7 +7261,7 @@ class AttributeRedundancySettings extends AttributeDBField protected function GetSQLCol($bFullSpec = false) { return "VARCHAR(20)" - .CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION + .CMDBSource::GetSqlStringColumnDefinition() .($bFullSpec ? $this->GetSQLColSpec() : ''); } diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index 86f62c07d..0bdeab500 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -108,16 +108,6 @@ class MySQLHasGoneAwayException extends MySQLException */ class CMDBSource { - /** - * SQL charset & collation declaration for text columns - * - * Using an attribute instead of a constant to avoid crash in the setup for older PHP versions - * - * @see https://dev.mysql.com/doc/refman/5.7/en/charset-column.html - * @since 2.5 #1001 switch to utf8mb4 - */ - public static $SQL_STRING_COLUMNS_CHARSET_DEFINITION = ' CHARACTER SET '.DEFAULT_CHARACTER_SET.' COLLATE '.DEFAULT_COLLATION; - protected static $m_sDBHost; protected static $m_sDBUser; protected static $m_sDBPwd; @@ -136,6 +126,20 @@ class CMDBSource /** @var mysqli $m_oMysqli */ protected static $m_oMysqli; + /** + * SQL charset & collation declaration for text columns + * + * Using a function instead of a constant or attribute to avoid crash in the setup for older PHP versions (cannot + * use expression as value) + * + * @see https://dev.mysql.com/doc/refman/5.7/en/charset-column.html + * @since 2.5 #1001 switch to utf8mb4 + */ + public static function GetSqlStringColumnDefinition() + { + return ' CHARACTER SET '.DEFAULT_CHARACTER_SET.' COLLATE '.DEFAULT_COLLATION; + } + /** * @param Config $oConfig * @@ -1059,7 +1063,7 @@ class CMDBSource } - return 'ALTER TABLE `'.$sTableName.'` '.self::$SQL_STRING_COLUMNS_CHARSET_DEFINITION.';'; + return 'ALTER TABLE `'.$sTableName.'` '.self::GetSqlStringColumnDefinition().';'; } @@ -1205,6 +1209,6 @@ class CMDBSource return null; } - return 'ALTER DATABASE'.CMDBSource::$SQL_STRING_COLUMNS_CHARSET_DEFINITION.';'; + return 'ALTER DATABASE'.CMDBSource::GetSqlStringColumnDefinition().';'; } } From e184eb6aaef7264610b87f6cde7c33b1ed480cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Tue, 17 Jul 2018 12:19:26 +0000 Subject: [PATCH 04/27] Retrofit from trunk DBObject->GetOriginal() hardening (now support attributes not set: for example sla_tto_passed for UserRequest until it is closed) [from revision 5932] SVN:2.5[5942] --- core/dbobject.class.php | 3 ++- test/ItopDataTestCase.php | 20 ++++++++++++++++++++ test/core/DBObjectTest.php | 16 +++++++++++----- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/core/dbobject.class.php b/core/dbobject.class.php index cc55628e8..79bce7ff8 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -568,7 +568,8 @@ abstract class DBObject implements iDisplay { throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this)); } - return $this->m_aOrigValues[$sAttCode]; + $aOrigValues = $this->m_aOrigValues; + return isset($aOrigValues[$sAttCode]) ? $aOrigValues[$sAttCode] : null; } /** diff --git a/test/ItopDataTestCase.php b/test/ItopDataTestCase.php index 9a7ec5d35..0123f3bae 100644 --- a/test/ItopDataTestCase.php +++ b/test/ItopDataTestCase.php @@ -112,6 +112,26 @@ class ItopDataTestCase extends ItopTestCase return $oMyObj; } + /** + * @param string $sClass + * @param $iKey + * @param array $aParams + * + * @return DBObject + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + */ + protected static function updateObject($sClass, $iKey, $aParams) + { + $oMyObj = MetaModel::GetObject($sClass, $iKey); + foreach($aParams as $sAttCode => $oValue) + { + $oMyObj->Set($sAttCode, $oValue); + } + $oMyObj->DBUpdate(); + return $oMyObj; + } /** * Create an Organization in database diff --git a/test/core/DBObjectTest.php b/test/core/DBObjectTest.php index 24afa0904..a6e2ea25c 100644 --- a/test/core/DBObjectTest.php +++ b/test/core/DBObjectTest.php @@ -26,9 +26,8 @@ namespace Combodo\iTop\Test\UnitTest\Core; -use Combodo\iTop\Test\UnitTest\ItopTestCase; +use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use DBObject; -use PHPUnit\Framework\TestCase; /** @@ -36,7 +35,7 @@ use PHPUnit\Framework\TestCase; * @preserveGlobalState disabled * @backupGlobals disabled */ -class DBObjectTest extends ItopTestCase +class DBObjectTest extends ItopDataTestCase { protected function setUp() { @@ -50,7 +49,7 @@ class DBObjectTest extends ItopTestCase */ public function testGetUIPage() { - $this->assertEquals('UI.php', DBObject::GetUIPage()); + static::assertEquals('UI.php', DBObject::GetUIPage()); } /** @@ -61,7 +60,7 @@ class DBObjectTest extends ItopTestCase */ public function testIsValidPKeyOK($key, $res) { - $this->assertEquals(DBObject::IsValidPKey($key), $res); + static::assertEquals(DBObject::IsValidPKey($key), $res); } public function keyProviderOK() @@ -80,4 +79,11 @@ class DBObjectTest extends ItopTestCase array('PHP_INT_MIN', false)); } + public function testGetOriginal() + { + $oObject = $this->CreateUserRequest(190664); + + static::assertNull($oObject->GetOriginal('sla_tto_passed')); + } + } From efa1f4ee43517367f822f8cd9c39c2f27767574f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Tue, 17 Jul 2018 12:22:09 +0000 Subject: [PATCH 05/27] =?UTF-8?q?Retrofit=20from=20trunk=20N=C2=B01551=20-?= =?UTF-8?q?=20Search:=20selected=20org=20&=20default=20criteria=20Display?= =?UTF-8?q?=20the=20default=20search=20criteria=20also=20when=20an=20org?= =?UTF-8?q?=20is=20selected.=20The=20org=20restriction=20criterion=20is=20?= =?UTF-8?q?read=20only.=20[from=20revision=205939]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5943] --- application/displayblock.class.inc.php | 3 +- .../search/searchform.class.inc.php | 63 +++++++++++++++++-- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 9c69f1caf..476b34b4a 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -326,7 +326,8 @@ class DisplayBlock { $aQueryParams = $aExtraParams['query_params']; } - if ($this->m_sStyle != 'links') + // In case of search, the context filtering is done by the search itself + if (($this->m_sStyle != 'links') && ($this->m_sStyle != 'search')) { $oAppContext = new ApplicationContext(); $sClass = $this->m_oFilter->GetClass(); diff --git a/sources/application/search/searchform.class.inc.php b/sources/application/search/searchform.class.inc.php index 2c0700def..6e4a9db6c 100644 --- a/sources/application/search/searchform.class.inc.php +++ b/sources/application/search/searchform.class.inc.php @@ -480,13 +480,12 @@ class SearchForm { $aOrCriterion = array(); $bIsEmptyExpression = true; + $aArgs = MetaModel::PrepareQueryArguments($aArgs, $oSearch->GetInternalParams()); if (method_exists($oSearch, 'GetCriteria')) { $oExpression = $oSearch->GetCriteria(); - $aArgs = MetaModel::PrepareQueryArguments($aArgs, $oSearch->GetInternalParams()); - if (!empty($aArgs)) { try @@ -521,12 +520,50 @@ class SearchForm } } + // Context induced criteria are read-only + $oAppContext = new ApplicationContext(); + $sClass = $oSearch->GetClass(); + $aCallSpec = array($sClass, 'MapContextParam'); + $aContextParams = array(); + if (is_callable($aCallSpec)) + { + foreach($oAppContext->GetNames() as $sContextParam) + { + $sParamCode = call_user_func($aCallSpec, $sContextParam); //Map context parameter to the value/filter code depending on the class + if (!is_null($sParamCode)) + { + $sParamValue = $oAppContext->GetCurrentValue($sContextParam, null); + if (!is_null($sParamValue)) + { + $aContextParams[$sParamCode] = $sParamValue; + } + } + } + } + if ($bIsEmptyExpression) { // Add default criterion - $aOrCriterion = $this->GetDefaultCriterion($oSearch); + $aOrCriterion = $this->GetDefaultCriterion($oSearch, $aContextParams); } + foreach($aContextParams as $sParamCode => $sParamValue) + { + // Add Context criteria in read only mode + $sAlias = $oSearch->GetClassAlias(); + $oFieldExpression = new FieldExpression($sParamCode, $sAlias); + $oScalarExpression = new \ScalarExpression($sParamValue); + $oExpression = new \BinaryExpression($oFieldExpression, '=', $oScalarExpression); + $aCriterion = $oExpression->GetCriterion($oSearch, $aArgs); + $aCriterion['is_removable'] = false; + foreach($aOrCriterion as &$aAndExpression) + { + $aAndExpression['and'][] = $aCriterion; + } + } + + + return array('or' => $aOrCriterion); } @@ -640,9 +677,11 @@ class SearchForm /** * @param DBObjectSearch $oSearch + * @param $aContextParams + * * @return array */ - protected function GetDefaultCriterion($oSearch) + protected function GetDefaultCriterion($oSearch, &$aContextParams = array()) { $aAndCriterion = array(); $sClass = $oSearch->GetClass(); @@ -661,8 +700,20 @@ class SearchForm $sAlias = $oSearch->GetClassAlias(); foreach($aList as $sAttCode) { - $oFieldExpression = new FieldExpression($sAttCode, $sAlias); - $aCriterion = $oFieldExpression->GetCriterion($oSearch); + $oExpression = new FieldExpression($sAttCode, $sAlias); + $bIsRemovable = true; + if (isset($aContextParams[$sAttCode])) + { + // When a context parameter exists, use it with the default search criteria + $oFieldExpression = $oExpression; + $oScalarExpression = new \ScalarExpression($aContextParams[$sAttCode]); + $oExpression = new \BinaryExpression($oFieldExpression, '=', $oScalarExpression); + unset($aContextParams[$sAttCode]); + // Read only mode for search criteria from context + $bIsRemovable = false; + } + $aCriterion = $oExpression->GetCriterion($oSearch); + $aCriterion['is_removable'] = $bIsRemovable; if (isset($aCriterion['widget']) && ($aCriterion['widget'] != AttributeDefinition::SEARCH_WIDGET_TYPE_RAW)) { $aAndCriterion[] = $aCriterion; From 0bbb5860946081b43e2eaef29906e8f59ce4b1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Tue, 17 Jul 2018 12:23:27 +0000 Subject: [PATCH 06/27] Retrofit from trunk Advanced search: Fix an error when using search form from an union. [from revision 5940] SVN:2.5[5944] --- core/oql/expression.class.inc.php | 46 ++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/core/oql/expression.class.inc.php b/core/oql/expression.class.inc.php index 8f0e9ec22..3d3f4e80e 100644 --- a/core/oql/expression.class.inc.php +++ b/core/oql/expression.class.inc.php @@ -461,15 +461,23 @@ class BinaryExpression extends Expression public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) { $bReverseOperator = false; + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $aClasses = $oSearch->GetJoinedClasses(); + } + else + { + $aClasses = array($oSearch->GetClass()); + } $oLeftExpr = $this->GetLeftExpr(); if ($oLeftExpr instanceof FieldExpression) { - $oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses()); + $oAttDef = $oLeftExpr->GetAttDef($aClasses); } $oRightExpr = $this->GetRightExpr(); if ($oRightExpr instanceof FieldExpression) { - $oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses()); + $oAttDef = $oRightExpr->GetAttDef($aClasses); $bReverseOperator = true; } @@ -523,17 +531,33 @@ class BinaryExpression extends Expression return Dict::S('Expression:Operator:'.$sOperator, " $sOperator "); } + /** + * @param DBSearch $oSearch + * @param null $aArgs + * @param bool $bRetrofitParams + * @param null $oAttDef + * + * @return array + */ public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) { $bReverseOperator = false; $oLeftExpr = $this->GetLeftExpr(); $oRightExpr = $this->GetRightExpr(); - $oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses()); + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $aClasses = $oSearch->GetJoinedClasses(); + } + else + { + $aClasses = array($oSearch->GetClass()); + } + $oAttDef = $oLeftExpr->GetAttDef($aClasses); if (is_null($oAttDef)) { - $oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses()); + $oAttDef = $oRightExpr->GetAttDef($aClasses); $bReverseOperator = true; } @@ -547,7 +571,7 @@ class BinaryExpression extends Expression { $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); // $oAttDef can be different now - $oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses()); + $oAttDef = $oRightExpr->GetAttDef($aClasses); $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); // switch left and right expressions so reverse the operator @@ -576,7 +600,7 @@ class BinaryExpression extends Expression { $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); // $oAttDef can be different now - $oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses()); + $oAttDef = $oLeftExpr->GetAttDef($aClasses); $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); $aCriteria = self::MergeCriteria($aCriteriaLeft, $aCriteriaRight, $this->GetOperator()); @@ -937,7 +961,15 @@ class FieldExpression extends UnaryExpression { return "`{$this->m_sName}`"; } - $sClass = $this->GetClassName($oSearch->GetJoinedClasses()); + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $aClasses = $oSearch->GetJoinedClasses(); + } + else + { + $aClasses = array($oSearch->GetClass()); + } + $sClass = $this->GetClassName($aClasses); $sAttName = MetaModel::GetLabel($sClass, $this->m_sName); if ($sClass != $oSearch->GetClass()) { From c630676792b202bbba9ef8ff9f14af5e34e27263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Thu, 19 Jul 2018 08:06:39 +0000 Subject: [PATCH 07/27] =?UTF-8?q?Retrofit=20from=20trunk=20N=C2=B01555=20-?= =?UTF-8?q?=20Search:=20ExternalField=20label=20not=20displayed.=20The=20s?= =?UTF-8?q?earch=20is=20not=20restricted=20for=20external=20fields=20anymo?= =?UTF-8?q?re.=20[from=20revision=205945]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5953] --- sources/application/search/searchform.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/application/search/searchform.class.inc.php b/sources/application/search/searchform.class.inc.php index 6e4a9db6c..4493f154a 100644 --- a/sources/application/search/searchform.class.inc.php +++ b/sources/application/search/searchform.class.inc.php @@ -400,7 +400,7 @@ class SearchForm protected function IsSubAttribute($oAttDef) { - return (($oAttDef instanceof AttributeFriendlyName) || ($oAttDef instanceof AttributeExternalField) || ($oAttDef instanceof AttributeSubItem)); + return (($oAttDef instanceof AttributeFriendlyName) || ($oAttDef instanceof AttributeSubItem)); } /** From 8ffea22f0e4e357b9cd558007aab717d5a76cd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Thu, 19 Jul 2018 08:10:32 +0000 Subject: [PATCH 08/27] =?UTF-8?q?Retrofit=20from=20trunk=20N=C2=B01561=20-?= =?UTF-8?q?=20Fix=20auto-complete=20error=20when=20the=20friendlyname=20de?= =?UTF-8?q?pends=20on=20other=20classes=20[from=20revision=205948]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5954] --- core/valuesetdef.class.inc.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index 855507104..5ac87bfaa 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -228,10 +228,23 @@ class ValueSetObjects extends ValueSetDefinition } } + $oExpression = DBObjectSearch::GetPolymorphicExpression($oFilter->GetClass(), 'friendlyname'); + $aFields = $oExpression->ListRequiredFields(); + $sClass = $oFilter->GetClass(); + foreach($aFields as $sField) + { + $aFieldItems = explode('.', $sField); + if ($aFieldItems[0] != $sClass) + { + $sOperation = 'contains'; + break; + } + } + switch ($sOperation) { case 'equals': - $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($oFilter->GetClass()); + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); $sClassAlias = $oFilter->GetClassAlias(); $aFilters = array(); $oValueExpr = new ScalarExpression($sContains); @@ -247,7 +260,7 @@ class ValueSetObjects extends ValueSetDefinition $oFilter = new DBUnionSearch($aFilters); break; case 'start_with': - $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($oFilter->GetClass()); + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); $sClassAlias = $oFilter->GetClassAlias(); $aFilters = array(); $oValueExpr = new ScalarExpression($sContains.'%'); From 045f58144ea077f9718f89c8711a4cac8bd667c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Thu, 19 Jul 2018 08:14:26 +0000 Subject: [PATCH 09/27] =?UTF-8?q?Retrofit=20from=20trunk=20N=C2=B01553=20-?= =?UTF-8?q?=20Search:=20Fix=20operator=20on=20indexed=20attributes.=20It?= =?UTF-8?q?=20was=20previously=20always=20forced=20to=20'=3D',=20now=20it'?= =?UTF-8?q?s=20only=20defaulted=20to=20'=3D'=20[from=20revision=205950]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5955] --- js/search/search_form_criteria.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/search/search_form_criteria.js b/js/search/search_form_criteria.js index 569e5fffb..11df536c6 100644 --- a/js/search/search_form_criteria.js +++ b/js/search/search_form_criteria.js @@ -152,7 +152,7 @@ $(function() _initChooseDefaultOperator: function() { //if the class has an index, in order to maximize the performance, we force the default operator to "equal" - if (this.options.field.has_index && typeof this.options.available_operators['='] == 'object') + if (this.options.field.has_index && typeof this.options.available_operators['='] == 'object' && this.options.values.length == 0) { this.options.operator = '='; this.options.available_operators['='].rank = -1;//we want it to be the first displayed From a454a4311186f9d48240065f465df9f9e9043a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Thu, 19 Jul 2018 08:17:46 +0000 Subject: [PATCH 10/27] =?UTF-8?q?Retrofit=20from=20trunk=20N=C2=B01556=20-?= =?UTF-8?q?=20Search:=20Fix=20removing=20last=20criterion=20on=20a=20'or'?= =?UTF-8?q?=20line=20resulted=20in=20'OR=201'.=20The=20empty=20OR=20condit?= =?UTF-8?q?ion=20is=20now=20removed=20completely=20from=20the=20screen=20a?= =?UTF-8?q?nd=20the=20criterion=20list.=20[from=20revision=205951]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5956] --- js/search/search_form_handler.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/js/search/search_form_handler.js b/js/search/search_form_handler.js index 38f2e5e39..ec32d43fe 100644 --- a/js/search/search_form_handler.js +++ b/js/search/search_form_handler.js @@ -238,15 +238,30 @@ $(function() this.elements.criterion_area.find('.sf_criterion_row').each(function(iIdx){ var oCriterionRowElem = $(this); - oCriterion['or'][iIdx] = {'and': []}; - oCriterionRowElem.find('.search_form_criteria').each(function(){ - var oCriteriaData = $(this).triggerHandler('itop.search.criteria.get_data'); - - if (null != oCriteriaData) + if (oCriterionRowElem.find('.search_form_criteria').length == 0 && iIdx > 0) + { + $(this).remove(); + } + else + { + oCriterionRowElem.find('.search_form_criteria').each(function () { - oCriterion['or'][iIdx]['and'].push(oCriteriaData); - } - }); + var oCriteriaData = $(this).triggerHandler('itop.search.criteria.get_data'); + + if (null != oCriteriaData) + { + if (!oCriterion['or'][iIdx]) + { + oCriterion['or'][iIdx] = {'and': []}; + } + oCriterion['or'][iIdx]['and'].push(oCriteriaData); + } + else + { + $(this).remove(); + } + }); + } }); // - Update search this.options.search.criterion = oCriterion; From e276587fdc6fb0a9c3f25cbbb55741eae5f3366d Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Wed, 25 Jul 2018 07:57:31 +0000 Subject: [PATCH 11/27] =?UTF-8?q?N=C2=B0931=20change=20iTop=202.6=20MySQL?= =?UTF-8?q?=20requirements=20from=205.5.3=20to=205.6=20(to=20have=20fullte?= =?UTF-8?q?xt=20on=20InnoDB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5978] --- setup/setuputils.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index 34c2e2a43..a27ebd6e2 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -54,7 +54,7 @@ class SetupUtils const MYSQL_MIN_VERSION = '5.5.3'; // 5.5 branch that is shipped with most distribution, and 5.5.3 to have utf8mb4 (see N°942) // -- versions that will be the minimum in next iTop major release (warning if not met) const PHP_NEXT_MIN_VERSION = ''; // no new PHP requirement for now in iTop 2.6 - const MYSQL_NEXT_MIN_VERSION = ''; // no new MySQL requirement for now in iTop 2.6 + const MYSQL_NEXT_MIN_VERSION = '5.6.0'; // 5.6 to have fulltext on InnoDB for Tags fields (N°931) // -- First recent version that is not yet validated by Combodo (warning) const PHP_NOT_VALIDATED_VERSION = '7.3.0'; From 821eb4df8cdf7aa7f89b8436981fddc16c5ad440 Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Mon, 30 Jul 2018 10:31:22 +0000 Subject: [PATCH 12/27] (Retrofit from trunk) PHP 7.2 compatibility: fix another count(null) (r5988) SVN:2.5[5989] --- application/cmdbabstract.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index e19a985b6..9059107e6 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -3505,7 +3505,7 @@ EOF foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { $aNewIssues = $oExtensionInstance->OnCheckToDelete($this); - if (count($aNewIssues) > 0) + if (is_array($aNewIssues) && count($aNewIssues) > 0) { $this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues); } From c3f80a58763efee72020d8963c847ff738e52cfb Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Wed, 1 Aug 2018 09:13:15 +0000 Subject: [PATCH 13/27] =?UTF-8?q?(Retrofit=20from=20trunk)=20N=C2=B01582?= =?UTF-8?q?=20Fix=20audit=20when=20a=20current=20organization=20is=20set?= =?UTF-8?q?=20and=20there=20is=20an=20audit=20rule=20with=20valid=3Dtrue?= =?UTF-8?q?=20Was=20generating=20an=20OQL=20missing=20query=20argument=20e?= =?UTF-8?q?rror=20(r5991)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[5992] --- core/coreexception.class.inc.php | 2 -- core/dbobjectsearch.class.php | 38 +++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/coreexception.class.inc.php b/core/coreexception.class.inc.php index fe1f58b6c..ae1a18af8 100644 --- a/core/coreexception.class.inc.php +++ b/core/coreexception.class.inc.php @@ -127,5 +127,3 @@ class SecurityException extends CoreException class ArchivedObjectException extends CoreException { } - -?> diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index 8c2b24eb0..ce6153aa4 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -497,6 +497,8 @@ class DBObjectSearch extends DBSearch * @param bool $bPositiveMatch if true will add a IN filter, else a NOT IN * * @throws \CoreException + * + * @since 2.5 N°1418 */ public function AddConditionForInOperatorUsingParam($sFilterCode, $aValues, $bPositiveMatch = true) { @@ -506,7 +508,7 @@ class DBObjectSearch extends DBSearch $sInParamName = $this->GenerateUniqueParamName(); $oParamExpression = new VariableExpression($sInParamName); - $this->SetInternalParams(array($sInParamName => $aValues)); + $this->GetInternalParamsByRef()[$sInParamName] = $aValues; $oListExpression = new ListExpression(array($oParamExpression)); @@ -1086,11 +1088,45 @@ class DBObjectSearch extends DBSearch return $this->m_aParams = $aParams; } + /** + * @return array warning : array returned by value + * @see self::GetInternalParamsByRef to get the attribute by reference + */ public function GetInternalParams() { return $this->m_aParams; } + /** + * @return array + * @see http://php.net/manual/en/language.references.return.php + * @since 2.5.1 N°1582 + */ + public function &GetInternalParamsByRef() + { + return $this->m_aParams; + } + + /** + * @param string $sKey + * @param mixed $value + * @param bool $bDoNotOverride + * + * @throws \CoreUnexpectedValue if $bDoNotOverride and $sKey already exists + */ + public function AddInternalParam($sKey, $value, $bDoNotOverride = false) + { + if ($bDoNotOverride) + { + if (array_key_exists($sKey, $this->m_aParams)) + { + throw new CoreUnexpectedValue("The key $sKey already exists with value : ".$this->m_aParams[$sKey]); + } + } + + $this->m_aParams[$sKey] = $value; + } + public function GetQueryParams($bExcludeMagicParams = true) { $aParams = array(); From 7092dc6f862d680879316cb54ffb52915d19633f Mon Sep 17 00:00:00 2001 From: Stephen Abello Date: Wed, 8 Aug 2018 10:12:43 +0000 Subject: [PATCH 14/27] (Retrofit from trunk) German Translation: typos (UserRequest #18704) SVN:2.5[6002] --- .../itop-config-mgmt/de.dict.itop-config-mgmt.php | 12 ++++++------ .../de.dict.itop-service-mgmt-provider.php | 4 ++++ datamodels/2.x/itop-tickets/de.dict.itop-tickets.php | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/datamodels/2.x/itop-config-mgmt/de.dict.itop-config-mgmt.php b/datamodels/2.x/itop-config-mgmt/de.dict.itop-config-mgmt.php index f65b2ff51..97d904775 100755 --- a/datamodels/2.x/itop-config-mgmt/de.dict.itop-config-mgmt.php +++ b/datamodels/2.x/itop-config-mgmt/de.dict.itop-config-mgmt.php @@ -129,7 +129,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Class:ApplicationSolution+' => '', 'Class:ApplicationSolution/Attribute:functionalcis_list' => 'CIs', 'Class:ApplicationSolution/Attribute:functionalcis_list+' => '', - 'Class:ApplicationSolution/Attribute:businessprocess_list' => 'Geschäftsprozesse', + 'Class:ApplicationSolution/Attribute:businessprocess_list' => 'Business-Prozesse', 'Class:ApplicationSolution/Attribute:businessprocess_list+' => '', 'Class:ApplicationSolution/Attribute:status' => 'Status', 'Class:ApplicationSolution/Attribute:status+' => '', @@ -139,7 +139,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Class:ApplicationSolution/Attribute:status/Value:inactive+' => '', 'Class:BusinessProcess' => 'Business-Prozess', 'Class:BusinessProcess+' => '', - 'Class:BusinessProcess/Attribute:applicationsolutions_list' => 'Applikationslösungen', + 'Class:BusinessProcess/Attribute:applicationsolutions_list' => 'Anwendungslösungen', 'Class:BusinessProcess/Attribute:applicationsolutions_list+' => '', 'Class:BusinessProcess/Attribute:status' => 'Status', 'Class:BusinessProcess/Attribute:status+' => '', @@ -373,9 +373,9 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Class:lnkApplicationSolutionToFunctionalCI/Attribute:applicationsolution_id+' => '', 'Class:lnkApplicationSolutionToFunctionalCI/Attribute:functionalci_id' => 'FunctionalCI', 'Class:lnkApplicationSolutionToFunctionalCI/Attribute:functionalci_id+' => '', - 'Class:lnkApplicationSolutionToBusinessProcess' => 'Verknüpfung Anwendungslösung/Geschäftsprozess', + 'Class:lnkApplicationSolutionToBusinessProcess' => 'Verknüpfung Anwendungslösung/Business-Prozess', 'Class:lnkApplicationSolutionToBusinessProcess+' => '', - 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:businessprocess_id' => 'Geschäftsprozes', + 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:businessprocess_id' => 'Business-Prozess', 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:businessprocess_id+' => '', 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:applicationsolution_id' => 'Anwendungslösung', 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:applicationsolution_id+' => '', @@ -999,7 +999,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Class:lnkApplicationSolutionToFunctionalCI/Attribute:applicationsolution_name+' => '', 'Class:lnkApplicationSolutionToFunctionalCI/Attribute:functionalci_name' => 'FunctionalCI-Name', 'Class:lnkApplicationSolutionToFunctionalCI/Attribute:functionalci_name+' => '', - 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:businessprocess_name' => 'Geschäftsprozess-Name', + 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:businessprocess_name' => 'Business-Prozess-Name', 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:businessprocess_name+' => '', 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:applicationsolution_name' => 'Applikationslösungs-Name', 'Class:lnkApplicationSolutionToBusinessProcess/Attribute:applicationsolution_name+' => '', @@ -1039,7 +1039,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Menu:ConfigManagementOverview+' => 'Übersicht', 'Menu:Contact' => 'Kontakte', 'Menu:Contact+' => 'Kontakte', - 'Menu:Contact:Count' => '%1$d Kontakte', + 'Menu:Contact:Count' => '%1$d kontakten', 'Menu:Person' => 'Personen', 'Menu:Person+' => 'Alle Personen', 'Menu:Team' => 'Teams', diff --git a/datamodels/2.x/itop-service-mgmt-provider/de.dict.itop-service-mgmt-provider.php b/datamodels/2.x/itop-service-mgmt-provider/de.dict.itop-service-mgmt-provider.php index 03b3d9dcc..ff4dee483 100644 --- a/datamodels/2.x/itop-service-mgmt-provider/de.dict.itop-service-mgmt-provider.php +++ b/datamodels/2.x/itop-service-mgmt-provider/de.dict.itop-service-mgmt-provider.php @@ -79,6 +79,10 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Class:Service/Attribute:org_id+' => '', 'Class:Service/Attribute:organization_name' => 'Provider Name', 'Class:Service/Attribute:organization_name+' => '', + 'Class:Service/Attribute:servicefamily_id' => 'Service-Familie', + 'Class:Service/Attribute:servicefamily_id+' => '', + 'Class:Service/Attribute:servicefamily_name' => 'Service-Familien-Name', + 'Class:Service/Attribute:servicefamily_name+' => '', 'Class:Service/Attribute:description' => 'Beschreibung', 'Class:Service/Attribute:description+' => '', 'Class:Service/Attribute:documents_list' => 'Dokumente', diff --git a/datamodels/2.x/itop-tickets/de.dict.itop-tickets.php b/datamodels/2.x/itop-tickets/de.dict.itop-tickets.php index 93bcc3f14..ada0401c4 100755 --- a/datamodels/2.x/itop-tickets/de.dict.itop-tickets.php +++ b/datamodels/2.x/itop-tickets/de.dict.itop-tickets.php @@ -199,7 +199,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Brick:Portal:OngoingRequests:Tab:OnGoing' => 'Offen', 'Brick:Portal:OngoingRequests:Tab:Resolved' => 'Gelöst', 'Brick:Portal:ClosedRequests:Title' => 'Geschlossene Störungen/Anfragen', - 'Class:Ticket/Attribute:operational_status' => 'Status', + 'Class:Ticket/Attribute:operational_status' => 'Betriebsstatus', 'Class:Ticket/Attribute:operational_status+' => 'Berechnet nach detailliertem Status', 'Class:Ticket/Attribute:operational_status/Value:ongoing' => 'In Bearbeitung', 'Class:Ticket/Attribute:operational_status/Value:ongoing+' => 'In Bearbeitung', From 931593a59e28270167c59108e5bb7f62f17c4380 Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Fri, 17 Aug 2018 08:42:41 +0000 Subject: [PATCH 15/27] =?UTF-8?q?(Retrofit=20from=20trunk)=20N=C2=B01572?= =?UTF-8?q?=20Add=20composer.json=20for=20PHP=20language=20level=20(r5967)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[6007] --- composer.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..dbe0c210d --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "require": { + "php": ">=5.6.0", + "ext-mysql": "*", + "ext-ldap": "*", + "ext-mcrypt": "*", + "ext-cli": "*", + "ext-soap": "*", + "ext-json": "*" + }, + "config": { + "platform": { + "php": "5.6.0" + } + } +} \ No newline at end of file From 22df5d09fb25d14a18c39ed655047ec3a9088f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Tue, 28 Aug 2018 12:14:56 +0000 Subject: [PATCH 16/27] =?UTF-8?q?Retrofit=20from=20trunk=20N=C2=B01595=20-?= =?UTF-8?q?=20Setup=20:=20Blocking=20error=20on=20backup=20failure=20[from?= =?UTF-8?q?=20revision=206010]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:2.5[6023] --- composer.json | 4 +++- setup/backup.class.inc.php | 16 +++++++++------- setup/tar.php | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index dbe0c210d..8bd34a0a7 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,9 @@ "ext-mcrypt": "*", "ext-cli": "*", "ext-soap": "*", - "ext-json": "*" + "ext-json": "*", + "ext-zip": "*", + "ext-mysqli": "*" }, "config": { "platform": { diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index 25bdbeae3..cb1816814 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -235,18 +235,20 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " /** * Create a normalized backup name, depending on the current date/time and Database * - * @param sNameSpec string Name and path, eventually containing itop placeholders + time formatting specs + * @param string sMySQLBinDir Name and path, eventually containing itop placeholders + time formatting specs */ public function SetMySQLBinDir($sMySQLBinDir) { $this->sMySQLBinDir = $sMySQLBinDir; } - /** - * Create a normalized backup name, depending on the current date/time and Database - * - * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting specs - */ + /** + * Create a normalized backup name, depending on the current date/time and Database + * + * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting specs + * + * @return string + */ public function MakeName($sNameSpec = "__DB__-%Y-%m-%d") { $sFileName = $sNameSpec; @@ -646,7 +648,7 @@ if (class_exists('ZipArchive')) // The setup must be able to start even if the " { if (empty($sData)) { - return; + return ''; } return ' --'.$sCliArgName.'='.self::EscapeShellArg($sData); diff --git a/setup/tar.php b/setup/tar.php index b07589715..72c074bc5 100644 --- a/setup/tar.php +++ b/setup/tar.php @@ -680,7 +680,7 @@ class ArchiveTar */ public function _error($p_message) { - $this->error_object = $this->raiseError($p_message); + IssueLog::Error($p_message); } /** @@ -688,7 +688,7 @@ class ArchiveTar */ public function _warning($p_message) { - $this->error_object = $this->raiseError($p_message); + IssueLog::Warning($p_message); } /** From 8df287f45e3ba8f6e6a4da04cc7141f57cac2d59 Mon Sep 17 00:00:00 2001 From: bruno DA SILVA Date: Thu, 30 Aug 2018 12:25:20 +0200 Subject: [PATCH 17/27] updated readme --- README.md | 59 +++++++++++++++++++++++++++++++++--------------------- readme.txt | 48 ++++++++++++++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 311f2c9c4..a8f07cd63 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,41 @@ -## Special news for developers: -To get closer to the Open Source community, we are pleased to announce that since the 30th of August iTop code is available on the GitHub platform. +# iTop - ITSM & CMDB + +iTop stands for *IT Operations Portal*. +It is a complete open source, ITIL, web based service management tool including a fully customizable CMDB, a helpdesk system and a document management tool. +iTop also offers mass import tools and web services to integrate with your IT + +## Features +- Fully configurable **CMDB** +- **HelpDesk** and Incident Management +- **Service and Contract Management** +- **Change** Management +- **Configuration** Management +- Automatic **SLA** management +- Automatic **impact analysis** +- **CSV import** tool for all data +- Consistency **audit** to check data quality +- **Data synchronization** (for data federation) +## Resources -# iTop code moves to GitHub -The iTop project was hosted on a SourceForge project since its beginning in 2009. Combodo wants to ease collaboration with iTop developer community, and so we decided to move our code to the popular GitHub platform! - - -The iTop repository is part of the [GitHub Combodo](https://github.com/Combodo) organization. It can be used with either Git or SVN. -Any contributor can fork the code and submit contributions using GitHub pull requests ! - - - - - -## Important -[iTop SourceForge project](https://sourceforge.net/projects/itop/) will still host: - - [Forums](https://sourceforge.net/p/itop/discussion/): for support request - - [Tickets](https://sourceforge.net/p/itop/tickets/): for feature requests and bug reports + - [iTop Forums](https://sourceforge.net/p/itop/discussion/): for support request + - [iTop Tickets](https://sourceforge.net/p/itop/tickets/): for feature requests and bug reports - [Releases download](https://sourceforge.net/projects/itop/files/itop/) + - [iTop documentation](https://www.itophub.io/wiki/page) for iTop and official extensions + - [iTop extensions](https://store.itophub.io/en_US/) for discovering ans installing extensions + + +## Releases +### Version 2.5 + - [Changes since the previous version](https://wiki.openitop.org/doku.php?id=2_5_0:release:change_log) + - [New features](https://wiki.openitop.org/doku.php?id=2_5_0:release:2_5_whats_new) + - [Migration notes](https://wiki.openitop.org/doku.php?id=2_5_0:install:240_to_250_migration_notes) + -**Warning!** Starting of 30th august, you should get the code from GitHub only. - - - - -If you have any questions, remarks or need any assistance please do not hesitate to [contact us](https://www.combodo.com/nous-contacter) ! +### Version 2.4 + - [Changes since the previous version](https://wiki.openitop.org/doku.php?id=2_4_0:release:change_log) + - [New features](https://wiki.openitop.org/doku.php?id=2_4_0:release:2_4_whats_new) + - [Migration notes](https://wiki.openitop.org/doku.php?id=2_4_0:install:230_to_240_migration_notes) + + \ No newline at end of file diff --git a/readme.txt b/readme.txt index 4dbc2628f..1c7a46141 100644 --- a/readme.txt +++ b/readme.txt @@ -1,18 +1,40 @@ -iTop - version 2.5.0 - 27-jun-2018 -Readme file +# iTop - ITSM & CMDB + +iTop stands for *IT Operations Portal*. +It is a complete open source, ITIL, web based service management tool including a fully customizable CMDB, a helpdesk system and a document management tool. +iTop also offers mass import tools and web services to integrate with your IT -iTop 2.5.0 is the 33rd release of iTop. - -Changes since the previous version -------------------------------------------------------------------- -https://wiki.openitop.org/doku.php?id=2_5_0:release:change_log +## Features +- Fully configurable **CMDB** +- **HelpDesk** and Incident Management +- **Service and Contract Management** +- **Change** Management +- **Configuration** Management +- Automatic **SLA** management +- Automatic **impact analysis** +- **CSV import** tool for all data +- Consistency **audit** to check data quality +- **Data synchronization** (for data federation) -New features -------------------------------------------------------------------- -https://wiki.openitop.org/doku.php?id=2_5_0:release:2_5_whats_new +## Resources + - [iTop Forums](https://sourceforge.net/p/itop/discussion/): for support request + - [iTop Tickets](https://sourceforge.net/p/itop/tickets/): for feature requests and bug reports + - [Releases download](https://sourceforge.net/projects/itop/files/itop/) + - [iTop documentation](https://www.itophub.io/wiki/page) for iTop and official extensions + - [iTop extensions](https://store.itophub.io/en_US/) for discovering ans installing extensions + + +## Releases +### Version 2.5 + - [Changes since the previous version](https://wiki.openitop.org/doku.php?id=2_5_0:release:change_log) + - [New features](https://wiki.openitop.org/doku.php?id=2_5_0:release:2_5_whats_new) + - [Migration notes](https://wiki.openitop.org/doku.php?id=2_5_0:install:240_to_250_migration_notes) + -Migration notes -------------------------------------------------------------------- -https://wiki.openitop.org/doku.php?id=2_5_0:install:240_to_250_migration_notes +### Version 2.4 + - [Changes since the previous version](https://wiki.openitop.org/doku.php?id=2_4_0:release:change_log) + - [New features](https://wiki.openitop.org/doku.php?id=2_4_0:release:2_4_whats_new) + - [Migration notes](https://wiki.openitop.org/doku.php?id=2_4_0:install:230_to_240_migration_notes) + \ No newline at end of file From 55fad3a9ec01377cfbbaf8b9f9f6087b1cf3dfd2 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Thu, 30 Aug 2018 15:32:41 +0200 Subject: [PATCH 18/27] Added .project files to ignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6379fb815..140ed9431 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ local.properties .settings/ .loadpath .recommenders +.project # External tool builders .externalToolBuilders/ From af63579f318e219ff6a712d4bd5cf3ed088aac57 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Thu, 30 Aug 2018 16:35:56 +0200 Subject: [PATCH 19/27] Rework of the readme... --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a8f07cd63..e428140b8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ +

+ # iTop - ITSM & CMDB iTop stands for *IT Operations Portal*. @@ -5,37 +9,69 @@ It is a complete open source, ITIL, web based service management tool including iTop also offers mass import tools and web services to integrate with your IT ## Features -- Fully configurable **CMDB** -- **HelpDesk** and Incident Management -- **Service and Contract Management** -- **Change** Management -- **Configuration** Management -- Automatic **SLA** management -- Automatic **impact analysis** -- **CSV import** tool for all data -- Consistency **audit** to check data quality -- **Data synchronization** (for data federation) +- Fully configurable [Configuration Management (CMDB)][20] +- [HelpDesk][21] and Incident Management +- [Service and Contract Management][22] +- [Change][23] Management +- Configurable [SLA][24] Management +- Graphical [impact analysis][25] +- [CSV import][26] tool for any data +- Consistency [audit][27] to check data quality +- [Data synchronization][28] (for data federation) ## Resources - - [iTop Forums](https://sourceforge.net/p/itop/discussion/): for support request - - [iTop Tickets](https://sourceforge.net/p/itop/tickets/): for feature requests and bug reports - - [Releases download](https://sourceforge.net/projects/itop/files/itop/) - - [iTop documentation](https://www.itophub.io/wiki/page) for iTop and official extensions - - [iTop extensions](https://store.itophub.io/en_US/) for discovering ans installing extensions + - [iTop Forums][1]: for support request + - [iTop Tickets][2]: for feature requests and bug reports + - [Releases download][3] + - [iTop documentation][4] for iTop and official extensions + - [iTop extensions][5] for discovering and installing extensions ## Releases ### Version 2.5 - - [Changes since the previous version](https://wiki.openitop.org/doku.php?id=2_5_0:release:change_log) - - [New features](https://wiki.openitop.org/doku.php?id=2_5_0:release:2_5_whats_new) - - [Migration notes](https://wiki.openitop.org/doku.php?id=2_5_0:install:240_to_250_migration_notes) + - [Changes since the previous version][6] + - [New features][7] + - [Migration notes][8] + - [Download iTop 2.5.0][9] ### Version 2.4 - - [Changes since the previous version](https://wiki.openitop.org/doku.php?id=2_4_0:release:change_log) - - [New features](https://wiki.openitop.org/doku.php?id=2_4_0:release:2_4_whats_new) - - [Migration notes](https://wiki.openitop.org/doku.php?id=2_4_0:install:230_to_240_migration_notes) + - [Changes since the previous version][10] + - [New features][11] + - [Migration notes][12] + - [Download iTop 2.4.1][13] + +# About Us + +iTop development is sponsored, led and supported by [Combodo][14]. - \ No newline at end of file +[1]: https://sourceforge.net/p/itop/discussion/ +[2]: https://sourceforge.net/p/itop/tickets/ +[3]: https://sourceforge.net/projects/itop/files/itop/ +[4]: https://www.itophub.io/wiki +[5]: https://store.itophub.io/en_US/ +[6]: https://www.itophub.io/wiki/page?id=2_5_0:release:change_log +[7]: https://www.itophub.io/wiki/page?id=2_5_0:release:2_5_whats_new +[8]: https://www.itophub.io/wiki/page?id=2_5_0:install:240_to_250_migration_notes +[9]: https://sourceforge.net/projects/itop/files/itop/2.5.0/iTop-2.5.0-3935.zip/download +[10]: https://www.itophub.io/wiki/page?id=2_4_0:release:change_log +[11]: https://www.itophub.io/wiki/page?id=2_4_0:release:2_4_whats_new +[12]: https://www.itophub.io/wiki/page?id=2_4_0:install:230_to_240_migration_notes +[13]: https://sourceforge.net/projects/itop/files/itop/2.4.1/iTop-2.4.1-3714.zip/download +[14]: https://www.combodo.com + +[20]: https://www.itophub.io/wiki/page?id=2_5_0%3Adatamodel%3Astart#configuration_management_cmdb +[21]: https://www.itophub.io/wiki/page?id=2_5_0%3Adatamodel%3Astart#ticketing +[22]: https://www.itophub.io/wiki/page?id=2_5_0%3Adatamodel%3Astart#service_management +[23]: https://www.itophub.io/wiki/page?id=2_5_0%3Adatamodel%3Astart#change_management +[24]: https://www.itophub.io/wiki/page?id=2_5_0%3Aimplementation%3Astart#service_level_agreements_and_targets +[25]: https://www.itophub.io/wiki/page?id=2_5_0%3Auser%3Aactions#relations +[26]: https://www.itophub.io/wiki/page?id=2_5_0%3Auser%3Abulk_modify#uploading_data +[27]: https://manage-wiki.openitop.org/doku.php?id=2_5_0:admin:audit +[28]: https://manage-wiki.openitop.org/doku.php?id=2_5_0:advancedtopics:data_synchro_overview + + + + From 8897d9f82b6727696d238bf4c3d898814ba879d4 Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Fri, 31 Aug 2018 17:36:52 +0200 Subject: [PATCH 20/27] ignore : add other shared idea files in index --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 140ed9431..80a7b65d6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,8 @@ env-*/* # Jetbrains .idea/** !.idea/encodings.xml -!.idea/codeStyles -!.idea/inspectionProfiles +!.idea/codeStyles/* +!.idea/inspectionProfiles/* From fc0e5ecd3b60bd5c3d019917dd66c45bb740d6cb Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Fri, 31 Aug 2018 17:46:12 +0200 Subject: [PATCH 21/27] Ignore : add other shared files in index --- .gitignore | 2 + .idea/codeStyles/Project.xml | 37 +++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 +++ .idea/inspectionProfiles/Combodo.xml | 19 ++++++++++ .idea/inspectionProfiles/Project_Default.xml | 19 ++++++++++ .../inspectionProfiles/profiles_settings.xml | 7 ++++ 6 files changed, 89 insertions(+) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Combodo.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml diff --git a/.gitignore b/.gitignore index 80a7b65d6..f416fd47e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,9 @@ env-*/* # Jetbrains .idea/** !.idea/encodings.xml +!.idea/codeStyles !.idea/codeStyles/* +!.idea/inspectionProfiles !.idea/inspectionProfiles/* diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..5a8b91ac8 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,37 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..307554b77 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Combodo.xml b/.idea/inspectionProfiles/Combodo.xml new file mode 100644 index 000000000..335715cff --- /dev/null +++ b/.idea/inspectionProfiles/Combodo.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..b9013fdbd --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..408277a96 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file From 799106928217c306e329eb18c8f140cd0b0e648e Mon Sep 17 00:00:00 2001 From: bruno DA SILVA Date: Mon, 3 Sep 2018 12:13:57 +0200 Subject: [PATCH 22/27] .gitignore completion - cache - logs - composer's vendor dir --- .gitignore | 220 ++++++++++++++++++++++++++++------------------------- 1 file changed, 117 insertions(+), 103 deletions(-) diff --git a/.gitignore b/.gitignore index f416fd47e..44b43a8f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,103 +1,117 @@ - -conf/* -env-*/* - - - -# Jetbrains -.idea/** -!.idea/encodings.xml -!.idea/codeStyles -!.idea/codeStyles/* -!.idea/inspectionProfiles -!.idea/inspectionProfiles/* - - - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests -### Eclipse template - -.metadata -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders -.project - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# CDT- autotools -.autotools - -# Java annotation processor (APT) -.factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Annotation Processing -.apt_generated/ - -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - + +conf/* +env-*/* + +# composer reserver directory, from sources, populate/update using "composer install" +vendor/* + +# all datas but listing prevention +data/* +!data/.htaccess +!data/index.php +!data/web.config + +# all logs but listing prevention +log/* +!log/.htaccess +!log/index.php +!log/web.config + + +# Jetbrains +.idea/** +!.idea/encodings.xml +!.idea/codeStyles +!.idea/codeStyles/* +!.idea/inspectionProfiles +!.idea/inspectionProfiles/* + + + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests +### Eclipse template + +.metadata +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + From 894eeef140a2f0002348681b1169fcc4dbb25fb6 Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Mon, 3 Sep 2018 15:10:51 +0200 Subject: [PATCH 23/27] ignore : add extensions --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 44b43a8f1..43cf000c4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ data/* !data/index.php !data/web.config +# iTop extensions +extensions/* +!extensions/readme.txt + # all logs but listing prevention log/* !log/.htaccess From 10683d943ff53163ee99c324b08d3ff907797874 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 3 Sep 2018 16:50:30 +0200 Subject: [PATCH 24/27] =?UTF-8?q?N=C2=B01620:=20Fix=20'forgot=20your=20pas?= =?UTF-8?q?sword=3F'=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/loginwebpage.class.inc.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php index 246f0d826..8b7b3a95a 100644 --- a/application/loginwebpage.class.inc.php +++ b/application/loginwebpage.class.inc.php @@ -230,7 +230,8 @@ class LoginWebPage extends NiceWebPage try { UserRights::Login($sAuthUser); // Set the user's language (if possible!) - $oUser = UserRights::GetUserObject(); + /** @var UserInternal $oUser */ + $oUser = UserRights::GetUserObject(); if ($oUser == null) { throw new Exception(Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)); @@ -254,6 +255,7 @@ class LoginWebPage extends NiceWebPage $sToken = substr(md5(APPROOT.uniqid()), 0, 16); $oUser->Set('reset_pwd_token', $sToken); CMDBObject::SetTrackInfo('Reset password'); + $oUser->AllowWrite(true); $oUser->DBUpdate(); $oEmail = new Email(); From 7ac0e50bd91a2eb150bec469160f050dc577be77 Mon Sep 17 00:00:00 2001 From: bruno DA SILVA Date: Mon, 3 Sep 2018 17:36:37 +0200 Subject: [PATCH 25/27] phpstorm's line separator in edited files --- .idea/codeStyles/Project.xml | 73 ++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 5a8b91ac8..222b0c7df 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,37 +1,38 @@ - - - - - - - + + + \ No newline at end of file From cad18bee739a7697b5abf3cd3d60f5c139751c9f Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Tue, 4 Sep 2018 12:02:16 +0200 Subject: [PATCH 26/27] SetupUtils : update comments to match current branch version --- setup/setuputils.class.inc.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index f60b9860a..4753d5082 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -50,11 +50,11 @@ class CheckResult class SetupUtils { // -- Minimum versions (requirements : forbids installation if not met) - const PHP_MIN_VERSION = '5.6.0'; // 5.6 will be supported untill the end of 2018 (see http://php.net/supported-versions.php) + const PHP_MIN_VERSION = '5.6.0'; // 5.6 will be supported until the end of 2018 (see http://php.net/supported-versions.php) const MYSQL_MIN_VERSION = '5.6.0'; // 5.6 to have fulltext on InnoDB for Tags fields (N°931) // -- versions that will be the minimum in next iTop major release (warning if not met) - const PHP_NEXT_MIN_VERSION = ''; // no new PHP requirement for now in iTop 2.6 - const MYSQL_NEXT_MIN_VERSION = ''; // no new MySQL requirement for now in iTop 2.6 + const PHP_NEXT_MIN_VERSION = ''; // no new PHP requirement for next iTop version + const MYSQL_NEXT_MIN_VERSION = ''; // no new MySQL requirement for next iTop version // -- First recent version that is not yet validated by Combodo (warning) const PHP_NOT_VALIDATED_VERSION = '7.3.0'; From 40a4e6d7b0ee1812b9eb3559a4eda9401e592b9f Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Tue, 4 Sep 2018 17:38:22 +0200 Subject: [PATCH 27/27] Fix files using CrLf, convert them to Lf to have the whole repo using Lf Warn your git config (core.autocrlf = input or true) --- .../userrights/userrightsmatrix.class.inc.php | 736 +- .../userrights/userrightsnull.class.inc.php | 156 +- .../userrightsprofile.class.inc.php | 1740 +- .../userrightsprofile.db.class.inc.php | 2182 +- .../userrightsprojection.class.inc.php | 2506 +-- application/ajaxwebpage.class.inc.php | 782 +- application/application.inc.php | 80 +- application/applicationextension.inc.php | 2466 +-- application/audit.category.class.inc.php | 120 +- application/audit.rule.class.inc.php | 128 +- application/capturewebpage.class.inc.php | 168 +- application/clipage.class.inc.php | 194 +- application/csvpage.class.inc.php | 222 +- application/iotask.class.inc.php | 138 +- application/itopwebpage.class.inc.php | 2774 +-- application/itopwizardwebpage.class.inc.php | 114 +- application/loginwebpage.class.inc.php | 1760 +- application/menunode.class.inc.php | 2838 +-- application/nicewebpage.class.inc.php | 502 +- application/query.class.inc.php | 282 +- application/shortcut.class.inc.php | 676 +- application/startup.inc.php | 136 +- application/template.class.inc.php | 854 +- application/templates/audit_category.html | 24 +- application/ui.linkswidget.class.inc.php | 1216 +- application/uiwizard.class.inc.php | 662 +- application/utils.inc.php | 3910 ++-- application/webpage.class.inc.php | 2484 +-- application/wizardhelper.class.inc.php | 692 +- application/xmlpage.class.inc.php | 212 +- core/MyHelpers.class.inc.php | 1056 +- core/action.class.inc.php | 854 +- core/apc-emulation.php | 666 +- core/archive.class.inc.php | 670 +- core/asynctask.class.inc.php | 770 +- core/attributedef.class.inc.php | 17424 ++++++++-------- core/background.inc.php | 110 +- core/backgroundprocess.inc.php | 176 +- core/bulkchange.class.inc.php | 2700 +-- core/cmdbchange.class.inc.php | 174 +- core/cmdbchangeop.class.inc.php | 2084 +- core/cmdbobject.class.inc.php | 1388 +- core/cmdbsource.class.inc.php | 2428 +-- core/computing.inc.php | 278 +- core/config.class.inc.php | 4336 ++-- core/coreexception.class.inc.php | 258 +- core/csvparser.class.inc.php | 542 +- core/data.generator.class.inc.php | 748 +- core/dbobject.class.php | 7466 +++---- core/dbobjectiterator.php | 126 +- core/dbobjectsearch.class.php | 4792 ++--- core/dbobjectset.class.php | 3368 +-- core/dbproperty.class.inc.php | 320 +- core/dbunionsearch.class.php | 1202 +- core/deletionplan.class.inc.php | 596 +- core/designdocument.class.inc.php | 564 +- core/email.class.inc.php | 1078 +- core/event.class.inc.php | 874 +- core/expression.class.inc.php | 8 +- core/expressioncache.class.inc.php | 220 +- core/filterdef.class.inc.php | 424 +- core/kpi.class.inc.php | 802 +- core/metamodelmodifier.inc.php | 64 +- core/modelreflection.class.inc.php | 576 +- core/moduledesign.class.inc.php | 238 +- core/modulehandler.class.inc.php | 102 +- core/oql/build/build.cmd | 10 +- core/oql/expression.class.inc.php | 4812 ++--- core/oql/oql-lexer.plex | 888 +- core/oql/oql-parser.y | 606 +- core/oql/oqlinterpreter.class.inc.php | 224 +- core/oql/oqlquery.class.inc.php | 1492 +- core/ormcustomfieldsvalue.class.inc.php | 206 +- core/ormdocument.class.inc.php | 396 +- core/ormlinkset.class.inc.php | 1498 +- core/ormpassword.class.inc.php | 256 +- core/ormstopwatch.class.inc.php | 1170 +- core/querymodifier.class.inc.php | 68 +- core/restservices.class.inc.php | 1554 +- core/sqlobjectquery.class.inc.php | 1354 +- core/sqlquery.class.inc.php | 402 +- core/sqlunionquery.class.inc.php | 416 +- core/stimulus.class.inc.php | 276 +- core/templatestring.class.inc.php | 354 +- core/trigger.class.inc.php | 786 +- core/userrights.class.inc.php | 3686 ++-- core/valuesetdef.class.inc.php | 924 +- css/blue_green.css | 444 +- css/date.picker.css | 232 +- css/default.css | 330 +- css/font-combodo/glyphs/0.svg | 52 +- css/font-combodo/glyphs/1.svg | 26 +- css/font-combodo/glyphs/2.svg | 36 +- css/font-combodo/glyphs/3.svg | 34 +- css/font-combodo/glyphs/4.svg | 40 +- css/font-combodo/glyphs/C.svg | 130 +- css/font-combodo/glyphs/D.svg | 46 +- css/font-combodo/glyphs/I.svg | 72 +- css/font-combodo/test.html | 166 +- css/jqModal.css | 78 +- css/jquery-ui-timepicker-addon.css | 58 +- css/jquery.tabs.css | 150 +- css/jquery.treeview.css | 90 +- css/light-grey.scss | 6522 +++--- data/.htaccess | 26 +- data/index.php | 4 +- data/web.config | 14 +- .../en.dict.authent-external.php | 96 +- .../fr.dict.authent-external.php | 54 +- .../it.dict.authent-external.php | 96 +- .../model.authent-external.php | 180 +- .../module.authent-external.php | 130 +- .../tr.dict.authent-external.php | 100 +- .../zh.dict.authent-external.php | 98 +- .../2.x/authent-ldap/en.dict.authent-ldap.php | 100 +- .../2.x/authent-ldap/fr.dict.authent-ldap.php | 58 +- .../2.x/authent-ldap/it.dict.authent-ldap.php | 100 +- .../2.x/authent-ldap/model.authent-ldap.php | 412 +- .../2.x/authent-ldap/module.authent-ldap.php | 130 +- .../2.x/authent-ldap/tr.dict.authent-ldap.php | 102 +- .../2.x/authent-ldap/zh.dict.authent-ldap.php | 102 +- .../authent-local/en.dict.authent-local.php | 100 +- .../authent-local/fr.dict.authent-local.php | 58 +- .../authent-local/it.dict.authent-local.php | 100 +- .../2.x/authent-local/model.authent-local.php | 266 +- .../authent-local/module.authent-local.php | 84 +- .../authent-local/tr.dict.authent-local.php | 102 +- .../authent-local/zh.dict.authent-local.php | 102 +- .../2.x/itop-attachments/ajax.attachment.php | 224 +- .../en.dict.itop-attachments.php | 84 +- .../2.x/itop-attachments/main.attachments.php | 1622 +- .../itop-attachments/module.attachments.php | 294 +- .../nl.dict.itop-attachments.php | 96 +- .../pt_br.dict.itop-attachments.php | 84 +- .../2.x/itop-backup/dbrestore.class.inc.php | 406 +- .../2.x/itop-backup/module.itop-backup.php | 116 +- datamodels/2.x/itop-backup/status.php | 796 +- ...ule.itop-bridge-virtualization-storage.php | 90 +- .../en.dict.itop-change-mgmt-itil.php | 598 +- .../it.dict.itop-change-mgmt-itil.php | 640 +- .../module.itop-change-mgmt-itil.php | 88 +- .../pt_br.dict.itop-change-mgmt-itil.php | 598 +- .../tr.dict.itop-change-mgmt-itil.php | 742 +- .../zh.dict.itop-change-mgmt-itil.php | 740 +- .../en.dict.itop-change-mgmt.php | 290 +- .../module.itop-change-mgmt.php | 186 +- .../nl.dict.itop-change-mgmt.php | 296 +- .../pt_br.dict.itop-change-mgmt.php | 284 +- .../en.dict.itop-config-mgmt.php | 3794 ++-- .../it.dict.itop-config-mgmt.php | 2562 +-- .../main.itop-config-mgmt.php | 50 +- .../module.itop-config-mgmt.php | 234 +- .../nl.dict.itop-config-mgmt.php | 3788 ++-- .../pt_br.dict.itop-config-mgmt.php | 3774 ++-- .../tr.dict.itop-config-mgmt.php | 3264 +-- .../zh.dict.itop-config-mgmt.php | 3268 +-- datamodels/2.x/itop-config/config.php | 664 +- datamodels/2.x/itop-config/js/ace.js | 26 +- .../2.x/itop-config/js/ext-searchbox.js | 8 +- .../2.x/itop-config/js/theme-eclipse.js | 2 +- .../2.x/itop-config/license.itop-config.xml | 68 +- .../2.x/itop-config/module.itop-config.php | 82 +- .../datamodel.itop-full-itil.xml | 148 +- .../itop-full-itil/module.itop-full-itil.php | 94 +- .../en.dict.itop-incident-mgmt-itil.php | 480 +- .../module.itop-incident-mgmt-itil.php | 94 +- .../pt_br.dict.itop-incident-mgmt-itil.php | 490 +- .../de.dict.itop-knownerror-mgmt.php | 178 +- .../en.dict.itop-knownerror-mgmt.php | 384 +- .../it.dict.itop-knownerror-mgmt.php | 370 +- .../module.itop-knownerror-mgmt.php | 172 +- .../nl.dict.itop-knownerror-mgmt.php | 386 +- .../pt_br.dict.itop-knownerror-mgmt.php | 386 +- .../tr.dict.itop-knownerror-mgmt.php | 372 +- .../zh.dict.itop-knownerror-mgmt.php | 372 +- .../module.itop-portal-base.php | 84 +- .../abstractcontroller.class.inc.php | 64 +- .../controllers/brickcontroller.class.inc.php | 64 +- .../browsebrickcontroller.class.inc.php | 1572 +- .../createbrickcontroller.class.inc.php | 226 +- .../defaultcontroller.class.inc.php | 144 +- .../managebrickcontroller.class.inc.php | 1912 +- .../objectcontroller.class.inc.php | 3062 +-- .../userprofilebrickcontroller.class.inc.php | 690 +- .../src/routers/abstractrouter.class.inc.php | 280 +- .../routers/browsebrickrouter.class.inc.php | 140 +- .../routers/createbrickrouter.class.inc.php | 74 +- .../src/routers/defaultrouter.class.inc.php | 88 +- .../routers/managebrickrouter.class.inc.php | 154 +- .../src/routers/objectrouter.class.inc.php | 240 +- .../userprofilebrickrouter.class.inc.php | 82 +- .../js/TreeListFilter.js | 166 +- .../2.x/itop-portal/datamodel.itop-portal.xml | 28 +- .../2.x/itop-portal/module.itop-portal.php | 74 +- .../en.dict.itop-problem-mgmt.php | 316 +- .../it.dict.itop-problem-mgmt.php | 274 +- .../module.itop-problem-mgmt.php | 88 +- .../nl.dict.itop-problem-mgmt.php | 340 +- .../pt_br.dict.itop-problem-mgmt.php | 316 +- .../zh.dict.itop-problem-mgmt.php | 354 +- .../module.itop-profiles-itil.php | 128 +- .../en.dict.itop-request-mgmt-itil.php | 534 +- .../module.itop-request-mgmt-itil.php | 90 +- .../nl.dict.itop-request-mgmt-itil.php | 534 +- .../pt_br.dict.itop-request-mgmt-itil.php | 540 +- .../en.dict.itop-request-mgmt.php | 594 +- .../module.itop-request-mgmt.php | 90 +- .../nl.dict.itop-request-mgmt.php | 602 +- .../pt_br.dict.itop-request-mgmt.php | 594 +- .../en.dict.itop-service-mgmt-provider.php | 1056 +- .../main.itop-service-mgmt-provider.php | 40 +- .../module.itop-service-mgmt-provider.php | 194 +- .../nl.dict.itop-service-mgmt-provider.php | 1084 +- .../pt_br.dict.itop-service-mgmt-provider.php | 1070 +- .../en.dict.itop-service-mgmt.php | 1056 +- .../it.dict.itop-service-mgmt.php | 1240 +- .../main.itop-service-mgmt.php | 40 +- .../module.itop-service-mgmt.php | 188 +- .../pt_br.dict.itop-service-mgmt.php | 1056 +- .../tr.dict.itop-service-mgmt.php | 1220 +- .../zh.dict.itop-service-mgmt.php | 1242 +- .../itop-tickets/data.struct.ta-actions.xml | 136 +- .../2.x/itop-tickets/en.dict.itop-tickets.php | 512 +- .../2.x/itop-tickets/it.dict.itop-tickets.php | 436 +- .../2.x/itop-tickets/main.itop-tickets.php | 570 +- .../2.x/itop-tickets/module.itop-tickets.php | 144 +- .../2.x/itop-tickets/nl.dict.itop-tickets.php | 508 +- .../itop-tickets/pt_br.dict.itop-tickets.php | 488 +- .../2.x/itop-tickets/zh.dict.itop-tickets.php | 536 +- .../main.itop-welcome-itil.php | 122 +- .../module.itop-welcome-itil.php | 96 +- dictionaries/da.dictionary.itop.core.php | 5036 ++--- dictionaries/da.dictionary.itop.ui.php | 2266 +- dictionaries/en.dictionary.itop.core.php | 1808 +- dictionaries/en.dictionary.itop.model.php | 54 +- dictionaries/en.dictionary.itop.ui.php | 3062 +-- dictionaries/fr.dictionary.itop.core.php | 1530 +- dictionaries/hu.dictionary.itop.core.php | 1264 +- dictionaries/it.dictionary.itop.core.php | 1760 +- dictionaries/it.dictionary.itop.ui.php | 2496 +-- dictionaries/ja.dictionary.itop.ui.php | 2254 +- dictionaries/nl.dictionary.itop.core.php | 1772 +- dictionaries/nl.dictionary.itop.ui.php | 2632 +-- dictionaries/pt_br.dictionary.itop.core.php | 1758 +- dictionaries/pt_br.dictionary.itop.ui.php | 2614 +-- dictionaries/tr.dictionary.itop.core.php | 1608 +- dictionaries/tr.dictionary.itop.ui.php | 2548 +-- dictionaries/zh.dictionary.itop.core.php | 1606 +- dictionaries/zh.dictionary.itop.ui.php | 2556 +-- images/logo-itop-dark-bg.svg | 118 +- index.php | 64 +- js/ajaxfileupload.js | 410 +- js/ckeditor/CHANGES.md | 2608 +-- js/ckeditor/LICENSE.md | 2840 +-- js/ckeditor/contents.css | 426 +- js/ckeditor/lang/de.js | 6 +- js/ckeditor/lang/en.js | 6 +- js/ckeditor/lang/es.js | 6 +- js/ckeditor/lang/fr.js | 6 +- js/ckeditor/lang/it.js | 6 +- js/ckeditor/lang/pt-br.js | 6 +- .../dialogs/lang/_translationstatus.txt | 50 +- .../colordialog/dialogs/colordialog.css | 40 +- .../plugins/confighelper/docs/install.html | 248 +- .../plugins/confighelper/docs/styles.css | 118 +- js/ckeditor/plugins/scayt/CHANGELOG.md | 40 +- js/ckeditor/plugins/scayt/LICENSE.md | 56 +- js/ckeditor/plugins/scayt/README.md | 50 +- js/ckeditor/plugins/scayt/dialogs/dialog.css | 46 +- js/ckeditor/plugins/scayt/dialogs/toolbar.css | 142 +- .../plugins/scayt/skins/moono-lisa/scayt.css | 50 +- .../dialogs/lang/_translationstatus.txt | 40 +- js/ckeditor/plugins/wsc/LICENSE.md | 56 +- js/ckeditor/plugins/wsc/README.md | 50 +- js/ckeditor/plugins/wsc/dialogs/ciframe.html | 132 +- .../plugins/wsc/dialogs/tmpFrameset.html | 104 +- js/ckeditor/plugins/wsc/dialogs/wsc.css | 164 +- .../plugins/wsc/skins/moono-lisa/wsc.css | 86 +- js/ckeditor/skins/flat/dialog.css | 8 +- js/ckeditor/skins/flat/dialog_ie.css | 8 +- js/ckeditor/skins/flat/dialog_ie7.css | 8 +- js/ckeditor/skins/flat/dialog_ie8.css | 8 +- js/ckeditor/skins/flat/dialog_iequirks.css | 8 +- js/ckeditor/skins/flat/editor.css | 8 +- js/ckeditor/skins/flat/editor_gecko.css | 8 +- js/ckeditor/skins/flat/editor_ie.css | 8 +- js/ckeditor/skins/flat/editor_ie7.css | 8 +- js/ckeditor/skins/flat/editor_ie8.css | 8 +- js/ckeditor/skins/flat/editor_iequirks.css | 8 +- js/ckeditor/skins/flat/readme.md | 16 +- js/ckeditor/styles.js | 278 +- js/forms-json-utils.js | 1024 +- js/jquery-migrate-3.0.1.min.js | 1250 +- js/jquery-ui-timepicker-addon.js | 4538 ++-- js/jquery.layout.js | 11876 +++++------ js/jquery.layout.min.js | 282 +- js/jquery.tablehover.js | 852 +- js/jquery.tablesorter.js | 2064 +- js/jquery.tablesorter.pager.js | 1070 +- js/json.js | 946 +- js/linkswidget.js | 942 +- js/searchformforeignkeys.js | 682 +- js/themes/flora/flora.accordion.css | 22 +- js/themes/flora/flora.all.css | 18 +- js/themes/flora/flora.calendar.css | 334 +- js/themes/flora/flora.css | 2 +- js/themes/flora/flora.dialog.css | 172 +- js/themes/flora/flora.resizable.css | 38 +- js/themes/flora/flora.shadow.css | 66 +- js/themes/flora/flora.slider.css | 14 +- js/themes/flora/flora.tablesorter.css | 80 +- js/utils.js | 1426 +- js/wizard.utils.js | 294 +- js/wizardhelper.js | 522 +- license.txt | 1322 +- log/.htaccess | 26 +- log/index.php | 4 +- log/web.config | 14 +- pages/UniversalSearch.php | 270 +- pages/audit.php | 868 +- pages/exec.php | 154 +- pages/graphviz.php | 326 +- pages/index.php | 6 +- pages/navigator.php | 162 +- pages/notifications.php | 210 +- pages/opensearch.xml.php | 90 +- pages/run_query.php | 550 +- pages/schema.php | 2254 +- portal/index.php | 2946 +-- portal/readme.txt | 144 +- readme.txt | 78 +- setup/ajax.dataloader.php | 416 +- setup/email.test.php | 570 +- setup/modulediscovery.class.inc.php | 1064 +- setup/moduleinstallation.class.inc.php | 204 +- setup/moduleinstaller.class.inc.php | 454 +- setup/runtimeenv.class.inc.php | 2496 +-- setup/setuppage.class.inc.php | 610 +- setup/tar.php | 4724 ++--- setup/xmldataloader.class.inc.php | 898 +- .../bssubformfieldrenderer.class.inc.php | 144 +- .../console/consoleformrenderer.class.inc.php | 88 +- ...oleselectobjectfieldrenderer.class.inc.php | 526 +- .../consolesimplefieldrenderer.class.inc.php | 772 +- .../consolesubformfieldrenderer.class.inc.php | 110 +- synchro/synchro_import.php | 1606 +- synchro/synchrodatasource.class.inc.php | 5912 +++--- test/GroupByAndFunctions.php | 684 +- test/ItopDataTestCase.php | 1152 +- test/ItopTestCase.php | 116 +- test/benchmark.php | 1742 +- test/config-test-farm.php | 76 +- test/core/DBObjectTest.php | 178 +- test/core/DBSearchTest.php | 1042 +- test/core/UserRightsTest.php | 472 +- test/core/apcEmulationTest.php | 388 +- test/core/dictTest.php | 114 +- test/core/mockApcEmulation.incphp | 102 +- test/core/mockDict.incphp | 50 +- test/core/ormLinkSetTest.php | 552 +- test/display_cache_content.php | 208 +- test/itop-tickets/itopTicketTest.php | 1856 +- test/phpunit.xml.dist | 128 +- test/test.php | 338 +- test/unittestautoload.php | 12 +- web.config | 30 +- webservices/backoffice.dataloader.php | 340 +- webservices/cron.cmd | 36 +- webservices/cron.distrib | 10 +- webservices/export.php | 730 +- webservices/import.php | 1818 +- webservices/itoprest.examples.php | 638 +- webservices/itopsoap.examples.php | 290 +- webservices/itopsoaptypes.class.inc.php | 376 +- webservices/rest.php | 590 +- webservices/soapserver.php | 218 +- webservices/webservices.basic.php | 590 +- webservices/webservices.class.inc.php | 1188 +- 378 files changed, 152833 insertions(+), 152833 deletions(-) diff --git a/addons/userrights/userrightsmatrix.class.inc.php b/addons/userrights/userrightsmatrix.class.inc.php index f8e05ba2a..f9949477a 100644 --- a/addons/userrights/userrightsmatrix.class.inc.php +++ b/addons/userrights/userrightsmatrix.class.inc.php @@ -1,368 +1,368 @@ - - -/** - * UserRightsMatrix (User management Module) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class UserRightsMatrixClassGrant extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_ur_matrixclasses", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - } -} - -class UserRightsMatrixClassStimulusGrant extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_ur_matrixclassesstimulus", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - } -} - -class UserRightsMatrixAttributeGrant extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_ur_matrixattributes", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - } -} - - - - -class UserRightsMatrix extends UserRightsAddOnAPI -{ - static public $m_aActionCodes = array( - UR_ACTION_READ => 'read', - UR_ACTION_MODIFY => 'modify', - UR_ACTION_DELETE => 'delete', - UR_ACTION_BULK_READ => 'bulk read', - UR_ACTION_BULK_MODIFY => 'bulk modify', - UR_ACTION_BULK_DELETE => 'bulk delete', - ); - - // Installation: create the very first user - public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') - { - // Maybe we should check that no other user with userid == 0 exists - $oUser = new UserLocal(); - $oUser->Set('login', $sAdminUser); - $oUser->Set('password', $sAdminPwd); - $oUser->Set('contactid', 1); // one is for root ! - $oUser->Set('language', $sLanguage); // Language was chosen during the installation - - // Create a change to record the history of the User object - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); - - // Now record the admin user object - $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); - $this->SetupUser($iUserId, true); - return true; - } - - public function IsAdministrator($oUser) - { - return ($oUser->GetKey() == 1); - } - - public function IsPortalUser($oUser) - { - return ($oUser->GetKey() == 1); - } - - // Deprecated - create a new module ! - public function Setup() - { - // Users must be added manually - // This procedure will then update the matrix when a new user is found or a new class/attribute appears - $oUserSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT User")); - while ($oUser = $oUserSet->Fetch()) - { - $this->SetupUser($oUser->GetKey()); - } - return true; - } - - protected function SetupUser($iUserId, $bNewUser = false) - { - foreach(array('bizmodel', 'application', 'gui', 'core/cmdb') as $sCategory) - { - foreach (MetaModel::GetClasses($sCategory) as $sClass) - { - foreach (self::$m_aActionCodes as $iActionCode => $sAction) - { - if ($bNewUser) - { - $bAddCell = true; - } - else - { - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassGrant WHERE class = '$sClass' AND action = '$sAction' AND userid = $iUserId")); - $bAddCell = ($oSet->Count() < 1); - } - if ($bAddCell) - { - // Create a new entry - $oMyClassGrant = MetaModel::NewObject("UserRightsMatrixClassGrant"); - $oMyClassGrant->Set("userid", $iUserId); - $oMyClassGrant->Set("class", $sClass); - $oMyClassGrant->Set("action", $sAction); - $oMyClassGrant->Set("permission", "yes"); - $iId = $oMyClassGrant->DBInsertNoReload(); - } - } - foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) - { - if ($bNewUser) - { - $bAddCell = true; - } - else - { - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassStimulusGrant WHERE class = '$sClass' AND stimulus = '$sStimulusCode' AND userid = $iUserId")); - $bAddCell = ($oSet->Count() < 1); - } - if ($bAddCell) - { - // Create a new entry - $oMyClassGrant = MetaModel::NewObject("UserRightsMatrixClassStimulusGrant"); - $oMyClassGrant->Set("userid", $iUserId); - $oMyClassGrant->Set("class", $sClass); - $oMyClassGrant->Set("stimulus", $sStimulusCode); - $oMyClassGrant->Set("permission", "yes"); - $iId = $oMyClassGrant->DBInsertNoReload(); - } - } - foreach (MetaModel::GetAttributesList($sClass) as $sAttCode) - { - if ($bNewUser) - { - $bAddCell = true; - } - else - { - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixAttributeGrant WHERE class = '$sClass' AND attcode = '$sAttCode' AND userid = $iUserId")); - $bAddCell = ($oSet->Count() < 1); - } - if ($bAddCell) - { - foreach (array('read', 'modify') as $sAction) - { - // Create a new entry - $oMyAttGrant = MetaModel::NewObject("UserRightsMatrixAttributeGrant"); - $oMyAttGrant->Set("userid", $iUserId); - $oMyAttGrant->Set("class", $sClass); - $oMyAttGrant->Set("attcode", $sAttCode); - $oMyAttGrant->Set("action", $sAction); - $oMyAttGrant->Set("permission", "yes"); - $iId = $oMyAttGrant->DBInsertNoReload(); - } - } - } - } - } - /* - // Create the "My Bookmarks" menu item (parent_id = 0, rank = 6) - if ($bNewUser) - { - $bAddMenu = true; - } - else - { - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT menuNode WHERE type = 'user' AND parent_id = 0 AND user_id = $iUserId")); - $bAddMenu = ($oSet->Count() < 1); - } - if ($bAddMenu) - { - $oMenu = MetaModel::NewObject('menuNode'); - $oMenu->Set('type', 'user'); - $oMenu->Set('parent_id', 0); // It's a toplevel entry - $oMenu->Set('rank', 6); // Located just above the Admin Tools section (=7) - $oMenu->Set('name', 'My Bookmarks'); - $oMenu->Set('label', 'My Favorite Items'); - $oMenu->Set('hyperlink', 'UI.php'); - $oMenu->Set('template', '

My bookmarks

This section contains my most favorite search results

'); - $oMenu->Set('user_id', $iUserId); - $oMenu->DBInsert(); - } - */ - } - - - public function Init() - { - // Could be loaded in a shared memory (?) - return true; - } - - public function GetSelectFilter($oUser, $sClass, $aSettings = array()) - { - $oNullFilter = new DBObjectSearch($sClass); - return $oNullFilter; - } - - public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) - { - if (!array_key_exists($iActionCode, self::$m_aActionCodes)) - { - return UR_ALLOWED_NO; - } - $sAction = self::$m_aActionCodes[$iActionCode]; - - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassGrant WHERE class = '$sClass' AND action = '$sAction' AND userid = '{$oUser->GetKey()}'")); - if ($oSet->Count() < 1) - { - return UR_ALLOWED_NO; - } - - $oGrantRecord = $oSet->Fetch(); - switch ($oGrantRecord->Get('permission')) - { - case 'yes': - $iRetCode = UR_ALLOWED_YES; - break; - case 'no': - default: - $iRetCode = UR_ALLOWED_NO; - break; - } - return $iRetCode; - } - - public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) - { - if (!array_key_exists($iActionCode, self::$m_aActionCodes)) - { - return UR_ALLOWED_NO; - } - $sAction = self::$m_aActionCodes[$iActionCode]; - - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixAttributeGrant WHERE class = '$sClass' AND attcode = '$sAttCode' AND action = '$sAction' AND userid = '{$oUser->GetKey()}'")); - if ($oSet->Count() < 1) - { - return UR_ALLOWED_NO; - } - - $oGrantRecord = $oSet->Fetch(); - switch ($oGrantRecord->Get('permission')) - { - case 'yes': - $iRetCode = UR_ALLOWED_YES; - break; - case 'no': - default: - $iRetCode = UR_ALLOWED_NO; - break; - } - return $iRetCode; - } - - public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) - { - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassStimulusGrant WHERE class = '$sClass' AND stimulus = '$sStimulusCode' AND userid = '{$oUser->GetKey()}'")); - if ($oSet->Count() < 1) - { - return UR_ALLOWED_NO; - } - - $oGrantRecord = $oSet->Fetch(); - switch ($oGrantRecord->Get('permission')) - { - case 'yes': - $iRetCode = UR_ALLOWED_YES; - break; - case 'no': - default: - $iRetCode = UR_ALLOWED_NO; - break; - } - return $iRetCode; - } - - public function FlushPrivileges() - { - } -} - -UserRights::SelectModule('UserRightsMatrix'); - -?> + + +/** + * UserRightsMatrix (User management Module) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +class UserRightsMatrixClassGrant extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_ur_matrixclasses", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + +class UserRightsMatrixClassStimulusGrant extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_ur_matrixclassesstimulus", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + +class UserRightsMatrixAttributeGrant extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_ur_matrixattributes", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("login", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + MetaModel::Init_AddAttribute(new AttributeString("class", array("allowed_values"=>null, "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + + + + +class UserRightsMatrix extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'read', + UR_ACTION_MODIFY => 'modify', + UR_ACTION_DELETE => 'delete', + UR_ACTION_BULK_READ => 'bulk read', + UR_ACTION_BULK_MODIFY => 'bulk modify', + UR_ACTION_BULK_DELETE => 'bulk delete', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + // Maybe we should check that no other user with userid == 0 exists + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + $oUser->Set('contactid', 1); // one is for root ! + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + + // Create a change to record the history of the User object + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + + // Now record the admin user object + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + $this->SetupUser($iUserId, true); + return true; + } + + public function IsAdministrator($oUser) + { + return ($oUser->GetKey() == 1); + } + + public function IsPortalUser($oUser) + { + return ($oUser->GetKey() == 1); + } + + // Deprecated - create a new module ! + public function Setup() + { + // Users must be added manually + // This procedure will then update the matrix when a new user is found or a new class/attribute appears + $oUserSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT User")); + while ($oUser = $oUserSet->Fetch()) + { + $this->SetupUser($oUser->GetKey()); + } + return true; + } + + protected function SetupUser($iUserId, $bNewUser = false) + { + foreach(array('bizmodel', 'application', 'gui', 'core/cmdb') as $sCategory) + { + foreach (MetaModel::GetClasses($sCategory) as $sClass) + { + foreach (self::$m_aActionCodes as $iActionCode => $sAction) + { + if ($bNewUser) + { + $bAddCell = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassGrant WHERE class = '$sClass' AND action = '$sAction' AND userid = $iUserId")); + $bAddCell = ($oSet->Count() < 1); + } + if ($bAddCell) + { + // Create a new entry + $oMyClassGrant = MetaModel::NewObject("UserRightsMatrixClassGrant"); + $oMyClassGrant->Set("userid", $iUserId); + $oMyClassGrant->Set("class", $sClass); + $oMyClassGrant->Set("action", $sAction); + $oMyClassGrant->Set("permission", "yes"); + $iId = $oMyClassGrant->DBInsertNoReload(); + } + } + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + if ($bNewUser) + { + $bAddCell = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassStimulusGrant WHERE class = '$sClass' AND stimulus = '$sStimulusCode' AND userid = $iUserId")); + $bAddCell = ($oSet->Count() < 1); + } + if ($bAddCell) + { + // Create a new entry + $oMyClassGrant = MetaModel::NewObject("UserRightsMatrixClassStimulusGrant"); + $oMyClassGrant->Set("userid", $iUserId); + $oMyClassGrant->Set("class", $sClass); + $oMyClassGrant->Set("stimulus", $sStimulusCode); + $oMyClassGrant->Set("permission", "yes"); + $iId = $oMyClassGrant->DBInsertNoReload(); + } + } + foreach (MetaModel::GetAttributesList($sClass) as $sAttCode) + { + if ($bNewUser) + { + $bAddCell = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixAttributeGrant WHERE class = '$sClass' AND attcode = '$sAttCode' AND userid = $iUserId")); + $bAddCell = ($oSet->Count() < 1); + } + if ($bAddCell) + { + foreach (array('read', 'modify') as $sAction) + { + // Create a new entry + $oMyAttGrant = MetaModel::NewObject("UserRightsMatrixAttributeGrant"); + $oMyAttGrant->Set("userid", $iUserId); + $oMyAttGrant->Set("class", $sClass); + $oMyAttGrant->Set("attcode", $sAttCode); + $oMyAttGrant->Set("action", $sAction); + $oMyAttGrant->Set("permission", "yes"); + $iId = $oMyAttGrant->DBInsertNoReload(); + } + } + } + } + } + /* + // Create the "My Bookmarks" menu item (parent_id = 0, rank = 6) + if ($bNewUser) + { + $bAddMenu = true; + } + else + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT menuNode WHERE type = 'user' AND parent_id = 0 AND user_id = $iUserId")); + $bAddMenu = ($oSet->Count() < 1); + } + if ($bAddMenu) + { + $oMenu = MetaModel::NewObject('menuNode'); + $oMenu->Set('type', 'user'); + $oMenu->Set('parent_id', 0); // It's a toplevel entry + $oMenu->Set('rank', 6); // Located just above the Admin Tools section (=7) + $oMenu->Set('name', 'My Bookmarks'); + $oMenu->Set('label', 'My Favorite Items'); + $oMenu->Set('hyperlink', 'UI.php'); + $oMenu->Set('template', '

My bookmarks

This section contains my most favorite search results

'); + $oMenu->Set('user_id', $iUserId); + $oMenu->DBInsert(); + } + */ + } + + + public function Init() + { + // Could be loaded in a shared memory (?) + return true; + } + + public function GetSelectFilter($oUser, $sClass, $aSettings = array()) + { + $oNullFilter = new DBObjectSearch($sClass); + return $oNullFilter; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + if (!array_key_exists($iActionCode, self::$m_aActionCodes)) + { + return UR_ALLOWED_NO; + } + $sAction = self::$m_aActionCodes[$iActionCode]; + + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassGrant WHERE class = '$sClass' AND action = '$sAction' AND userid = '{$oUser->GetKey()}'")); + if ($oSet->Count() < 1) + { + return UR_ALLOWED_NO; + } + + $oGrantRecord = $oSet->Fetch(); + switch ($oGrantRecord->Get('permission')) + { + case 'yes': + $iRetCode = UR_ALLOWED_YES; + break; + case 'no': + default: + $iRetCode = UR_ALLOWED_NO; + break; + } + return $iRetCode; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + if (!array_key_exists($iActionCode, self::$m_aActionCodes)) + { + return UR_ALLOWED_NO; + } + $sAction = self::$m_aActionCodes[$iActionCode]; + + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixAttributeGrant WHERE class = '$sClass' AND attcode = '$sAttCode' AND action = '$sAction' AND userid = '{$oUser->GetKey()}'")); + if ($oSet->Count() < 1) + { + return UR_ALLOWED_NO; + } + + $oGrantRecord = $oSet->Fetch(); + switch ($oGrantRecord->Get('permission')) + { + case 'yes': + $iRetCode = UR_ALLOWED_YES; + break; + case 'no': + default: + $iRetCode = UR_ALLOWED_NO; + break; + } + return $iRetCode; + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserRightsMatrixClassStimulusGrant WHERE class = '$sClass' AND stimulus = '$sStimulusCode' AND userid = '{$oUser->GetKey()}'")); + if ($oSet->Count() < 1) + { + return UR_ALLOWED_NO; + } + + $oGrantRecord = $oSet->Fetch(); + switch ($oGrantRecord->Get('permission')) + { + case 'yes': + $iRetCode = UR_ALLOWED_YES; + break; + case 'no': + default: + $iRetCode = UR_ALLOWED_NO; + break; + } + return $iRetCode; + } + + public function FlushPrivileges() + { + } +} + +UserRights::SelectModule('UserRightsMatrix'); + +?> diff --git a/addons/userrights/userrightsnull.class.inc.php b/addons/userrights/userrightsnull.class.inc.php index 1549456c8..a94aa1d9e 100644 --- a/addons/userrights/userrightsnull.class.inc.php +++ b/addons/userrights/userrightsnull.class.inc.php @@ -1,78 +1,78 @@ - - -/** - * UserRightsNull - * User management Module - say Yeah! to everything - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class UserRightsNull extends UserRightsAddOnAPI -{ - // Installation: create the very first user - public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') - { - return true; - } - - public function IsAdministrator($oUser) - { - return true; - } - - public function IsPortalUser($oUser) - { - return true; - } - - public function Init() - { - return true; - } - - public function GetSelectFilter($oUser, $sClass, $aSettings = array()) - { - $oNullFilter = new DBObjectSearch($sClass); - return $oNullFilter; - } - - public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) - { - return UR_ALLOWED_YES; - } - - public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) - { - return UR_ALLOWED_YES; - } - - public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) - { - return UR_ALLOWED_YES; - } - - public function FlushPrivileges() - { - } -} - -UserRights::SelectModule('UserRightsNull'); - -?> + + +/** + * UserRightsNull + * User management Module - say Yeah! to everything + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class UserRightsNull extends UserRightsAddOnAPI +{ + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + return true; + } + + public function IsAdministrator($oUser) + { + return true; + } + + public function IsPortalUser($oUser) + { + return true; + } + + public function Init() + { + return true; + } + + public function GetSelectFilter($oUser, $sClass, $aSettings = array()) + { + $oNullFilter = new DBObjectSearch($sClass); + return $oNullFilter; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + return UR_ALLOWED_YES; + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + return UR_ALLOWED_YES; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + return UR_ALLOWED_YES; + } + + public function FlushPrivileges() + { + } +} + +UserRights::SelectModule('UserRightsNull'); + +?> diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php index cf943fb5d..a4cee533f 100644 --- a/addons/userrights/userrightsprofile.class.inc.php +++ b/addons/userrights/userrightsprofile.class.inc.php @@ -1,870 +1,870 @@ - - -/** - * UserRightsProfile - * User management Module, basing the right on profiles and a matrix (similar to UserRightsMatrix, but profiles and other decorations have been added) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -define('ADMIN_PROFILE_NAME', 'Administrator'); -define('PORTAL_PROFILE_NAME', 'Portal user'); - -class UserRightsBaseClassGUI extends cmdbAbstractObject -{ - // Whenever something changes, reload the privileges - - protected function AfterInsert() - { - UserRights::FlushPrivileges(); - } - - protected function AfterUpdate() - { - UserRights::FlushPrivileges(); - } - - protected function AfterDelete() - { - UserRights::FlushPrivileges(); - } -} - - -class URP_Profiles extends UserRightsBaseClassGUI -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_profiles", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name','description')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array ('name','description')); - } - - protected static $m_aCacheProfiles = null; - - public static function DoCreateProfile($sName, $sDescription) - { - if (is_null(self::$m_aCacheProfiles)) - { - self::$m_aCacheProfiles = array(); - $oFilterAll = new DBObjectSearch('URP_Profiles'); - $oSet = new DBObjectSet($oFilterAll); - while ($oProfile = $oSet->Fetch()) - { - self::$m_aCacheProfiles[$oProfile->Get('name')] = $oProfile->GetKey(); - } - } - - $sCacheKey = $sName; - if (isset(self::$m_aCacheProfiles[$sCacheKey])) - { - return self::$m_aCacheProfiles[$sCacheKey]; - } - $oNewObj = MetaModel::NewObject("URP_Profiles"); - $oNewObj->Set('name', $sName); - $oNewObj->Set('description', $sDescription); - $iId = $oNewObj->DBInsertNoReload(); - self::$m_aCacheProfiles[$sCacheKey] = $iId; - return $iId; - } - - function GetGrantAsHtml($oUserRights, $sClass, $sAction) - { - $bGrant = $oUserRights->GetProfileActionGrant($this->GetKey(), $sClass, $sAction); - if (is_null($bGrant)) - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; - } - elseif ($bGrant) - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; - } - else - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; - } - } - - function DoShowGrantSumary($oPage) - { - if ($this->GetRawName() == "Administrator") - { - // Looks dirty, but ok that's THE ONE - $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); - return; - } - - // Note: for sure, we assume that the instance is derived from UserRightsProfile - $oUserRights = UserRights::GetModuleInstance(); - - $aDisplayData = array(); - foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) - { - $aStimuli = array(); - foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) - { - $bGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); - if ($bGrant === true) - { - $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; - } - } - $sStimuli = implode(', ', $aStimuli); - - $aDisplayData[] = array( - 'class' => MetaModel::GetName($sClass), - 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'r'), - 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'br'), - 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'w'), - 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'bw'), - 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'd'), - 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'bd'), - 'stimuli' => $sStimuli, - ); - } - - $aDisplayConfig = array(); - $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); - $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); - $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); - $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); - $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); - $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); - $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); - $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); - $oPage->table($aDisplayConfig, $aDisplayData); - } - - function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, $bEditMode); - if (!$bEditMode) - { - $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); - $this->DoShowGrantSumary($oPage); - } - } - - public static function GetReadOnlyAttributes() - { - return array('name', 'description'); - } - - - // returns an array of id => array of column => php value(so-called "real value") - public static function GetPredefinedObjects() - { - return ProfilesConfig::GetProfilesValues(); - } - - // Before deleting a profile, - // preserve DB integrity by deleting links to users - protected function OnDelete() - { - // Don't remove admin profile - if ($this->Get('name') === ADMIN_PROFILE_NAME) - { - throw new SecurityException(Dict::Format('UI:Login:Error:AccessAdmin')); - } - - // Note: this may break the rule that says: "a user must have at least ONE profile" ! - $oLnkSet = $this->Get('user_list'); - while($oLnk = $oLnkSet->Fetch()) - { - $oLnk->DBDelete(); - } - } - - /** - * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) - * for the given attribute in the current state of the object - * @param $sAttCode string $sAttCode The code of the attribute - * @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas) - * @param $sTargetState string The target state in which to evalutate the flags, if empty the current state will be used - * @return integer Flags: the binary combination of the flags applicable to this attribute - */ - public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '') - { - $iFlags = parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState); - if (MetaModel::GetConfig()->Get('demo_mode')) - { - $aReasons[] = 'Sorry, profiles are read-only in the demonstration mode!'; - $iFlags |= OPT_ATT_READONLY; - } - return $iFlags; - } -} - - - -class URP_UserProfile extends UserRightsBaseClassGUI -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "userid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_userprofile", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('userid', 'profileid', 'reason')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form - } - - public function GetName() - { - return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); - } - - public function CheckToDelete(&$oDeletionPlan) - { - if (MetaModel::GetConfig()->Get('demo_mode')) - { - // Users deletion is NOT allowed in demo mode - $oDeletionPlan->AddToDelete($this, null); - $oDeletionPlan->SetDeletionIssues($this, array('deletion not allowed in demo mode.'), true); - $oDeletionPlan->ComputeResults(); - return false; - } - return parent::CheckToDelete($oDeletionPlan); - } - - protected function OnInsert() - { - $this->CheckIfProfileIsAllowed(UR_ACTION_CREATE); - } - - protected function OnUpdate() - { - $this->CheckIfProfileIsAllowed(UR_ACTION_MODIFY); - } - - protected function OnDelete() - { - $this->CheckIfProfileIsAllowed(UR_ACTION_DELETE); - } - - /** - * @param $iActionCode - * - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \SecurityException - */ - protected function CheckIfProfileIsAllowed($iActionCode) - { - // When initializing or admin, we need to let everything pass trough - if (!UserRights::IsLoggedIn() || UserRights::IsAdministrator()) { return; } - - // Only administrators can manage administrators - $iOrigUserId = $this->GetOriginal('userid'); - if (!empty($iOrigUserId)) - { - $oUser = MetaModel::GetObject('User', $iOrigUserId, true, true); - if (UserRights::IsAdministrator($oUser) && !UserRights::IsAdministrator()) - { - throw new SecurityException(Dict::Format('UI:Login:Error:AccessRestricted')); - } - } - $oUser = MetaModel::GetObject('User', $this->Get('userid'), true, true); - if (UserRights::IsAdministrator($oUser) && !UserRights::IsAdministrator()) - { - throw new SecurityException(Dict::Format('UI:Login:Error:AccessRestricted')); - } - if (!UserRights::IsActionAllowed(get_class($this), $iActionCode, DBObjectSet::FromObject($this))) - { - throw new SecurityException(Dict::Format('UI:Error:ObjectCannotBeUpdated')); - } - if (!UserRights::IsAdministrator() && ($this->Get('profile') === ADMIN_PROFILE_NAME)) - { - throw new SecurityException(Dict::Format('UI:Login:Error:AccessAdmin')); - } - } - -} - -class URP_UserOrg extends UserRightsBaseClassGUI -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "userid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_userorg", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("allowed_org_id", array("targetclass"=>"Organization", "jointype"=> "", "allowed_values"=>null, "sql"=>"allowed_org_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("allowed_org_name", array("allowed_values"=>null, "extkey_attcode"=> 'allowed_org_id', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"reason", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('userid', 'allowed_org_id', 'reason')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('allowed_org_id', 'reason')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('userid', 'allowed_org_id')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('userid', 'allowed_org_id')); // Criteria of the advanced search form - } - - public function GetName() - { - return Dict::Format('UI:UserManagement:LinkBetween_User_And_Org', $this->Get('userlogin'), $this->Get('allowed_org_name')); - } - - - protected function OnInsert() - { - $this->CheckIfOrgIsAllowed(); - } - - protected function OnUpdate() - { - $this->CheckIfOrgIsAllowed(); - } - - protected function OnDelete() - { - $this->CheckIfOrgIsAllowed(); - } - - /** - * @throws \CoreException - */ - protected function CheckIfOrgIsAllowed() - { - if (!UserRights::IsLoggedIn() || UserRights::IsAdministrator()) { return; } - - $oUser = UserRights::GetUserObject(); - $oAddon = UserRights::GetModuleInstance(); - $aOrgs = $oAddon->GetUserOrgs($oUser, ''); - if (count($aOrgs) > 0) - { - $iOrigOrgId = $this->GetOriginal('allowed_org_id'); - if ((!empty($iOrigOrgId) && !in_array($iOrigOrgId, $aOrgs)) || !in_array($this->Get('allowed_org_id'), $aOrgs)) - { - throw new SecurityException(Dict::Format('Class:User/Error:OrganizationNotAllowed')); - } - } - } -} - - - - -class UserRightsProfile extends UserRightsAddOnAPI -{ - static public $m_aActionCodes = array( - UR_ACTION_READ => 'r', - UR_ACTION_MODIFY => 'w', - UR_ACTION_DELETE => 'd', - UR_ACTION_BULK_READ => 'br', - UR_ACTION_BULK_MODIFY => 'bw', - UR_ACTION_BULK_DELETE => 'bd', - ); - - // Installation: create the very first user - public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') - { - CMDBObject::SetTrackInfo('Initialization'); - - $oChange = CMDBObject::GetCurrentChange(); - - $iContactId = 0; - // Support drastic data model changes: no organization class (or not writable)! - if (MetaModel::IsValidClass('Organization') && !MetaModel::IsAbstract('Organization')) - { - $oOrg = new Organization(); - $oOrg->Set('name', 'My Company/Department'); - $oOrg->Set('code', 'SOMECODE'); - $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip security */); - - // Support drastic data model changes: no Person class (or not writable)! - if (MetaModel::IsValidClass('Person') && !MetaModel::IsAbstract('Person')) - { - $oContact = new Person(); - $oContact->Set('name', 'My last name'); - $oContact->Set('first_name', 'My first name'); - if (MetaModel::IsValidAttCode('Person', 'org_id')) - { - $oContact->Set('org_id', $iOrgId); - } - if (MetaModel::IsValidAttCode('Person', 'phone')) - { - $oContact->Set('phone', '+00 000 000 000'); - } - $oContact->Set('email', 'my.email@foo.org'); - $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); - } - } - - - $oUser = new UserLocal(); - $oUser->Set('login', $sAdminUser); - $oUser->Set('password', $sAdminPwd); - if (MetaModel::IsValidAttCode('UserLocal', 'contactid') && ($iContactId != 0)) - { - $oUser->Set('contactid', $iContactId); - } - $oUser->Set('language', $sLanguage); // Language was chosen during the installation - - // Add this user to the very specific 'admin' profile - $oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => ADMIN_PROFILE_NAME), true /*all data*/); - if (is_object($oAdminProfile)) - { - $oUserProfile = new URP_UserProfile(); - //$oUserProfile->Set('userid', $iUserId); - $oUserProfile->Set('profileid', $oAdminProfile->GetKey()); - $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); - //$oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); - $oSet = DBObjectSet::FromObject($oUserProfile); - $oUser->Set('profile_list', $oSet); - } - $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); - return true; - } - - public function Init() - { - } - - protected $m_aUserOrgs = array(); // userid -> array of orgid - - // Built on demand, could be optimized if necessary (doing a query for each attribute that needs to be read) - protected $m_aObjectActionGrants = array(); - - /** - * Read and cache organizations allowed to the given user - * - * @param $oUser - * @param $sClass (not used here but can be used in overloads) - * - * @return array - * @throws \CoreException - * @throws \Exception - */ - public function GetUserOrgs($oUser, $sClass) - { - $iUser = $oUser->GetKey(); - if (!array_key_exists($iUser, $this->m_aUserOrgs)) - { - $this->m_aUserOrgs[$iUser] = array(); - - $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization'); - if ($sHierarchicalKeyCode !== false) - { - $sUserOrgQuery = 'SELECT UserOrg, Org FROM Organization AS Org JOIN Organization AS Root ON Org.'.$sHierarchicalKeyCode.' BELOW Root.id JOIN URP_UserOrg AS UserOrg ON UserOrg.allowed_org_id = Root.id WHERE UserOrg.userid = :userid'; - $oUserOrgSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sUserOrgQuery), array(), array('userid' => $iUser)); - while ($aRow = $oUserOrgSet->FetchAssoc()) - { - $oOrg = $aRow['Org']; - $this->m_aUserOrgs[$iUser][] = $oOrg->GetKey(); - } - } - else - { - $oSearch = new DBObjectSearch('URP_UserOrg'); - $oSearch->AllowAllData(); - $oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid')); - $oSearch->AddConditionExpression($oCondition); - - $oUserOrgSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser)); - while ($oUserOrg = $oUserOrgSet->Fetch()) - { - $this->m_aUserOrgs[$iUser][] = $oUserOrg->Get('allowed_org_id'); - } - } - } - return $this->m_aUserOrgs[$iUser]; - } - - public function ResetCache() - { - // Loaded by Load cache - $this->m_aUserOrgs = array(); - - // Cache - $this->m_aObjectActionGrants = array(); - } - - public function LoadCache() - { - static $bSharedObjectInitialized = false; - if (!$bSharedObjectInitialized) - { - $bSharedObjectInitialized = true; - if (self::HasSharing()) - { - SharedObject::InitSharedClassProperties(); - } - } - return true; - } - - /** - * @param $oUser User - * @return array - */ - public function IsAdministrator($oUser) - { - // UserRights caches the list for us - return UserRights::HasProfile(ADMIN_PROFILE_NAME, $oUser); - } - - /** - * @param $oUser User - * @return array - */ - public function IsPortalUser($oUser) - { - // UserRights caches the list for us - return UserRights::HasProfile(PORTAL_PROFILE_NAME, $oUser); - } - /** - * @param $oUser User - * @return bool - */ - public function ListProfiles($oUser) - { - $aRet = array(); - $oSearch = new DBObjectSearch('URP_UserProfile'); - $oSearch->AllowAllData(); - $oSearch->NoContextParameters(); - $oSearch->Addcondition('userid', $oUser->GetKey(), '='); - $oProfiles = new DBObjectSet($oSearch); - while ($oUserProfile = $oProfiles->Fetch()) - { - $aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname'); - } - return $aRet; - } - - public function GetSelectFilter($oUser, $sClass, $aSettings = array()) - { - $this->LoadCache(); - - $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, UR_ACTION_READ); - if ($aObjectPermissions['permission'] == UR_ALLOWED_NO) - { - return false; - } - - // Determine how to position the objects of this class - // - $sAttCode = self::GetOwnerOrganizationAttCode($sClass); - if (is_null($sAttCode)) - { - // No filtering for this object - return true; - } - // Position the user - // - $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); - if (count($aUserOrgs) == 0) - { - // No org means 'any org' - return true; - } - - return $this->MakeSelectFilter($sClass, $aUserOrgs, $aSettings, $sAttCode); - } - - - // This verb has been made public to allow the development of an accurate feedback for the current configuration - public function GetProfileActionGrant($iProfile, $sClass, $sAction) - { - // Note: action is forced lowercase to be more flexible (historical bug) - $sAction = strtolower($sAction); - - return ProfilesConfig::GetProfileActionGrant($iProfile, $sClass, $sAction); - } - - protected function GetUserActionGrant($oUser, $sClass, $iActionCode) - { - $this->LoadCache(); - - // load and cache permissions for the current user on the given class - // - $iUser = $oUser->GetKey(); - $aTest = @$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode]; - if (is_array($aTest)) return $aTest; - - $sAction = self::$m_aActionCodes[$iActionCode]; - - $bStatus = null; - // Call the API of UserRights because it caches the list for us - foreach(UserRights::ListProfiles($oUser) as $iProfile => $oProfile) - { - $bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction); - if (!is_null($bGrant)) - { - if ($bGrant) - { - if (is_null($bStatus)) - { - $bStatus = true; - } - } - else - { - $bStatus = false; - } - } - } - - $iPermission = $bStatus ? UR_ALLOWED_YES : UR_ALLOWED_NO; - - $aRes = array( - 'permission' => $iPermission, - ); - $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes; - return $aRes; - } - - public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) - { - $this->LoadCache(); - - $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); - $iPermission = $aObjectPermissions['permission']; - - // Note: In most cases the object set is ignored because it was interesting to optimize for huge data sets - // and acceptable to consider only the root class of the object set - - if ($iPermission != UR_ALLOWED_YES) - { - // It is already NO for everyone... that's the final word! - } - elseif ($iActionCode == UR_ACTION_READ) - { - // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading - } - elseif ($iActionCode == UR_ACTION_BULK_READ) - { - // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading - } - elseif ($oInstanceSet) - { - // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading - // We have to answer NO for objects shared for reading purposes - if (self::HasSharing()) - { - $aClassProps = SharedObject::GetSharedClassProperties($sClass); - if ($aClassProps) - { - // This class is shared, GetSelectFilter may allow some objects for read only - // But currently we are checking wether the objects might be written... - // Let's exclude the objects based on the relevant criteria - - $sOrgAttCode = self::GetOwnerOrganizationAttCode($sClass); - if (!is_null($sOrgAttCode)) - { - $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); - if (!is_null($aUserOrgs) && count($aUserOrgs) > 0) - { - $iCountNO = 0; - $iCountYES = 0; - $oInstanceSet->Rewind(); - while($oObject = $oInstanceSet->Fetch()) - { - $iOrg = $oObject->Get($sOrgAttCode); - if (in_array($iOrg, $aUserOrgs)) - { - $iCountYES++; - } - else - { - $iCountNO++; - } - } - if ($iCountNO == 0) - { - $iPermission = UR_ALLOWED_YES; - } - elseif ($iCountYES == 0) - { - $iPermission = UR_ALLOWED_NO; - } - else - { - $iPermission = UR_ALLOWED_DEPENDS; - } - } - } - } - } - } - return $iPermission; - } - - public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) - { - $this->LoadCache(); - - // Note: The object set is ignored because it was interesting to optimize for huge data sets - // and acceptable to consider only the root class of the object set - $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); - return $aObjectPermissions['permission']; - } - - // This verb has been made public to allow the development of an accurate feedback for the current configuration - public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) - { - return ProfilesConfig::GetProfileStimulusGrant($iProfile, $sClass, $sStimulusCode); - } - - public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) - { - $this->LoadCache(); - // Note: this code is VERY close to the code of IsActionAllowed() - $iUser = $oUser->GetKey(); - - // Note: The object set is ignored because it was interesting to optimize for huge data sets - // and acceptable to consider only the root class of the object set - $bStatus = null; - // Call the API of UserRights because it caches the list for us - foreach(UserRights::ListProfiles($oUser) as $iProfile => $oProfile) - { - $bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); - if (!is_null($bGrant)) - { - if ($bGrant) - { - if (is_null($bStatus)) - { - $bStatus = true; - } - } - else - { - $bStatus = false; - } - } - } - - $iPermission = $bStatus ? UR_ALLOWED_YES : UR_ALLOWED_NO; - return $iPermission; - } - - public function FlushPrivileges() - { - $this->ResetCache(); - } - - /** - * Find out which attribute is corresponding the the dimension 'owner org' - * returns null if no such attribute has been found (no filtering should occur) - */ - public static function GetOwnerOrganizationAttCode($sClass) - { - $sAttCode = null; - - $aCallSpec = array($sClass, 'MapContextParam'); - if (($sClass == 'Organization') || is_subclass_of($sClass, 'Organization')) - { - $sAttCode = 'id'; - } - elseif (is_callable($aCallSpec)) - { - $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter - if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) - { - // Skip silently. The data model checker will tell you something about this... - $sAttCode = null; - } - } - elseif(MetaModel::IsValidAttCode($sClass, 'org_id')) - { - $sAttCode = 'org_id'; - } - - return $sAttCode; - } - - /** - * Determine wether the objects can be shared by the mean of a class SharedObject - **/ - protected static function HasSharing() - { - static $bHasSharing; - if (!isset($bHasSharing)) - { - $bHasSharing = class_exists('SharedObject'); - } - return $bHasSharing; - } -} - - -UserRights::SelectModule('UserRightsProfile'); - -?> + + +/** + * UserRightsProfile + * User management Module, basing the right on profiles and a matrix (similar to UserRightsMatrix, but profiles and other decorations have been added) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +define('ADMIN_PROFILE_NAME', 'Administrator'); +define('PORTAL_PROFILE_NAME', 'Portal user'); + +class UserRightsBaseClassGUI extends cmdbAbstractObject +{ + // Whenever something changes, reload the privileges + + protected function AfterInsert() + { + UserRights::FlushPrivileges(); + } + + protected function AfterUpdate() + { + UserRights::FlushPrivileges(); + } + + protected function AfterDelete() + { + UserRights::FlushPrivileges(); + } +} + + +class URP_Profiles extends UserRightsBaseClassGUI +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profiles", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name','description')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array ('name','description')); + } + + protected static $m_aCacheProfiles = null; + + public static function DoCreateProfile($sName, $sDescription) + { + if (is_null(self::$m_aCacheProfiles)) + { + self::$m_aCacheProfiles = array(); + $oFilterAll = new DBObjectSearch('URP_Profiles'); + $oSet = new DBObjectSet($oFilterAll); + while ($oProfile = $oSet->Fetch()) + { + self::$m_aCacheProfiles[$oProfile->Get('name')] = $oProfile->GetKey(); + } + } + + $sCacheKey = $sName; + if (isset(self::$m_aCacheProfiles[$sCacheKey])) + { + return self::$m_aCacheProfiles[$sCacheKey]; + } + $oNewObj = MetaModel::NewObject("URP_Profiles"); + $oNewObj->Set('name', $sName); + $oNewObj->Set('description', $sDescription); + $iId = $oNewObj->DBInsertNoReload(); + self::$m_aCacheProfiles[$sCacheKey] = $iId; + return $iId; + } + + function GetGrantAsHtml($oUserRights, $sClass, $sAction) + { + $bGrant = $oUserRights->GetProfileActionGrant($this->GetKey(), $sClass, $sAction); + if (is_null($bGrant)) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + elseif ($bGrant) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage) + { + if ($this->GetRawName() == "Administrator") + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + // Note: for sure, we assume that the instance is derived from UserRightsProfile + $oUserRights = UserRights::GetModuleInstance(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) + { + $aStimuli = array(); + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + $bGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); + if ($bGrant === true) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; + } + } + $sStimuli = implode(', ', $aStimuli); + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'r'), + 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'br'), + 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'w'), + 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'bw'), + 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'd'), + 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'bd'), + 'stimuli' => $sStimuli, + ); + } + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); + $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage); + } + } + + public static function GetReadOnlyAttributes() + { + return array('name', 'description'); + } + + + // returns an array of id => array of column => php value(so-called "real value") + public static function GetPredefinedObjects() + { + return ProfilesConfig::GetProfilesValues(); + } + + // Before deleting a profile, + // preserve DB integrity by deleting links to users + protected function OnDelete() + { + // Don't remove admin profile + if ($this->Get('name') === ADMIN_PROFILE_NAME) + { + throw new SecurityException(Dict::Format('UI:Login:Error:AccessAdmin')); + } + + // Note: this may break the rule that says: "a user must have at least ONE profile" ! + $oLnkSet = $this->Get('user_list'); + while($oLnk = $oLnkSet->Fetch()) + { + $oLnk->DBDelete(); + } + } + + /** + * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) + * for the given attribute in the current state of the object + * @param $sAttCode string $sAttCode The code of the attribute + * @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas) + * @param $sTargetState string The target state in which to evalutate the flags, if empty the current state will be used + * @return integer Flags: the binary combination of the flags applicable to this attribute + */ + public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '') + { + $iFlags = parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState); + if (MetaModel::GetConfig()->Get('demo_mode')) + { + $aReasons[] = 'Sorry, profiles are read-only in the demonstration mode!'; + $iFlags |= OPT_ATT_READONLY; + } + return $iFlags; + } +} + + + +class URP_UserProfile extends UserRightsBaseClassGUI +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userprofile", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('userid', 'profileid', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); + } + + public function CheckToDelete(&$oDeletionPlan) + { + if (MetaModel::GetConfig()->Get('demo_mode')) + { + // Users deletion is NOT allowed in demo mode + $oDeletionPlan->AddToDelete($this, null); + $oDeletionPlan->SetDeletionIssues($this, array('deletion not allowed in demo mode.'), true); + $oDeletionPlan->ComputeResults(); + return false; + } + return parent::CheckToDelete($oDeletionPlan); + } + + protected function OnInsert() + { + $this->CheckIfProfileIsAllowed(UR_ACTION_CREATE); + } + + protected function OnUpdate() + { + $this->CheckIfProfileIsAllowed(UR_ACTION_MODIFY); + } + + protected function OnDelete() + { + $this->CheckIfProfileIsAllowed(UR_ACTION_DELETE); + } + + /** + * @param $iActionCode + * + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \SecurityException + */ + protected function CheckIfProfileIsAllowed($iActionCode) + { + // When initializing or admin, we need to let everything pass trough + if (!UserRights::IsLoggedIn() || UserRights::IsAdministrator()) { return; } + + // Only administrators can manage administrators + $iOrigUserId = $this->GetOriginal('userid'); + if (!empty($iOrigUserId)) + { + $oUser = MetaModel::GetObject('User', $iOrigUserId, true, true); + if (UserRights::IsAdministrator($oUser) && !UserRights::IsAdministrator()) + { + throw new SecurityException(Dict::Format('UI:Login:Error:AccessRestricted')); + } + } + $oUser = MetaModel::GetObject('User', $this->Get('userid'), true, true); + if (UserRights::IsAdministrator($oUser) && !UserRights::IsAdministrator()) + { + throw new SecurityException(Dict::Format('UI:Login:Error:AccessRestricted')); + } + if (!UserRights::IsActionAllowed(get_class($this), $iActionCode, DBObjectSet::FromObject($this))) + { + throw new SecurityException(Dict::Format('UI:Error:ObjectCannotBeUpdated')); + } + if (!UserRights::IsAdministrator() && ($this->Get('profile') === ADMIN_PROFILE_NAME)) + { + throw new SecurityException(Dict::Format('UI:Login:Error:AccessAdmin')); + } + } + +} + +class URP_UserOrg extends UserRightsBaseClassGUI +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userorg", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("allowed_org_id", array("targetclass"=>"Organization", "jointype"=> "", "allowed_values"=>null, "sql"=>"allowed_org_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("allowed_org_name", array("allowed_values"=>null, "extkey_attcode"=> 'allowed_org_id', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"reason", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'allowed_org_id', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('allowed_org_id', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'allowed_org_id')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'allowed_org_id')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Org', $this->Get('userlogin'), $this->Get('allowed_org_name')); + } + + + protected function OnInsert() + { + $this->CheckIfOrgIsAllowed(); + } + + protected function OnUpdate() + { + $this->CheckIfOrgIsAllowed(); + } + + protected function OnDelete() + { + $this->CheckIfOrgIsAllowed(); + } + + /** + * @throws \CoreException + */ + protected function CheckIfOrgIsAllowed() + { + if (!UserRights::IsLoggedIn() || UserRights::IsAdministrator()) { return; } + + $oUser = UserRights::GetUserObject(); + $oAddon = UserRights::GetModuleInstance(); + $aOrgs = $oAddon->GetUserOrgs($oUser, ''); + if (count($aOrgs) > 0) + { + $iOrigOrgId = $this->GetOriginal('allowed_org_id'); + if ((!empty($iOrigOrgId) && !in_array($iOrigOrgId, $aOrgs)) || !in_array($this->Get('allowed_org_id'), $aOrgs)) + { + throw new SecurityException(Dict::Format('Class:User/Error:OrganizationNotAllowed')); + } + } + } +} + + + + +class UserRightsProfile extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'r', + UR_ACTION_MODIFY => 'w', + UR_ACTION_DELETE => 'd', + UR_ACTION_BULK_READ => 'br', + UR_ACTION_BULK_MODIFY => 'bw', + UR_ACTION_BULK_DELETE => 'bd', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + CMDBObject::SetTrackInfo('Initialization'); + + $oChange = CMDBObject::GetCurrentChange(); + + $iContactId = 0; + // Support drastic data model changes: no organization class (or not writable)! + if (MetaModel::IsValidClass('Organization') && !MetaModel::IsAbstract('Organization')) + { + $oOrg = new Organization(); + $oOrg->Set('name', 'My Company/Department'); + $oOrg->Set('code', 'SOMECODE'); + $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip security */); + + // Support drastic data model changes: no Person class (or not writable)! + if (MetaModel::IsValidClass('Person') && !MetaModel::IsAbstract('Person')) + { + $oContact = new Person(); + $oContact->Set('name', 'My last name'); + $oContact->Set('first_name', 'My first name'); + if (MetaModel::IsValidAttCode('Person', 'org_id')) + { + $oContact->Set('org_id', $iOrgId); + } + if (MetaModel::IsValidAttCode('Person', 'phone')) + { + $oContact->Set('phone', '+00 000 000 000'); + } + $oContact->Set('email', 'my.email@foo.org'); + $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); + } + } + + + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + if (MetaModel::IsValidAttCode('UserLocal', 'contactid') && ($iContactId != 0)) + { + $oUser->Set('contactid', $iContactId); + } + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + + // Add this user to the very specific 'admin' profile + $oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => ADMIN_PROFILE_NAME), true /*all data*/); + if (is_object($oAdminProfile)) + { + $oUserProfile = new URP_UserProfile(); + //$oUserProfile->Set('userid', $iUserId); + $oUserProfile->Set('profileid', $oAdminProfile->GetKey()); + $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); + //$oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); + $oSet = DBObjectSet::FromObject($oUserProfile); + $oUser->Set('profile_list', $oSet); + } + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + return true; + } + + public function Init() + { + } + + protected $m_aUserOrgs = array(); // userid -> array of orgid + + // Built on demand, could be optimized if necessary (doing a query for each attribute that needs to be read) + protected $m_aObjectActionGrants = array(); + + /** + * Read and cache organizations allowed to the given user + * + * @param $oUser + * @param $sClass (not used here but can be used in overloads) + * + * @return array + * @throws \CoreException + * @throws \Exception + */ + public function GetUserOrgs($oUser, $sClass) + { + $iUser = $oUser->GetKey(); + if (!array_key_exists($iUser, $this->m_aUserOrgs)) + { + $this->m_aUserOrgs[$iUser] = array(); + + $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization'); + if ($sHierarchicalKeyCode !== false) + { + $sUserOrgQuery = 'SELECT UserOrg, Org FROM Organization AS Org JOIN Organization AS Root ON Org.'.$sHierarchicalKeyCode.' BELOW Root.id JOIN URP_UserOrg AS UserOrg ON UserOrg.allowed_org_id = Root.id WHERE UserOrg.userid = :userid'; + $oUserOrgSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sUserOrgQuery), array(), array('userid' => $iUser)); + while ($aRow = $oUserOrgSet->FetchAssoc()) + { + $oOrg = $aRow['Org']; + $this->m_aUserOrgs[$iUser][] = $oOrg->GetKey(); + } + } + else + { + $oSearch = new DBObjectSearch('URP_UserOrg'); + $oSearch->AllowAllData(); + $oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid')); + $oSearch->AddConditionExpression($oCondition); + + $oUserOrgSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser)); + while ($oUserOrg = $oUserOrgSet->Fetch()) + { + $this->m_aUserOrgs[$iUser][] = $oUserOrg->Get('allowed_org_id'); + } + } + } + return $this->m_aUserOrgs[$iUser]; + } + + public function ResetCache() + { + // Loaded by Load cache + $this->m_aUserOrgs = array(); + + // Cache + $this->m_aObjectActionGrants = array(); + } + + public function LoadCache() + { + static $bSharedObjectInitialized = false; + if (!$bSharedObjectInitialized) + { + $bSharedObjectInitialized = true; + if (self::HasSharing()) + { + SharedObject::InitSharedClassProperties(); + } + } + return true; + } + + /** + * @param $oUser User + * @return array + */ + public function IsAdministrator($oUser) + { + // UserRights caches the list for us + return UserRights::HasProfile(ADMIN_PROFILE_NAME, $oUser); + } + + /** + * @param $oUser User + * @return array + */ + public function IsPortalUser($oUser) + { + // UserRights caches the list for us + return UserRights::HasProfile(PORTAL_PROFILE_NAME, $oUser); + } + /** + * @param $oUser User + * @return bool + */ + public function ListProfiles($oUser) + { + $aRet = array(); + $oSearch = new DBObjectSearch('URP_UserProfile'); + $oSearch->AllowAllData(); + $oSearch->NoContextParameters(); + $oSearch->Addcondition('userid', $oUser->GetKey(), '='); + $oProfiles = new DBObjectSet($oSearch); + while ($oUserProfile = $oProfiles->Fetch()) + { + $aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname'); + } + return $aRet; + } + + public function GetSelectFilter($oUser, $sClass, $aSettings = array()) + { + $this->LoadCache(); + + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, UR_ACTION_READ); + if ($aObjectPermissions['permission'] == UR_ALLOWED_NO) + { + return false; + } + + // Determine how to position the objects of this class + // + $sAttCode = self::GetOwnerOrganizationAttCode($sClass); + if (is_null($sAttCode)) + { + // No filtering for this object + return true; + } + // Position the user + // + $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); + if (count($aUserOrgs) == 0) + { + // No org means 'any org' + return true; + } + + return $this->MakeSelectFilter($sClass, $aUserOrgs, $aSettings, $sAttCode); + } + + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetProfileActionGrant($iProfile, $sClass, $sAction) + { + // Note: action is forced lowercase to be more flexible (historical bug) + $sAction = strtolower($sAction); + + return ProfilesConfig::GetProfileActionGrant($iProfile, $sClass, $sAction); + } + + protected function GetUserActionGrant($oUser, $sClass, $iActionCode) + { + $this->LoadCache(); + + // load and cache permissions for the current user on the given class + // + $iUser = $oUser->GetKey(); + $aTest = @$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode]; + if (is_array($aTest)) return $aTest; + + $sAction = self::$m_aActionCodes[$iActionCode]; + + $bStatus = null; + // Call the API of UserRights because it caches the list for us + foreach(UserRights::ListProfiles($oUser) as $iProfile => $oProfile) + { + $bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction); + if (!is_null($bGrant)) + { + if ($bGrant) + { + if (is_null($bStatus)) + { + $bStatus = true; + } + } + else + { + $bStatus = false; + } + } + } + + $iPermission = $bStatus ? UR_ALLOWED_YES : UR_ALLOWED_NO; + + $aRes = array( + 'permission' => $iPermission, + ); + $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes; + return $aRes; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + $this->LoadCache(); + + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); + $iPermission = $aObjectPermissions['permission']; + + // Note: In most cases the object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + + if ($iPermission != UR_ALLOWED_YES) + { + // It is already NO for everyone... that's the final word! + } + elseif ($iActionCode == UR_ACTION_READ) + { + // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading + } + elseif ($iActionCode == UR_ACTION_BULK_READ) + { + // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading + } + elseif ($oInstanceSet) + { + // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading + // We have to answer NO for objects shared for reading purposes + if (self::HasSharing()) + { + $aClassProps = SharedObject::GetSharedClassProperties($sClass); + if ($aClassProps) + { + // This class is shared, GetSelectFilter may allow some objects for read only + // But currently we are checking wether the objects might be written... + // Let's exclude the objects based on the relevant criteria + + $sOrgAttCode = self::GetOwnerOrganizationAttCode($sClass); + if (!is_null($sOrgAttCode)) + { + $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); + if (!is_null($aUserOrgs) && count($aUserOrgs) > 0) + { + $iCountNO = 0; + $iCountYES = 0; + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $iOrg = $oObject->Get($sOrgAttCode); + if (in_array($iOrg, $aUserOrgs)) + { + $iCountYES++; + } + else + { + $iCountNO++; + } + } + if ($iCountNO == 0) + { + $iPermission = UR_ALLOWED_YES; + } + elseif ($iCountYES == 0) + { + $iPermission = UR_ALLOWED_NO; + } + else + { + $iPermission = UR_ALLOWED_DEPENDS; + } + } + } + } + } + } + return $iPermission; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + $this->LoadCache(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); + return $aObjectPermissions['permission']; + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) + { + return ProfilesConfig::GetProfileStimulusGrant($iProfile, $sClass, $sStimulusCode); + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + $this->LoadCache(); + // Note: this code is VERY close to the code of IsActionAllowed() + $iUser = $oUser->GetKey(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $bStatus = null; + // Call the API of UserRights because it caches the list for us + foreach(UserRights::ListProfiles($oUser) as $iProfile => $oProfile) + { + $bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($bGrant)) + { + if ($bGrant) + { + if (is_null($bStatus)) + { + $bStatus = true; + } + } + else + { + $bStatus = false; + } + } + } + + $iPermission = $bStatus ? UR_ALLOWED_YES : UR_ALLOWED_NO; + return $iPermission; + } + + public function FlushPrivileges() + { + $this->ResetCache(); + } + + /** + * Find out which attribute is corresponding the the dimension 'owner org' + * returns null if no such attribute has been found (no filtering should occur) + */ + public static function GetOwnerOrganizationAttCode($sClass) + { + $sAttCode = null; + + $aCallSpec = array($sClass, 'MapContextParam'); + if (($sClass == 'Organization') || is_subclass_of($sClass, 'Organization')) + { + $sAttCode = 'id'; + } + elseif (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter + if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + // Skip silently. The data model checker will tell you something about this... + $sAttCode = null; + } + } + elseif(MetaModel::IsValidAttCode($sClass, 'org_id')) + { + $sAttCode = 'org_id'; + } + + return $sAttCode; + } + + /** + * Determine wether the objects can be shared by the mean of a class SharedObject + **/ + protected static function HasSharing() + { + static $bHasSharing; + if (!isset($bHasSharing)) + { + $bHasSharing = class_exists('SharedObject'); + } + return $bHasSharing; + } +} + + +UserRights::SelectModule('UserRightsProfile'); + +?> diff --git a/addons/userrights/userrightsprofile.db.class.inc.php b/addons/userrights/userrightsprofile.db.class.inc.php index a1293a272..5183fa1f4 100644 --- a/addons/userrights/userrightsprofile.db.class.inc.php +++ b/addons/userrights/userrightsprofile.db.class.inc.php @@ -1,1091 +1,1091 @@ - - -/** - * UserRightsProfile - * User management Module, basing the right on profiles and a matrix (similar to UserRightsMatrix, but profiles and other decorations have been added) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -define('ADMIN_PROFILE_NAME', 'Administrator'); -define('PORTAL_PROFILE_NAME', 'Portal user'); - -class UserRightsBaseClassGUI extends cmdbAbstractObject -{ - // Whenever something changes, reload the privileges - - protected function AfterInsert() - { - UserRights::FlushPrivileges(); - } - - protected function AfterUpdate() - { - UserRights::FlushPrivileges(); - } - - protected function AfterDelete() - { - UserRights::FlushPrivileges(); - } -} - -class UserRightsBaseClass extends DBObject -{ - // Whenever something changes, reload the privileges - - protected function AfterInsert() - { - UserRights::FlushPrivileges(); - } - - protected function AfterUpdate() - { - UserRights::FlushPrivileges(); - } - - protected function AfterDelete() - { - UserRights::FlushPrivileges(); - } -} - - - - -class URP_Profiles extends UserRightsBaseClassGUI -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_profiles", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name', 'description')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array ('name', 'description')); - } - - protected $m_bCheckReservedNames = true; - protected function DisableCheckOnReservedNames() - { - $this->m_bCheckReservedNames = false; - } - - - protected static $m_aActions = array( - UR_ACTION_READ => 'Read', - UR_ACTION_MODIFY => 'Modify', - UR_ACTION_DELETE => 'Delete', - UR_ACTION_BULK_READ => 'Bulk Read', - UR_ACTION_BULK_MODIFY => 'Bulk Modify', - UR_ACTION_BULK_DELETE => 'Bulk Delete', - ); - - protected static $m_aCacheActionGrants = null; - protected static $m_aCacheStimulusGrants = null; - protected static $m_aCacheProfiles = null; - - public static function DoCreateProfile($sName, $sDescription, $bReservedName = false) - { - if (is_null(self::$m_aCacheProfiles)) - { - self::$m_aCacheProfiles = array(); - $oFilterAll = new DBObjectSearch('URP_Profiles'); - $oSet = new DBObjectSet($oFilterAll); - while ($oProfile = $oSet->Fetch()) - { - self::$m_aCacheProfiles[$oProfile->Get('name')] = $oProfile->GetKey(); - } - } - - $sCacheKey = $sName; - if (isset(self::$m_aCacheProfiles[$sCacheKey])) - { - return self::$m_aCacheProfiles[$sCacheKey]; - } - $oNewObj = MetaModel::NewObject("URP_Profiles"); - $oNewObj->Set('name', $sName); - $oNewObj->Set('description', $sDescription); - if ($bReservedName) - { - $oNewObj->DisableCheckOnReservedNames(); - } - $iId = $oNewObj->DBInsertNoReload(); - self::$m_aCacheProfiles[$sCacheKey] = $iId; - return $iId; - } - - public static function DoCreateActionGrant($iProfile, $iAction, $sClass, $bPermission = true) - { - $sAction = self::$m_aActions[$iAction]; - - if (is_null(self::$m_aCacheActionGrants)) - { - self::$m_aCacheActionGrants = array(); - $oFilterAll = new DBObjectSearch('URP_ActionGrant'); - $oSet = new DBObjectSet($oFilterAll); - while ($oGrant = $oSet->Fetch()) - { - self::$m_aCacheActionGrants[$oGrant->Get('profileid').'-'.$oGrant->Get('action').'-'.$oGrant->Get('class')] = $oGrant->GetKey(); - } - } - - $sCacheKey = "$iProfile-$sAction-$sClass"; - if (isset(self::$m_aCacheActionGrants[$sCacheKey])) - { - return self::$m_aCacheActionGrants[$sCacheKey]; - } - - $oNewObj = MetaModel::NewObject("URP_ActionGrant"); - $oNewObj->Set('profileid', $iProfile); - $oNewObj->Set('permission', $bPermission ? 'yes' : 'no'); - $oNewObj->Set('class', $sClass); - $oNewObj->Set('action', $sAction); - $iId = $oNewObj->DBInsertNoReload(); - self::$m_aCacheActionGrants[$sCacheKey] = $iId; - return $iId; - } - - public static function DoCreateStimulusGrant($iProfile, $sStimulusCode, $sClass) - { - if (is_null(self::$m_aCacheStimulusGrants)) - { - self::$m_aCacheStimulusGrants = array(); - $oFilterAll = new DBObjectSearch('URP_StimulusGrant'); - $oSet = new DBObjectSet($oFilterAll); - while ($oGrant = $oSet->Fetch()) - { - self::$m_aCacheStimulusGrants[$oGrant->Get('profileid').'-'.$oGrant->Get('stimulus').'-'.$oGrant->Get('class')] = $oGrant->GetKey(); - } - } - - $sCacheKey = "$iProfile-$sStimulusCode-$sClass"; - if (isset(self::$m_aCacheStimulusGrants[$sCacheKey])) - { - return self::$m_aCacheStimulusGrants[$sCacheKey]; - } - $oNewObj = MetaModel::NewObject("URP_StimulusGrant"); - $oNewObj->Set('profileid', $iProfile); - $oNewObj->Set('permission', 'yes'); - $oNewObj->Set('class', $sClass); - $oNewObj->Set('stimulus', $sStimulusCode); - $iId = $oNewObj->DBInsertNoReload(); - self::$m_aCacheStimulusGrants[$sCacheKey] = $iId; - return $iId; - } - - /* - * Create the built-in Administrator profile with its reserved name - */ - public static function DoCreateAdminProfile() - { - self::DoCreateProfile(ADMIN_PROFILE_NAME, 'Has the rights on everything (bypassing any control)', true /* reserved name */); - } - - /* - * Overload the standard behavior to preserve reserved names - */ - public function DoCheckToWrite() - { - parent::DoCheckToWrite(); - - if ($this->m_bCheckReservedNames) - { - $aChanges = $this->ListChanges(); - if (array_key_exists('name', $aChanges)) - { - if ($this->GetOriginal('name') == ADMIN_PROFILE_NAME) - { - $this->m_aCheckIssues[] = "The name of the Administrator profile must not be changed"; - } - elseif ($this->Get('name') == ADMIN_PROFILE_NAME) - { - $this->m_aCheckIssues[] = ADMIN_PROFILE_NAME." is a reserved to the built-in Administrator profile"; - } - elseif ($this->GetOriginal('name') == PORTAL_PROFILE_NAME) - { - $this->m_aCheckIssues[] = "The name of the User Portal profile must not be changed"; - } - elseif ($this->Get('name') == PORTAL_PROFILE_NAME) - { - $this->m_aCheckIssues[] = PORTAL_PROFILE_NAME." is a reserved to the built-in User Portal profile"; - } - } - } - } - - function GetGrantAsHtml($oUserRights, $sClass, $sAction) - { - $iGrant = $oUserRights->GetProfileActionGrant($this->GetKey(), $sClass, $sAction); - if (!is_null($iGrant)) - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; - } - else - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; - } - } - - function DoShowGrantSumary($oPage) - { - if ($this->GetRawName() == "Administrator") - { - // Looks dirty, but ok that's THE ONE - $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); - return; - } - - // Note: for sure, we assume that the instance is derived from UserRightsProfile - $oUserRights = UserRights::GetModuleInstance(); - - $aDisplayData = array(); - foreach (MetaModel::GetClasses('bizmodel') as $sClass) - { - // Skip non instantiable classes - if (MetaModel::IsAbstract($sClass)) continue; - - $aStimuli = array(); - foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) - { - $oGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); - if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) - { - $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; - } - } - $sStimuli = implode(', ', $aStimuli); - - $aDisplayData[] = array( - 'class' => MetaModel::GetName($sClass), - 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'), - 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Read'), - 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Modify'), - 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Modify'), - 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Delete'), - 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Delete'), - 'stimuli' => $sStimuli, - ); - } - - $aDisplayConfig = array(); - $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); - $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); - $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); - $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); - $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); - $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); - $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); - $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); - $oPage->table($aDisplayConfig, $aDisplayData); - } - - function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, $bEditMode); - if (!$bEditMode) - { - $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); - $this->DoShowGrantSumary($oPage); - } - } -} - - - -class URP_UserProfile extends UserRightsBaseClassGUI -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "userid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_userprofile", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('userid', 'profileid', 'reason')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form - } - - public function GetName() - { - return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); - } -} - -class URP_UserOrg extends UserRightsBaseClassGUI -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "userid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_userorg", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("allowed_org_id", array("targetclass"=>"Organization", "jointype"=> "", "allowed_values"=>null, "sql"=>"allowed_org_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("allowed_org_name", array("allowed_values"=>null, "extkey_attcode"=> 'allowed_org_id', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"reason", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('userid', 'allowed_org_id', 'reason')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('allowed_org_id', 'reason')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('userid', 'allowed_org_id')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('userid', 'allowed_org_id')); // Criteria of the advanced search form - } - - public function GetName() - { - return Dict::Format('UI:UserManagement:LinkBetween_User_And_Org', $this->Get('userlogin'), $this->Get('allowed_org_name')); - } -} - - -class URP_ActionGrant extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "profileid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_grant_actions", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'action')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('class', 'permission', 'action')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the advanced search form - } -} - - -class URP_StimulusGrant extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "profileid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_grant_stimulus", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'stimulus')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('class', 'permission', 'stimulus')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the advanced search form - } -} - - -class URP_AttributeGrant extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "actiongrantid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_grant_attributes", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("actiongrantid", array("targetclass"=>"URP_ActionGrant", "jointype"=> "", "allowed_values"=>null, "sql"=>"actiongrantid", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('actiongrantid', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('attcode')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('actiongrantid', 'attcode')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('actiongrantid', 'attcode')); // Criteria of the advanced search form - } -} - - - - -class UserRightsProfile extends UserRightsAddOnAPI -{ - static public $m_aActionCodes = array( - UR_ACTION_READ => 'read', - UR_ACTION_MODIFY => 'modify', - UR_ACTION_DELETE => 'delete', - UR_ACTION_BULK_READ => 'bulk read', - UR_ACTION_BULK_MODIFY => 'bulk modify', - UR_ACTION_BULK_DELETE => 'bulk delete', - ); - - // Installation: create the very first user - public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') - { - // Create a change to record the history of the User object - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); - - $iContactId = 0; - // Support drastic data model changes: no organization class (or not writable)! - if (MetaModel::IsValidClass('Organization') && !MetaModel::IsAbstract('Organization')) - { - $oOrg = new Organization(); - $oOrg->Set('name', 'My Company/Department'); - $oOrg->Set('code', 'SOMECODE'); - $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip security */); - - // Support drastic data model changes: no Person class (or not writable)! - if (MetaModel::IsValidClass('Person') && !MetaModel::IsAbstract('Person')) - { - $oContact = new Person(); - $oContact->Set('name', 'My last name'); - $oContact->Set('first_name', 'My first name'); - if (MetaModel::IsValidAttCode('Person', 'org_id')) - { - $oContact->Set('org_id', $iOrgId); - } - if (MetaModel::IsValidAttCode('Person', 'phone')) - { - $oContact->Set('phone', '+00 000 000 000'); - } - $oContact->Set('email', 'my.email@foo.org'); - $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); - } - } - - - $oUser = new UserLocal(); - $oUser->Set('login', $sAdminUser); - $oUser->Set('password', $sAdminPwd); - if (MetaModel::IsValidAttCode('UserLocal', 'contactid') && ($iContactId != 0)) - { - $oUser->Set('contactid', $iContactId); - } - $oUser->Set('language', $sLanguage); // Language was chosen during the installation - - // Add this user to the very specific 'admin' profile - $oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => ADMIN_PROFILE_NAME), true /*all data*/); - if (is_object($oAdminProfile)) - { - $oUserProfile = new URP_UserProfile(); - //$oUserProfile->Set('userid', $iUserId); - $oUserProfile->Set('profileid', $oAdminProfile->GetKey()); - $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); - //$oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); - $oSet = DBObjectSet::FromObject($oUserProfile); - $oUser->Set('profile_list', $oSet); - } - $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); - return true; - } - - public function Init() - { - } - - - protected $m_aAdmins = array(); // id -> bool, true if the user has the well-known admin profile - protected $m_aPortalUsers = array(); // id -> bool, true if the user has the well-known portal user profile - - protected $m_aProfiles; // id -> object - protected $m_aUserProfiles = array(); // userid,profileid -> object - protected $m_aUserOrgs = array(); // userid -> array of orgid - - // Those arrays could be completed on demand (inheriting parent permissions) - protected $m_aClassActionGrants = null; // profile, class, action -> actiongrantid (or false if NO, or null/missing if undefined) - protected $m_aClassStimulusGrants = array(); // profile, class, stimulus -> permission - - // Built on demand, could be optimized if necessary (doing a query for each attribute that needs to be read) - protected $m_aObjectActionGrants = array(); - - /** - * Read and cache organizations allowed to the given user - * - * @param $oUser - * @param $sClass (not used here but can be used in overloads) - * - * @return array - * @throws \CoreException - * @throws \Exception - */ - public function GetUserOrgs($oUser, $sClass) - { - $iUser = $oUser->GetKey(); - if (!array_key_exists($iUser, $this->m_aUserOrgs)) - { - $this->m_aUserOrgs[$iUser] = array(); - - $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization'); - if ($sHierarchicalKeyCode !== false) - { - $sUserOrgQuery = 'SELECT UserOrg, Org FROM Organization AS Org JOIN Organization AS Root ON Org.'.$sHierarchicalKeyCode.' BELOW Root.id JOIN URP_UserOrg AS UserOrg ON UserOrg.allowed_org_id = Root.id WHERE UserOrg.userid = :userid'; - $oUserOrgSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sUserOrgQuery), array(), array('userid' => $iUser)); - while ($aRow = $oUserOrgSet->FetchAssoc()) - { - $oOrg = $aRow['Org']; - $this->m_aUserOrgs[$iUser][] = $oOrg->GetKey(); - } - } - else - { - $oSearch = new DBObjectSearch('URP_UserOrg'); - $oSearch->AllowAllData(); - $oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid')); - $oSearch->AddConditionExpression($oCondition); - - $oUserOrgSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser)); - while ($oUserOrg = $oUserOrgSet->Fetch()) - { - $this->m_aUserOrgs[$iUser][] = $oUserOrg->Get('allowed_org_id'); - } - } - } - return $this->m_aUserOrgs[$iUser]; - } - - /** - * Read and cache profiles of the given user - */ - protected function GetUserProfiles($iUser) - { - if (!array_key_exists($iUser, $this->m_aUserProfiles)) - { - $oSearch = new DBObjectSearch('URP_UserProfile'); - $oSearch->AllowAllData(); - $oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid')); - $oSearch->AddConditionExpression($oCondition); - - $this->m_aUserProfiles[$iUser] = array(); - $oUserProfileSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser)); - while ($oUserProfile = $oUserProfileSet->Fetch()) - { - $this->m_aUserProfiles[$iUser][$oUserProfile->Get('profileid')] = $oUserProfile; - } - } - return $this->m_aUserProfiles[$iUser]; - - } - - public function ResetCache() - { - // Loaded by Load cache - $this->m_aProfiles = null; - $this->m_aUserProfiles = array(); - $this->m_aUserOrgs = array(); - - $this->m_aAdmins = array(); - $this->m_aPortalUsers = array(); - - // Loaded on demand (time consuming as compared to the others) - $this->m_aClassActionGrants = null; - $this->m_aClassStimulusGrants = null; - - $this->m_aObjectActionGrants = array(); - } - - // Separate load: this cache is much more time consuming while loading - // Thus it is loaded iif required - // Could be improved by specifying the profile id - public function LoadActionGrantCache() - { - if (!is_null($this->m_aClassActionGrants)) return; - - $oKPI = new ExecutionKPI(); - - $oFilter = DBObjectSearch::FromOQL_AllData("SELECT URP_ActionGrant AS p WHERE p.permission = 'yes'"); - $aGrants = $oFilter->ToDataArray(); - foreach($aGrants as $aGrant) - { - $this->m_aClassActionGrants[$aGrant['profileid']][$aGrant['class']][strtolower($aGrant['action'])] = $aGrant['id']; - } - - $oKPI->ComputeAndReport('Load of action grants'); - } - - public function LoadCache() - { - if (!is_null($this->m_aProfiles)) return; - // Could be loaded in a shared memory (?) - - $oKPI = new ExecutionKPI(); - - if (self::HasSharing()) - { - SharedObject::InitSharedClassProperties(); - } - - $oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles")); - $this->m_aProfiles = array(); - while ($oProfile = $oProfileSet->Fetch()) - { - $this->m_aProfiles[$oProfile->GetKey()] = $oProfile; - } - - $this->m_aClassStimulusGrants = array(); - $oStimGrantSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_StimulusGrant")); - $this->m_aStimGrants = array(); - while ($oStimGrant = $oStimGrantSet->Fetch()) - { - $this->m_aClassStimulusGrants[$oStimGrant->Get('profileid')][$oStimGrant->Get('class')][$oStimGrant->Get('stimulus')] = $oStimGrant; - } - - $oKPI->ComputeAndReport('Load of user management cache (excepted Action Grants)'); - -/* - echo "
\n";
-		print_r($this->m_aProfiles);
-		print_r($this->m_aUserProfiles);
-		print_r($this->m_aUserOrgs);
-		print_r($this->m_aClassActionGrants);
-		print_r($this->m_aClassStimulusGrants);
-		echo "
\n"; -exit; -*/ - - return true; - } - - public function IsAdministrator($oUser) - { - //$this->LoadCache(); - $iUser = $oUser->GetKey(); - if (!array_key_exists($iUser, $this->m_aAdmins)) - { - $bIsAdmin = false; - foreach($this->GetUserProfiles($iUser) as $oUserProfile) - { - if ($oUserProfile->Get('profile') == ADMIN_PROFILE_NAME) - { - $bIsAdmin = true; - break; - } - } - $this->m_aAdmins[$iUser] = $bIsAdmin; - } - return $this->m_aAdmins[$iUser]; - } - - public function IsPortalUser($oUser) - { - //$this->LoadCache(); - $iUser = $oUser->GetKey(); - if (!array_key_exists($iUser, $this->m_aPortalUsers)) - { - $bIsPortalUser = false; - foreach($this->GetUserProfiles($iUser) as $oUserProfile) - { - if ($oUserProfile->Get('profile') == PORTAL_PROFILE_NAME) - { - $bIsPortalUser = true; - break; - } - } - $this->m_aPortalUsers[$iUser] = $bIsPortalUser; - } - return $this->m_aPortalUsers[$iUser]; - } - - public function GetSelectFilter($oUser, $sClass, $aSettings = array()) - { - $this->LoadCache(); - - $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, UR_ACTION_READ); - if ($aObjectPermissions['permission'] == UR_ALLOWED_NO) - { - return false; - } - - // Determine how to position the objects of this class - // - $sAttCode = self::GetOwnerOrganizationAttCode($sClass); - if (is_null($sAttCode)) - { - // No filtering for this object - return true; - } - // Position the user - // - $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); - if (count($aUserOrgs) == 0) - { - // No org means 'any org' - return true; - } - - return $this->MakeSelectFilter($sClass, $aUserOrgs, $aSettings, $sAttCode); - } - - // This verb has been made public to allow the development of an accurate feedback for the current configuration - public function GetProfileActionGrant($iProfile, $sClass, $sAction) - { - $this->LoadActionGrantCache(); - - // Note: action is forced lowercase to be more flexible (historical bug) - $sAction = strtolower($sAction); - if (isset($this->m_aClassActionGrants[$iProfile][$sClass][$sAction])) - { - return $this->m_aClassActionGrants[$iProfile][$sClass][$sAction]; - } - - // Recursively look for the grant record in the class hierarchy - $sParentClass = MetaModel::GetParentPersistentClass($sClass); - if (empty($sParentClass)) - { - $iGrant = null; - } - else - { - // Recursively look for the grant record in the class hierarchy - $iGrant = $this->GetProfileActionGrant($iProfile, $sParentClass, $sAction); - } - - $this->m_aClassActionGrants[$iProfile][$sClass][$sAction] = $iGrant; - return $iGrant; - } - - protected function GetUserActionGrant($oUser, $sClass, $iActionCode) - { - $this->LoadCache(); - - // load and cache permissions for the current user on the given class - // - $iUser = $oUser->GetKey(); - $aTest = @$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode]; - if (is_array($aTest)) return $aTest; - - $sAction = self::$m_aActionCodes[$iActionCode]; - - $iPermission = UR_ALLOWED_NO; - $aAttributes = array(); - foreach($this->GetUserProfiles($iUser) as $iProfile => $oProfile) - { - $iGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction); - if (is_null($iGrant) || !$iGrant) - { - continue; // loop to the next profile - } - else - { - $iPermission = UR_ALLOWED_YES; - - // update the list of attributes with those allowed for this profile - // - $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_AttributeGrant WHERE actiongrantid = :actiongrantid"); - $oSet = new DBObjectSet($oSearch, array(), array('actiongrantid' => $iGrant)); - $aProfileAttributes = $oSet->GetColumnAsArray('attcode', false); - if (count($aProfileAttributes) == 0) - { - $aAllAttributes = array_keys(MetaModel::ListAttributeDefs($sClass)); - $aAttributes = array_merge($aAttributes, $aAllAttributes); - } - else - { - $aAttributes = array_merge($aAttributes, $aProfileAttributes); - } - } - } - - $aRes = array( - 'permission' => $iPermission, - 'attributes' => $aAttributes, - ); - $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes; - return $aRes; - } - - public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) - { - $this->LoadCache(); - - $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); - $iPermission = $aObjectPermissions['permission']; - - // Note: In most cases the object set is ignored because it was interesting to optimize for huge data sets - // and acceptable to consider only the root class of the object set - - if ($iPermission != UR_ALLOWED_YES) - { - // It is already NO for everyone... that's the final word! - } - elseif ($iActionCode == UR_ACTION_READ) - { - // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading - } - elseif ($iActionCode == UR_ACTION_BULK_READ) - { - // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading - } - elseif ($oInstanceSet) - { - // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading - // We have to answer NO for objects shared for reading purposes - if (self::HasSharing()) - { - $aClassProps = SharedObject::GetSharedClassProperties($sClass); - if ($aClassProps) - { - // This class is shared, GetSelectFilter may allow some objects for read only - // But currently we are checking wether the objects might be written... - // Let's exclude the objects based on the relevant criteria - - $sOrgAttCode = self::GetOwnerOrganizationAttCode($sClass); - if (!is_null($sOrgAttCode)) - { - $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); - if (!is_null($aUserOrgs) && count($aUserOrgs) > 0) - { - $iCountNO = 0; - $iCountYES = 0; - $oInstanceSet->Rewind(); - while($oObject = $oInstanceSet->Fetch()) - { - $iOrg = $oObject->Get($sOrgAttCode); - if (in_array($iOrg, $aUserOrgs)) - { - $iCountYES++; - } - else - { - $iCountNO++; - } - } - if ($iCountNO == 0) - { - $iPermission = UR_ALLOWED_YES; - } - elseif ($iCountYES == 0) - { - $iPermission = UR_ALLOWED_NO; - } - else - { - $iPermission = UR_ALLOWED_DEPENDS; - } - } - } - } - } - } - return $iPermission; - } - - public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) - { - $this->LoadCache(); - - // Note: The object set is ignored because it was interesting to optimize for huge data sets - // and acceptable to consider only the root class of the object set - $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); - $aAttributes = $aObjectPermissions['attributes']; - if (in_array($sAttCode, $aAttributes)) - { - return $aObjectPermissions['permission']; - } - else - { - return UR_ALLOWED_NO; - } - } - - // This verb has been made public to allow the development of an accurate feedback for the current configuration - public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) - { - $this->LoadCache(); - - if (isset($this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode])) - { - return $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode]; - } - else - { - return null; - } - } - - public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) - { - $this->LoadCache(); - // Note: this code is VERY close to the code of IsActionAllowed() - $iUser = $oUser->GetKey(); - - // Note: The object set is ignored because it was interesting to optimize for huge data sets - // and acceptable to consider only the root class of the object set - $iPermission = UR_ALLOWED_NO; - foreach($this->GetUserProfiles($iUser) as $iProfile => $oProfile) - { - $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); - if (!is_null($oGrantRecord)) - { - // no need to fetch the record, we've requested the records having permission = 'yes' - $iPermission = UR_ALLOWED_YES; - } - } - return $iPermission; - } - - public function FlushPrivileges() - { - $this->ResetCache(); - } - - /** - * Find out which attribute is corresponding the the dimension 'owner org' - * returns null if no such attribute has been found (no filtering should occur) - */ - public static function GetOwnerOrganizationAttCode($sClass) - { - $sAttCode = null; - - $aCallSpec = array($sClass, 'MapContextParam'); - if (($sClass == 'Organization') || is_subclass_of($sClass, 'Organization')) - { - $sAttCode = 'id'; - } - elseif (is_callable($aCallSpec)) - { - $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter - if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) - { - // Skip silently. The data model checker will tell you something about this... - $sAttCode = null; - } - } - elseif(MetaModel::IsValidAttCode($sClass, 'org_id')) - { - $sAttCode = 'org_id'; - } - - return $sAttCode; - } - - /** - * Determine wether the objects can be shared by the mean of a class SharedObject - **/ - protected static function HasSharing() - { - static $bHasSharing; - if (!isset($bHasSharing)) - { - $bHasSharing = class_exists('SharedObject'); - } - return $bHasSharing; - } -} - - -UserRights::SelectModule('UserRightsProfile'); - -?> + + +/** + * UserRightsProfile + * User management Module, basing the right on profiles and a matrix (similar to UserRightsMatrix, but profiles and other decorations have been added) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +define('ADMIN_PROFILE_NAME', 'Administrator'); +define('PORTAL_PROFILE_NAME', 'Portal user'); + +class UserRightsBaseClassGUI extends cmdbAbstractObject +{ + // Whenever something changes, reload the privileges + + protected function AfterInsert() + { + UserRights::FlushPrivileges(); + } + + protected function AfterUpdate() + { + UserRights::FlushPrivileges(); + } + + protected function AfterDelete() + { + UserRights::FlushPrivileges(); + } +} + +class UserRightsBaseClass extends DBObject +{ + // Whenever something changes, reload the privileges + + protected function AfterInsert() + { + UserRights::FlushPrivileges(); + } + + protected function AfterUpdate() + { + UserRights::FlushPrivileges(); + } + + protected function AfterDelete() + { + UserRights::FlushPrivileges(); + } +} + + + + +class URP_Profiles extends UserRightsBaseClassGUI +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profiles", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name', 'description')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array ('name', 'description')); + } + + protected $m_bCheckReservedNames = true; + protected function DisableCheckOnReservedNames() + { + $this->m_bCheckReservedNames = false; + } + + + protected static $m_aActions = array( + UR_ACTION_READ => 'Read', + UR_ACTION_MODIFY => 'Modify', + UR_ACTION_DELETE => 'Delete', + UR_ACTION_BULK_READ => 'Bulk Read', + UR_ACTION_BULK_MODIFY => 'Bulk Modify', + UR_ACTION_BULK_DELETE => 'Bulk Delete', + ); + + protected static $m_aCacheActionGrants = null; + protected static $m_aCacheStimulusGrants = null; + protected static $m_aCacheProfiles = null; + + public static function DoCreateProfile($sName, $sDescription, $bReservedName = false) + { + if (is_null(self::$m_aCacheProfiles)) + { + self::$m_aCacheProfiles = array(); + $oFilterAll = new DBObjectSearch('URP_Profiles'); + $oSet = new DBObjectSet($oFilterAll); + while ($oProfile = $oSet->Fetch()) + { + self::$m_aCacheProfiles[$oProfile->Get('name')] = $oProfile->GetKey(); + } + } + + $sCacheKey = $sName; + if (isset(self::$m_aCacheProfiles[$sCacheKey])) + { + return self::$m_aCacheProfiles[$sCacheKey]; + } + $oNewObj = MetaModel::NewObject("URP_Profiles"); + $oNewObj->Set('name', $sName); + $oNewObj->Set('description', $sDescription); + if ($bReservedName) + { + $oNewObj->DisableCheckOnReservedNames(); + } + $iId = $oNewObj->DBInsertNoReload(); + self::$m_aCacheProfiles[$sCacheKey] = $iId; + return $iId; + } + + public static function DoCreateActionGrant($iProfile, $iAction, $sClass, $bPermission = true) + { + $sAction = self::$m_aActions[$iAction]; + + if (is_null(self::$m_aCacheActionGrants)) + { + self::$m_aCacheActionGrants = array(); + $oFilterAll = new DBObjectSearch('URP_ActionGrant'); + $oSet = new DBObjectSet($oFilterAll); + while ($oGrant = $oSet->Fetch()) + { + self::$m_aCacheActionGrants[$oGrant->Get('profileid').'-'.$oGrant->Get('action').'-'.$oGrant->Get('class')] = $oGrant->GetKey(); + } + } + + $sCacheKey = "$iProfile-$sAction-$sClass"; + if (isset(self::$m_aCacheActionGrants[$sCacheKey])) + { + return self::$m_aCacheActionGrants[$sCacheKey]; + } + + $oNewObj = MetaModel::NewObject("URP_ActionGrant"); + $oNewObj->Set('profileid', $iProfile); + $oNewObj->Set('permission', $bPermission ? 'yes' : 'no'); + $oNewObj->Set('class', $sClass); + $oNewObj->Set('action', $sAction); + $iId = $oNewObj->DBInsertNoReload(); + self::$m_aCacheActionGrants[$sCacheKey] = $iId; + return $iId; + } + + public static function DoCreateStimulusGrant($iProfile, $sStimulusCode, $sClass) + { + if (is_null(self::$m_aCacheStimulusGrants)) + { + self::$m_aCacheStimulusGrants = array(); + $oFilterAll = new DBObjectSearch('URP_StimulusGrant'); + $oSet = new DBObjectSet($oFilterAll); + while ($oGrant = $oSet->Fetch()) + { + self::$m_aCacheStimulusGrants[$oGrant->Get('profileid').'-'.$oGrant->Get('stimulus').'-'.$oGrant->Get('class')] = $oGrant->GetKey(); + } + } + + $sCacheKey = "$iProfile-$sStimulusCode-$sClass"; + if (isset(self::$m_aCacheStimulusGrants[$sCacheKey])) + { + return self::$m_aCacheStimulusGrants[$sCacheKey]; + } + $oNewObj = MetaModel::NewObject("URP_StimulusGrant"); + $oNewObj->Set('profileid', $iProfile); + $oNewObj->Set('permission', 'yes'); + $oNewObj->Set('class', $sClass); + $oNewObj->Set('stimulus', $sStimulusCode); + $iId = $oNewObj->DBInsertNoReload(); + self::$m_aCacheStimulusGrants[$sCacheKey] = $iId; + return $iId; + } + + /* + * Create the built-in Administrator profile with its reserved name + */ + public static function DoCreateAdminProfile() + { + self::DoCreateProfile(ADMIN_PROFILE_NAME, 'Has the rights on everything (bypassing any control)', true /* reserved name */); + } + + /* + * Overload the standard behavior to preserve reserved names + */ + public function DoCheckToWrite() + { + parent::DoCheckToWrite(); + + if ($this->m_bCheckReservedNames) + { + $aChanges = $this->ListChanges(); + if (array_key_exists('name', $aChanges)) + { + if ($this->GetOriginal('name') == ADMIN_PROFILE_NAME) + { + $this->m_aCheckIssues[] = "The name of the Administrator profile must not be changed"; + } + elseif ($this->Get('name') == ADMIN_PROFILE_NAME) + { + $this->m_aCheckIssues[] = ADMIN_PROFILE_NAME." is a reserved to the built-in Administrator profile"; + } + elseif ($this->GetOriginal('name') == PORTAL_PROFILE_NAME) + { + $this->m_aCheckIssues[] = "The name of the User Portal profile must not be changed"; + } + elseif ($this->Get('name') == PORTAL_PROFILE_NAME) + { + $this->m_aCheckIssues[] = PORTAL_PROFILE_NAME." is a reserved to the built-in User Portal profile"; + } + } + } + } + + function GetGrantAsHtml($oUserRights, $sClass, $sAction) + { + $iGrant = $oUserRights->GetProfileActionGrant($this->GetKey(), $sClass, $sAction); + if (!is_null($iGrant)) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage) + { + if ($this->GetRawName() == "Administrator") + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + // Note: for sure, we assume that the instance is derived from UserRightsProfile + $oUserRights = UserRights::GetModuleInstance(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses('bizmodel') as $sClass) + { + // Skip non instantiable classes + if (MetaModel::IsAbstract($sClass)) continue; + + $aStimuli = array(); + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + $oGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); + if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; + } + } + $sStimuli = implode(', ', $aStimuli); + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'), + 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Read'), + 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Modify'), + 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Modify'), + 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Delete'), + 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Delete'), + 'stimuli' => $sStimuli, + ); + } + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); + $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage); + } + } +} + + + +class URP_UserProfile extends UserRightsBaseClassGUI +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userprofile", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('userid', 'profileid', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); + } +} + +class URP_UserOrg extends UserRightsBaseClassGUI +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userorg", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("allowed_org_id", array("targetclass"=>"Organization", "jointype"=> "", "allowed_values"=>null, "sql"=>"allowed_org_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("allowed_org_name", array("allowed_values"=>null, "extkey_attcode"=> 'allowed_org_id', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"reason", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'allowed_org_id', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('allowed_org_id', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'allowed_org_id')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'allowed_org_id')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Org', $this->Get('userlogin'), $this->Get('allowed_org_name')); + } +} + + +class URP_ActionGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_actions", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'action')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'action')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the advanced search form + } +} + + +class URP_StimulusGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_stimulus", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'stimulus')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'stimulus')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the advanced search form + } +} + + +class URP_AttributeGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "actiongrantid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_attributes", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("actiongrantid", array("targetclass"=>"URP_ActionGrant", "jointype"=> "", "allowed_values"=>null, "sql"=>"actiongrantid", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('actiongrantid', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('attcode')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('actiongrantid', 'attcode')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('actiongrantid', 'attcode')); // Criteria of the advanced search form + } +} + + + + +class UserRightsProfile extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'read', + UR_ACTION_MODIFY => 'modify', + UR_ACTION_DELETE => 'delete', + UR_ACTION_BULK_READ => 'bulk read', + UR_ACTION_BULK_MODIFY => 'bulk modify', + UR_ACTION_BULK_DELETE => 'bulk delete', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + // Create a change to record the history of the User object + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + + $iContactId = 0; + // Support drastic data model changes: no organization class (or not writable)! + if (MetaModel::IsValidClass('Organization') && !MetaModel::IsAbstract('Organization')) + { + $oOrg = new Organization(); + $oOrg->Set('name', 'My Company/Department'); + $oOrg->Set('code', 'SOMECODE'); + $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip security */); + + // Support drastic data model changes: no Person class (or not writable)! + if (MetaModel::IsValidClass('Person') && !MetaModel::IsAbstract('Person')) + { + $oContact = new Person(); + $oContact->Set('name', 'My last name'); + $oContact->Set('first_name', 'My first name'); + if (MetaModel::IsValidAttCode('Person', 'org_id')) + { + $oContact->Set('org_id', $iOrgId); + } + if (MetaModel::IsValidAttCode('Person', 'phone')) + { + $oContact->Set('phone', '+00 000 000 000'); + } + $oContact->Set('email', 'my.email@foo.org'); + $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); + } + } + + + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + if (MetaModel::IsValidAttCode('UserLocal', 'contactid') && ($iContactId != 0)) + { + $oUser->Set('contactid', $iContactId); + } + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + + // Add this user to the very specific 'admin' profile + $oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => ADMIN_PROFILE_NAME), true /*all data*/); + if (is_object($oAdminProfile)) + { + $oUserProfile = new URP_UserProfile(); + //$oUserProfile->Set('userid', $iUserId); + $oUserProfile->Set('profileid', $oAdminProfile->GetKey()); + $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); + //$oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); + $oSet = DBObjectSet::FromObject($oUserProfile); + $oUser->Set('profile_list', $oSet); + } + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + return true; + } + + public function Init() + { + } + + + protected $m_aAdmins = array(); // id -> bool, true if the user has the well-known admin profile + protected $m_aPortalUsers = array(); // id -> bool, true if the user has the well-known portal user profile + + protected $m_aProfiles; // id -> object + protected $m_aUserProfiles = array(); // userid,profileid -> object + protected $m_aUserOrgs = array(); // userid -> array of orgid + + // Those arrays could be completed on demand (inheriting parent permissions) + protected $m_aClassActionGrants = null; // profile, class, action -> actiongrantid (or false if NO, or null/missing if undefined) + protected $m_aClassStimulusGrants = array(); // profile, class, stimulus -> permission + + // Built on demand, could be optimized if necessary (doing a query for each attribute that needs to be read) + protected $m_aObjectActionGrants = array(); + + /** + * Read and cache organizations allowed to the given user + * + * @param $oUser + * @param $sClass (not used here but can be used in overloads) + * + * @return array + * @throws \CoreException + * @throws \Exception + */ + public function GetUserOrgs($oUser, $sClass) + { + $iUser = $oUser->GetKey(); + if (!array_key_exists($iUser, $this->m_aUserOrgs)) + { + $this->m_aUserOrgs[$iUser] = array(); + + $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization'); + if ($sHierarchicalKeyCode !== false) + { + $sUserOrgQuery = 'SELECT UserOrg, Org FROM Organization AS Org JOIN Organization AS Root ON Org.'.$sHierarchicalKeyCode.' BELOW Root.id JOIN URP_UserOrg AS UserOrg ON UserOrg.allowed_org_id = Root.id WHERE UserOrg.userid = :userid'; + $oUserOrgSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sUserOrgQuery), array(), array('userid' => $iUser)); + while ($aRow = $oUserOrgSet->FetchAssoc()) + { + $oOrg = $aRow['Org']; + $this->m_aUserOrgs[$iUser][] = $oOrg->GetKey(); + } + } + else + { + $oSearch = new DBObjectSearch('URP_UserOrg'); + $oSearch->AllowAllData(); + $oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid')); + $oSearch->AddConditionExpression($oCondition); + + $oUserOrgSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser)); + while ($oUserOrg = $oUserOrgSet->Fetch()) + { + $this->m_aUserOrgs[$iUser][] = $oUserOrg->Get('allowed_org_id'); + } + } + } + return $this->m_aUserOrgs[$iUser]; + } + + /** + * Read and cache profiles of the given user + */ + protected function GetUserProfiles($iUser) + { + if (!array_key_exists($iUser, $this->m_aUserProfiles)) + { + $oSearch = new DBObjectSearch('URP_UserProfile'); + $oSearch->AllowAllData(); + $oCondition = new BinaryExpression(new FieldExpression('userid'), '=', new VariableExpression('userid')); + $oSearch->AddConditionExpression($oCondition); + + $this->m_aUserProfiles[$iUser] = array(); + $oUserProfileSet = new DBObjectSet($oSearch, array(), array('userid' => $iUser)); + while ($oUserProfile = $oUserProfileSet->Fetch()) + { + $this->m_aUserProfiles[$iUser][$oUserProfile->Get('profileid')] = $oUserProfile; + } + } + return $this->m_aUserProfiles[$iUser]; + + } + + public function ResetCache() + { + // Loaded by Load cache + $this->m_aProfiles = null; + $this->m_aUserProfiles = array(); + $this->m_aUserOrgs = array(); + + $this->m_aAdmins = array(); + $this->m_aPortalUsers = array(); + + // Loaded on demand (time consuming as compared to the others) + $this->m_aClassActionGrants = null; + $this->m_aClassStimulusGrants = null; + + $this->m_aObjectActionGrants = array(); + } + + // Separate load: this cache is much more time consuming while loading + // Thus it is loaded iif required + // Could be improved by specifying the profile id + public function LoadActionGrantCache() + { + if (!is_null($this->m_aClassActionGrants)) return; + + $oKPI = new ExecutionKPI(); + + $oFilter = DBObjectSearch::FromOQL_AllData("SELECT URP_ActionGrant AS p WHERE p.permission = 'yes'"); + $aGrants = $oFilter->ToDataArray(); + foreach($aGrants as $aGrant) + { + $this->m_aClassActionGrants[$aGrant['profileid']][$aGrant['class']][strtolower($aGrant['action'])] = $aGrant['id']; + } + + $oKPI->ComputeAndReport('Load of action grants'); + } + + public function LoadCache() + { + if (!is_null($this->m_aProfiles)) return; + // Could be loaded in a shared memory (?) + + $oKPI = new ExecutionKPI(); + + if (self::HasSharing()) + { + SharedObject::InitSharedClassProperties(); + } + + $oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles")); + $this->m_aProfiles = array(); + while ($oProfile = $oProfileSet->Fetch()) + { + $this->m_aProfiles[$oProfile->GetKey()] = $oProfile; + } + + $this->m_aClassStimulusGrants = array(); + $oStimGrantSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_StimulusGrant")); + $this->m_aStimGrants = array(); + while ($oStimGrant = $oStimGrantSet->Fetch()) + { + $this->m_aClassStimulusGrants[$oStimGrant->Get('profileid')][$oStimGrant->Get('class')][$oStimGrant->Get('stimulus')] = $oStimGrant; + } + + $oKPI->ComputeAndReport('Load of user management cache (excepted Action Grants)'); + +/* + echo "
\n";
+		print_r($this->m_aProfiles);
+		print_r($this->m_aUserProfiles);
+		print_r($this->m_aUserOrgs);
+		print_r($this->m_aClassActionGrants);
+		print_r($this->m_aClassStimulusGrants);
+		echo "
\n"; +exit; +*/ + + return true; + } + + public function IsAdministrator($oUser) + { + //$this->LoadCache(); + $iUser = $oUser->GetKey(); + if (!array_key_exists($iUser, $this->m_aAdmins)) + { + $bIsAdmin = false; + foreach($this->GetUserProfiles($iUser) as $oUserProfile) + { + if ($oUserProfile->Get('profile') == ADMIN_PROFILE_NAME) + { + $bIsAdmin = true; + break; + } + } + $this->m_aAdmins[$iUser] = $bIsAdmin; + } + return $this->m_aAdmins[$iUser]; + } + + public function IsPortalUser($oUser) + { + //$this->LoadCache(); + $iUser = $oUser->GetKey(); + if (!array_key_exists($iUser, $this->m_aPortalUsers)) + { + $bIsPortalUser = false; + foreach($this->GetUserProfiles($iUser) as $oUserProfile) + { + if ($oUserProfile->Get('profile') == PORTAL_PROFILE_NAME) + { + $bIsPortalUser = true; + break; + } + } + $this->m_aPortalUsers[$iUser] = $bIsPortalUser; + } + return $this->m_aPortalUsers[$iUser]; + } + + public function GetSelectFilter($oUser, $sClass, $aSettings = array()) + { + $this->LoadCache(); + + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, UR_ACTION_READ); + if ($aObjectPermissions['permission'] == UR_ALLOWED_NO) + { + return false; + } + + // Determine how to position the objects of this class + // + $sAttCode = self::GetOwnerOrganizationAttCode($sClass); + if (is_null($sAttCode)) + { + // No filtering for this object + return true; + } + // Position the user + // + $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); + if (count($aUserOrgs) == 0) + { + // No org means 'any org' + return true; + } + + return $this->MakeSelectFilter($sClass, $aUserOrgs, $aSettings, $sAttCode); + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetProfileActionGrant($iProfile, $sClass, $sAction) + { + $this->LoadActionGrantCache(); + + // Note: action is forced lowercase to be more flexible (historical bug) + $sAction = strtolower($sAction); + if (isset($this->m_aClassActionGrants[$iProfile][$sClass][$sAction])) + { + return $this->m_aClassActionGrants[$iProfile][$sClass][$sAction]; + } + + // Recursively look for the grant record in the class hierarchy + $sParentClass = MetaModel::GetParentPersistentClass($sClass); + if (empty($sParentClass)) + { + $iGrant = null; + } + else + { + // Recursively look for the grant record in the class hierarchy + $iGrant = $this->GetProfileActionGrant($iProfile, $sParentClass, $sAction); + } + + $this->m_aClassActionGrants[$iProfile][$sClass][$sAction] = $iGrant; + return $iGrant; + } + + protected function GetUserActionGrant($oUser, $sClass, $iActionCode) + { + $this->LoadCache(); + + // load and cache permissions for the current user on the given class + // + $iUser = $oUser->GetKey(); + $aTest = @$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode]; + if (is_array($aTest)) return $aTest; + + $sAction = self::$m_aActionCodes[$iActionCode]; + + $iPermission = UR_ALLOWED_NO; + $aAttributes = array(); + foreach($this->GetUserProfiles($iUser) as $iProfile => $oProfile) + { + $iGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction); + if (is_null($iGrant) || !$iGrant) + { + continue; // loop to the next profile + } + else + { + $iPermission = UR_ALLOWED_YES; + + // update the list of attributes with those allowed for this profile + // + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_AttributeGrant WHERE actiongrantid = :actiongrantid"); + $oSet = new DBObjectSet($oSearch, array(), array('actiongrantid' => $iGrant)); + $aProfileAttributes = $oSet->GetColumnAsArray('attcode', false); + if (count($aProfileAttributes) == 0) + { + $aAllAttributes = array_keys(MetaModel::ListAttributeDefs($sClass)); + $aAttributes = array_merge($aAttributes, $aAllAttributes); + } + else + { + $aAttributes = array_merge($aAttributes, $aProfileAttributes); + } + } + } + + $aRes = array( + 'permission' => $iPermission, + 'attributes' => $aAttributes, + ); + $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes; + return $aRes; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + $this->LoadCache(); + + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); + $iPermission = $aObjectPermissions['permission']; + + // Note: In most cases the object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + + if ($iPermission != UR_ALLOWED_YES) + { + // It is already NO for everyone... that's the final word! + } + elseif ($iActionCode == UR_ACTION_READ) + { + // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading + } + elseif ($iActionCode == UR_ACTION_BULK_READ) + { + // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading + } + elseif ($oInstanceSet) + { + // We are protected by GetSelectFilter: the object set contains objects allowed or shared for reading + // We have to answer NO for objects shared for reading purposes + if (self::HasSharing()) + { + $aClassProps = SharedObject::GetSharedClassProperties($sClass); + if ($aClassProps) + { + // This class is shared, GetSelectFilter may allow some objects for read only + // But currently we are checking wether the objects might be written... + // Let's exclude the objects based on the relevant criteria + + $sOrgAttCode = self::GetOwnerOrganizationAttCode($sClass); + if (!is_null($sOrgAttCode)) + { + $aUserOrgs = $this->GetUserOrgs($oUser, $sClass); + if (!is_null($aUserOrgs) && count($aUserOrgs) > 0) + { + $iCountNO = 0; + $iCountYES = 0; + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $iOrg = $oObject->Get($sOrgAttCode); + if (in_array($iOrg, $aUserOrgs)) + { + $iCountYES++; + } + else + { + $iCountNO++; + } + } + if ($iCountNO == 0) + { + $iPermission = UR_ALLOWED_YES; + } + elseif ($iCountYES == 0) + { + $iPermission = UR_ALLOWED_NO; + } + else + { + $iPermission = UR_ALLOWED_DEPENDS; + } + } + } + } + } + } + return $iPermission; + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + $this->LoadCache(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $aObjectPermissions = $this->GetUserActionGrant($oUser, $sClass, $iActionCode); + $aAttributes = $aObjectPermissions['attributes']; + if (in_array($sAttCode, $aAttributes)) + { + return $aObjectPermissions['permission']; + } + else + { + return UR_ALLOWED_NO; + } + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) + { + $this->LoadCache(); + + if (isset($this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode])) + { + return $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode]; + } + else + { + return null; + } + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + $this->LoadCache(); + // Note: this code is VERY close to the code of IsActionAllowed() + $iUser = $oUser->GetKey(); + + // Note: The object set is ignored because it was interesting to optimize for huge data sets + // and acceptable to consider only the root class of the object set + $iPermission = UR_ALLOWED_NO; + foreach($this->GetUserProfiles($iUser) as $iProfile => $oProfile) + { + $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($oGrantRecord)) + { + // no need to fetch the record, we've requested the records having permission = 'yes' + $iPermission = UR_ALLOWED_YES; + } + } + return $iPermission; + } + + public function FlushPrivileges() + { + $this->ResetCache(); + } + + /** + * Find out which attribute is corresponding the the dimension 'owner org' + * returns null if no such attribute has been found (no filtering should occur) + */ + public static function GetOwnerOrganizationAttCode($sClass) + { + $sAttCode = null; + + $aCallSpec = array($sClass, 'MapContextParam'); + if (($sClass == 'Organization') || is_subclass_of($sClass, 'Organization')) + { + $sAttCode = 'id'; + } + elseif (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter + if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + // Skip silently. The data model checker will tell you something about this... + $sAttCode = null; + } + } + elseif(MetaModel::IsValidAttCode($sClass, 'org_id')) + { + $sAttCode = 'org_id'; + } + + return $sAttCode; + } + + /** + * Determine wether the objects can be shared by the mean of a class SharedObject + **/ + protected static function HasSharing() + { + static $bHasSharing; + if (!isset($bHasSharing)) + { + $bHasSharing = class_exists('SharedObject'); + } + return $bHasSharing; + } +} + + +UserRights::SelectModule('UserRightsProfile'); + +?> diff --git a/addons/userrights/userrightsprojection.class.inc.php b/addons/userrights/userrightsprojection.class.inc.php index 4ff9aeff3..4e3fc0fa2 100644 --- a/addons/userrights/userrightsprojection.class.inc.php +++ b/addons/userrights/userrightsprojection.class.inc.php @@ -1,1253 +1,1253 @@ - - -/** - * UserRightsProjection - * User management Module, basing the right on profiles and a matrix (similar to UserRightsProfile, but enhanced with dimensions and projection of classes and profile over the dimensions) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -define('ADMIN_PROFILE_ID', 1); - -class UserRightsBaseClass extends cmdbAbstractObject -{ - // Whenever something changes, reload the privileges - - // Whenever something changes, reload the privileges - - protected function AfterInsert() - { - UserRights::FlushPrivileges(); - } - - protected function AfterUpdate() - { - UserRights::FlushPrivileges(); - } - - protected function AfterDelete() - { - UserRights::FlushPrivileges(); - } -} - - - - -class URP_Profiles extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_profiles", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name', 'description')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array ('name', 'description')); - } - - function GetGrantAsHtml($oUserRights, $sClass, $sAction) - { - $oGrant = $oUserRights->GetClassActionGrant($this->GetKey(), $sClass, $sAction); - if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; - } - else - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; - } - } - - function DoShowGrantSumary($oPage) - { - if ($this->GetRawName() == "Administrator") - { - // Looks dirty, but ok that's THE ONE - $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); - return; - } - - // Note: for sure, we assume that the instance is derived from UserRightsProjection - $oUserRights = UserRights::GetModuleInstance(); - - $aDisplayData = array(); - foreach (MetaModel::GetClasses('bizmodel') as $sClass) - { - // Skip non instantiable classes - if (MetaModel::IsAbstract($sClass)) continue; - - $aStimuli = array(); - foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) - { - $oGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); - if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) - { - $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; - } - } - $sStimuli = implode(', ', $aStimuli); - - $aDisplayData[] = array( - 'class' => MetaModel::GetName($sClass), - 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'), - 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Read'), - 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Modify'), - 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Modify'), - 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Delete'), - 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Delete'), - 'stimuli' => $sStimuli, - ); - } - - $aDisplayConfig = array(); - $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); - $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); - $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); - $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); - $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); - $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); - $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); - $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); - $oPage->table($aDisplayConfig, $aDisplayData); - } - - function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, $bEditMode); - if (!$bEditMode) - { - $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); - $this->DoShowGrantSumary($oPage); - } - } -} - - -class URP_Dimensions extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_dimensions", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeClass("type", array("class_category"=>"bizmodel", "more_values"=>"String,Integer", "sql"=>"type", "default_value"=>'String', "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'type')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - public function CheckProjectionSpec($oProjectionSpec, $sProjectedClass) - { - $sExpression = $oProjectionSpec->Get('value'); - $sAttribute = $oProjectionSpec->Get('attribute'); - - // Shortcut: "any value" or "no value" means no projection - if (empty($sExpression)) return; - if ($sExpression == '') return; - - // 1st - compute the data type for the dimension - // - $sType = $this->Get('type'); - if (MetaModel::IsValidClass($sType)) - { - $sExpectedType = $sType; - } - else - { - $sExpectedType = '_scalar_'; - } - - // 2nd - compute the data type for the projection - // - $sTargetClass = ''; - if (($sExpression == '') || ($sExpression == '')) - { - $sTargetClass = $sProjectedClass; - } - elseif ($sExpression == '') - { - $sTargetClass = ''; - } - else - { - // Evaluate wether it is a constant or not - try - { - $oObjectSearch = DBObjectSearch::FromOQL_AllData($sExpression); - - $sTargetClass = $oObjectSearch->GetClass(); - } - catch (OqlException $e) - { - } - } - - if (empty($sTargetClass)) - { - $sFoundType = '_void_'; - } - else - { - if (empty($sAttribute)) - { - $sFoundType = $sTargetClass; - } - else - { - if (!MetaModel::IsValidAttCode($sTargetClass, $sAttribute)) - { - throw new CoreException('Unkown attribute code in projection specification', array('found' => $sAttribute, 'expecting' => MetaModel::GetAttributesList($sTargetClass), 'class' => $sTargetClass, 'projection' => $oProjectionSpec)); - } - $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttribute); - if ($oAttDef->IsExternalKey()) - { - $sFoundType = $oAttDef->GetTargetClass(); - } - else - { - $sFoundType = '_scalar_'; - } - } - } - - // Compare the dimension type and projection type - if (($sFoundType != '_void_') && ($sFoundType != $sExpectedType)) - { - throw new CoreException('Wrong type in projection specification', array('found' => $sFoundType, 'expecting' => $sExpectedType, 'expression' => $sExpression, 'attribute' => $sAttribute, 'projection' => $oProjectionSpec)); - } - } -} - - -class URP_UserProfile extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "userid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_userprofile", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('profileid', 'reason')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form - } - - public function GetName() - { - return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); - } -} - - -class URP_ProfileProjection extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "profileid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_profileprojection", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("dimensionid", array("targetclass"=>"URP_Dimensions", "jointype"=> "", "allowed_values"=>null, "sql"=>"dimensionid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("dimension", array("allowed_values"=>null, "extkey_attcode"=> 'dimensionid', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("attribute", array("allowed_values"=>null, "sql"=>"attribute", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('dimensionid', 'profileid', 'value', 'attribute')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('profileid', 'value', 'attribute')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('dimensionid', 'profileid')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('dimensionid', 'profileid')); // Criteria of the advanced search form - } - - protected $m_aUserProjections; // cache - - public function ProjectUser(User $oUser) - { - if (is_array($this->m_aUserProjections)) - { - // Hit! - return $this->m_aUserProjections; - } - - $sExpr = $this->Get('value'); - if ($sExpr == '') - { - $sColumn = $this->Get('attribute'); - if (empty($sColumn)) - { - $aRes = array($oUser->GetKey()); - } - else - { - $aRes = array($oUser->Get($sColumn)); - } - - } - elseif (($sExpr == '') || ($sExpr == '')) - { - $aRes = null; - } - elseif (strtolower(substr($sExpr, 0, 6)) == 'select') - { - $sColumn = $this->Get('attribute'); - // SELECT... - $oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/); - $aRes = $oValueSetDef->GetValues(array('user' => $oUser), ''); - } - else - { - // Constant value(s) - $aRes = explode(';', trim($sExpr)); - } - $this->m_aUserProjections = $aRes; - return $aRes; - } -} - - -class URP_ClassProjection extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "dimensionid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_classprojection", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("dimensionid", array("targetclass"=>"URP_Dimensions", "jointype"=> "", "allowed_values"=>null, "sql"=>"dimensionid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("dimension", array("allowed_values"=>null, "extkey_attcode"=> 'dimensionid', "target_attcode"=>"name"))); - - MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("attribute", array("allowed_values"=>null, "sql"=>"attribute", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('dimensionid', 'class', 'value', 'attribute')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('class', 'value', 'attribute')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('dimensionid', 'class')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('dimensionid', 'class')); // Criteria of the advanced search form - } - - public function ProjectObject($oObject) - { - $sExpr = $this->Get('value'); - if ($sExpr == '') - { - $sColumn = $this->Get('attribute'); - if (empty($sColumn)) - { - $aRes = array($oObject->GetKey()); - } - else - { - $aRes = array($oObject->Get($sColumn)); - } - - } - elseif (($sExpr == '') || ($sExpr == '')) - { - $aRes = null; - } - elseif (strtolower(substr($sExpr, 0, 6)) == 'select') - { - $sColumn = $this->Get('attribute'); - // SELECT... - $oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/); - $aRes = $oValueSetDef->GetValues(array('this' => $oObject), ''); - } - else - { - // Constant value(s) - $aRes = explode(';', trim($sExpr)); - } - return $aRes; - } - -} - - -class URP_ActionGrant extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "profileid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_grant_actions", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'action')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('class', 'permission', 'action')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the advanced search form - } -} - - -class URP_StimulusGrant extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "profileid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_grant_stimulus", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) - MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); - MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'stimulus')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('class', 'permission', 'stimulus')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the advanced search form - } -} - - -class URP_AttributeGrant extends UserRightsBaseClass -{ - public static function Init() - { - $aParams = array - ( - "category" => "addon/userrights", - "key_type" => "autoincrement", - "name_attcode" => "actiongrantid", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_urp_grant_attributes", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("actiongrantid", array("targetclass"=>"URP_ActionGrant", "jointype"=> "", "allowed_values"=>null, "sql"=>"actiongrantid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('actiongrantid', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('attcode')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('actiongrantid', 'attcode')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('actiongrantid', 'attcode')); // Criteria of the advanced search form - } -} - - - - -class UserRightsProjection extends UserRightsAddOnAPI -{ - static public $m_aActionCodes = array( - UR_ACTION_READ => 'read', - UR_ACTION_MODIFY => 'modify', - UR_ACTION_DELETE => 'delete', - UR_ACTION_BULK_READ => 'bulk read', - UR_ACTION_BULK_MODIFY => 'bulk modify', - UR_ACTION_BULK_DELETE => 'bulk delete', - ); - - // Installation: create the very first user - public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') - { - // Create a change to record the history of the User object - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); - - $oOrg = new Organization(); - $oOrg->Set('name', 'My Company/Department'); - $oOrg->Set('code', 'SOMECODE'); -// $oOrg->Set('status', 'implementation'); - //$oOrg->Set('parent_id', xxx); - $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip strong security */); - - // Location : optional - //$oLocation = new bizLocation(); - //$oLocation->Set('name', 'MyOffice'); - //$oLocation->Set('status', 'implementation'); - //$oLocation->Set('org_id', $iOrgId); - //$oLocation->Set('severity', 'high'); - //$oLocation->Set('address', 'my building in my city'); - //$oLocation->Set('country', 'my country'); - //$oLocation->Set('parent_location_id', xxx); - //$iLocationId = $oLocation->DBInsertNoReload(); - - $oContact = new Person(); - $oContact->Set('name', 'My last name'); - $oContact->Set('first_name', 'My first name'); - //$oContact->Set('status', 'available'); - $oContact->Set('org_id', $iOrgId); - $oContact->Set('email', 'my.email@foo.org'); - //$oContact->Set('phone', ''); - //$oContact->Set('location_id', $iLocationId); - //$oContact->Set('employee_number', ''); - $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); - - $oUser = new UserLocal(); - $oUser->Set('login', $sAdminUser); - $oUser->Set('password', $sAdminPwd); - $oUser->Set('contactid', $iContactId); - $oUser->Set('language', $sLanguage); // Language was chosen during the installation - $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); - - // Add this user to the very specific 'admin' profile - $oUserProfile = new URP_UserProfile(); - $oUserProfile->Set('userid', $iUserId); - $oUserProfile->Set('profileid', ADMIN_PROFILE_ID); - $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); - $oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); - return true; - } - - public function IsAdministrator($oUser) - { - if (in_array($oUser->GetKey(), $this->m_aAdmins)) - { - return true; - } - else - { - return false; - } - } - - public function IsPortalUser($oUser) - { - return true; - // See implementation of userrightsprofile - } - - public function Init() - { - // CacheData to be invoked in a module extension - //MetaModel::RegisterPlugin('userrights', 'ACbyProfile', array($this, 'CacheData')); - } - - protected $m_aDimensions = array(); // id -> object - protected $m_aClassProj = array(); // class,dimensionid -> object - protected $m_aProfiles = array(); // id -> object - protected $m_aUserProfiles = array(); // userid,profileid -> object - protected $m_aProPro = array(); // profileid,dimensionid -> object - - protected $m_aAdmins = array(); // id of users being linked to the well-known admin profile - - protected $m_aClassActionGrants = array(); // profile, class, action -> permission - protected $m_aClassStimulusGrants = array(); // profile, class, stimulus -> permission - protected $m_aObjectActionGrants = array(); // userid, class, id, action -> permission, list of attributes - - public function CacheData() - { - // Could be loaded in a shared memory (?) - - $oDimensionSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Dimensions")); - $this->m_aDimensions = array(); - while ($oDimension = $oDimensionSet->Fetch()) - { - $this->m_aDimensions[$oDimension->GetKey()] = $oDimension; - } - - $oClassProjSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ClassProjection")); - $this->m_aClassProjs = array(); - while ($oClassProj = $oClassProjSet->Fetch()) - { - $this->m_aClassProjs[$oClassProj->Get('class')][$oClassProj->Get('dimensionid')] = $oClassProj; - } - - $oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles")); - $this->m_aProfiles = array(); - while ($oProfile = $oProfileSet->Fetch()) - { - $this->m_aProfiles[$oProfile->GetKey()] = $oProfile; - } - - $oUserProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_UserProfile")); - $this->m_aUserProfiles = array(); - $this->m_aAdmins = array(); - while ($oUserProfile = $oUserProfileSet->Fetch()) - { - $this->m_aUserProfiles[$oUserProfile->Get('userid')][$oUserProfile->Get('profileid')] = $oUserProfile; - if ($oUserProfile->Get('profileid') == ADMIN_PROFILE_ID) - { - $this->m_aAdmins[] = $oUserProfile->Get('userid'); - } - } - - $oProProSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ProfileProjection")); - $this->m_aProPros = array(); - while ($oProPro = $oProProSet->Fetch()) - { - $this->m_aProPros[$oProPro->Get('profileid')][$oProPro->Get('dimensionid')] = $oProPro; - } - -/* - echo "
\n";
-		print_r($this->m_aDimensions);
-		print_r($this->m_aClassProjs);
-		print_r($this->m_aProfiles);
-		print_r($this->m_aUserProfiles);
-		print_r($this->m_aProPros);
-		echo "
\n"; -exit; -*/ - - return true; - } - - public function GetSelectFilter($oUser, $sClass, $aSettings = array()) - { - $aConditions = array(); - foreach ($this->m_aDimensions as $iDimension => $oDimension) - { - $oClassProj = @$this->m_aClassProjs[$sClass][$iDimension]; - if (is_null($oClassProj)) - { - // Authorize any for this dimension, then no additional criteria is required - continue; - } - - // 1 - Get class projection info - // - $oExpression = null; - $sExpr = $oClassProj->Get('value'); - if ($sExpr == '') - { - $sColumn = $oClassProj->Get('attribute'); - if (empty($sColumn)) - { - $oExpression = new FieldExpression('id', $sClass); - } - else - { - $oExpression = new FieldExpression($sColumn, $sClass); - } - } - elseif (($sExpr == '') || ($sExpr == '')) - { - // Authorize any for this dimension, then no additional criteria is required - continue; - } - elseif (strtolower(substr($sExpr, 0, 6)) == 'select') - { - throw new CoreException('Sorry, projections by the mean of OQL are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr)); - } - else - { - // Constant value(s) - // unsupported - throw new CoreException('Sorry, constant projections are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr)); -// $aRes = explode(';', trim($sExpr)); - } - - // 2 - Get profile projection info and use it if needed - // - $aProjections = self::GetReadableProjectionsByDim($oUser, $sClass, $oDimension); - if (is_null($aProjections)) - { - // Authorize any for this dimension, then no additional criteria is required - continue; - } - elseif (count($aProjections) == 0) - { - // Authorize none, then exit as quickly as possible - return false; - } - else - { - // Authorize the given set of values - $oListExpr = ListExpression::FromScalars($aProjections); - $oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr); - $aConditions[] = $oCondition; - } - } - - if (count($aConditions) == 0) - { - // allow all - return true; - } - else - { - $oFilter = new DBObjectSearch($sClass); - foreach($aConditions as $oCondition) - { - $oFilter->AddConditionExpression($oCondition); - } - //return true; - return $oFilter; - } - } - - // This verb has been made public to allow the development of an accurate feedback for the current configuration - public function GetClassActionGrant($iProfile, $sClass, $sAction) - { - if (isset($this->m_aClassActionGrants[$iProfile][$sClass][$sAction])) - { - return $this->m_aClassActionGrants[$iProfile][$sClass][$sAction]; - } - - // Get the permission for this profile/class/action - $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_ActionGrant WHERE class = :class AND action = :action AND profileid = :profile AND permission = 'yes'"); - $oSet = new DBObjectSet($oSearch, array(), array('class'=>$sClass, 'action'=>$sAction, 'profile'=>$iProfile)); - if ($oSet->Count() >= 1) - { - $oGrantRecord = $oSet->Fetch(); - } - else - { - $sParentClass = MetaModel::GetParentPersistentClass($sClass); - if (empty($sParentClass)) - { - $oGrantRecord = null; - } - else - { - $oGrantRecord = $this->GetClassActionGrant($iProfile, $sParentClass, $sAction); - } - } - - $this->m_aClassActionGrants[$iProfile][$sClass][$sAction] = $oGrantRecord; - return $oGrantRecord; - } - - protected function GetObjectActionGrant($oUser, $sClass, $iActionCode, /*DBObject*/ $oObject = null) - { - if(is_null($oObject)) - { - $iObjectRef = -999; - } - else - { - $iObjectRef = $oObject->GetKey(); - } - // load and cache permissions for the current user on the given object - // - $aTest = @$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode]; - if (is_array($aTest)) return $aTest; - - $sAction = self::$m_aActionCodes[$iActionCode]; - - $iInstancePermission = UR_ALLOWED_NO; - $aAttributes = array(); - foreach($this->GetMatchingProfiles($oUser, $sClass, $oObject) as $iProfile) - { - $oGrantRecord = $this->GetClassActionGrant($iProfile, $sClass, $sAction); - if (is_null($oGrantRecord)) - { - continue; // loop to the next profile - } - else - { - $iInstancePermission = UR_ALLOWED_YES; - - // update the list of attributes with those allowed for this profile - // - $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_AttributeGrant WHERE actiongrantid = :actiongrantid"); - $oSet = new DBObjectSet($oSearch, array(), array('actiongrantid' => $oGrantRecord->GetKey())); - $aProfileAttributes = $oSet->GetColumnAsArray('attcode', false); - if (count($aProfileAttributes) == 0) - { - $aAllAttributes = array_keys(MetaModel::ListAttributeDefs($sClass)); - $aAttributes = array_merge($aAttributes, $aAllAttributes); - } - else - { - $aAttributes = array_merge($aAttributes, $aProfileAttributes); - } - } - } - - $aRes = array( - 'permission' => $iInstancePermission, - 'attributes' => $aAttributes, - ); - $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode] = $aRes; - return $aRes; - } - - public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) - { - if (is_null($oInstanceSet)) - { - $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode); - return $aObjectPermissions['permission']; - } - - $oInstanceSet->Rewind(); - while($oObject = $oInstanceSet->Fetch()) - { - $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode, $oObject); - - $iInstancePermission = $aObjectPermissions['permission']; - if (isset($iGlobalPermission)) - { - if ($iInstancePermission != $iGlobalPermission) - { - $iGlobalPermission = UR_ALLOWED_DEPENDS; - break; - } - } - else - { - $iGlobalPermission = $iInstancePermission; - } - } - $oInstanceSet->Rewind(); - - if (isset($iGlobalPermission)) - { - return $iGlobalPermission; - } - else - { - return UR_ALLOWED_NO; - } - } - - public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) - { - if (is_null($oInstanceSet)) - { - $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode); - $aAttributes = $aObjectPermissions['attributes']; - if (in_array($sAttCode, $aAttributes)) - { - return $aObjectPermissions['permission']; - } - else - { - return UR_ALLOWED_NO; - } - } - - $oInstanceSet->Rewind(); - while($oObject = $oInstanceSet->Fetch()) - { - $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode, $oObject); - - $aAttributes = $aObjectPermissions['attributes']; - if (in_array($sAttCode, $aAttributes)) - { - $iInstancePermission = $aObjectPermissions['permission']; - } - else - { - $iInstancePermission = UR_ALLOWED_NO; - } - - if (isset($iGlobalPermission)) - { - if ($iInstancePermission != $iGlobalPermission) - { - $iGlobalPermission = UR_ALLOWED_DEPENDS; - } - } - else - { - $iGlobalPermission = $iInstancePermission; - } - } - $oInstanceSet->Rewind(); - - if (isset($iGlobalPermission)) - { - return $iGlobalPermission; - } - else - { - return UR_ALLOWED_NO; - } - } - - // This verb has been made public to allow the development of an accurate feedback for the current configuration - public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) - { - if (isset($this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode])) - { - return $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode]; - } - - // Get the permission for this profile/class/stimulus - $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_StimulusGrant WHERE class = :class AND stimulus = :stimulus AND profileid = :profile AND permission = 'yes'"); - $oSet = new DBObjectSet($oSearch, array(), array('class'=>$sClass, 'stimulus'=>$sStimulusCode, 'profile'=>$iProfile)); - if ($oSet->Count() >= 1) - { - $oGrantRecord = $oSet->Fetch(); - } - else - { - $oGrantRecord = null; - } - - $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode] = $oGrantRecord; - return $oGrantRecord; - } - - public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) - { - // Note: this code is VERY close to the code of IsActionAllowed() - - if (is_null($oInstanceSet)) - { - $iInstancePermission = UR_ALLOWED_NO; - foreach($this->GetMatchingProfiles($oUser, $sClass) as $iProfile) - { - $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); - if (!is_null($oGrantRecord)) - { - // no need to fetch the record, we've requested the records having permission = 'yes' - $iInstancePermission = UR_ALLOWED_YES; - } - } - return $iInstancePermission; - } - - $oInstanceSet->Rewind(); - while($oObject = $oInstanceSet->Fetch()) - { - $iInstancePermission = UR_ALLOWED_NO; - foreach($this->GetMatchingProfiles($oUser, $sClass, $oObject) as $iProfile) - { - $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); - if (!is_null($oGrantRecord)) - { - // no need to fetch the record, we've requested the records having permission = 'yes' - $iInstancePermission = UR_ALLOWED_YES; - } - } - if (isset($iGlobalPermission)) - { - if ($iInstancePermission != $iGlobalPermission) - { - $iGlobalPermission = UR_ALLOWED_DEPENDS; - } - } - else - { - $iGlobalPermission = $iInstancePermission; - } - } - $oInstanceSet->Rewind(); - - if (isset($iGlobalPermission)) - { - return $iGlobalPermission; - } - else - { - return UR_ALLOWED_NO; - } - } - - // Copied from GetMatchingProfilesByDim() - // adapted to the optimized implementation of GetSelectFilter() - // Note: shares the cache m_aProPros with GetMatchingProfilesByDim() - // Returns null if any object is readable - // an array of allowed projections otherwise (could be an empty array if none is allowed) - protected function GetReadableProjectionsByDim($oUser, $sClass, $oDimension) - { - // - // Given a dimension, lists the values for which the user will be allowed to read the objects - // - $iUser = $oUser->GetKey(); - $iDimension = $oDimension->GetKey(); - - $aRes = array(); - if (array_key_exists($iUser, $this->m_aUserProfiles)) - { - foreach ($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) - { - // user projection to be cached on a given page ! - if (!isset($this->m_aProPros[$iProfile][$iDimension])) - { - // No projection for a given profile: default to 'any' - return null; - } - - $aUserProjection = $this->m_aProPros[$iProfile][$iDimension]->ProjectUser($oUser); - if (is_null($aUserProjection)) - { - // No projection for a given profile: default to 'any' - return null; - } - $aRes = array_unique(array_merge($aRes, $aUserProjection)); - } - } - return $aRes; - } - - // Note: shares the cache m_aProPros with GetReadableProjectionsByDim() - protected function GetMatchingProfilesByDim($oUser, $oObject, $oDimension) - { - // - // List profiles for which the user projection overlaps the object projection in the given dimension - // - $iUser = $oUser->GetKey(); - $sClass = get_class($oObject); - $iPKey = $oObject->GetKey(); - $iDimension = $oDimension->GetKey(); - - if (isset($this->m_aClassProjs[$sClass][$iDimension])) - { - $aObjectProjection = $this->m_aClassProjs[$sClass][$iDimension]->ProjectObject($oObject); - } - else - { - // No projection for a given class: default to 'any' - $aObjectProjection = null; - } - - $aRes = array(); - if (array_key_exists($iUser, $this->m_aUserProfiles)) - { - foreach ($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) - { - if (is_null($aObjectProjection)) - { - $aRes[] = $iProfile; - } - else - { - // user projection to be cached on a given page ! - if (isset($this->m_aProPros[$iProfile][$iDimension])) - { - $aUserProjection = $this->m_aProPros[$iProfile][$iDimension]->ProjectUser($oUser); - } - else - { - // No projection for a given profile: default to 'any' - $aUserProjection = null; - } - - if (is_null($aUserProjection)) - { - $aRes[] = $iProfile; - } - else - { - $aMatchingValues = array_intersect($aObjectProjection, $aUserProjection); - if (count($aMatchingValues) > 0) - { - $aRes[] = $iProfile; - } - } - } - } - } - return $aRes; - } - - protected $m_aMatchingProfiles = array(); // cache of the matching profiles for a given user/object - - protected function GetMatchingProfiles($oUser, $sClass, /*DBObject*/ $oObject = null) - { - $iUser = $oUser->GetKey(); - - if(is_null($oObject)) - { - $iObjectRef = -999; - } - else - { - $iObjectRef = $oObject->GetKey(); - } - - // - // List profiles for which the user projection overlaps the object projection in each and every dimension - // Caches the result - // - $aTest = @$this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef]; - if (is_array($aTest)) - { - return $aTest; - } - - if (is_null($oObject)) - { - if (array_key_exists($iUser, $this->m_aUserProfiles)) - { - $aRes = array_keys($this->m_aUserProfiles[$iUser]); - } - else - { - // no profile has been defined for this user - $aRes = array(); - } - } - else - { - $aProfileRes = array(); - foreach ($this->m_aDimensions as $iDimension => $oDimension) - { - foreach ($this->GetMatchingProfilesByDim($oUser, $oObject, $oDimension) as $iProfile) - { - @$aProfileRes[$iProfile] += 1; - } - } - - $aRes = array(); - $iDimCount = count($this->m_aDimensions); - foreach ($aProfileRes as $iProfile => $iMatches) - { - if ($iMatches == $iDimCount) - { - $aRes[] = $iProfile; - } - } - } - - // store into the cache - $this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef] = $aRes; - return $aRes; - } - - public function FlushPrivileges() - { - $this->CacheData(); - } -} - - -UserRights::SelectModule('UserRightsProjection'); - -?> + + +/** + * UserRightsProjection + * User management Module, basing the right on profiles and a matrix (similar to UserRightsProfile, but enhanced with dimensions and projection of classes and profile over the dimensions) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +define('ADMIN_PROFILE_ID', 1); + +class UserRightsBaseClass extends cmdbAbstractObject +{ + // Whenever something changes, reload the privileges + + // Whenever something changes, reload the privileges + + protected function AfterInsert() + { + UserRights::FlushPrivileges(); + } + + protected function AfterUpdate() + { + UserRights::FlushPrivileges(); + } + + protected function AfterDelete() + { + UserRights::FlushPrivileges(); + } +} + + + + +class URP_Profiles extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profiles", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("user_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"profileid", "ext_key_to_remote"=>"userid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'user_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name', 'description')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array ('name', 'description')); + } + + function GetGrantAsHtml($oUserRights, $sClass, $sAction) + { + $oGrant = $oUserRights->GetClassActionGrant($this->GetKey(), $sClass, $sAction); + if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage) + { + if ($this->GetRawName() == "Administrator") + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + // Note: for sure, we assume that the instance is derived from UserRightsProjection + $oUserRights = UserRights::GetModuleInstance(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses('bizmodel') as $sClass) + { + // Skip non instantiable classes + if (MetaModel::IsAbstract($sClass)) continue; + + $aStimuli = array(); + foreach (MetaModel::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) + { + $oGrant = $oUserRights->GetClassStimulusGrant($this->GetKey(), $sClass, $sStimulusCode); + if (is_object($oGrant) && ($oGrant->Get('permission') == 'yes')) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; + } + } + $sStimuli = implode(', ', $aStimuli); + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Read'), + 'bulkread' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Read'), + 'write' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Modify'), + 'bulkwrite' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Modify'), + 'delete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Delete'), + 'bulkdelete' => $this->GetGrantAsHtml($oUserRights, $sClass, 'Bulk Delete'), + 'stimuli' => $sStimuli, + ); + } + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); + $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage); + } + } +} + + +class URP_Dimensions extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_dimensions", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeClass("type", array("class_category"=>"bizmodel", "more_values"=>"String,Integer", "sql"=>"type", "default_value"=>'String', "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'type')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + public function CheckProjectionSpec($oProjectionSpec, $sProjectedClass) + { + $sExpression = $oProjectionSpec->Get('value'); + $sAttribute = $oProjectionSpec->Get('attribute'); + + // Shortcut: "any value" or "no value" means no projection + if (empty($sExpression)) return; + if ($sExpression == '') return; + + // 1st - compute the data type for the dimension + // + $sType = $this->Get('type'); + if (MetaModel::IsValidClass($sType)) + { + $sExpectedType = $sType; + } + else + { + $sExpectedType = '_scalar_'; + } + + // 2nd - compute the data type for the projection + // + $sTargetClass = ''; + if (($sExpression == '') || ($sExpression == '')) + { + $sTargetClass = $sProjectedClass; + } + elseif ($sExpression == '') + { + $sTargetClass = ''; + } + else + { + // Evaluate wether it is a constant or not + try + { + $oObjectSearch = DBObjectSearch::FromOQL_AllData($sExpression); + + $sTargetClass = $oObjectSearch->GetClass(); + } + catch (OqlException $e) + { + } + } + + if (empty($sTargetClass)) + { + $sFoundType = '_void_'; + } + else + { + if (empty($sAttribute)) + { + $sFoundType = $sTargetClass; + } + else + { + if (!MetaModel::IsValidAttCode($sTargetClass, $sAttribute)) + { + throw new CoreException('Unkown attribute code in projection specification', array('found' => $sAttribute, 'expecting' => MetaModel::GetAttributesList($sTargetClass), 'class' => $sTargetClass, 'projection' => $oProjectionSpec)); + } + $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttribute); + if ($oAttDef->IsExternalKey()) + { + $sFoundType = $oAttDef->GetTargetClass(); + } + else + { + $sFoundType = '_scalar_'; + } + } + } + + // Compare the dimension type and projection type + if (($sFoundType != '_void_') && ($sFoundType != $sExpectedType)) + { + throw new CoreException('Wrong type in projection specification', array('found' => $sFoundType, 'expecting' => $sExpectedType, 'expression' => $sExpression, 'attribute' => $sAttribute, 'projection' => $oProjectionSpec)); + } + } +} + + +class URP_UserProfile extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "userid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_userprofile", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("userid", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"userid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userlogin", array("allowed_values"=>null, "extkey_attcode"=> 'userid', "target_attcode"=>"login"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("reason", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('userid', 'profileid', 'reason')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('profileid', 'reason')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('userid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('userid', 'profileid')); // Criteria of the advanced search form + } + + public function GetName() + { + return Dict::Format('UI:UserManagement:LinkBetween_User_And_Profile', $this->Get('userlogin'), $this->Get('profile')); + } +} + + +class URP_ProfileProjection extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_profileprojection", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("dimensionid", array("targetclass"=>"URP_Dimensions", "jointype"=> "", "allowed_values"=>null, "sql"=>"dimensionid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("dimension", array("allowed_values"=>null, "extkey_attcode"=> 'dimensionid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attribute", array("allowed_values"=>null, "sql"=>"attribute", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('dimensionid', 'profileid', 'value', 'attribute')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('profileid', 'value', 'attribute')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('dimensionid', 'profileid')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('dimensionid', 'profileid')); // Criteria of the advanced search form + } + + protected $m_aUserProjections; // cache + + public function ProjectUser(User $oUser) + { + if (is_array($this->m_aUserProjections)) + { + // Hit! + return $this->m_aUserProjections; + } + + $sExpr = $this->Get('value'); + if ($sExpr == '') + { + $sColumn = $this->Get('attribute'); + if (empty($sColumn)) + { + $aRes = array($oUser->GetKey()); + } + else + { + $aRes = array($oUser->Get($sColumn)); + } + + } + elseif (($sExpr == '') || ($sExpr == '')) + { + $aRes = null; + } + elseif (strtolower(substr($sExpr, 0, 6)) == 'select') + { + $sColumn = $this->Get('attribute'); + // SELECT... + $oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/); + $aRes = $oValueSetDef->GetValues(array('user' => $oUser), ''); + } + else + { + // Constant value(s) + $aRes = explode(';', trim($sExpr)); + } + $this->m_aUserProjections = $aRes; + return $aRes; + } +} + + +class URP_ClassProjection extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "dimensionid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_classprojection", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("dimensionid", array("targetclass"=>"URP_Dimensions", "jointype"=> "", "allowed_values"=>null, "sql"=>"dimensionid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("dimension", array("allowed_values"=>null, "extkey_attcode"=> 'dimensionid', "target_attcode"=>"name"))); + + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attribute", array("allowed_values"=>null, "sql"=>"attribute", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('dimensionid', 'class', 'value', 'attribute')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'value', 'attribute')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('dimensionid', 'class')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('dimensionid', 'class')); // Criteria of the advanced search form + } + + public function ProjectObject($oObject) + { + $sExpr = $this->Get('value'); + if ($sExpr == '') + { + $sColumn = $this->Get('attribute'); + if (empty($sColumn)) + { + $aRes = array($oObject->GetKey()); + } + else + { + $aRes = array($oObject->Get($sColumn)); + } + + } + elseif (($sExpr == '') || ($sExpr == '')) + { + $aRes = null; + } + elseif (strtolower(substr($sExpr, 0, 6)) == 'select') + { + $sColumn = $this->Get('attribute'); + // SELECT... + $oValueSetDef = new ValueSetObjects($sExpr, $sColumn, array(), true /*allow all data*/); + $aRes = $oValueSetDef->GetValues(array('this' => $oObject), ''); + } + else + { + // Constant value(s) + $aRes = explode(';', trim($sExpr)); + } + return $aRes; + } + +} + + +class URP_ActionGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_actions", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("action", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'action')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'action')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'action')); // Criteria of the advanced search form + } +} + + +class URP_StimulusGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "profileid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_stimulus", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + // Common to all grant classes (could be factorized by class inheritence, but this has to be benchmarked) + MetaModel::Init_AddAttribute(new AttributeExternalKey("profileid", array("targetclass"=>"URP_Profiles", "jointype"=> "", "allowed_values"=>null, "sql"=>"profileid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("profile", array("allowed_values"=>null, "extkey_attcode"=> 'profileid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeClass("class", array("class_category"=>"", "more_values"=>"", "sql"=>"class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("permission", array("allowed_values"=>new ValueSetEnum('yes,no'), "sql"=>"permission", "default_value"=>"yes", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("stimulus", array("allowed_values"=>null, "sql"=>"action", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('profileid', 'class', 'permission', 'stimulus')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('class', 'permission', 'stimulus')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('profileid', 'class', 'permission', 'stimulus')); // Criteria of the advanced search form + } +} + + +class URP_AttributeGrant extends UserRightsBaseClass +{ + public static function Init() + { + $aParams = array + ( + "category" => "addon/userrights", + "key_type" => "autoincrement", + "name_attcode" => "actiongrantid", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_urp_grant_attributes", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("actiongrantid", array("targetclass"=>"URP_ActionGrant", "jointype"=> "", "allowed_values"=>null, "sql"=>"actiongrantid", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('actiongrantid', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('attcode')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('actiongrantid', 'attcode')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('actiongrantid', 'attcode')); // Criteria of the advanced search form + } +} + + + + +class UserRightsProjection extends UserRightsAddOnAPI +{ + static public $m_aActionCodes = array( + UR_ACTION_READ => 'read', + UR_ACTION_MODIFY => 'modify', + UR_ACTION_DELETE => 'delete', + UR_ACTION_BULK_READ => 'bulk read', + UR_ACTION_BULK_MODIFY => 'bulk modify', + UR_ACTION_BULK_DELETE => 'bulk delete', + ); + + // Installation: create the very first user + public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + // Create a change to record the history of the User object + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + + $oOrg = new Organization(); + $oOrg->Set('name', 'My Company/Department'); + $oOrg->Set('code', 'SOMECODE'); +// $oOrg->Set('status', 'implementation'); + //$oOrg->Set('parent_id', xxx); + $iOrgId = $oOrg->DBInsertTrackedNoReload($oChange, true /* skip strong security */); + + // Location : optional + //$oLocation = new bizLocation(); + //$oLocation->Set('name', 'MyOffice'); + //$oLocation->Set('status', 'implementation'); + //$oLocation->Set('org_id', $iOrgId); + //$oLocation->Set('severity', 'high'); + //$oLocation->Set('address', 'my building in my city'); + //$oLocation->Set('country', 'my country'); + //$oLocation->Set('parent_location_id', xxx); + //$iLocationId = $oLocation->DBInsertNoReload(); + + $oContact = new Person(); + $oContact->Set('name', 'My last name'); + $oContact->Set('first_name', 'My first name'); + //$oContact->Set('status', 'available'); + $oContact->Set('org_id', $iOrgId); + $oContact->Set('email', 'my.email@foo.org'); + //$oContact->Set('phone', ''); + //$oContact->Set('location_id', $iLocationId); + //$oContact->Set('employee_number', ''); + $iContactId = $oContact->DBInsertTrackedNoReload($oChange, true /* skip security */); + + $oUser = new UserLocal(); + $oUser->Set('login', $sAdminUser); + $oUser->Set('password', $sAdminPwd); + $oUser->Set('contactid', $iContactId); + $oUser->Set('language', $sLanguage); // Language was chosen during the installation + $iUserId = $oUser->DBInsertTrackedNoReload($oChange, true /* skip security */); + + // Add this user to the very specific 'admin' profile + $oUserProfile = new URP_UserProfile(); + $oUserProfile->Set('userid', $iUserId); + $oUserProfile->Set('profileid', ADMIN_PROFILE_ID); + $oUserProfile->Set('reason', 'By definition, the administrator must have the administrator profile'); + $oUserProfile->DBInsertTrackedNoReload($oChange, true /* skip security */); + return true; + } + + public function IsAdministrator($oUser) + { + if (in_array($oUser->GetKey(), $this->m_aAdmins)) + { + return true; + } + else + { + return false; + } + } + + public function IsPortalUser($oUser) + { + return true; + // See implementation of userrightsprofile + } + + public function Init() + { + // CacheData to be invoked in a module extension + //MetaModel::RegisterPlugin('userrights', 'ACbyProfile', array($this, 'CacheData')); + } + + protected $m_aDimensions = array(); // id -> object + protected $m_aClassProj = array(); // class,dimensionid -> object + protected $m_aProfiles = array(); // id -> object + protected $m_aUserProfiles = array(); // userid,profileid -> object + protected $m_aProPro = array(); // profileid,dimensionid -> object + + protected $m_aAdmins = array(); // id of users being linked to the well-known admin profile + + protected $m_aClassActionGrants = array(); // profile, class, action -> permission + protected $m_aClassStimulusGrants = array(); // profile, class, stimulus -> permission + protected $m_aObjectActionGrants = array(); // userid, class, id, action -> permission, list of attributes + + public function CacheData() + { + // Could be loaded in a shared memory (?) + + $oDimensionSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Dimensions")); + $this->m_aDimensions = array(); + while ($oDimension = $oDimensionSet->Fetch()) + { + $this->m_aDimensions[$oDimension->GetKey()] = $oDimension; + } + + $oClassProjSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ClassProjection")); + $this->m_aClassProjs = array(); + while ($oClassProj = $oClassProjSet->Fetch()) + { + $this->m_aClassProjs[$oClassProj->Get('class')][$oClassProj->Get('dimensionid')] = $oClassProj; + } + + $oProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_Profiles")); + $this->m_aProfiles = array(); + while ($oProfile = $oProfileSet->Fetch()) + { + $this->m_aProfiles[$oProfile->GetKey()] = $oProfile; + } + + $oUserProfileSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_UserProfile")); + $this->m_aUserProfiles = array(); + $this->m_aAdmins = array(); + while ($oUserProfile = $oUserProfileSet->Fetch()) + { + $this->m_aUserProfiles[$oUserProfile->Get('userid')][$oUserProfile->Get('profileid')] = $oUserProfile; + if ($oUserProfile->Get('profileid') == ADMIN_PROFILE_ID) + { + $this->m_aAdmins[] = $oUserProfile->Get('userid'); + } + } + + $oProProSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData("SELECT URP_ProfileProjection")); + $this->m_aProPros = array(); + while ($oProPro = $oProProSet->Fetch()) + { + $this->m_aProPros[$oProPro->Get('profileid')][$oProPro->Get('dimensionid')] = $oProPro; + } + +/* + echo "
\n";
+		print_r($this->m_aDimensions);
+		print_r($this->m_aClassProjs);
+		print_r($this->m_aProfiles);
+		print_r($this->m_aUserProfiles);
+		print_r($this->m_aProPros);
+		echo "
\n"; +exit; +*/ + + return true; + } + + public function GetSelectFilter($oUser, $sClass, $aSettings = array()) + { + $aConditions = array(); + foreach ($this->m_aDimensions as $iDimension => $oDimension) + { + $oClassProj = @$this->m_aClassProjs[$sClass][$iDimension]; + if (is_null($oClassProj)) + { + // Authorize any for this dimension, then no additional criteria is required + continue; + } + + // 1 - Get class projection info + // + $oExpression = null; + $sExpr = $oClassProj->Get('value'); + if ($sExpr == '') + { + $sColumn = $oClassProj->Get('attribute'); + if (empty($sColumn)) + { + $oExpression = new FieldExpression('id', $sClass); + } + else + { + $oExpression = new FieldExpression($sColumn, $sClass); + } + } + elseif (($sExpr == '') || ($sExpr == '')) + { + // Authorize any for this dimension, then no additional criteria is required + continue; + } + elseif (strtolower(substr($sExpr, 0, 6)) == 'select') + { + throw new CoreException('Sorry, projections by the mean of OQL are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr)); + } + else + { + // Constant value(s) + // unsupported + throw new CoreException('Sorry, constant projections are not supported currently, please specify an attribute instead', array('class' => $sClass, 'expression' => $sExpr)); +// $aRes = explode(';', trim($sExpr)); + } + + // 2 - Get profile projection info and use it if needed + // + $aProjections = self::GetReadableProjectionsByDim($oUser, $sClass, $oDimension); + if (is_null($aProjections)) + { + // Authorize any for this dimension, then no additional criteria is required + continue; + } + elseif (count($aProjections) == 0) + { + // Authorize none, then exit as quickly as possible + return false; + } + else + { + // Authorize the given set of values + $oListExpr = ListExpression::FromScalars($aProjections); + $oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr); + $aConditions[] = $oCondition; + } + } + + if (count($aConditions) == 0) + { + // allow all + return true; + } + else + { + $oFilter = new DBObjectSearch($sClass); + foreach($aConditions as $oCondition) + { + $oFilter->AddConditionExpression($oCondition); + } + //return true; + return $oFilter; + } + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassActionGrant($iProfile, $sClass, $sAction) + { + if (isset($this->m_aClassActionGrants[$iProfile][$sClass][$sAction])) + { + return $this->m_aClassActionGrants[$iProfile][$sClass][$sAction]; + } + + // Get the permission for this profile/class/action + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_ActionGrant WHERE class = :class AND action = :action AND profileid = :profile AND permission = 'yes'"); + $oSet = new DBObjectSet($oSearch, array(), array('class'=>$sClass, 'action'=>$sAction, 'profile'=>$iProfile)); + if ($oSet->Count() >= 1) + { + $oGrantRecord = $oSet->Fetch(); + } + else + { + $sParentClass = MetaModel::GetParentPersistentClass($sClass); + if (empty($sParentClass)) + { + $oGrantRecord = null; + } + else + { + $oGrantRecord = $this->GetClassActionGrant($iProfile, $sParentClass, $sAction); + } + } + + $this->m_aClassActionGrants[$iProfile][$sClass][$sAction] = $oGrantRecord; + return $oGrantRecord; + } + + protected function GetObjectActionGrant($oUser, $sClass, $iActionCode, /*DBObject*/ $oObject = null) + { + if(is_null($oObject)) + { + $iObjectRef = -999; + } + else + { + $iObjectRef = $oObject->GetKey(); + } + // load and cache permissions for the current user on the given object + // + $aTest = @$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode]; + if (is_array($aTest)) return $aTest; + + $sAction = self::$m_aActionCodes[$iActionCode]; + + $iInstancePermission = UR_ALLOWED_NO; + $aAttributes = array(); + foreach($this->GetMatchingProfiles($oUser, $sClass, $oObject) as $iProfile) + { + $oGrantRecord = $this->GetClassActionGrant($iProfile, $sClass, $sAction); + if (is_null($oGrantRecord)) + { + continue; // loop to the next profile + } + else + { + $iInstancePermission = UR_ALLOWED_YES; + + // update the list of attributes with those allowed for this profile + // + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_AttributeGrant WHERE actiongrantid = :actiongrantid"); + $oSet = new DBObjectSet($oSearch, array(), array('actiongrantid' => $oGrantRecord->GetKey())); + $aProfileAttributes = $oSet->GetColumnAsArray('attcode', false); + if (count($aProfileAttributes) == 0) + { + $aAllAttributes = array_keys(MetaModel::ListAttributeDefs($sClass)); + $aAttributes = array_merge($aAttributes, $aAllAttributes); + } + else + { + $aAttributes = array_merge($aAttributes, $aProfileAttributes); + } + } + } + + $aRes = array( + 'permission' => $iInstancePermission, + 'attributes' => $aAttributes, + ); + $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iObjectRef][$iActionCode] = $aRes; + return $aRes; + } + + public function IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet = null) + { + if (is_null($oInstanceSet)) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode); + return $aObjectPermissions['permission']; + } + + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode, $oObject); + + $iInstancePermission = $aObjectPermissions['permission']; + if (isset($iGlobalPermission)) + { + if ($iInstancePermission != $iGlobalPermission) + { + $iGlobalPermission = UR_ALLOWED_DEPENDS; + break; + } + } + else + { + $iGlobalPermission = $iInstancePermission; + } + } + $oInstanceSet->Rewind(); + + if (isset($iGlobalPermission)) + { + return $iGlobalPermission; + } + else + { + return UR_ALLOWED_NO; + } + } + + public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet = null) + { + if (is_null($oInstanceSet)) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode); + $aAttributes = $aObjectPermissions['attributes']; + if (in_array($sAttCode, $aAttributes)) + { + return $aObjectPermissions['permission']; + } + else + { + return UR_ALLOWED_NO; + } + } + + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $aObjectPermissions = $this->GetObjectActionGrant($oUser, $sClass, $iActionCode, $oObject); + + $aAttributes = $aObjectPermissions['attributes']; + if (in_array($sAttCode, $aAttributes)) + { + $iInstancePermission = $aObjectPermissions['permission']; + } + else + { + $iInstancePermission = UR_ALLOWED_NO; + } + + if (isset($iGlobalPermission)) + { + if ($iInstancePermission != $iGlobalPermission) + { + $iGlobalPermission = UR_ALLOWED_DEPENDS; + } + } + else + { + $iGlobalPermission = $iInstancePermission; + } + } + $oInstanceSet->Rewind(); + + if (isset($iGlobalPermission)) + { + return $iGlobalPermission; + } + else + { + return UR_ALLOWED_NO; + } + } + + // This verb has been made public to allow the development of an accurate feedback for the current configuration + public function GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode) + { + if (isset($this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode])) + { + return $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode]; + } + + // Get the permission for this profile/class/stimulus + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT URP_StimulusGrant WHERE class = :class AND stimulus = :stimulus AND profileid = :profile AND permission = 'yes'"); + $oSet = new DBObjectSet($oSearch, array(), array('class'=>$sClass, 'stimulus'=>$sStimulusCode, 'profile'=>$iProfile)); + if ($oSet->Count() >= 1) + { + $oGrantRecord = $oSet->Fetch(); + } + else + { + $oGrantRecord = null; + } + + $this->m_aClassStimulusGrants[$iProfile][$sClass][$sStimulusCode] = $oGrantRecord; + return $oGrantRecord; + } + + public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet = null) + { + // Note: this code is VERY close to the code of IsActionAllowed() + + if (is_null($oInstanceSet)) + { + $iInstancePermission = UR_ALLOWED_NO; + foreach($this->GetMatchingProfiles($oUser, $sClass) as $iProfile) + { + $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($oGrantRecord)) + { + // no need to fetch the record, we've requested the records having permission = 'yes' + $iInstancePermission = UR_ALLOWED_YES; + } + } + return $iInstancePermission; + } + + $oInstanceSet->Rewind(); + while($oObject = $oInstanceSet->Fetch()) + { + $iInstancePermission = UR_ALLOWED_NO; + foreach($this->GetMatchingProfiles($oUser, $sClass, $oObject) as $iProfile) + { + $oGrantRecord = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode); + if (!is_null($oGrantRecord)) + { + // no need to fetch the record, we've requested the records having permission = 'yes' + $iInstancePermission = UR_ALLOWED_YES; + } + } + if (isset($iGlobalPermission)) + { + if ($iInstancePermission != $iGlobalPermission) + { + $iGlobalPermission = UR_ALLOWED_DEPENDS; + } + } + else + { + $iGlobalPermission = $iInstancePermission; + } + } + $oInstanceSet->Rewind(); + + if (isset($iGlobalPermission)) + { + return $iGlobalPermission; + } + else + { + return UR_ALLOWED_NO; + } + } + + // Copied from GetMatchingProfilesByDim() + // adapted to the optimized implementation of GetSelectFilter() + // Note: shares the cache m_aProPros with GetMatchingProfilesByDim() + // Returns null if any object is readable + // an array of allowed projections otherwise (could be an empty array if none is allowed) + protected function GetReadableProjectionsByDim($oUser, $sClass, $oDimension) + { + // + // Given a dimension, lists the values for which the user will be allowed to read the objects + // + $iUser = $oUser->GetKey(); + $iDimension = $oDimension->GetKey(); + + $aRes = array(); + if (array_key_exists($iUser, $this->m_aUserProfiles)) + { + foreach ($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) + { + // user projection to be cached on a given page ! + if (!isset($this->m_aProPros[$iProfile][$iDimension])) + { + // No projection for a given profile: default to 'any' + return null; + } + + $aUserProjection = $this->m_aProPros[$iProfile][$iDimension]->ProjectUser($oUser); + if (is_null($aUserProjection)) + { + // No projection for a given profile: default to 'any' + return null; + } + $aRes = array_unique(array_merge($aRes, $aUserProjection)); + } + } + return $aRes; + } + + // Note: shares the cache m_aProPros with GetReadableProjectionsByDim() + protected function GetMatchingProfilesByDim($oUser, $oObject, $oDimension) + { + // + // List profiles for which the user projection overlaps the object projection in the given dimension + // + $iUser = $oUser->GetKey(); + $sClass = get_class($oObject); + $iPKey = $oObject->GetKey(); + $iDimension = $oDimension->GetKey(); + + if (isset($this->m_aClassProjs[$sClass][$iDimension])) + { + $aObjectProjection = $this->m_aClassProjs[$sClass][$iDimension]->ProjectObject($oObject); + } + else + { + // No projection for a given class: default to 'any' + $aObjectProjection = null; + } + + $aRes = array(); + if (array_key_exists($iUser, $this->m_aUserProfiles)) + { + foreach ($this->m_aUserProfiles[$iUser] as $iProfile => $oProfile) + { + if (is_null($aObjectProjection)) + { + $aRes[] = $iProfile; + } + else + { + // user projection to be cached on a given page ! + if (isset($this->m_aProPros[$iProfile][$iDimension])) + { + $aUserProjection = $this->m_aProPros[$iProfile][$iDimension]->ProjectUser($oUser); + } + else + { + // No projection for a given profile: default to 'any' + $aUserProjection = null; + } + + if (is_null($aUserProjection)) + { + $aRes[] = $iProfile; + } + else + { + $aMatchingValues = array_intersect($aObjectProjection, $aUserProjection); + if (count($aMatchingValues) > 0) + { + $aRes[] = $iProfile; + } + } + } + } + } + return $aRes; + } + + protected $m_aMatchingProfiles = array(); // cache of the matching profiles for a given user/object + + protected function GetMatchingProfiles($oUser, $sClass, /*DBObject*/ $oObject = null) + { + $iUser = $oUser->GetKey(); + + if(is_null($oObject)) + { + $iObjectRef = -999; + } + else + { + $iObjectRef = $oObject->GetKey(); + } + + // + // List profiles for which the user projection overlaps the object projection in each and every dimension + // Caches the result + // + $aTest = @$this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef]; + if (is_array($aTest)) + { + return $aTest; + } + + if (is_null($oObject)) + { + if (array_key_exists($iUser, $this->m_aUserProfiles)) + { + $aRes = array_keys($this->m_aUserProfiles[$iUser]); + } + else + { + // no profile has been defined for this user + $aRes = array(); + } + } + else + { + $aProfileRes = array(); + foreach ($this->m_aDimensions as $iDimension => $oDimension) + { + foreach ($this->GetMatchingProfilesByDim($oUser, $oObject, $oDimension) as $iProfile) + { + @$aProfileRes[$iProfile] += 1; + } + } + + $aRes = array(); + $iDimCount = count($this->m_aDimensions); + foreach ($aProfileRes as $iProfile => $iMatches) + { + if ($iMatches == $iDimCount) + { + $aRes[] = $iProfile; + } + } + } + + // store into the cache + $this->m_aMatchingProfiles[$iUser][$sClass][$iObjectRef] = $aRes; + return $aRes; + } + + public function FlushPrivileges() + { + $this->CacheData(); + } +} + + +UserRights::SelectModule('UserRightsProjection'); + +?> diff --git a/application/ajaxwebpage.class.inc.php b/application/ajaxwebpage.class.inc.php index f1e9a8c1e..05124e473 100644 --- a/application/ajaxwebpage.class.inc.php +++ b/application/ajaxwebpage.class.inc.php @@ -1,391 +1,391 @@ - - -/** - * Simple web page with no includes, header or fancy formatting, useful to - * generate HTML fragments when called by an AJAX method - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/webpage.class.inc.php"); - -class ajax_page extends WebPage implements iTabbedPage -{ - /** - * Jquery style ready script - * @var Hash - */ - protected $m_sReadyScript; - protected $m_oTabs; - private $m_sMenu; // If set, then the menu will be updated - - /** - * constructor for the web page - * @param string $s_title Not used - */ - function __construct($s_title) - { - $sPrintable = utils::ReadParam('printable', '0'); - $bPrintable = ($sPrintable == '1'); - - parent::__construct($s_title, $bPrintable); - $this->m_sReadyScript = ""; - //$this->add_header("Content-type: text/html; charset=utf-8"); - $this->add_header("Cache-control: no-cache"); - $this->m_oTabs = new TabManager(); - $this->sContentType = 'text/html'; - $this->sContentDisposition = 'inline'; - $this->m_sMenu = ""; - - utils::InitArchiveMode(); - } - - public function AddTabContainer($sTabContainer, $sPrefix = '') - { - $this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix)); - } - - public function AddToTab($sTabContainer, $sTabLabel, $sHtml) - { - $this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml)); - } - - public function SetCurrentTabContainer($sTabContainer = '') - { - return $this->m_oTabs->SetCurrentTabContainer($sTabContainer); - } - - public function SetCurrentTab($sTabLabel = '') - { - return $this->m_oTabs->SetCurrentTab($sTabLabel); - } - - /** - * Add a tab which content will be loaded asynchronously via the supplied URL - * - * Limitations: - * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server. - * Static content cannot be added inside such tabs. - * - * @param string $sTabLabel The (localised) label of the tab - * @param string $sUrl The URL to load (on the same server) - * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation. - * @since 2.0.3 - */ - public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) - { - $this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache)); - } - - public function GetCurrentTab() - { - return $this->m_oTabs->GetCurrentTab(); - } - - public function RemoveTab($sTabLabel, $sTabContainer = null) - { - $this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer); - } - - /** - * Finds the tab whose title matches a given pattern - * @return mixed The name of the tab as a string or false if not found - */ - public function FindTab($sPattern, $sTabContainer = null) - { - return $this->m_oTabs->FindTab($sPattern, $sTabContainer); - } - - /** - * Make the given tab the active one, as if it were clicked - * DOES NOT WORK: apparently in the *old* version of jquery - * that we are using this is not supported... TO DO upgrade - * the whole jquery bundle... - */ - public function SelectTab($sTabContainer, $sTabLabel) - { - $this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel)); - } - - public function AddToMenu($sHtml) - { - $this->m_sMenu .= $sHtml; - } - - /** - * Echoes the content of the whole page - * @return void - */ - public function output() - { - if (!empty($this->sContentType)) - { - $this->add_header('Content-type: '.$this->sContentType); - } - if (!empty($this->sContentDisposition)) - { - $this->add_header('Content-Disposition: '.$this->sContentDisposition.'; filename="'.$this->sContentFileName.'"'); - } - foreach($this->a_headers as $s_header) - { - header($s_header); - } - if ($this->m_oTabs->TabsContainerCount() > 0) - { - $this->add_ready_script( -<< tag in the page - // is taken into account and causes "local" tabs to be considered as Ajax - // unless their URL is equal to the URL of the page... - if ($('base').length > 0) - { - $('div[id^=tabbedContent] > ul > li > a').each(function() { - var sHash = location.hash; - var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, ''); - $(this).attr("href", sCleanLocation+$(this).attr("href")); - }); - } - if ($.bbq) - { - // This selector will be reused when selecting actual tab widget A elements. - var tab_a_selector = 'ul.ui-tabs-nav a'; - - // Enable tabs on all tab widgets. The `event` property must be overridden so - // that the tabs aren't changed on click, and any custom event name can be - // specified. Note that if you define a callback for the 'select' event, it - // will be executed for the selected tab whenever the hash changes. - tabs.tabs({ event: 'change' }); - - // Define our own click handler for the tabs, overriding the default. - tabs.find( tab_a_selector ).click(function() - { - var state = {}; - - // Get the id of this tab widget. - var id = $(this).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); - - // Get the index of this tab. - var idx = $(this).parent().prevAll().length; - - // Set the state! - state[ id ] = idx; - $.bbq.pushState( state ); - }); - } - else - { - tabs.tabs(); - } -EOF -); - } - // Render the tabs in the page (if any) - $this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content, $this); - - // Additional UI widgets to be activated inside the ajax fragment - // Important: Testing the content type is not enough because some ajax handlers have not correctly positionned the flag (e.g json response corrupted by the script) - if (($this->sContentType == 'text/html') && (preg_match('/class="date-pick"/', $this->s_content) || preg_match('/class="datetime-pick"/', $this->s_content)) ) - { - $this->add_ready_script( -<<ob_get_clean_safe(); - if (($this->sContentType == 'text/html') && ($this->sContentDisposition == 'inline')) - { - // inline content != attachment && html => filter all scripts for malicious XSS scripts - echo self::FilterXSS($this->s_content); - } - else - { - echo $this->s_content; - } - if (!empty($this->m_sMenu)) - { - $uid = time(); - echo "
\n"; - echo "
\n"; - echo "\n"; - echo self::FilterXSS($this->m_sMenu); - echo "\n"; - echo "
\n"; - echo "
\n"; - - echo "\n"; - } - - //echo $this->s_deferred_content; - if (count($this->a_scripts) > 0) - { - echo "\n"; - } - if (count($this->a_linked_scripts) > 0) - { - echo "\n"; - } - if (!empty($this->s_deferred_content)) - { - echo "\n"; - } - if (!empty($this->m_sReadyScript)) - { - echo "\n"; - } - - if (trim($s_captured_output) != "") - { - echo self::FilterXSS($s_captured_output); - } - - if (class_exists('DBSearch')) - { - DBSearch::RecordQueryTrace(); - } - } - - /** - * Adds a paragraph with a smaller font into the page - * NOT implemented (i.e does nothing) - * @param string $sText Content of the (small) paragraph - * @return void - */ - public function small_p($sText) - { - } - - public function add($sHtml) - { - if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != '')) - { - $this->m_oTabs->AddToTab($this->m_oTabs->GetCurrentTabContainer(), $this->m_oTabs->GetCurrentTab(), $sHtml); - } - else - { - parent::add($sHtml); - } - } - - /** - * Records the current state of the 'html' part of the page output - * @return mixed The current state of the 'html' output - */ - public function start_capture() - { - $sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer(); - $sCurrentTab = $this->m_oTabs->GetCurrentTab(); - - if (!empty($sCurrentTabContainer) && !empty($sCurrentTab)) - { - $iOffset = $this->m_oTabs->GetCurrentTabLength(); - return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset); - } - else - { - return parent::start_capture(); - } - } - - /** - * Returns the part of the html output that occurred since the call to start_capture - * and removes this part from the current html output - * @param $offset mixed The value returned by start_capture - * @return string The part of the html output that was added since the call to start_capture - */ - public function end_capture($offset) - { - if (is_array($offset)) - { - if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab'])) - { - $sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']); - } - else - { - $sCaptured = ''; - } - } - else - { - $sCaptured = parent::end_capture($offset); - } - return $sCaptured; - } - - /** - * Add any text or HTML fragment (identified by an ID) at the end of the body of the page - * This is useful to add hidden content, DIVs or FORMs that should not - * be embedded into each other. - */ - public function add_at_the_end($s_html, $sId = '') - { - if ($sId != '') - { - $this->add_script("$('#{$sId}').remove();"); // Remove any previous instance of the same Id - } - $this->s_deferred_content .= $s_html; - } - - /** - * Adds a script to be executed when the DOM is ready (typical JQuery use) - * NOT implemented in this version of the class. - * @return void - */ - public function add_ready_script($sScript) - { - $this->m_sReadyScript .= $sScript."\n"; - } - - /** - * Cannot be called in this context, since Ajax pages do not share - * any context with the calling page !! - */ - public function GetUniqueId() - { - assert(false); - return 0; - } - - public static function FilterXSS($sHTML) - { - return str_ireplace(array(''), array(''), $sHTML); - } -} - + + +/** + * Simple web page with no includes, header or fancy formatting, useful to + * generate HTML fragments when called by an AJAX method + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); + +class ajax_page extends WebPage implements iTabbedPage +{ + /** + * Jquery style ready script + * @var Hash + */ + protected $m_sReadyScript; + protected $m_oTabs; + private $m_sMenu; // If set, then the menu will be updated + + /** + * constructor for the web page + * @param string $s_title Not used + */ + function __construct($s_title) + { + $sPrintable = utils::ReadParam('printable', '0'); + $bPrintable = ($sPrintable == '1'); + + parent::__construct($s_title, $bPrintable); + $this->m_sReadyScript = ""; + //$this->add_header("Content-type: text/html; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->m_oTabs = new TabManager(); + $this->sContentType = 'text/html'; + $this->sContentDisposition = 'inline'; + $this->m_sMenu = ""; + + utils::InitArchiveMode(); + } + + public function AddTabContainer($sTabContainer, $sPrefix = '') + { + $this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix)); + } + + public function AddToTab($sTabContainer, $sTabLabel, $sHtml) + { + $this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml)); + } + + public function SetCurrentTabContainer($sTabContainer = '') + { + return $this->m_oTabs->SetCurrentTabContainer($sTabContainer); + } + + public function SetCurrentTab($sTabLabel = '') + { + return $this->m_oTabs->SetCurrentTab($sTabLabel); + } + + /** + * Add a tab which content will be loaded asynchronously via the supplied URL + * + * Limitations: + * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server. + * Static content cannot be added inside such tabs. + * + * @param string $sTabLabel The (localised) label of the tab + * @param string $sUrl The URL to load (on the same server) + * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation. + * @since 2.0.3 + */ + public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) + { + $this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache)); + } + + public function GetCurrentTab() + { + return $this->m_oTabs->GetCurrentTab(); + } + + public function RemoveTab($sTabLabel, $sTabContainer = null) + { + $this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer); + } + + /** + * Finds the tab whose title matches a given pattern + * @return mixed The name of the tab as a string or false if not found + */ + public function FindTab($sPattern, $sTabContainer = null) + { + return $this->m_oTabs->FindTab($sPattern, $sTabContainer); + } + + /** + * Make the given tab the active one, as if it were clicked + * DOES NOT WORK: apparently in the *old* version of jquery + * that we are using this is not supported... TO DO upgrade + * the whole jquery bundle... + */ + public function SelectTab($sTabContainer, $sTabLabel) + { + $this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel)); + } + + public function AddToMenu($sHtml) + { + $this->m_sMenu .= $sHtml; + } + + /** + * Echoes the content of the whole page + * @return void + */ + public function output() + { + if (!empty($this->sContentType)) + { + $this->add_header('Content-type: '.$this->sContentType); + } + if (!empty($this->sContentDisposition)) + { + $this->add_header('Content-Disposition: '.$this->sContentDisposition.'; filename="'.$this->sContentFileName.'"'); + } + foreach($this->a_headers as $s_header) + { + header($s_header); + } + if ($this->m_oTabs->TabsContainerCount() > 0) + { + $this->add_ready_script( +<< tag in the page + // is taken into account and causes "local" tabs to be considered as Ajax + // unless their URL is equal to the URL of the page... + if ($('base').length > 0) + { + $('div[id^=tabbedContent] > ul > li > a').each(function() { + var sHash = location.hash; + var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, ''); + $(this).attr("href", sCleanLocation+$(this).attr("href")); + }); + } + if ($.bbq) + { + // This selector will be reused when selecting actual tab widget A elements. + var tab_a_selector = 'ul.ui-tabs-nav a'; + + // Enable tabs on all tab widgets. The `event` property must be overridden so + // that the tabs aren't changed on click, and any custom event name can be + // specified. Note that if you define a callback for the 'select' event, it + // will be executed for the selected tab whenever the hash changes. + tabs.tabs({ event: 'change' }); + + // Define our own click handler for the tabs, overriding the default. + tabs.find( tab_a_selector ).click(function() + { + var state = {}; + + // Get the id of this tab widget. + var id = $(this).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); + + // Get the index of this tab. + var idx = $(this).parent().prevAll().length; + + // Set the state! + state[ id ] = idx; + $.bbq.pushState( state ); + }); + } + else + { + tabs.tabs(); + } +EOF +); + } + // Render the tabs in the page (if any) + $this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content, $this); + + // Additional UI widgets to be activated inside the ajax fragment + // Important: Testing the content type is not enough because some ajax handlers have not correctly positionned the flag (e.g json response corrupted by the script) + if (($this->sContentType == 'text/html') && (preg_match('/class="date-pick"/', $this->s_content) || preg_match('/class="datetime-pick"/', $this->s_content)) ) + { + $this->add_ready_script( +<<ob_get_clean_safe(); + if (($this->sContentType == 'text/html') && ($this->sContentDisposition == 'inline')) + { + // inline content != attachment && html => filter all scripts for malicious XSS scripts + echo self::FilterXSS($this->s_content); + } + else + { + echo $this->s_content; + } + if (!empty($this->m_sMenu)) + { + $uid = time(); + echo "
\n"; + echo "
\n"; + echo "\n"; + echo self::FilterXSS($this->m_sMenu); + echo "\n"; + echo "
\n"; + echo "
\n"; + + echo "\n"; + } + + //echo $this->s_deferred_content; + if (count($this->a_scripts) > 0) + { + echo "\n"; + } + if (count($this->a_linked_scripts) > 0) + { + echo "\n"; + } + if (!empty($this->s_deferred_content)) + { + echo "\n"; + } + if (!empty($this->m_sReadyScript)) + { + echo "\n"; + } + + if (trim($s_captured_output) != "") + { + echo self::FilterXSS($s_captured_output); + } + + if (class_exists('DBSearch')) + { + DBSearch::RecordQueryTrace(); + } + } + + /** + * Adds a paragraph with a smaller font into the page + * NOT implemented (i.e does nothing) + * @param string $sText Content of the (small) paragraph + * @return void + */ + public function small_p($sText) + { + } + + public function add($sHtml) + { + if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != '')) + { + $this->m_oTabs->AddToTab($this->m_oTabs->GetCurrentTabContainer(), $this->m_oTabs->GetCurrentTab(), $sHtml); + } + else + { + parent::add($sHtml); + } + } + + /** + * Records the current state of the 'html' part of the page output + * @return mixed The current state of the 'html' output + */ + public function start_capture() + { + $sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer(); + $sCurrentTab = $this->m_oTabs->GetCurrentTab(); + + if (!empty($sCurrentTabContainer) && !empty($sCurrentTab)) + { + $iOffset = $this->m_oTabs->GetCurrentTabLength(); + return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset); + } + else + { + return parent::start_capture(); + } + } + + /** + * Returns the part of the html output that occurred since the call to start_capture + * and removes this part from the current html output + * @param $offset mixed The value returned by start_capture + * @return string The part of the html output that was added since the call to start_capture + */ + public function end_capture($offset) + { + if (is_array($offset)) + { + if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab'])) + { + $sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']); + } + else + { + $sCaptured = ''; + } + } + else + { + $sCaptured = parent::end_capture($offset); + } + return $sCaptured; + } + + /** + * Add any text or HTML fragment (identified by an ID) at the end of the body of the page + * This is useful to add hidden content, DIVs or FORMs that should not + * be embedded into each other. + */ + public function add_at_the_end($s_html, $sId = '') + { + if ($sId != '') + { + $this->add_script("$('#{$sId}').remove();"); // Remove any previous instance of the same Id + } + $this->s_deferred_content .= $s_html; + } + + /** + * Adds a script to be executed when the DOM is ready (typical JQuery use) + * NOT implemented in this version of the class. + * @return void + */ + public function add_ready_script($sScript) + { + $this->m_sReadyScript .= $sScript."\n"; + } + + /** + * Cannot be called in this context, since Ajax pages do not share + * any context with the calling page !! + */ + public function GetUniqueId() + { + assert(false); + return 0; + } + + public static function FilterXSS($sHTML) + { + return str_ireplace(array(''), array(''), $sHTML); + } +} + diff --git a/application/application.inc.php b/application/application.inc.php index e75e44fd3..a41b19712 100644 --- a/application/application.inc.php +++ b/application/application.inc.php @@ -1,40 +1,40 @@ - - - -/** - * Includes all the classes to have the application up and running - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/applicationcontext.class.inc.php'); -require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); -require_once(APPROOT.'/application/displayblock.class.inc.php'); -require_once(APPROOT.'/application/audit.category.class.inc.php'); -require_once(APPROOT.'/application/audit.rule.class.inc.php'); -require_once(APPROOT.'/application/query.class.inc.php'); -require_once(APPROOT.'/setup/moduleinstallation.class.inc.php'); -//require_once(APPROOT.'/application/menunode.class.inc.php'); -require_once(APPROOT.'/application/utils.inc.php'); - -class ApplicationException extends CoreException -{ -} -?> + + + +/** + * Includes all the classes to have the application up and running + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/applicationcontext.class.inc.php'); +require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); +require_once(APPROOT.'/application/displayblock.class.inc.php'); +require_once(APPROOT.'/application/audit.category.class.inc.php'); +require_once(APPROOT.'/application/audit.rule.class.inc.php'); +require_once(APPROOT.'/application/query.class.inc.php'); +require_once(APPROOT.'/setup/moduleinstallation.class.inc.php'); +//require_once(APPROOT.'/application/menunode.class.inc.php'); +require_once(APPROOT.'/application/utils.inc.php'); + +class ApplicationException extends CoreException +{ +} +?> diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 84311db42..164ed0949 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -1,1233 +1,1233 @@ - - -/** - * Management of application plugins - * - * Definition of interfaces that can be implemented to customize iTop. - * You may implement such interfaces in a module file (e.g. main.mymodule.php) - * - * @package Extensibility - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @api - */ - -/** - * Implement this interface to change the behavior of the GUI for some objects. - * - * All methods are invoked by iTop for a given object. There are basically two usages: - * - * 1) To tweak the form of an object, you will have to implement a specific behavior within: - * - * * OnDisplayProperties (bEditMode = true) - * * OnFormSubmit - * * OnFormCancel - * - * 2) To tune the display of the object details, you can use: - * - * * OnDisplayProperties - * * OnDisplayRelations - * * GetIcon - * * GetHilightClass - * - * Please note that some of the APIs can be called several times for a single page displayed. - * Therefore it is not recommended to perform too many operations, such as querying the database. - * A recommended pattern is to cache data by the mean of static members. - * - * @package Extensibility - * @api - */ -interface iApplicationUIExtension -{ - /** - * Invoked when an object is being displayed (wiew or edit) - * - * The method is called right after the main tab has been displayed. - * You can add output to the page, either to change the display, or to add a form input - * - * Example: - * - * if ($bEditMode) - * { - * $oPage->p('Age of the captain: <input type="text" name="captain_age"/>'); - * } - * else - * { - * $oPage->p('Age of the captain: '.$iCaptainAge); - * } - * - * - * @param DBObject $oObject The object being displayed - * @param WebPage $oPage The output context - * @param boolean $bEditMode True if the edition form is being displayed - * @return void - */ - public function OnDisplayProperties($oObject, WebPage $oPage, $bEditMode = false); - - /** - * Invoked when an object is being displayed (wiew or edit) - * - * The method is called rigth after all the tabs have been displayed - * - * @param DBObject $oObject The object being displayed - * @param WebPage $oPage The output context - * @param boolean $bEditMode True if the edition form is being displayed - * @return void - */ - public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false); - - /** - * Invoked when the end-user clicks on Modify from the object edition form - * - * The method is called after the changes from the standard form have been - * taken into account, and before saving the changes into the database. - * - * @param DBObject $oObject The object being edited - * @param string $sFormPrefix Prefix given to the HTML form inputs - * @return void - */ - public function OnFormSubmit($oObject, $sFormPrefix = ''); - - /** - * Invoked when the end-user clicks on Cancel from the object edition form - * - * Implement here any cleanup. This is necessary when you have injected some - * javascript into the edition form, and if that code requires to store temporary data - * (this is the case when a file must be uploaded). - * - * @param string $sTempId Unique temporary identifier made of session_id and transaction_id. It identifies the object in a unique way. - * @return void - */ - public function OnFormCancel($sTempId); - - /** - * Not yet called by the framework! - * - * Sorry, the verb has been reserved. You must implement it, but it is not called as of now. - * - * @param DBObject $oObject The object being displayed - * - * @return string[] desc - */ - public function EnumUsedAttributes($oObject); // Not yet implemented - - /** - * Not yet called by the framework! - * - * Sorry, the verb has been reserved. You must implement it, but it is not called as of now. - * - * @param DBObject $oObject The object being displayed - * @return string Path of the icon, relative to the modules directory. - */ - public function GetIcon($oObject); // Not yet implemented - - /** - * Invoked when the object is displayed alone or within a list - * - * Returns a value influencing the appearance of the object depending on its - * state. - * - * Possible values are: - * - * * HILIGHT_CLASS_CRITICAL - * * HILIGHT_CLASS_WARNING - * * HILIGHT_CLASS_OK - * * HILIGHT_CLASS_NONE - * - * @param DBObject $oObject The object being displayed - * @return integer The value representing the mood of the object - */ - public function GetHilightClass($oObject); - - /** - * Called when building the Actions menu for a single object or a list of objects - * - * Use this to add items to the Actions menu. You will have to specify a label and an URL. - * - * Example: - * - * $oObject = $oSet->fetch(); - * if ($oObject instanceof Sheep) - * { - * return array('View in my app' => 'http://myserver/view_sheeps?id='.$oObject->Get('name')); - * } - * else - * { - * return array(); - * } - * - * - * See also iPopupMenuExtension for greater flexibility - * - * @param DBObjectSet $oSet A set of persistent objects (DBObject) - * @return string[string] - */ - public function EnumAllowedActions(DBObjectSet $oSet); -} - -/** - * Implement this interface to perform specific things when objects are manipulated - * - * Note that those methods will be called when objects are manipulated, either in a programmatic way - * or through the GUI. - * - * @package Extensibility - * @api - */ -interface iApplicationObjectExtension -{ - /** - * Invoked to determine wether an object has been modified in memory - * - * The GUI calls this verb to determine the message that will be displayed to the end-user. - * Anyhow, this API can be called in other contexts such as the CSV import tool. - * - * If the extension returns false, then the framework will perform the usual evaluation. - * Otherwise, the answer is definitively "yes, the object has changed". - * - * @param DBObject $oObject The target object - * @return boolean True if something has changed for the target object - */ - public function OnIsModified($oObject); - - /** - * Invoked to determine wether an object can be written to the database - * - * The GUI calls this verb and reports any issue. - * Anyhow, this API can be called in other contexts such as the CSV import tool. - * - * @param DBObject $oObject The target object - * @return string[] A list of errors message. An error message is made of one line and it can be displayed to the end-user. - */ - public function OnCheckToWrite($oObject); - - /** - * Invoked to determine wether an object can be deleted from the database - * - * The GUI calls this verb and stops the deletion process if any issue is reported. - * - * Please not that it is not possible to cascade deletion by this mean: only stopper issues can be handled. - * - * @param DBObject $oObject The target object - * @return string[] A list of errors message. An error message is made of one line and it can be displayed to the end-user. - */ - public function OnCheckToDelete($oObject); - - /** - * Invoked when an object is updated into the database - * - * The method is called right after the object has been written to the database. - * - * @param DBObject $oObject The target object - * @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page - * @return void - */ - public function OnDBUpdate($oObject, $oChange = null); - - /** - * Invoked when an object is created into the database - * - * The method is called right after the object has been written to the database. - * - * @param DBObject $oObject The target object - * @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page - * @return void - */ - public function OnDBInsert($oObject, $oChange = null); - - /** - * Invoked when an object is deleted from the database - * - * The method is called right before the object will be deleted from the database. - * - * @param DBObject $oObject The target object - * @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page - * @return void - */ - public function OnDBDelete($oObject, $oChange = null); -} - -/** - * New extension to add menu items in the "popup" menus inside iTop. Provides a greater flexibility than - * iApplicationUIExtension::EnumAllowedActions. - * - * To add some menus into iTop, declare a class that implements this interface, it will be called automatically - * by the application, as long as the class definition is included somewhere in the code - * - * @package Extensibility - * @api - * @since 2.0 - */ -interface iPopupMenuExtension -{ - /** - * Insert an item into the Actions menu of a list - * - * $param is a DBObjectSet containing the list of objects - */ - const MENU_OBJLIST_ACTIONS = 1; - /** - * Insert an item into the Toolkit menu of a list - * - * $param is a DBObjectSet containing the list of objects - */ - const MENU_OBJLIST_TOOLKIT = 2; - /** - * Insert an item into the Actions menu on an object details page - * - * $param is a DBObject instance: the object currently displayed - */ - const MENU_OBJDETAILS_ACTIONS = 3; - /** - * Insert an item into the Dashboard menu - * - * The dashboad menu is shown on the top right corner when a dashboard - * is being displayed. - * - * $param is a Dashboard instance: the dashboard currently displayed - */ - const MENU_DASHBOARD_ACTIONS = 4; - /** - * Insert an item into the User menu (upper right corner) - * - * $param is null - */ - const MENU_USER_ACTIONS = 5; - /** - * Insert an item into the Action menu on an object item in an objects list in the portal - * - * $param is an array('portal_id' => $sPortalId, 'object' => $oObject) containing the portal id and a DBObject instance (the object on the current line) - */ - const PORTAL_OBJLISTITEM_ACTIONS = 7; - /** - * Insert an item into the Action menu on an object details page in the portal - * - * $param is an array('portal_id' => $sPortalId, 'object' => $oObject) containing the portal id and a DBObject instance (the object currently displayed) - */ - const PORTAL_OBJDETAILS_ACTIONS = 8; - - /** - * Insert an item into the Actions menu of a list in the portal - * Note: This is not implemented yet ! - * - * $param is an array('portal_id' => $sPortalId, 'object_set' => $oSet) containing DBObjectSet containing the list of objects - * @todo - */ - const PORTAL_OBJLIST_ACTIONS = 6; - /** - * Insert an item into the user menu of the portal - * Note: This is not implemented yet ! - * - * $param is the portal id - * @todo - */ - const PORTAL_USER_ACTIONS = 9; - /** - * Insert an item into the navigation menu of the portal - * Note: This is not implemented yet ! - * - * $param is the portal id - * @todo - */ - const PORTAL_MENU_ACTIONS = 10; - - /** - * Get the list of items to be added to a menu. - * - * This method is called by the framework for each menu. - * The items will be inserted in the menu in the order of the returned array. - * @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx - * @param mixed $param Depends on $iMenuId, see the constants defined above - * @return object[] An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu - */ - public static function EnumItems($iMenuId, $param); -} - -/** - * Base class for the various types of custom menus - * - * @package Extensibility - * @internal - * @since 2.0 - */ -abstract class ApplicationPopupMenuItem -{ - /** @ignore */ - protected $sUID; - /** @ignore */ - protected $sLabel; - /** @ignore */ - protected $aCssClasses; - - /** - * Constructor - * - * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough - * @param string $sLabel The display label of the menu (must be localized) - * @param array $aCssClasses The CSS classes to add to the menu - */ - public function __construct($sUID, $sLabel) - { - $this->sUID = $sUID; - $this->sLabel = $sLabel; - $this->aCssClasses = array(); - } - - /** - * Get the UID - * - * @return string The unique identifier - * @ignore - */ - public function GetUID() - { - return $this->sUID; - } - - /** - * Get the label - * - * @return string The label - * @ignore - */ - public function GetLabel() - { - return $this->sLabel; - } - - /** - * Get the CSS classes - * - * @return array - * @ignore - */ - public function GetCssClasses() - { - return $this->aCssClasses; - } - - /** - * @param $aCssClasses - */ - public function SetCssClasses($aCssClasses) - { - $this->aCssClasses = $aCssClasses; - } - - /** - * Adds a CSS class to the CSS classes that will be put on the menu item - * - * @param $sCssClass - */ - public function AddCssClass($sCssClass) - { - $this->aCssClasses[] = $sCssClass; - } - - /** - * Returns the components to create a popup menu item in HTML - * - * @return array A hash array: array('label' => , 'url' => , 'target' => , 'onclick' => ) - * @ignore - */ - abstract public function GetMenuItem(); - - /** @ignore */ - public function GetLinkedScripts() - { - return array(); - } -} - -/** - * Class for adding an item into a popup menu that browses to the given URL - * - * @package Extensibility - * @api - * @since 2.0 - */ -class URLPopupMenuItem extends ApplicationPopupMenuItem -{ - /** @ignore */ - protected $sURL; - /** @ignore */ - protected $sTarget; - - /** - * Constructor - * - * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough - * @param string $sLabel The display label of the menu (must be localized) - * @param string $sURL If the menu is an hyperlink, provide the absolute hyperlink here - * @param string $sTarget In case the menu is an hyperlink and a specific target is needed (_blank for example), pass it here - */ - public function __construct($sUID, $sLabel, $sURL, $sTarget = '_top') - { - parent::__construct($sUID, $sLabel); - $this->sURL = $sURL; - $this->sTarget = $sTarget; - } - - /** @ignore */ - public function GetMenuItem() - { - return array ('label' => $this->GetLabel(), 'url' => $this->sURL, 'target' => $this->sTarget, 'css_classes' => $this->aCssClasses); - } -} - -/** - * Class for adding an item into a popup menu that triggers some Javascript code - * - * @package Extensibility - * @api - * @since 2.0 - */ -class JSPopupMenuItem extends ApplicationPopupMenuItem -{ - /** @ignore */ - protected $sJSCode; - /** @ignore */ - protected $aIncludeJSFiles; - - /** - * Class for adding an item that triggers some Javascript code - * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough - * @param string $sLabel The display label of the menu (must be localized) - * @param string $sJSCode In case the menu consists in executing some havascript code inside the page, pass it here. If supplied $sURL ans $sTarget will be ignored - * @param array $aIncludeJSFiles An array of file URLs to be included (once) to provide some JS libraries for the page. - */ - public function __construct($sUID, $sLabel, $sJSCode, $aIncludeJSFiles = array()) - { - parent::__construct($sUID, $sLabel); - $this->sJSCode = $sJSCode; - $this->aIncludeJSFiles = $aIncludeJSFiles; - } - - /** @ignore */ - public function GetMenuItem() - { - // Note: the semicolumn is a must here! - return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode.'; return false;', 'url' => '#', 'css_classes' => $this->aCssClasses); - } - - /** @ignore */ - public function GetLinkedScripts() - { - return $this->aIncludeJSFiles; - } -} - -/** - * Class for adding a separator (horizontal line, not selectable) the output - * will automatically reduce several consecutive separators to just one - * - * @package Extensibility - * @api - * @since 2.0 - */ -class SeparatorPopupMenuItem extends ApplicationPopupMenuItem -{ - static $idx = 0; - /** - * Constructor - */ - public function __construct() - { - parent::__construct('_separator_'.(self::$idx++), ''); - } - - /** @ignore */ - public function GetMenuItem() - { - return array ('label' => '', 'url' => '', 'css_classes' => $this->aCssClasses); - } -} - -/** - * Class for adding an item as a button that browses to the given URL - * - * @package Extensibility - * @api - * @since 2.0 - */ -class URLButtonItem extends URLPopupMenuItem -{ - -} - -/** - * Class for adding an item as a button that runs some JS code - * - * @package Extensibility - * @api - * @since 2.0 - */ -class JSButtonItem extends JSPopupMenuItem -{ - -} - -/** - * Implement this interface to add content to any iTopWebPage - * - * There are 3 places where content can be added: - * - * * The north pane: (normaly empty/hidden) at the top of the page, spanning the whole - * width of the page - * * The south pane: (normaly empty/hidden) at the bottom of the page, spanning the whole - * width of the page - * * The admin banner (two tones gray background) at the left of the global search. - * Limited space, use it for short messages - * - * Each of the methods of this interface is supposed to return the HTML to be inserted at - * the specified place and can use the passed iTopWebPage object to add javascript or CSS definitions - * - * @package Extensibility - * @api - * @since 2.0 - */ -interface iPageUIExtension -{ - /** - * Add content to the North pane - * @param iTopWebPage $oPage The page to insert stuff into. - * @return string The HTML content to add into the page - */ - public function GetNorthPaneHtml(iTopWebPage $oPage); - /** - * Add content to the South pane - * @param iTopWebPage $oPage The page to insert stuff into. - * @return string The HTML content to add into the page - */ - public function GetSouthPaneHtml(iTopWebPage $oPage); - /** - * Add content to the "admin banner" - * @param iTopWebPage $oPage The page to insert stuff into. - * @return string The HTML content to add into the page - */ - public function GetBannerHtml(iTopWebPage $oPage); -} - -/** - * Implement this interface to add content to any enhanced portal page - * - * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now! - * - * @package Extensibility - * @api - * @since 2.4 - */ -interface iPortalUIExtension -{ - const ENUM_PORTAL_EXT_UI_BODY = 'Body'; - const ENUM_PORTAL_EXT_UI_NAVIGATION_MENU = 'NavigationMenu'; - const ENUM_PORTAL_EXT_UI_MAIN_CONTENT = 'MainContent'; - - /** - * Returns an array of CSS file urls - * - * @param \Silex\Application $oApp - * @return array - */ - public function GetCSSFiles(\Silex\Application $oApp); - /** - * Returns inline (raw) CSS - * - * @param \Silex\Application $oApp - * @return string - */ - public function GetCSSInline(\Silex\Application $oApp); - /** - * Returns an array of JS file urls - * - * @param \Silex\Application $oApp - * @return array - */ - public function GetJSFiles(\Silex\Application $oApp); - /** - * Returns raw JS code - * - * @param \Silex\Application $oApp - * @return string - */ - public function GetJSInline(\Silex\Application $oApp); - /** - * Returns raw HTML code to put at the end of the tag - * - * @param \Silex\Application $oApp - * @return string - */ - public function GetBodyHTML(\Silex\Application $oApp); - /** - * Returns raw HTML code to put at the end of the #main-wrapper element - * - * @param \Silex\Application $oApp - * @return string - */ - public function GetMainContentHTML(\Silex\Application $oApp); - /** - * Returns raw HTML code to put at the end of the #topbar and #sidebar elements - * - * @param \Silex\Application $oApp - * @return string - */ - public function GetNavigationMenuHTML(\Silex\Application $oApp); -} - -/** - * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now! - */ -abstract class AbstractPortalUIExtension implements iPortalUIExtension -{ - /** - * @inheritDoc - */ - public function GetCSSFiles(\Silex\Application $oApp) - { - return array(); - } - /** - * @inheritDoc - */ - public function GetCSSInline(\Silex\Application $oApp) - { - return null; - } - /** - * @inheritDoc - */ - public function GetJSFiles(\Silex\Application $oApp) - { - return array(); - } - /** - * @inheritDoc - */ - public function GetJSInline(\Silex\Application $oApp) - { - return null; - } - /** - * @inheritDoc - */ - public function GetBodyHTML(\Silex\Application $oApp) - { - return null; - } - /** - * @inheritDoc - */ - public function GetMainContentHTML(\Silex\Application $oApp) - { - return null; - } - /** - * @inheritDoc - */ - public function GetNavigationMenuHTML(\Silex\Application $oApp) - { - return null; - } -} - -/** - * Implement this interface to add new operations to the REST/JSON web service - * - * @package Extensibility - * @api - * @since 2.0.1 - */ -interface iRestServiceProvider -{ - /** - * Enumerate services delivered by this class - * @param string $sVersion The version (e.g. 1.0) supported by the services - * @return array An array of hash 'verb' => verb, 'description' => description - */ - public function ListOperations($sVersion); - - /** - * Enumerate services delivered by this class - * - * @param string $sVersion The version (e.g. 1.0) supported by the services - * @param string $sVerb - * @param array $aParams - * - * @return RestResult The standardized result structure (at least a message) - */ - public function ExecOperation($sVersion, $sVerb, $aParams); -} - -/** - * Minimal REST response structure. Derive this structure to add response data and error codes. - * - * @package Extensibility - * @api - * @since 2.0.1 - */ -class RestResult -{ - /** - * Result: no issue has been encountered - */ - const OK = 0; - /** - * Result: missing/wrong credentials or the user does not have enough rights to perform the requested operation - */ - const UNAUTHORIZED = 1; - /** - * Result: the parameter 'version' is missing - */ - const MISSING_VERSION = 2; - /** - * Result: the parameter 'json_data' is missing - */ - const MISSING_JSON = 3; - /** - * Result: the input structure is not a valid JSON string - */ - const INVALID_JSON = 4; - /** - * Result: the parameter 'auth_user' is missing, authentication aborted - */ - const MISSING_AUTH_USER = 5; - /** - * Result: the parameter 'auth_pwd' is missing, authentication aborted - */ - const MISSING_AUTH_PWD = 6; - /** - * Result: no operation is available for the specified version - */ - const UNSUPPORTED_VERSION = 10; - /** - * Result: the requested operation is not valid for the specified version - */ - const UNKNOWN_OPERATION = 11; - /** - * Result: the requested operation cannot be performed because it can cause data (integrity) loss - */ - const UNSAFE = 12; - /** - * Result: the operation could not be performed, see the message for troubleshooting - */ - const INTERNAL_ERROR = 100; - - /** - * Default constructor - ok! - */ - public function __construct() - { - $this->code = RestResult::OK; - } - - public $code; - public $message; -} - -/** - * Helpers for implementing REST services - * - * @package Extensibility - * @api - */ -class RestUtils -{ - /** - * Registering tracking information. Any further object modification be associated with the given comment, when the modification gets recorded into the DB - * - * @param StdClass $oData Structured input data. Must contain 'comment'. - * @return void - * @throws Exception - * @api - */ - public static function InitTrackingComment($oData) - { - $sComment = self::GetMandatoryParam($oData, 'comment'); - CMDBObject::SetTrackInfo($sComment); - } - - /** - * Read a mandatory parameter from from a Rest/Json structure. - * - * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName. - * @param string $sParamName Name of the parameter to fetch from the input data - * - * @return mixed parameter value if present - * @throws Exception If the parameter is missing - * @api - */ - public static function GetMandatoryParam($oData, $sParamName) - { - if (isset($oData->$sParamName)) - { - return $oData->$sParamName; - } - else - { - throw new Exception("Missing parameter '$sParamName'"); - } - } - - - /** - * Read an optional parameter from from a Rest/Json structure. - * - * @param StdClass $oData Structured input data. - * @param string $sParamName Name of the parameter to fetch from the input data - * @param mixed $default Default value if the parameter is not found in the input data - * - * @return mixed - * @throws Exception - * @api - */ - public static function GetOptionalParam($oData, $sParamName, $default) - { - if (isset($oData->$sParamName)) - { - return $oData->$sParamName; - } - else - { - return $default; - } - } - - - /** - * Read a class from a Rest/Json structure. - * - * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName. - * @param string $sParamName Name of the parameter to fetch from the input data - * - * @return string - * @throws Exception If the parameter is missing or the class is unknown - * @api - */ - public static function GetClass($oData, $sParamName) - { - $sClass = self::GetMandatoryParam($oData, $sParamName); - if (!MetaModel::IsValidClass($sClass)) - { - throw new Exception("$sParamName: '$sClass' is not a valid class'"); - } - return $sClass; - } - - - /** - * Read a list of attribute codes from a Rest/Json structure. - * - * @param string $sClass Name of the class - * @param StdClass $oData Structured input data. - * @param string $sParamName Name of the parameter to fetch from the input data - * - * @return array of class => list of attributes (see RestResultWithObjects::AddObject that uses it) - * @throws Exception - * @api - */ - public static function GetFieldList($sClass, $oData, $sParamName) - { - $sFields = self::GetOptionalParam($oData, $sParamName, '*'); - $aShowFields = array(); - if ($sFields == '*') - { - foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - $aShowFields[$sClass][] = $sAttCode; - } - } - elseif ($sFields == '*+') - { - foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) - { - foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) - { - $aShowFields[$sRefClass][] = $sAttCode; - } - } - } - else - { - foreach(explode(',', $sFields) as $sAttCode) - { - $sAttCode = trim($sAttCode); - if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode))) - { - throw new Exception("$sParamName: invalid attribute code '$sAttCode'"); - } - $aShowFields[$sClass][] = $sAttCode; - } - } - return $aShowFields; - } - - /** - * Read and interpret object search criteria from a Rest/Json structure - * - * @param string $sClass Name of the class - * @param StdClass $oCriteria Hash of attribute code => value (can be a substructure or a scalar, depending on the nature of the attriute) - * @return object The object found - * @throws Exception If the input structure is not valid or it could not find exactly one object - */ - protected static function FindObjectFromCriteria($sClass, $oCriteria) - { - $aCriteriaReport = array(); - if (isset($oCriteria->finalclass)) - { - if (!MetaModel::IsValidClass($oCriteria->finalclass)) - { - throw new Exception("finalclass: Unknown class '".$oCriteria->finalclass."'"); - } - if (!MetaModel::IsParentClass($sClass, $oCriteria->finalclass)) - { - throw new Exception("finalclass: '".$oCriteria->finalclass."' is not a child class of '$sClass'"); - } - $sClass = $oCriteria->finalclass; - } - $oSearch = new DBObjectSearch($sClass); - foreach ($oCriteria as $sAttCode => $value) - { - $realValue = static::MakeValue($sClass, $sAttCode, $value); - $oSearch->AddCondition($sAttCode, $realValue, '='); - if (is_object($value) || is_array($value)) - { - $value = json_encode($value); - } - $aCriteriaReport[] = "$sAttCode: $value ($realValue)"; - } - $oSet = new DBObjectSet($oSearch); - $iCount = $oSet->Count(); - if ($iCount == 0) - { - throw new Exception("No item found with criteria: ".implode(', ', $aCriteriaReport)); - } - elseif ($iCount > 1) - { - throw new Exception("Several items found ($iCount) with criteria: ".implode(', ', $aCriteriaReport)); - } - $res = $oSet->Fetch(); - return $res; - } - - - /** - * Find an object from a polymorph search specification (Rest/Json) - * - * @param string $sClass Name of the class - * @param mixed $key Either search criteria (substructure), or an object or an OQL string. - * @param bool $bAllowNullValue Allow the cases such as key = 0 or key = {null} and return null then - * @return DBObject The object found - * @throws Exception If the input structure is not valid or it could not find exactly one object - * @api - */ - public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false) - { - if (is_object($key)) - { - $res = static::FindObjectFromCriteria($sClass, $key); - } - elseif (is_numeric($key)) - { - if ($bAllowNullValue && ($key == 0)) - { - $res = null; - } - else - { - $res = MetaModel::GetObject($sClass, $key, false); - if (is_null($res)) - { - throw new Exception("Invalid object $sClass::$key"); - } - } - } - elseif (is_string($key)) - { - // OQL - $oSearch = DBObjectSearch::FromOQL($key); - $oSet = new DBObjectSet($oSearch); - $iCount = $oSet->Count(); - if ($iCount == 0) - { - throw new Exception("No item found for query: $key"); - } - elseif ($iCount > 1) - { - throw new Exception("Several items found ($iCount) for query: $key"); - } - $res = $oSet->Fetch(); - } - else - { - throw new Exception("Wrong format for key"); - } - return $res; - } - - /** - * Search objects from a polymorph search specification (Rest/Json) - * - * @param string $sClass Name of the class - * @param mixed $key Either search criteria (substructure), or an object or an OQL string. - * @return DBObjectSet The search result set - * @throws Exception If the input structure is not valid - */ - public static function GetObjectSetFromKey($sClass, $key) - { - if (is_object($key)) - { - if (isset($key->finalclass)) - { - $sClass = $key->finalclass; - if (!MetaModel::IsValidClass($sClass)) - { - throw new Exception("finalclass: Unknown class '$sClass'"); - } - } - - $oSearch = new DBObjectSearch($sClass); - foreach ($key as $sAttCode => $value) - { - $realValue = static::MakeValue($sClass, $sAttCode, $value); - $oSearch->AddCondition($sAttCode, $realValue, '='); - } - } - elseif (is_numeric($key)) - { - $oSearch = new DBObjectSearch($sClass); - $oSearch->AddCondition('id', $key); - } - elseif (is_string($key)) - { - // OQL - $oSearch = DBObjectSearch::FromOQL($key); - } - else - { - throw new Exception("Wrong format for key"); - } - $oObjectSet = new DBObjectSet($oSearch); - return $oObjectSet; - } - - /** - * Interpret the Rest/Json value and get a valid attribute value - * - * @param string $sClass Name of the class - * @param string $sAttCode Attribute code - * @param mixed $value Depending on the type of attribute (a scalar, or search criteria, or list of related objects...) - * @return mixed The value that can be used with DBObject::Set() - * @throws Exception If the specification of the value is not valid. - * @api - */ - public static function MakeValue($sClass, $sAttCode, $value) - { - try - { - if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) - { - throw new Exception("Unknown attribute"); - } - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef instanceof AttributeExternalKey) - { - $oExtKeyObject = static::FindObjectFromKey($oAttDef->GetTargetClass(), $value, true /* allow null */); - $value = ($oExtKeyObject != null) ? $oExtKeyObject->GetKey() : 0; - } - elseif ($oAttDef instanceof AttributeLinkedSet) - { - if (!is_array($value)) - { - throw new Exception("A link set must be defined by an array of objects"); - } - $sLnkClass = $oAttDef->GetLinkedClass(); - $aLinks = array(); - foreach($value as $oValues) - { - $oLnk = static::MakeObjectFromFields($sLnkClass, $oValues); - $aLinks[] = $oLnk; - } - $value = DBObjectSet::FromArray($sLnkClass, $aLinks); - } - else - { - $value = $oAttDef->FromJSONToValue($value); - } - } - catch (Exception $e) - { - throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); - } - return $value; - } - - /** - * Interpret a Rest/Json structure that defines attribute values, and build an object - * - * @param string $sClass Name of the class - * @param array $aFields A hash of attribute code => value specification. - * @return DBObject The newly created object - * @throws Exception If the specification of the values is not valid - * @api - */ - public static function MakeObjectFromFields($sClass, $aFields) - { - $oObject = MetaModel::NewObject($sClass); - foreach ($aFields as $sAttCode => $value) - { - $realValue = static::MakeValue($sClass, $sAttCode, $value); - try - { - $oObject->Set($sAttCode, $realValue); - } - catch (Exception $e) - { - throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); - } - } - return $oObject; - } - - /** - * Interpret a Rest/Json structure that defines attribute values, and update the given object - * - * @param DBObject $oObject The object being modified - * @param array $aFields A hash of attribute code => value specification. - * @return DBObject The object modified - * @throws Exception If the specification of the values is not valid - * @api - */ - public static function UpdateObjectFromFields($oObject, $aFields) - { - $sClass = get_class($oObject); - foreach ($aFields as $sAttCode => $value) - { - $realValue = static::MakeValue($sClass, $sAttCode, $value); - try - { - $oObject->Set($sAttCode, $realValue); - } - catch (Exception $e) - { - throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); - } - } - return $oObject; - } -} + + +/** + * Management of application plugins + * + * Definition of interfaces that can be implemented to customize iTop. + * You may implement such interfaces in a module file (e.g. main.mymodule.php) + * + * @package Extensibility + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @api + */ + +/** + * Implement this interface to change the behavior of the GUI for some objects. + * + * All methods are invoked by iTop for a given object. There are basically two usages: + * + * 1) To tweak the form of an object, you will have to implement a specific behavior within: + * + * * OnDisplayProperties (bEditMode = true) + * * OnFormSubmit + * * OnFormCancel + * + * 2) To tune the display of the object details, you can use: + * + * * OnDisplayProperties + * * OnDisplayRelations + * * GetIcon + * * GetHilightClass + * + * Please note that some of the APIs can be called several times for a single page displayed. + * Therefore it is not recommended to perform too many operations, such as querying the database. + * A recommended pattern is to cache data by the mean of static members. + * + * @package Extensibility + * @api + */ +interface iApplicationUIExtension +{ + /** + * Invoked when an object is being displayed (wiew or edit) + * + * The method is called right after the main tab has been displayed. + * You can add output to the page, either to change the display, or to add a form input + * + * Example: + * + * if ($bEditMode) + * { + * $oPage->p('Age of the captain: <input type="text" name="captain_age"/>'); + * } + * else + * { + * $oPage->p('Age of the captain: '.$iCaptainAge); + * } + * + * + * @param DBObject $oObject The object being displayed + * @param WebPage $oPage The output context + * @param boolean $bEditMode True if the edition form is being displayed + * @return void + */ + public function OnDisplayProperties($oObject, WebPage $oPage, $bEditMode = false); + + /** + * Invoked when an object is being displayed (wiew or edit) + * + * The method is called rigth after all the tabs have been displayed + * + * @param DBObject $oObject The object being displayed + * @param WebPage $oPage The output context + * @param boolean $bEditMode True if the edition form is being displayed + * @return void + */ + public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false); + + /** + * Invoked when the end-user clicks on Modify from the object edition form + * + * The method is called after the changes from the standard form have been + * taken into account, and before saving the changes into the database. + * + * @param DBObject $oObject The object being edited + * @param string $sFormPrefix Prefix given to the HTML form inputs + * @return void + */ + public function OnFormSubmit($oObject, $sFormPrefix = ''); + + /** + * Invoked when the end-user clicks on Cancel from the object edition form + * + * Implement here any cleanup. This is necessary when you have injected some + * javascript into the edition form, and if that code requires to store temporary data + * (this is the case when a file must be uploaded). + * + * @param string $sTempId Unique temporary identifier made of session_id and transaction_id. It identifies the object in a unique way. + * @return void + */ + public function OnFormCancel($sTempId); + + /** + * Not yet called by the framework! + * + * Sorry, the verb has been reserved. You must implement it, but it is not called as of now. + * + * @param DBObject $oObject The object being displayed + * + * @return string[] desc + */ + public function EnumUsedAttributes($oObject); // Not yet implemented + + /** + * Not yet called by the framework! + * + * Sorry, the verb has been reserved. You must implement it, but it is not called as of now. + * + * @param DBObject $oObject The object being displayed + * @return string Path of the icon, relative to the modules directory. + */ + public function GetIcon($oObject); // Not yet implemented + + /** + * Invoked when the object is displayed alone or within a list + * + * Returns a value influencing the appearance of the object depending on its + * state. + * + * Possible values are: + * + * * HILIGHT_CLASS_CRITICAL + * * HILIGHT_CLASS_WARNING + * * HILIGHT_CLASS_OK + * * HILIGHT_CLASS_NONE + * + * @param DBObject $oObject The object being displayed + * @return integer The value representing the mood of the object + */ + public function GetHilightClass($oObject); + + /** + * Called when building the Actions menu for a single object or a list of objects + * + * Use this to add items to the Actions menu. You will have to specify a label and an URL. + * + * Example: + * + * $oObject = $oSet->fetch(); + * if ($oObject instanceof Sheep) + * { + * return array('View in my app' => 'http://myserver/view_sheeps?id='.$oObject->Get('name')); + * } + * else + * { + * return array(); + * } + * + * + * See also iPopupMenuExtension for greater flexibility + * + * @param DBObjectSet $oSet A set of persistent objects (DBObject) + * @return string[string] + */ + public function EnumAllowedActions(DBObjectSet $oSet); +} + +/** + * Implement this interface to perform specific things when objects are manipulated + * + * Note that those methods will be called when objects are manipulated, either in a programmatic way + * or through the GUI. + * + * @package Extensibility + * @api + */ +interface iApplicationObjectExtension +{ + /** + * Invoked to determine wether an object has been modified in memory + * + * The GUI calls this verb to determine the message that will be displayed to the end-user. + * Anyhow, this API can be called in other contexts such as the CSV import tool. + * + * If the extension returns false, then the framework will perform the usual evaluation. + * Otherwise, the answer is definitively "yes, the object has changed". + * + * @param DBObject $oObject The target object + * @return boolean True if something has changed for the target object + */ + public function OnIsModified($oObject); + + /** + * Invoked to determine wether an object can be written to the database + * + * The GUI calls this verb and reports any issue. + * Anyhow, this API can be called in other contexts such as the CSV import tool. + * + * @param DBObject $oObject The target object + * @return string[] A list of errors message. An error message is made of one line and it can be displayed to the end-user. + */ + public function OnCheckToWrite($oObject); + + /** + * Invoked to determine wether an object can be deleted from the database + * + * The GUI calls this verb and stops the deletion process if any issue is reported. + * + * Please not that it is not possible to cascade deletion by this mean: only stopper issues can be handled. + * + * @param DBObject $oObject The target object + * @return string[] A list of errors message. An error message is made of one line and it can be displayed to the end-user. + */ + public function OnCheckToDelete($oObject); + + /** + * Invoked when an object is updated into the database + * + * The method is called right after the object has been written to the database. + * + * @param DBObject $oObject The target object + * @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page + * @return void + */ + public function OnDBUpdate($oObject, $oChange = null); + + /** + * Invoked when an object is created into the database + * + * The method is called right after the object has been written to the database. + * + * @param DBObject $oObject The target object + * @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page + * @return void + */ + public function OnDBInsert($oObject, $oChange = null); + + /** + * Invoked when an object is deleted from the database + * + * The method is called right before the object will be deleted from the database. + * + * @param DBObject $oObject The target object + * @param CMDBChange|null $oChange A change context. Since 2.0 it is fine to ignore it, as the framework does maintain this information once for all the changes made within the current page + * @return void + */ + public function OnDBDelete($oObject, $oChange = null); +} + +/** + * New extension to add menu items in the "popup" menus inside iTop. Provides a greater flexibility than + * iApplicationUIExtension::EnumAllowedActions. + * + * To add some menus into iTop, declare a class that implements this interface, it will be called automatically + * by the application, as long as the class definition is included somewhere in the code + * + * @package Extensibility + * @api + * @since 2.0 + */ +interface iPopupMenuExtension +{ + /** + * Insert an item into the Actions menu of a list + * + * $param is a DBObjectSet containing the list of objects + */ + const MENU_OBJLIST_ACTIONS = 1; + /** + * Insert an item into the Toolkit menu of a list + * + * $param is a DBObjectSet containing the list of objects + */ + const MENU_OBJLIST_TOOLKIT = 2; + /** + * Insert an item into the Actions menu on an object details page + * + * $param is a DBObject instance: the object currently displayed + */ + const MENU_OBJDETAILS_ACTIONS = 3; + /** + * Insert an item into the Dashboard menu + * + * The dashboad menu is shown on the top right corner when a dashboard + * is being displayed. + * + * $param is a Dashboard instance: the dashboard currently displayed + */ + const MENU_DASHBOARD_ACTIONS = 4; + /** + * Insert an item into the User menu (upper right corner) + * + * $param is null + */ + const MENU_USER_ACTIONS = 5; + /** + * Insert an item into the Action menu on an object item in an objects list in the portal + * + * $param is an array('portal_id' => $sPortalId, 'object' => $oObject) containing the portal id and a DBObject instance (the object on the current line) + */ + const PORTAL_OBJLISTITEM_ACTIONS = 7; + /** + * Insert an item into the Action menu on an object details page in the portal + * + * $param is an array('portal_id' => $sPortalId, 'object' => $oObject) containing the portal id and a DBObject instance (the object currently displayed) + */ + const PORTAL_OBJDETAILS_ACTIONS = 8; + + /** + * Insert an item into the Actions menu of a list in the portal + * Note: This is not implemented yet ! + * + * $param is an array('portal_id' => $sPortalId, 'object_set' => $oSet) containing DBObjectSet containing the list of objects + * @todo + */ + const PORTAL_OBJLIST_ACTIONS = 6; + /** + * Insert an item into the user menu of the portal + * Note: This is not implemented yet ! + * + * $param is the portal id + * @todo + */ + const PORTAL_USER_ACTIONS = 9; + /** + * Insert an item into the navigation menu of the portal + * Note: This is not implemented yet ! + * + * $param is the portal id + * @todo + */ + const PORTAL_MENU_ACTIONS = 10; + + /** + * Get the list of items to be added to a menu. + * + * This method is called by the framework for each menu. + * The items will be inserted in the menu in the order of the returned array. + * @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx + * @param mixed $param Depends on $iMenuId, see the constants defined above + * @return object[] An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu + */ + public static function EnumItems($iMenuId, $param); +} + +/** + * Base class for the various types of custom menus + * + * @package Extensibility + * @internal + * @since 2.0 + */ +abstract class ApplicationPopupMenuItem +{ + /** @ignore */ + protected $sUID; + /** @ignore */ + protected $sLabel; + /** @ignore */ + protected $aCssClasses; + + /** + * Constructor + * + * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough + * @param string $sLabel The display label of the menu (must be localized) + * @param array $aCssClasses The CSS classes to add to the menu + */ + public function __construct($sUID, $sLabel) + { + $this->sUID = $sUID; + $this->sLabel = $sLabel; + $this->aCssClasses = array(); + } + + /** + * Get the UID + * + * @return string The unique identifier + * @ignore + */ + public function GetUID() + { + return $this->sUID; + } + + /** + * Get the label + * + * @return string The label + * @ignore + */ + public function GetLabel() + { + return $this->sLabel; + } + + /** + * Get the CSS classes + * + * @return array + * @ignore + */ + public function GetCssClasses() + { + return $this->aCssClasses; + } + + /** + * @param $aCssClasses + */ + public function SetCssClasses($aCssClasses) + { + $this->aCssClasses = $aCssClasses; + } + + /** + * Adds a CSS class to the CSS classes that will be put on the menu item + * + * @param $sCssClass + */ + public function AddCssClass($sCssClass) + { + $this->aCssClasses[] = $sCssClass; + } + + /** + * Returns the components to create a popup menu item in HTML + * + * @return array A hash array: array('label' => , 'url' => , 'target' => , 'onclick' => ) + * @ignore + */ + abstract public function GetMenuItem(); + + /** @ignore */ + public function GetLinkedScripts() + { + return array(); + } +} + +/** + * Class for adding an item into a popup menu that browses to the given URL + * + * @package Extensibility + * @api + * @since 2.0 + */ +class URLPopupMenuItem extends ApplicationPopupMenuItem +{ + /** @ignore */ + protected $sURL; + /** @ignore */ + protected $sTarget; + + /** + * Constructor + * + * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough + * @param string $sLabel The display label of the menu (must be localized) + * @param string $sURL If the menu is an hyperlink, provide the absolute hyperlink here + * @param string $sTarget In case the menu is an hyperlink and a specific target is needed (_blank for example), pass it here + */ + public function __construct($sUID, $sLabel, $sURL, $sTarget = '_top') + { + parent::__construct($sUID, $sLabel); + $this->sURL = $sURL; + $this->sTarget = $sTarget; + } + + /** @ignore */ + public function GetMenuItem() + { + return array ('label' => $this->GetLabel(), 'url' => $this->sURL, 'target' => $this->sTarget, 'css_classes' => $this->aCssClasses); + } +} + +/** + * Class for adding an item into a popup menu that triggers some Javascript code + * + * @package Extensibility + * @api + * @since 2.0 + */ +class JSPopupMenuItem extends ApplicationPopupMenuItem +{ + /** @ignore */ + protected $sJSCode; + /** @ignore */ + protected $aIncludeJSFiles; + + /** + * Class for adding an item that triggers some Javascript code + * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough + * @param string $sLabel The display label of the menu (must be localized) + * @param string $sJSCode In case the menu consists in executing some havascript code inside the page, pass it here. If supplied $sURL ans $sTarget will be ignored + * @param array $aIncludeJSFiles An array of file URLs to be included (once) to provide some JS libraries for the page. + */ + public function __construct($sUID, $sLabel, $sJSCode, $aIncludeJSFiles = array()) + { + parent::__construct($sUID, $sLabel); + $this->sJSCode = $sJSCode; + $this->aIncludeJSFiles = $aIncludeJSFiles; + } + + /** @ignore */ + public function GetMenuItem() + { + // Note: the semicolumn is a must here! + return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode.'; return false;', 'url' => '#', 'css_classes' => $this->aCssClasses); + } + + /** @ignore */ + public function GetLinkedScripts() + { + return $this->aIncludeJSFiles; + } +} + +/** + * Class for adding a separator (horizontal line, not selectable) the output + * will automatically reduce several consecutive separators to just one + * + * @package Extensibility + * @api + * @since 2.0 + */ +class SeparatorPopupMenuItem extends ApplicationPopupMenuItem +{ + static $idx = 0; + /** + * Constructor + */ + public function __construct() + { + parent::__construct('_separator_'.(self::$idx++), ''); + } + + /** @ignore */ + public function GetMenuItem() + { + return array ('label' => '', 'url' => '', 'css_classes' => $this->aCssClasses); + } +} + +/** + * Class for adding an item as a button that browses to the given URL + * + * @package Extensibility + * @api + * @since 2.0 + */ +class URLButtonItem extends URLPopupMenuItem +{ + +} + +/** + * Class for adding an item as a button that runs some JS code + * + * @package Extensibility + * @api + * @since 2.0 + */ +class JSButtonItem extends JSPopupMenuItem +{ + +} + +/** + * Implement this interface to add content to any iTopWebPage + * + * There are 3 places where content can be added: + * + * * The north pane: (normaly empty/hidden) at the top of the page, spanning the whole + * width of the page + * * The south pane: (normaly empty/hidden) at the bottom of the page, spanning the whole + * width of the page + * * The admin banner (two tones gray background) at the left of the global search. + * Limited space, use it for short messages + * + * Each of the methods of this interface is supposed to return the HTML to be inserted at + * the specified place and can use the passed iTopWebPage object to add javascript or CSS definitions + * + * @package Extensibility + * @api + * @since 2.0 + */ +interface iPageUIExtension +{ + /** + * Add content to the North pane + * @param iTopWebPage $oPage The page to insert stuff into. + * @return string The HTML content to add into the page + */ + public function GetNorthPaneHtml(iTopWebPage $oPage); + /** + * Add content to the South pane + * @param iTopWebPage $oPage The page to insert stuff into. + * @return string The HTML content to add into the page + */ + public function GetSouthPaneHtml(iTopWebPage $oPage); + /** + * Add content to the "admin banner" + * @param iTopWebPage $oPage The page to insert stuff into. + * @return string The HTML content to add into the page + */ + public function GetBannerHtml(iTopWebPage $oPage); +} + +/** + * Implement this interface to add content to any enhanced portal page + * + * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now! + * + * @package Extensibility + * @api + * @since 2.4 + */ +interface iPortalUIExtension +{ + const ENUM_PORTAL_EXT_UI_BODY = 'Body'; + const ENUM_PORTAL_EXT_UI_NAVIGATION_MENU = 'NavigationMenu'; + const ENUM_PORTAL_EXT_UI_MAIN_CONTENT = 'MainContent'; + + /** + * Returns an array of CSS file urls + * + * @param \Silex\Application $oApp + * @return array + */ + public function GetCSSFiles(\Silex\Application $oApp); + /** + * Returns inline (raw) CSS + * + * @param \Silex\Application $oApp + * @return string + */ + public function GetCSSInline(\Silex\Application $oApp); + /** + * Returns an array of JS file urls + * + * @param \Silex\Application $oApp + * @return array + */ + public function GetJSFiles(\Silex\Application $oApp); + /** + * Returns raw JS code + * + * @param \Silex\Application $oApp + * @return string + */ + public function GetJSInline(\Silex\Application $oApp); + /** + * Returns raw HTML code to put at the end of the tag + * + * @param \Silex\Application $oApp + * @return string + */ + public function GetBodyHTML(\Silex\Application $oApp); + /** + * Returns raw HTML code to put at the end of the #main-wrapper element + * + * @param \Silex\Application $oApp + * @return string + */ + public function GetMainContentHTML(\Silex\Application $oApp); + /** + * Returns raw HTML code to put at the end of the #topbar and #sidebar elements + * + * @param \Silex\Application $oApp + * @return string + */ + public function GetNavigationMenuHTML(\Silex\Application $oApp); +} + +/** + * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now! + */ +abstract class AbstractPortalUIExtension implements iPortalUIExtension +{ + /** + * @inheritDoc + */ + public function GetCSSFiles(\Silex\Application $oApp) + { + return array(); + } + /** + * @inheritDoc + */ + public function GetCSSInline(\Silex\Application $oApp) + { + return null; + } + /** + * @inheritDoc + */ + public function GetJSFiles(\Silex\Application $oApp) + { + return array(); + } + /** + * @inheritDoc + */ + public function GetJSInline(\Silex\Application $oApp) + { + return null; + } + /** + * @inheritDoc + */ + public function GetBodyHTML(\Silex\Application $oApp) + { + return null; + } + /** + * @inheritDoc + */ + public function GetMainContentHTML(\Silex\Application $oApp) + { + return null; + } + /** + * @inheritDoc + */ + public function GetNavigationMenuHTML(\Silex\Application $oApp) + { + return null; + } +} + +/** + * Implement this interface to add new operations to the REST/JSON web service + * + * @package Extensibility + * @api + * @since 2.0.1 + */ +interface iRestServiceProvider +{ + /** + * Enumerate services delivered by this class + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return array An array of hash 'verb' => verb, 'description' => description + */ + public function ListOperations($sVersion); + + /** + * Enumerate services delivered by this class + * + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @param string $sVerb + * @param array $aParams + * + * @return RestResult The standardized result structure (at least a message) + */ + public function ExecOperation($sVersion, $sVerb, $aParams); +} + +/** + * Minimal REST response structure. Derive this structure to add response data and error codes. + * + * @package Extensibility + * @api + * @since 2.0.1 + */ +class RestResult +{ + /** + * Result: no issue has been encountered + */ + const OK = 0; + /** + * Result: missing/wrong credentials or the user does not have enough rights to perform the requested operation + */ + const UNAUTHORIZED = 1; + /** + * Result: the parameter 'version' is missing + */ + const MISSING_VERSION = 2; + /** + * Result: the parameter 'json_data' is missing + */ + const MISSING_JSON = 3; + /** + * Result: the input structure is not a valid JSON string + */ + const INVALID_JSON = 4; + /** + * Result: the parameter 'auth_user' is missing, authentication aborted + */ + const MISSING_AUTH_USER = 5; + /** + * Result: the parameter 'auth_pwd' is missing, authentication aborted + */ + const MISSING_AUTH_PWD = 6; + /** + * Result: no operation is available for the specified version + */ + const UNSUPPORTED_VERSION = 10; + /** + * Result: the requested operation is not valid for the specified version + */ + const UNKNOWN_OPERATION = 11; + /** + * Result: the requested operation cannot be performed because it can cause data (integrity) loss + */ + const UNSAFE = 12; + /** + * Result: the operation could not be performed, see the message for troubleshooting + */ + const INTERNAL_ERROR = 100; + + /** + * Default constructor - ok! + */ + public function __construct() + { + $this->code = RestResult::OK; + } + + public $code; + public $message; +} + +/** + * Helpers for implementing REST services + * + * @package Extensibility + * @api + */ +class RestUtils +{ + /** + * Registering tracking information. Any further object modification be associated with the given comment, when the modification gets recorded into the DB + * + * @param StdClass $oData Structured input data. Must contain 'comment'. + * @return void + * @throws Exception + * @api + */ + public static function InitTrackingComment($oData) + { + $sComment = self::GetMandatoryParam($oData, 'comment'); + CMDBObject::SetTrackInfo($sComment); + } + + /** + * Read a mandatory parameter from from a Rest/Json structure. + * + * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName. + * @param string $sParamName Name of the parameter to fetch from the input data + * + * @return mixed parameter value if present + * @throws Exception If the parameter is missing + * @api + */ + public static function GetMandatoryParam($oData, $sParamName) + { + if (isset($oData->$sParamName)) + { + return $oData->$sParamName; + } + else + { + throw new Exception("Missing parameter '$sParamName'"); + } + } + + + /** + * Read an optional parameter from from a Rest/Json structure. + * + * @param StdClass $oData Structured input data. + * @param string $sParamName Name of the parameter to fetch from the input data + * @param mixed $default Default value if the parameter is not found in the input data + * + * @return mixed + * @throws Exception + * @api + */ + public static function GetOptionalParam($oData, $sParamName, $default) + { + if (isset($oData->$sParamName)) + { + return $oData->$sParamName; + } + else + { + return $default; + } + } + + + /** + * Read a class from a Rest/Json structure. + * + * @param StdClass $oData Structured input data. Must contain the entry defined by sParamName. + * @param string $sParamName Name of the parameter to fetch from the input data + * + * @return string + * @throws Exception If the parameter is missing or the class is unknown + * @api + */ + public static function GetClass($oData, $sParamName) + { + $sClass = self::GetMandatoryParam($oData, $sParamName); + if (!MetaModel::IsValidClass($sClass)) + { + throw new Exception("$sParamName: '$sClass' is not a valid class'"); + } + return $sClass; + } + + + /** + * Read a list of attribute codes from a Rest/Json structure. + * + * @param string $sClass Name of the class + * @param StdClass $oData Structured input data. + * @param string $sParamName Name of the parameter to fetch from the input data + * + * @return array of class => list of attributes (see RestResultWithObjects::AddObject that uses it) + * @throws Exception + * @api + */ + public static function GetFieldList($sClass, $oData, $sParamName) + { + $sFields = self::GetOptionalParam($oData, $sParamName, '*'); + $aShowFields = array(); + if ($sFields == '*') + { + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + $aShowFields[$sClass][] = $sAttCode; + } + } + elseif ($sFields == '*+') + { + foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) + { + foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) + { + $aShowFields[$sRefClass][] = $sAttCode; + } + } + } + else + { + foreach(explode(',', $sFields) as $sAttCode) + { + $sAttCode = trim($sAttCode); + if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode))) + { + throw new Exception("$sParamName: invalid attribute code '$sAttCode'"); + } + $aShowFields[$sClass][] = $sAttCode; + } + } + return $aShowFields; + } + + /** + * Read and interpret object search criteria from a Rest/Json structure + * + * @param string $sClass Name of the class + * @param StdClass $oCriteria Hash of attribute code => value (can be a substructure or a scalar, depending on the nature of the attriute) + * @return object The object found + * @throws Exception If the input structure is not valid or it could not find exactly one object + */ + protected static function FindObjectFromCriteria($sClass, $oCriteria) + { + $aCriteriaReport = array(); + if (isset($oCriteria->finalclass)) + { + if (!MetaModel::IsValidClass($oCriteria->finalclass)) + { + throw new Exception("finalclass: Unknown class '".$oCriteria->finalclass."'"); + } + if (!MetaModel::IsParentClass($sClass, $oCriteria->finalclass)) + { + throw new Exception("finalclass: '".$oCriteria->finalclass."' is not a child class of '$sClass'"); + } + $sClass = $oCriteria->finalclass; + } + $oSearch = new DBObjectSearch($sClass); + foreach ($oCriteria as $sAttCode => $value) + { + $realValue = static::MakeValue($sClass, $sAttCode, $value); + $oSearch->AddCondition($sAttCode, $realValue, '='); + if (is_object($value) || is_array($value)) + { + $value = json_encode($value); + } + $aCriteriaReport[] = "$sAttCode: $value ($realValue)"; + } + $oSet = new DBObjectSet($oSearch); + $iCount = $oSet->Count(); + if ($iCount == 0) + { + throw new Exception("No item found with criteria: ".implode(', ', $aCriteriaReport)); + } + elseif ($iCount > 1) + { + throw new Exception("Several items found ($iCount) with criteria: ".implode(', ', $aCriteriaReport)); + } + $res = $oSet->Fetch(); + return $res; + } + + + /** + * Find an object from a polymorph search specification (Rest/Json) + * + * @param string $sClass Name of the class + * @param mixed $key Either search criteria (substructure), or an object or an OQL string. + * @param bool $bAllowNullValue Allow the cases such as key = 0 or key = {null} and return null then + * @return DBObject The object found + * @throws Exception If the input structure is not valid or it could not find exactly one object + * @api + */ + public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false) + { + if (is_object($key)) + { + $res = static::FindObjectFromCriteria($sClass, $key); + } + elseif (is_numeric($key)) + { + if ($bAllowNullValue && ($key == 0)) + { + $res = null; + } + else + { + $res = MetaModel::GetObject($sClass, $key, false); + if (is_null($res)) + { + throw new Exception("Invalid object $sClass::$key"); + } + } + } + elseif (is_string($key)) + { + // OQL + $oSearch = DBObjectSearch::FromOQL($key); + $oSet = new DBObjectSet($oSearch); + $iCount = $oSet->Count(); + if ($iCount == 0) + { + throw new Exception("No item found for query: $key"); + } + elseif ($iCount > 1) + { + throw new Exception("Several items found ($iCount) for query: $key"); + } + $res = $oSet->Fetch(); + } + else + { + throw new Exception("Wrong format for key"); + } + return $res; + } + + /** + * Search objects from a polymorph search specification (Rest/Json) + * + * @param string $sClass Name of the class + * @param mixed $key Either search criteria (substructure), or an object or an OQL string. + * @return DBObjectSet The search result set + * @throws Exception If the input structure is not valid + */ + public static function GetObjectSetFromKey($sClass, $key) + { + if (is_object($key)) + { + if (isset($key->finalclass)) + { + $sClass = $key->finalclass; + if (!MetaModel::IsValidClass($sClass)) + { + throw new Exception("finalclass: Unknown class '$sClass'"); + } + } + + $oSearch = new DBObjectSearch($sClass); + foreach ($key as $sAttCode => $value) + { + $realValue = static::MakeValue($sClass, $sAttCode, $value); + $oSearch->AddCondition($sAttCode, $realValue, '='); + } + } + elseif (is_numeric($key)) + { + $oSearch = new DBObjectSearch($sClass); + $oSearch->AddCondition('id', $key); + } + elseif (is_string($key)) + { + // OQL + $oSearch = DBObjectSearch::FromOQL($key); + } + else + { + throw new Exception("Wrong format for key"); + } + $oObjectSet = new DBObjectSet($oSearch); + return $oObjectSet; + } + + /** + * Interpret the Rest/Json value and get a valid attribute value + * + * @param string $sClass Name of the class + * @param string $sAttCode Attribute code + * @param mixed $value Depending on the type of attribute (a scalar, or search criteria, or list of related objects...) + * @return mixed The value that can be used with DBObject::Set() + * @throws Exception If the specification of the value is not valid. + * @api + */ + public static function MakeValue($sClass, $sAttCode, $value) + { + try + { + if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + throw new Exception("Unknown attribute"); + } + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef instanceof AttributeExternalKey) + { + $oExtKeyObject = static::FindObjectFromKey($oAttDef->GetTargetClass(), $value, true /* allow null */); + $value = ($oExtKeyObject != null) ? $oExtKeyObject->GetKey() : 0; + } + elseif ($oAttDef instanceof AttributeLinkedSet) + { + if (!is_array($value)) + { + throw new Exception("A link set must be defined by an array of objects"); + } + $sLnkClass = $oAttDef->GetLinkedClass(); + $aLinks = array(); + foreach($value as $oValues) + { + $oLnk = static::MakeObjectFromFields($sLnkClass, $oValues); + $aLinks[] = $oLnk; + } + $value = DBObjectSet::FromArray($sLnkClass, $aLinks); + } + else + { + $value = $oAttDef->FromJSONToValue($value); + } + } + catch (Exception $e) + { + throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); + } + return $value; + } + + /** + * Interpret a Rest/Json structure that defines attribute values, and build an object + * + * @param string $sClass Name of the class + * @param array $aFields A hash of attribute code => value specification. + * @return DBObject The newly created object + * @throws Exception If the specification of the values is not valid + * @api + */ + public static function MakeObjectFromFields($sClass, $aFields) + { + $oObject = MetaModel::NewObject($sClass); + foreach ($aFields as $sAttCode => $value) + { + $realValue = static::MakeValue($sClass, $sAttCode, $value); + try + { + $oObject->Set($sAttCode, $realValue); + } + catch (Exception $e) + { + throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); + } + } + return $oObject; + } + + /** + * Interpret a Rest/Json structure that defines attribute values, and update the given object + * + * @param DBObject $oObject The object being modified + * @param array $aFields A hash of attribute code => value specification. + * @return DBObject The object modified + * @throws Exception If the specification of the values is not valid + * @api + */ + public static function UpdateObjectFromFields($oObject, $aFields) + { + $sClass = get_class($oObject); + foreach ($aFields as $sAttCode => $value) + { + $realValue = static::MakeValue($sClass, $sAttCode, $value); + try + { + $oObject->Set($sAttCode, $realValue); + } + catch (Exception $e) + { + throw new Exception("$sAttCode: ".$e->getMessage(), $e->getCode()); + } + } + return $oObject; + } +} diff --git a/application/audit.category.class.inc.php b/application/audit.category.class.inc.php index 40f15f9d1..14d80f0ec 100644 --- a/application/audit.category.class.inc.php +++ b/application/audit.category.class.inc.php @@ -1,60 +1,60 @@ - - - -/** - * This class manages the audit "categories". Each category defines a set of objects - * to check and is linked to a set of rules that determine the valid or invalid objects - * inside the set - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); - -class AuditCategory extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "application, grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array('name'), - "db_table" => "priv_auditcategory", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_AddAttribute(new AttributeString("name", array("description"=>"Short name for this category", "allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("definition_set", array("allowed_values"=>null, "sql"=>"definition_set", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeLinkedSet("rules_list", array("linked_class"=>"AuditRule", "ext_key_to_me"=>"category_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array(), "edit_mode" => LINKSET_EDITMODE_INPLACE, "tracking_level" => LINKSET_TRACKING_ALL))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'definition_set', 'rules_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description', )); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('description', 'definition_set')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array('name', 'description')); // Criteria of the default search form - } -} -?> + + + +/** + * This class manages the audit "categories". Each category defines a set of objects + * to check and is linked to a set of rules that determine the valid or invalid objects + * inside the set + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); + +class AuditCategory extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "application, grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array('name'), + "db_table" => "priv_auditcategory", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeString("name", array("description"=>"Short name for this category", "allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("definition_set", array("allowed_values"=>null, "sql"=>"definition_set", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeLinkedSet("rules_list", array("linked_class"=>"AuditRule", "ext_key_to_me"=>"category_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array(), "edit_mode" => LINKSET_EDITMODE_INPLACE, "tracking_level" => LINKSET_TRACKING_ALL))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'definition_set', 'rules_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description', )); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('description', 'definition_set')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array('name', 'description')); // Criteria of the default search form + } +} +?> diff --git a/application/audit.rule.class.inc.php b/application/audit.rule.class.inc.php index 11796a49a..8d67dd471 100644 --- a/application/audit.rule.class.inc.php +++ b/application/audit.rule.class.inc.php @@ -1,64 +1,64 @@ - - - -/** - * This class manages the audit "rule" linked to a given audit category. - * Each rule is based on an OQL expression that returns either the "good" objects - * or the "bad" ones. The core audit engines computes the complement to the definition - * set when needed to obtain either the valid objects, or the ones with an error - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/audit.category.class.inc.php'); - -class AuditRule extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "application, grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array('name'), - "db_table" => "priv_auditrule", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("query", array("allowed_values"=>null, "sql"=>"query", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("valid_flag", array("allowed_values"=>new ValueSetEnum('true,false'), "sql"=>"valid_flag", "default_value"=>"true", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("category_id", array("allowed_values"=>null, "sql"=>"category_id", "targetclass"=>"AuditCategory", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("category_name", array("allowed_values"=>null, "extkey_attcode"=> 'category_id', "target_attcode"=>"name"))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('category_id', 'name', 'description', 'query', 'valid_flag')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('category_id', 'description', 'valid_flag')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('category_id', 'name', 'description', 'valid_flag', 'query')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'category_id')); // Criteria of the advanced search form - } -} -?> + + + +/** + * This class manages the audit "rule" linked to a given audit category. + * Each rule is based on an OQL expression that returns either the "good" objects + * or the "bad" ones. The core audit engines computes the complement to the definition + * set when needed to obtain either the valid objects, or the ones with an error + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/audit.category.class.inc.php'); + +class AuditRule extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "application, grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array('name'), + "db_table" => "priv_auditrule", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("query", array("allowed_values"=>null, "sql"=>"query", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("valid_flag", array("allowed_values"=>new ValueSetEnum('true,false'), "sql"=>"valid_flag", "default_value"=>"true", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("category_id", array("allowed_values"=>null, "sql"=>"category_id", "targetclass"=>"AuditCategory", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("category_name", array("allowed_values"=>null, "extkey_attcode"=> 'category_id', "target_attcode"=>"name"))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('category_id', 'name', 'description', 'query', 'valid_flag')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('category_id', 'description', 'valid_flag')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('category_id', 'name', 'description', 'valid_flag', 'query')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'category_id')); // Criteria of the advanced search form + } +} +?> diff --git a/application/capturewebpage.class.inc.php b/application/capturewebpage.class.inc.php index fff7d1f2c..287345167 100644 --- a/application/capturewebpage.class.inc.php +++ b/application/capturewebpage.class.inc.php @@ -1,84 +1,84 @@ - - -/** - * Adapter class: when an API requires WebPage and you want to produce something else - * - * @copyright Copyright (C) 2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/webpage.class.inc.php"); - -class CaptureWebPage extends WebPage -{ - protected $aReadyScripts; - - function __construct() - { - parent::__construct('capture web page'); - $this->aReadyScripts = array(); - } - - public function GetHtml() - { - $trash = $this->ob_get_clean_safe(); - return $this->s_content; - } - - public function GetJS() - { - $sRet = implode("\n", $this->a_scripts); - if (!empty($this->s_deferred_content)) - { - $sRet .= "\n\$('body').append('".addslashes(str_replace("\n", '', $this->s_deferred_content))."');"; - } - return $sRet; - } - - public function GetReadyJS() - { - return "\$(document).ready(function() {\n".implode("\n", $this->aReadyScripts)."\n});"; - } - - public function GetCSS() - { - return $this->a_styles; - } - - public function GetJSFiles() - { - return $this->a_linked_scripts; - } - - public function GetCSSFiles() - { - return $this->a_linked_stylesheets; - } - - public function output() - { - throw new Exception(__method__.' should not be called'); - } - - public function add_ready_script($sScript) - { - $this->aReadyScripts[] = $sScript; - } -} - + + +/** + * Adapter class: when an API requires WebPage and you want to produce something else + * + * @copyright Copyright (C) 2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); + +class CaptureWebPage extends WebPage +{ + protected $aReadyScripts; + + function __construct() + { + parent::__construct('capture web page'); + $this->aReadyScripts = array(); + } + + public function GetHtml() + { + $trash = $this->ob_get_clean_safe(); + return $this->s_content; + } + + public function GetJS() + { + $sRet = implode("\n", $this->a_scripts); + if (!empty($this->s_deferred_content)) + { + $sRet .= "\n\$('body').append('".addslashes(str_replace("\n", '', $this->s_deferred_content))."');"; + } + return $sRet; + } + + public function GetReadyJS() + { + return "\$(document).ready(function() {\n".implode("\n", $this->aReadyScripts)."\n});"; + } + + public function GetCSS() + { + return $this->a_styles; + } + + public function GetJSFiles() + { + return $this->a_linked_scripts; + } + + public function GetCSSFiles() + { + return $this->a_linked_stylesheets; + } + + public function output() + { + throw new Exception(__method__.' should not be called'); + } + + public function add_ready_script($sScript) + { + $this->aReadyScripts[] = $sScript; + } +} + diff --git a/application/clipage.class.inc.php b/application/clipage.class.inc.php index 89f43b3db..25153d17e 100644 --- a/application/clipage.class.inc.php +++ b/application/clipage.class.inc.php @@ -1,97 +1,97 @@ - - - -/** - * CLI page - * The page adds the content-type text/XML and the encoding into the headers - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/webpage.class.inc.php"); - -class CLIPage implements Page -{ - function __construct($s_title) - { - } - - public function output() - { - if (class_exists('DBSearch')) - { - DBSearch::RecordQueryTrace(); - } - if (class_exists('ExecutionKPI')) - { - ExecutionKPI::ReportStats(); - } - } - - public function add($sText) - { - echo $sText; - } - - public function p($sText) - { - echo $sText."\n"; - } - - public function pre($sText) - { - echo $sText."\n"; - } - - public function add_comment($sText) - { - echo "#".$sText."\n"; - } - - public function table($aConfig, $aData, $aParams = array()) - { - $aCells = array(); - foreach($aConfig as $sName=>$aDef) - { - if (strlen($aDef['description']) > 0) - { - $aCells[] = $aDef['label'].' ('.$aDef['description'].')'; - } - else - { - $aCells[] = $aDef['label']; - } - } - echo implode(';', $aCells)."\n"; - - foreach($aData as $aRow) - { - $aCells = array(); - foreach($aConfig as $sName=>$aAttribs) - { - $sValue = $aRow["$sName"]; - $aCells[] = $sValue; - } - echo implode(';', $aCells)."\n"; - } - } -} - -?> + + + +/** + * CLI page + * The page adds the content-type text/XML and the encoding into the headers + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); + +class CLIPage implements Page +{ + function __construct($s_title) + { + } + + public function output() + { + if (class_exists('DBSearch')) + { + DBSearch::RecordQueryTrace(); + } + if (class_exists('ExecutionKPI')) + { + ExecutionKPI::ReportStats(); + } + } + + public function add($sText) + { + echo $sText; + } + + public function p($sText) + { + echo $sText."\n"; + } + + public function pre($sText) + { + echo $sText."\n"; + } + + public function add_comment($sText) + { + echo "#".$sText."\n"; + } + + public function table($aConfig, $aData, $aParams = array()) + { + $aCells = array(); + foreach($aConfig as $sName=>$aDef) + { + if (strlen($aDef['description']) > 0) + { + $aCells[] = $aDef['label'].' ('.$aDef['description'].')'; + } + else + { + $aCells[] = $aDef['label']; + } + } + echo implode(';', $aCells)."\n"; + + foreach($aData as $aRow) + { + $aCells = array(); + foreach($aConfig as $sName=>$aAttribs) + { + $sValue = $aRow["$sName"]; + $aCells[] = $sValue; + } + echo implode(';', $aCells)."\n"; + } + } +} + +?> diff --git a/application/csvpage.class.inc.php b/application/csvpage.class.inc.php index a39ec60a9..e16113408 100644 --- a/application/csvpage.class.inc.php +++ b/application/csvpage.class.inc.php @@ -1,111 +1,111 @@ - - - -/** - * Simple web page with no includes or fancy formatting, useful to generateXML documents - * The page adds the content-type text/XML and the encoding into the headers - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/webpage.class.inc.php"); - -class CSVPage extends WebPage -{ - function __construct($s_title) - { - parent::__construct($s_title); - $this->add_header("Content-type: text/plain; charset=utf-8"); - $this->add_header("Cache-control: no-cache"); - //$this->add_header("Content-Transfer-Encoding: binary"); - } - - public function output() - { - $this->add_header("Content-Length: ".strlen(trim($this->s_content))); - - // Get the unexpected output but do nothing with it - $sTrash = $this->ob_get_clean_safe(); - - foreach($this->a_headers as $s_header) - { - header($s_header); - } - echo trim($this->s_content); - echo "\n"; - - if (class_exists('DBSearch')) - { - DBSearch::RecordQueryTrace(); - } - if (class_exists('ExecutionKPI')) - { - ExecutionKPI::ReportStats(); - } - } - - public function small_p($sText) - { - } - - public function add($sText) - { - $this->s_content .= $sText; - } - - public function p($sText) - { - $this->s_content .= $sText."\n"; - } - - public function add_comment($sText) - { - $this->s_content .= "#".$sText."\n"; - } - - public function table($aConfig, $aData, $aParams = array()) - { - $aCells = array(); - foreach($aConfig as $sName=>$aDef) - { - if (strlen($aDef['description']) > 0) - { - $aCells[] = $aDef['label'].' ('.$aDef['description'].')'; - } - else - { - $aCells[] = $aDef['label']; - } - } - $this->s_content .= implode(';', $aCells)."\n"; - - foreach($aData as $aRow) - { - $aCells = array(); - foreach($aConfig as $sName=>$aAttribs) - { - $sValue = $aRow["$sName"]; - $aCells[] = $sValue; - } - $this->s_content .= implode(';', $aCells)."\n"; - } - } -} - + + + +/** + * Simple web page with no includes or fancy formatting, useful to generateXML documents + * The page adds the content-type text/XML and the encoding into the headers + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); + +class CSVPage extends WebPage +{ + function __construct($s_title) + { + parent::__construct($s_title); + $this->add_header("Content-type: text/plain; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + //$this->add_header("Content-Transfer-Encoding: binary"); + } + + public function output() + { + $this->add_header("Content-Length: ".strlen(trim($this->s_content))); + + // Get the unexpected output but do nothing with it + $sTrash = $this->ob_get_clean_safe(); + + foreach($this->a_headers as $s_header) + { + header($s_header); + } + echo trim($this->s_content); + echo "\n"; + + if (class_exists('DBSearch')) + { + DBSearch::RecordQueryTrace(); + } + if (class_exists('ExecutionKPI')) + { + ExecutionKPI::ReportStats(); + } + } + + public function small_p($sText) + { + } + + public function add($sText) + { + $this->s_content .= $sText; + } + + public function p($sText) + { + $this->s_content .= $sText."\n"; + } + + public function add_comment($sText) + { + $this->s_content .= "#".$sText."\n"; + } + + public function table($aConfig, $aData, $aParams = array()) + { + $aCells = array(); + foreach($aConfig as $sName=>$aDef) + { + if (strlen($aDef['description']) > 0) + { + $aCells[] = $aDef['label'].' ('.$aDef['description'].')'; + } + else + { + $aCells[] = $aDef['label']; + } + } + $this->s_content .= implode(';', $aCells)."\n"; + + foreach($aData as $aRow) + { + $aCells = array(); + foreach($aConfig as $sName=>$aAttribs) + { + $sValue = $aRow["$sName"]; + $aCells[] = $sValue; + } + $this->s_content .= implode(';', $aCells)."\n"; + } + } +} + diff --git a/application/iotask.class.inc.php b/application/iotask.class.inc.php index 59b647419..647340aa8 100644 --- a/application/iotask.class.inc.php +++ b/application/iotask.class.inc.php @@ -1,69 +1,69 @@ - - - -/** - * Persistent class InputOutputTask - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); - -/** - * This class manages the input/output tasks - * for synchronizing information with external data sources - */ -class InputOutputTask extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "application", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_iotask", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("category", array("allowed_values"=>new ValueSetEnum('Input, Ouput'), "sql"=>"category", "default_value"=>"Input", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("source_type", array("allowed_values"=>new ValueSetEnum('File, Database, Web Service'), "sql"=>"source_type", "default_value"=>"File", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("source_subtype", array("allowed_values"=>new ValueSetEnum('Oracle, MySQL, Postgress, MSSQL, SOAP, HTTP-Get, HTTP-Post, XML/RPC, CSV, XML, Excel'), "sql"=>"source_subtype", "default_value"=>"CSV", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("source_path", array("allowed_values"=>null, "sql"=>"source_path", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeClass("objects_class", array("class_category"=>"", "more_values"=>"", "sql"=>"objects_class", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("test_mode", array("allowed_values"=>new ValueSetEnum('Yes,No'), "sql"=>"test_mode", "default_value"=>'No', "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("verbose_mode", array("allowed_values"=>new ValueSetEnum('Yes,No'), "sql"=>"verbose_mode", "default_value" => 'No', "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("options", array("allowed_values"=>new ValueSetEnum('Full, Update Only, Creation Only'), "sql"=>"options", "default_value"=> 'Full', "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'category', 'objects_class', 'source_type', 'source_subtype', 'source_path' , 'options', 'test_mode', 'verbose_mode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description', 'category', 'objects_class', 'source_type', 'source_subtype', 'options')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name', 'category', 'objects_class', 'source_type', 'source_subtype')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('name', 'description', 'category', 'objects_class', 'source_type', 'source_subtype')); // Criteria of the advanced search form - } -} -?> + + + +/** + * Persistent class InputOutputTask + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); + +/** + * This class manages the input/output tasks + * for synchronizing information with external data sources + */ +class InputOutputTask extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "application", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_iotask", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("category", array("allowed_values"=>new ValueSetEnum('Input, Ouput'), "sql"=>"category", "default_value"=>"Input", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("source_type", array("allowed_values"=>new ValueSetEnum('File, Database, Web Service'), "sql"=>"source_type", "default_value"=>"File", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("source_subtype", array("allowed_values"=>new ValueSetEnum('Oracle, MySQL, Postgress, MSSQL, SOAP, HTTP-Get, HTTP-Post, XML/RPC, CSV, XML, Excel'), "sql"=>"source_subtype", "default_value"=>"CSV", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("source_path", array("allowed_values"=>null, "sql"=>"source_path", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeClass("objects_class", array("class_category"=>"", "more_values"=>"", "sql"=>"objects_class", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("test_mode", array("allowed_values"=>new ValueSetEnum('Yes,No'), "sql"=>"test_mode", "default_value"=>'No', "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("verbose_mode", array("allowed_values"=>new ValueSetEnum('Yes,No'), "sql"=>"verbose_mode", "default_value" => 'No', "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("options", array("allowed_values"=>new ValueSetEnum('Full, Update Only, Creation Only'), "sql"=>"options", "default_value"=> 'Full', "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'category', 'objects_class', 'source_type', 'source_subtype', 'source_path' , 'options', 'test_mode', 'verbose_mode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description', 'category', 'objects_class', 'source_type', 'source_subtype', 'options')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name', 'category', 'objects_class', 'source_type', 'source_subtype')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('name', 'description', 'category', 'objects_class', 'source_type', 'source_subtype')); // Criteria of the advanced search form + } +} +?> diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index 7fc9a176e..9b018c525 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -1,1387 +1,1387 @@ - - - -/** - * Class iTopWebPage - * - * @copyright Copyright (C) 2010-2018 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT . "/application/nicewebpage.class.inc.php"); -require_once(APPROOT . "/application/applicationcontext.class.inc.php"); -require_once(APPROOT . "/application/user.preferences.class.inc.php"); - -/** - * Web page with some associated CSS and scripts (jquery) for a fancier display - */ -class iTopWebPage extends NiceWebPage implements iTabbedPage -{ - private $m_sMenu; - // private $m_currentOrganization; - private $m_aMessages; - private $m_aInitScript = array(); - protected $m_oTabs; - protected $bBreadCrumbEnabled; - protected $sBreadCrumbEntryId; - protected $sBreadCrumbEntryLabel; - protected $sBreadCrumbEntryDescription; - protected $sBreadCrumbEntryUrl; - protected $sBreadCrumbEntryIcon; - protected $oCtx; - - protected $bHasCollapsibleSection = false; - - public function __construct($sTitle, $bPrintable = false) - { - parent::__construct($sTitle, $bPrintable); - $this->m_oTabs = new TabManager(); - $this->oCtx = new ContextTag('GUI:Console'); - - ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker'); - - if ((count($_POST) == 0) || (array_key_exists('loginop', $_POST))) { - // Create a breadcrumb entry for the current page, but get its title as late as possible (page title could be changed later) - $this->bBreadCrumbEnabled = true; - } else { - $this->bBreadCrumbEnabled = false; - } - - utils::InitArchiveMode(); - - $this->m_sMenu = ""; - $this->m_aMessages = array(); - $this->SetRootUrl(utils::GetAbsoluteUrlAppRoot()); - $this->add_header("Content-type: text/html; charset=utf-8"); - $this->add_header("Cache-control: no-cache"); - $this->add_linked_stylesheet("../css/jquery.treeview.css"); - $this->add_linked_stylesheet("../css/jquery.autocomplete.css"); - $this->add_linked_stylesheet("../css/jquery-ui-timepicker-addon.css"); - $this->add_linked_stylesheet("../css/jquery.multiselect.css"); - $this->add_linked_stylesheet("../css/magnific-popup.css"); - $this->add_linked_stylesheet("../css/c3.min.css"); - $this->add_linked_stylesheet("../css/font-awesome/css/font-awesome.min.css"); - - $this->add_linked_script('../js/jquery.layout.min.js'); - $this->add_linked_script('../js/jquery.ba-bbq.min.js'); - $this->add_linked_script("../js/jquery.treeview.js"); - $this->add_linked_script("../js/jquery.autocomplete.js"); - $this->add_linked_script("../js/date.js"); - $this->add_linked_script("../js/jquery-ui-timepicker-addon.js"); - $this->add_linked_script("../js/jquery-ui-timepicker-addon-i18n.min.js"); - $this->add_linked_script("../js/jquery.blockUI.js"); - $this->add_linked_script("../js/utils.js"); - $this->add_linked_script("../js/swfobject.js"); - $this->add_linked_script("../js/ckeditor/ckeditor.js"); - $this->add_linked_script("../js/ckeditor/adapters/jquery.js"); - $this->add_linked_script("../js/jquery.qtip-1.0.min.js"); - $this->add_linked_script('../js/property_field.js'); - $this->add_linked_script('../js/icon_select.js'); - $this->add_linked_script('../js/raphael-min.js'); - $this->add_linked_script('../js/d3.js'); - $this->add_linked_script('../js/c3.js'); - $this->add_linked_script('../js/jquery.multiselect.js'); - $this->add_linked_script('../js/ajaxfileupload.js'); - $this->add_linked_script('../js/jquery.mousewheel.js'); - $this->add_linked_script('../js/jquery.magnific-popup.min.js'); - $this->add_linked_script('../js/breadcrumb.js'); - $this->add_linked_script('../js/moment.min.js'); - - - $sSearchAny = addslashes(Dict::S('UI:SearchValue:Any')); - $sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected')); - $this->add_dict_entry('UI:FillAllMandatoryFields'); - - $this->add_dict_entries('Error:'); - $this->add_dict_entries('UI:Button:'); - $this->add_dict_entries('UI:Search:'); - $this->add_dict_entry('UI:UndefinedObject'); - $this->add_dict_entries('Enum:Undefined'); - - - if (!$this->IsPrintableVersion()) { - $this->PrepareLayout(); - $this->add_script( - <<Get('demo_mode')) { - // Leave the pane opened - } else { - if (utils::ReadParam('force_menu_pane', null) === 0) { - $bLeftPaneOpen = false; - } elseif (appUserPreferences::GetPref('menu_pane', 'open') == 'closed') { - $bLeftPaneOpen = false; - } - } - return $bLeftPaneOpen; - } - - protected function PrepareLayout() - { - if (MetaModel::GetConfig()->Get('demo_mode')) { - // No pin button - $sConfigureWestPane = ''; - } else { - $sConfigureWestPane = - <<IsMenuPaneVisible() ? '' : 'initClosed: true,'; - - $sJSDisconnectedMessage = json_encode(Dict::S('UI:DisconnectedDlgMessage')); - $sJSTitle = json_encode(Dict::S('UI:DisconnectedDlgTitle')); - $sJSLoginAgain = json_encode(Dict::S('UI:LoginAgain')); - $sJSStayOnThePage = json_encode(Dict::S('UI:StayOnThePage')); - $aDaysMin = array(Dict::S('DayOfWeek-Sunday-Min'), Dict::S('DayOfWeek-Monday-Min'), Dict::S('DayOfWeek-Tuesday-Min'), Dict::S('DayOfWeek-Wednesday-Min'), - Dict::S('DayOfWeek-Thursday-Min'), Dict::S('DayOfWeek-Friday-Min'), Dict::S('DayOfWeek-Saturday-Min')); - $aMonthsShort = array(Dict::S('Month-01-Short'), Dict::S('Month-02-Short'), Dict::S('Month-03-Short'), Dict::S('Month-04-Short'), Dict::S('Month-05-Short'), Dict::S('Month-06-Short'), - Dict::S('Month-07-Short'), Dict::S('Month-08-Short'), Dict::S('Month-09-Short'), Dict::S('Month-10-Short'), Dict::S('Month-11-Short'), Dict::S('Month-12-Short')); - $sTimeFormat = AttributeDateTime::GetFormat()->ToTimeFormat(); - $oTimeFormat = new DateTimeFormat($sTimeFormat); - $sJSLangShort = json_encode(strtolower(substr(Dict::GetUserLanguage(), 0, 2))); - - // Date picker options - $aPickerOptions = array( - 'showOn' => 'button', - 'buttonImage' => '../images/calendar.png', - 'buttonImageOnly' => true, - 'dateFormat' => AttributeDate::GetFormat()->ToDatePicker(), - 'constrainInput' => false, - 'changeMonth' => true, - 'changeYear' => true, - 'dayNamesMin' => $aDaysMin, - 'monthNamesShort' => $aMonthsShort, - 'firstDay' => (int)Dict::S('Calendar-FirstDayOfWeek'), - ); - $sJSDatePickerOptions = json_encode($aPickerOptions); - - // Time picker additional options - $aPickerOptions['showOn'] = ''; - $aPickerOptions['buttonImage'] = null; - $aPickerOptions['timeFormat'] = $oTimeFormat->ToDatePicker(); - $aPickerOptions['controlType'] = 'select'; - $aPickerOptions['closeText'] = Dict::S('UI:Button:Ok'); - $sJSDateTimePickerOptions = json_encode($aPickerOptions); - if ($sJSLangShort != '"en"') { - // More options that cannot be passed via json_encode since they must be evaluated client-side - $aMoreJSOptions = ", - 'timeText': $.timepicker.regional[$sJSLangShort].timeText, - 'hourText': $.timepicker.regional[$sJSLangShort].hourText, - 'minuteText': $.timepicker.regional[$sJSLangShort].minuteText, - 'secondText': $.timepicker.regional[$sJSLangShort].secondText, - 'currentText': $.timepicker.regional[$sJSLangShort].currentText - }"; - $sJSDateTimePickerOptions = substr($sJSDateTimePickerOptions, 0, -1) . $aMoreJSOptions; - } - $this->add_script( - <<< EOF - function PrepareWidgets() - { - // note: each action implemented here must be idempotent, - // because this helper function might be called several times on a given page - - // Note: Trigger image is wrapped in a span so we can display it we want - $(".date-pick").datepicker($sJSDatePickerOptions) - .next("img").wrap(""); - - // Hack for the date and time picker addon issue on Chrome (see #1305) - // The workaround is to instantiate the widget on demand - // It relies on the same markup, thus reverting to the original implementation should be straightforward - $(".datetime-pick:not(.is-widget-ready)").each(function(){ - var oInput = this; - $(oInput).addClass('is-widget-ready'); - $('') - .insertAfter($(this)) - .on('click', function(){ - $(oInput) - .datetimepicker($sJSDateTimePickerOptions) - .datetimepicker('show') - .datetimepicker('option', 'onClose', function(dateText,inst){ - $(oInput).datetimepicker('destroy'); - }) - .on('click keypress', function(){ - $(oInput).datetimepicker('hide'); - }); - }); - }); - } -EOF - ); - - $this->add_init_script( - <<< EOF - try - { - var myLayout; // a var is required because this page utilizes: myLayout.allowOverflow() method - - // Layout - paneSize = GetUserPreference('menu_size', 300); - if ($('body').length > 0) - { - myLayout = $('body').layout({ - west : { - $sInitClosed minSize: 200, size: paneSize, spacing_open: 16, spacing_close: 16, slideTrigger_open: "click", hideTogglerOnSlide: true, enableCursorHotkey: false, - onclose_end: function(name, elt, state, options, layout) - { - if (state.isSliding == false) - { - $('.menu-pane-exclusive').show(); - SetUserPreference('menu_pane', 'closed', true); - } - }, - onresize_end: function(name, elt, state, options, layout) - { - if (state.isSliding == false) - { - SetUserPreference('menu_size', state.size, true); - } - }, - - onopen_end: function(name, elt, state, options, layout) - { - if (state.isSliding == false) - { - $('.menu-pane-exclusive').hide(); - SetUserPreference('menu_pane', 'open', true); - } - } - }, - center: { - onresize_end: function(name, elt, state, options, layout) - { - $('.v-resizable').each( function() { - var fixedWidth = $(this).parent().innerWidth() - 6; - $(this).width(fixedWidth); - // Make sure it cannot be resized horizontally - $(this).resizable('options', { minWidth: fixedWidth, maxWidth: fixedWidth }); - // Now adjust all the child 'items' - var innerWidth = $(this).innerWidth() - 10; - $(this).find('.item').width(innerWidth); - }); - $('.panel-resized').trigger('resized'); - } - - } - }); - } - window.clearTimeout(iPaneVisWatchDog); - //myLayout.open( "west" ); - $('.ui-layout-resizer-west .ui-layout-toggler').css({background: 'transparent'}); - $sConfigureWestPane - if ($('#left-pane').length > 0) - { - $('#left-pane').layout({ resizable: false, spacing_open: 0, south: { size: 94 }, enableCursorHotkey: false }); - } - // Tabs, using JQuery BBQ to store the history - // The "tab widgets" to handle. - var tabs = $('div[id^=tabbedContent]'); - - // This selector will be reused when selecting actual tab widget A elements. - var tab_a_selector = 'ul.ui-tabs-nav a'; - - // Ugly patch for a change in the behavior of jQuery UI: - // Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax) - // when their href was beginning by #. Starting with 1.9, a tag in the page - // is taken into account and causes "local" tabs to be considered as Ajax - // unless their URL is equal to the URL of the page... - $('div[id^=tabbedContent] > ul > li > a').each(function() { - var sHash = location.hash; - var sHref = $(this).attr("href"); - if (sHref.match(/^#/)) - { - var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, ''); - $(this).attr("href", sCleanLocation+$(this).attr("href")); - } - }); - - // Enable tabs on all tab widgets. The `event` property must be overridden so - // that the tabs aren't changed on click, and any custom event name can be - // specified. Note that if you define a callback for the 'select' event, it - // will be executed for the selected tab whenever the hash changes. - tabs.tabs({ - event: 'change', 'show': function(event, ui) { - $('.resizable', ui.panel).resizable(); // Make resizable everything that claims to be resizable ! - }, - beforeLoad: function( event, ui ) { - if ( ui.tab.data('loaded') && (ui.tab.attr('data-cache') == 'true')) { - event.preventDefault(); - return; - } - ui.panel.html('
'); - ui.jqXHR.done(function() { - ui.tab.data( "loaded", true ); - }); - } - }); - } - catch(err) - { - // Do something with the error ! - alert(err); - } -EOF - ); - - $this->add_ready_script( - <<< EOF - - // Adjust initial size - $('.v-resizable').each( function() - { - var parent_id = $(this).parent().id; - // Restore the saved height - var iHeight = GetUserPreference(parent_id+'_'+this.id+'_height', undefined); - if (iHeight != undefined) - { - $(this).height(parseInt(iHeight, 10)); // Parse in base 10 !); - } - // Adjust the child 'item''s height and width to fit - var container = $(this); - var fixedWidth = container.parent().innerWidth() - 6; - // Set the width to fit the parent - $(this).width(fixedWidth); - var headerHeight = $(this).find('.drag_handle').height(); - // Now adjust the width and height of the child 'item' - container.find('.item').height(container.innerHeight() - headerHeight - 12).width(fixedWidth - 10); - } - ); - // Make resizable, vertically only everything that claims to be v-resizable ! - $('.v-resizable').resizable( { handles: 's', minHeight: $(this).find('.drag_handle').height(), minWidth: $(this).parent().innerWidth() - 6, maxWidth: $(this).parent().innerWidth() - 6, stop: function() - { - // Adjust the content - var container = $(this); - var headerHeight = $(this).find('.drag_handle').height(); - container.find('.item').height(container.innerHeight() - headerHeight - 12);//.width(container.innerWidth()); - var parent_id = $(this).parent().id; - SetUserPreference(parent_id+'_'+this.id+'_height', $(this).height(), true); // true => persistent - } - } ); - - // Tabs, using JQuery BBQ to store the history - // The "tab widgets" to handle. - var tabs = $('div[id^=tabbedContent]'); - - // This selector will be reused when selecting actual tab widget A elements. - var tab_a_selector = 'ul.ui-tabs-nav a'; - - // Define our own click handler for the tabs, overriding the default. - tabs.find( tab_a_selector ).click(function() - { - var state = {}; - - // Get the id of this tab widget. - var id = $(this).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); - - // Get the index of this tab. - var idx = $(this).parent().prevAll().length; - - // Set the state! - state[ id ] = idx; - $.bbq.pushState( state ); - }); - - // refresh the hash when the tab is changed (from a JS script) - $('body').on( 'tabsactivate', '.ui-tabs', function(event, ui) { - var state = {}; - - // Get the id of this tab widget. - var id = $(ui.newTab).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); - - // Get the index of this tab. - var idx = $(ui.newTab).prevAll().length; - - // Set the state! - state[ id ] = idx; - $.bbq.pushState( state ); - }); - - // Bind an event to window.onhashchange that, when the history state changes, - // iterates over all tab widgets, changing the current tab as necessary. - $(window).bind( 'hashchange', function(e) - { - // Iterate over all tab widgets. - tabs.each(function() - { - // Get the index for this tab widget from the hash, based on the - // appropriate id property. In jQuery 1.4, you should use e.getState() - // instead of $.bbq.getState(). The second, 'true' argument coerces the - // string value to a number. - var idx = $.bbq.getState( this.id, true ) || 0; - - // Select the appropriate tab for this tab widget by triggering the custom - // event specified in the .tabs() init above (you could keep track of what - // tab each widget is on using .data, and only select a tab if it has - // changed). - $(this).find( tab_a_selector ).eq( idx ).triggerHandler( 'change' ); - }); - - // Iterate over all truncated lists to find whether they are expanded or not - $('a.truncated').each(function() - { - var state = $.bbq.getState( this.id, true ) || 'close'; - if (state == 'open') - { - $(this).trigger('open'); - } - else - { - $(this).trigger('close'); - } - }); - }); - - // Shortcut menu actions - $('.actions_button a').click( function() { - aMatches = /#(.*)$/.exec(window.location.href); - if (aMatches != null) - { - currentHash = aMatches[1]; - if ( /#(.*)$/.test(this.href)) - { - this.href = this.href.replace(/#(.*)$/, '#'+currentHash); - } - } - }); - - // End of Tabs handling - - PrepareWidgets(); - - // Make sortable, everything that claims to be sortable - $('.sortable').sortable( {axis: 'y', cursor: 'move', handle: '.drag_handle', stop: function() - { - if ($(this).hasClass('persistent')) - { - // remember the sort order for next time the page is loaded... - sSerialized = $(this).sortable('serialize', {key: 'menu'}); - var sTemp = sSerialized.replace(/menu=/g, ''); - SetUserPreference(this.id+'_order', sTemp.replace(/&/g, ','), true); // true => persistent ! - } - } - }); - docWidth = $(document).width(); - $('#ModalDlg').dialog({ autoOpen: false, modal: true, width: 0.8*docWidth, height: 'auto', maxHeight: $(window).height() - 50 }); // JQuery UI dialogs - ShowDebug(); - $('#logOffBtn>ul').popupmenu(); - - $('.caselog_header').click( function () { $(this).toggleClass('open').next('.caselog_entry,.caselog_entry_html').toggle(); }); - - $(document).ajaxSend(function(event, jqxhr, options) { - jqxhr.setRequestHeader('X-Combodo-Ajax', 'true'); - }); - $(document).ajaxError(function(event, jqxhr, options) { - if (jqxhr.status == 401) - { - $('
'+$sJSDisconnectedMessage+'
').dialog({ - modal:true, - title: $sJSTitle, - close: function() { $(this).remove(); }, - minWidth: 400, - buttons: [ - { text: $sJSLoginAgain, click: function() { window.location.href= GetAbsoluteUrlAppRoot()+'pages/UI.php' } }, - { text: $sJSStayOnThePage, click: function() { $(this).dialog('close'); } } - ] - }); - } - }); -EOF - ); - $this->add_ready_script(InlineImage::FixImagesWidth()); - /* - * Not used since the sorting of the tables is always performed server-side - AttributeDateTime::InitTableSorter($this, 'custom_date_time'); - AttributeDate::InitTableSorter($this, 'custom_date'); - */ - - $sUserPrefs = appUserPreferences::GetAsJSON(); - $this->add_script( - << 0) - { - sToken = ''; - if (sOwnershipToken != undefined) - { - sToken = '&token='+sOwnershipToken; - } - window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=release_lock_and_details&class='+sClass+'&id='+id+sToken); - } - else - { - window.location.href = sDefaultUrl; // Already contains the context... - } - } - - function BackToList(sClass) - { - window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=search_oql&oql_class='+sClass+'&oql_clause=WHERE id=0'); - } - - function ShowDebug() - { - if ($('#rawOutput > div').html() != '') - { - $('#rawOutput').dialog( {autoOpen: true, modal:false, width: '80%'}); - } - } - - var oUserPreferences = $sUserPrefs; - - // For disabling the CKEditor at init time when the corresponding textarea is disabled ! - CKEDITOR.plugins.add( 'disabler', - { - init : function( editor ) - { - editor.on( 'instanceReady', function(e) - { - e.removeListener(); - $('#'+ editor.name).trigger('update'); - }); - } - - }); - - - function FixPaneVis() - { - $('.ui-layout-center, .ui-layout-north, .ui-layout-south').css({display: 'block'}); - } -EOF - ); - } - - - /** - * @param string $sId Identifies the item, to search after it in the current breadcrumb - * @param string $sLabel Label of the breadcrumb item - * @param string $sDescription More information, displayed as a tooltip - * @param string $sUrl Specify a URL if the current URL as perceived on the browser side is not relevant - * @param string $sIcon Icon (relative or absolute) path that will be displayed next to the label - */ - public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sIcon = '') - { - $this->bBreadCrumbEnabled = true; - $this->sBreadCrumbEntryId = $sId; - $this->sBreadCrumbEntryLabel = $sLabel; - $this->sBreadCrumbEntryDescription = $sDescription; - $this->sBreadCrumbEntryUrl = $sUrl; - $this->sBreadCrumbEntryIcon = $sIcon; - } - - /** - * State that there will be no breadcrumb item for the current page - */ - public function DisableBreadCrumb() - { - $this->bBreadCrumbEnabled = false; - $this->sBreadCrumbEntryId = null; - $this->sBreadCrumbEntryLabel = null; - $this->sBreadCrumbEntryDescription = null; - $this->sBreadCrumbEntryUrl = null; - $this->sBreadCrumbEntryIcon = null; - } - - public function AddToMenu($sHtml) - { - $this->m_sMenu .= $sHtml; - } - - public function GetSiloSelectionForm() - { - // List of visible Organizations - $iCount = 0; - $oSet = null; - if (MetaModel::IsValidClass('Organization')) { - // Display the list of *favorite* organizations... but keeping in mind what is the real number of organizations - $aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null); - $oSearchFilter = new DBObjectSearch('Organization'); - $oSearchFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true); - $oSet = new CMDBObjectSet($oSearchFilter); - $iCount = $oSet->Count(); // total number of existing Orgs - - // Now get the list of Orgs to be displayed in the menu - $oSearchFilter = DBObjectSearch::FromOQL(ApplicationMenu::GetFavoriteSiloQuery()); - $oSearchFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true); - if (!empty($aFavoriteOrgs)) { - $oSearchFilter->AddCondition('id', $aFavoriteOrgs, 'IN'); - } - $oSet = new CMDBObjectSet($oSearchFilter); // List of favorite orgs - } - switch ($iCount) { - case 0: - // No such dimension/silo => nothing to select - $sHtml = '
'; - break; - - case 1: - // Only one possible choice... no selection, but display the value - $oOrg = $oSet->Fetch(); - $sHtml = '
' . $oOrg->GetName() . '
'; - $sHtml .= ''; - break; - - default: - $sHtml = ''; - $oAppContext = new ApplicationContext(); - $iCurrentOrganization = $oAppContext->GetCurrentValue('org_id'); - $sHtml = '
'; - $sHtml .= '
'; //
'; - $sHtml .= ' '; - $sHtml .= ' ' . self::FilterXSS($sLogOffMenu) . ''; - $sHtml .= ' '; - $sHtml .= ' '; - $sHtml .= ' '; - $sHtml .= ' '; - -// $sHtml .= ' '; -// $sHtml .= '
'; - - $sHtml .= ' '; - $sHtml .= '
'; - $sHtml .= ' '; - $sHtml .= self::FilterXSS($this->s_content); - $sHtml .= ' '; - $sHtml .= '
'; - $sHtml .= ''; - $sHtml .= $sSouthPane; - - // Add the captured output - if (trim($s_captured_output) != "") { - $sHtml .= "
" . self::FilterXSS($s_captured_output) . "
\n"; - } - $sHtml .= "
" . self::FilterXSS($this->s_deferred_content) . "
"; - $sHtml .= "
Please wait...
\n"; // jqModal Window - $sHtml .= "
"; - $sHtml .= "
"; - } else { - $sHtml .= self::FilterXSS($this->s_content); - } - - $sHtml .= "\n"; - $sHtml .= "\n"; - - if ($this->GetOutputFormat() == 'html') { - $oKPI = new ExecutionKPI(); - echo $sHtml; - $oKPI->ComputeAndReport('Echoing (' . round(strlen($sHtml) / 1024) . ' Kb)'); - } else if ($this->GetOutputFormat() == 'pdf' && $this->IsOutputFormatAvailable('pdf')) { - if (@is_readable(APPROOT . 'lib/MPDF/mpdf.php')) { - require_once(APPROOT . 'lib/MPDF/mpdf.php'); - $oMPDF = new mPDF('c'); - $oMPDF->mirroMargins = false; - if ($this->a_base['href'] != '') { - $oMPDF->setBasePath($this->a_base['href']); // Seems that the tag is not recognized by mPDF... - } - $oMPDF->showWatermarkText = true; - if ($this->GetOutputOption('pdf', 'template_path')) { - $oMPDF->setImportUse(); // Allow templates - $oMPDF->SetDocTemplate($this->GetOutputOption('pdf', 'template_path'), 1); - } - $oMPDF->WriteHTML($sHtml); - $sOutputName = $this->s_title . '.pdf'; - if ($this->GetOutputOption('pdf', 'output_name')) { - $sOutputName = $this->GetOutputOption('pdf', 'output_name'); - } - $oMPDF->Output($sOutputName, 'I'); - } - } - DBSearch::RecordQueryTrace(); - ExecutionKPI::ReportStats(); - } - - /** - * Adds init scripts for the collapsible sections - */ - private function outputCollapsibleSectionInit() - { - if (!$this->bHasCollapsibleSection) { - return; - } - - $this->add_script(<<<'EOD' -function initCollapsibleSection(iSectionId, bOpenedByDefault, sSectionStateStorageKey) -{ -var bStoredSectionState = JSON.parse(localStorage.getItem(sSectionStateStorageKey)); -var bIsSectionOpenedInitially = (bStoredSectionState == null) ? bOpenedByDefault : bStoredSectionState; - -if (bIsSectionOpenedInitially) { - $("#LnkCollapse_"+iSectionId).toggleClass("open"); - $("#Collapse_"+iSectionId).toggle(); -} - -$("#LnkCollapse_"+iSectionId).click(function(e) { - localStorage.setItem(sSectionStateStorageKey, !($("#Collapse_"+iSectionId).is(":visible"))); - $("#LnkCollapse_"+iSectionId).toggleClass("open"); - $("#Collapse_"+iSectionId).slideToggle("normal"); - e.preventDefault(); // we don't want to do anything more (see #1030 : a non wanted tab switching was triggered) -}); -} -EOD - ); - } - - public function AddTabContainer($sTabContainer, $sPrefix = '') - { - $this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix)); - } - - public function AddToTab($sTabContainer, $sTabLabel, $sHtml) - { - $this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml)); - } - - public function SetCurrentTabContainer($sTabContainer = '') - { - return $this->m_oTabs->SetCurrentTabContainer($sTabContainer); - } - - public function SetCurrentTab($sTabLabel = '') - { - return $this->m_oTabs->SetCurrentTab($sTabLabel); - } - - /** - * Add a tab which content will be loaded asynchronously via the supplied URL - * - * Limitations: - * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server. - * Static content cannot be added inside such tabs. - * - * @param string $sTabLabel The (localised) label of the tab - * @param string $sUrl The URL to load (on the same server) - * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation. - * - * @since 2.0.3 - */ - public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) - { - $this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache)); - } - - public function GetCurrentTab() - { - return $this->m_oTabs->GetCurrentTab(); - } - - public function RemoveTab($sTabLabel, $sTabContainer = null) - { - $this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer); - } - - /** - * Finds the tab whose title matches a given pattern - * @return mixed The name of the tab as a string or false if not found - */ - public function FindTab($sPattern, $sTabContainer = null) - { - return $this->m_oTabs->FindTab($sPattern, $sTabContainer); - } - - /** - * Make the given tab the active one, as if it were clicked - * DOES NOT WORK: apparently in the *old* version of jquery - * that we are using this is not supported... TO DO upgrade - * the whole jquery bundle... - */ - public function SelectTab($sTabContainer, $sTabLabel) - { - $this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel)); - } - - public function StartCollapsibleSection( - $sSectionLabel, $bOpenedByDefault = false, $sSectionStateStorageBusinessKey = '' - ) - { - $this->add($this->GetStartCollapsibleSection($sSectionLabel, $bOpenedByDefault, - $sSectionStateStorageBusinessKey)); - } - - private function GetStartCollapsibleSection( - $sSectionLabel, $bOpenedByDefault = false, $sSectionStateStorageBusinessKey = '' - ) - { - $this->bHasCollapsibleSection = true; - $sHtml = ''; - static $iSectionId = 0; - $sHtml .= '
' . $sSectionLabel . '
' . "\n"; - $sHtml .= '"; - } - - public function add($sHtml) - { - if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != '')) { - $this->m_oTabs->AddToCurrentTab($sHtml); - } else { - parent::add($sHtml); - } - } - - /** - * Records the current state of the 'html' part of the page output - * @return mixed The current state of the 'html' output - */ - public function start_capture() - { - $sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer(); - $sCurrentTab = $this->m_oTabs->GetCurrentTab(); - - if (!empty($sCurrentTabContainer) && !empty($sCurrentTab)) { - $iOffset = $this->m_oTabs->GetCurrentTabLength(); - return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset); - } else { - return parent::start_capture(); - } - } - - /** - * Returns the part of the html output that occurred since the call to start_capture - * and removes this part from the current html output - * - * @param $offset mixed The value returned by start_capture - * - * @return string The part of the html output that was added since the call to start_capture - */ - public function end_capture($offset) - { - if (is_array($offset)) { - if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab'])) { - $sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']); - } else { - $sCaptured = ''; - } - } else { - $sCaptured = parent::end_capture($offset); - } - return $sCaptured; - } - - /** - * Set the message to be displayed in the 'app-banner' section at the top of the page - */ - public function SetMessage($sHtmlMessage) - { - $sHtmlIcon = ''; - $this->AddApplicationMessage($sHtmlMessage, $sHtmlIcon); - } - - /** - * Add message to be displayed in the 'app-banner' section at the top of the page - */ - public function AddApplicationMessage($sHtmlMessage, $sHtmlIcon = null, $sTip = null) - { - if (strlen($sHtmlMessage)) { - $this->m_aMessages[] = array( - 'icon' => $sHtmlIcon, - 'message' => $sHtmlMessage, - 'tip' => $sTip - ); - } - } - /** - * Adds a script to be executed when the DOM is ready (typical JQuery use), right before add_ready_script - * @return void - */ - public function add_init_script($sScript){ - $this->m_aInitScript[] = $sScript; - } -} + + + +/** + * Class iTopWebPage + * + * @copyright Copyright (C) 2010-2018 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT . "/application/nicewebpage.class.inc.php"); +require_once(APPROOT . "/application/applicationcontext.class.inc.php"); +require_once(APPROOT . "/application/user.preferences.class.inc.php"); + +/** + * Web page with some associated CSS and scripts (jquery) for a fancier display + */ +class iTopWebPage extends NiceWebPage implements iTabbedPage +{ + private $m_sMenu; + // private $m_currentOrganization; + private $m_aMessages; + private $m_aInitScript = array(); + protected $m_oTabs; + protected $bBreadCrumbEnabled; + protected $sBreadCrumbEntryId; + protected $sBreadCrumbEntryLabel; + protected $sBreadCrumbEntryDescription; + protected $sBreadCrumbEntryUrl; + protected $sBreadCrumbEntryIcon; + protected $oCtx; + + protected $bHasCollapsibleSection = false; + + public function __construct($sTitle, $bPrintable = false) + { + parent::__construct($sTitle, $bPrintable); + $this->m_oTabs = new TabManager(); + $this->oCtx = new ContextTag('GUI:Console'); + + ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker'); + + if ((count($_POST) == 0) || (array_key_exists('loginop', $_POST))) { + // Create a breadcrumb entry for the current page, but get its title as late as possible (page title could be changed later) + $this->bBreadCrumbEnabled = true; + } else { + $this->bBreadCrumbEnabled = false; + } + + utils::InitArchiveMode(); + + $this->m_sMenu = ""; + $this->m_aMessages = array(); + $this->SetRootUrl(utils::GetAbsoluteUrlAppRoot()); + $this->add_header("Content-type: text/html; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->add_linked_stylesheet("../css/jquery.treeview.css"); + $this->add_linked_stylesheet("../css/jquery.autocomplete.css"); + $this->add_linked_stylesheet("../css/jquery-ui-timepicker-addon.css"); + $this->add_linked_stylesheet("../css/jquery.multiselect.css"); + $this->add_linked_stylesheet("../css/magnific-popup.css"); + $this->add_linked_stylesheet("../css/c3.min.css"); + $this->add_linked_stylesheet("../css/font-awesome/css/font-awesome.min.css"); + + $this->add_linked_script('../js/jquery.layout.min.js'); + $this->add_linked_script('../js/jquery.ba-bbq.min.js'); + $this->add_linked_script("../js/jquery.treeview.js"); + $this->add_linked_script("../js/jquery.autocomplete.js"); + $this->add_linked_script("../js/date.js"); + $this->add_linked_script("../js/jquery-ui-timepicker-addon.js"); + $this->add_linked_script("../js/jquery-ui-timepicker-addon-i18n.min.js"); + $this->add_linked_script("../js/jquery.blockUI.js"); + $this->add_linked_script("../js/utils.js"); + $this->add_linked_script("../js/swfobject.js"); + $this->add_linked_script("../js/ckeditor/ckeditor.js"); + $this->add_linked_script("../js/ckeditor/adapters/jquery.js"); + $this->add_linked_script("../js/jquery.qtip-1.0.min.js"); + $this->add_linked_script('../js/property_field.js'); + $this->add_linked_script('../js/icon_select.js'); + $this->add_linked_script('../js/raphael-min.js'); + $this->add_linked_script('../js/d3.js'); + $this->add_linked_script('../js/c3.js'); + $this->add_linked_script('../js/jquery.multiselect.js'); + $this->add_linked_script('../js/ajaxfileupload.js'); + $this->add_linked_script('../js/jquery.mousewheel.js'); + $this->add_linked_script('../js/jquery.magnific-popup.min.js'); + $this->add_linked_script('../js/breadcrumb.js'); + $this->add_linked_script('../js/moment.min.js'); + + + $sSearchAny = addslashes(Dict::S('UI:SearchValue:Any')); + $sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected')); + $this->add_dict_entry('UI:FillAllMandatoryFields'); + + $this->add_dict_entries('Error:'); + $this->add_dict_entries('UI:Button:'); + $this->add_dict_entries('UI:Search:'); + $this->add_dict_entry('UI:UndefinedObject'); + $this->add_dict_entries('Enum:Undefined'); + + + if (!$this->IsPrintableVersion()) { + $this->PrepareLayout(); + $this->add_script( + <<Get('demo_mode')) { + // Leave the pane opened + } else { + if (utils::ReadParam('force_menu_pane', null) === 0) { + $bLeftPaneOpen = false; + } elseif (appUserPreferences::GetPref('menu_pane', 'open') == 'closed') { + $bLeftPaneOpen = false; + } + } + return $bLeftPaneOpen; + } + + protected function PrepareLayout() + { + if (MetaModel::GetConfig()->Get('demo_mode')) { + // No pin button + $sConfigureWestPane = ''; + } else { + $sConfigureWestPane = + <<IsMenuPaneVisible() ? '' : 'initClosed: true,'; + + $sJSDisconnectedMessage = json_encode(Dict::S('UI:DisconnectedDlgMessage')); + $sJSTitle = json_encode(Dict::S('UI:DisconnectedDlgTitle')); + $sJSLoginAgain = json_encode(Dict::S('UI:LoginAgain')); + $sJSStayOnThePage = json_encode(Dict::S('UI:StayOnThePage')); + $aDaysMin = array(Dict::S('DayOfWeek-Sunday-Min'), Dict::S('DayOfWeek-Monday-Min'), Dict::S('DayOfWeek-Tuesday-Min'), Dict::S('DayOfWeek-Wednesday-Min'), + Dict::S('DayOfWeek-Thursday-Min'), Dict::S('DayOfWeek-Friday-Min'), Dict::S('DayOfWeek-Saturday-Min')); + $aMonthsShort = array(Dict::S('Month-01-Short'), Dict::S('Month-02-Short'), Dict::S('Month-03-Short'), Dict::S('Month-04-Short'), Dict::S('Month-05-Short'), Dict::S('Month-06-Short'), + Dict::S('Month-07-Short'), Dict::S('Month-08-Short'), Dict::S('Month-09-Short'), Dict::S('Month-10-Short'), Dict::S('Month-11-Short'), Dict::S('Month-12-Short')); + $sTimeFormat = AttributeDateTime::GetFormat()->ToTimeFormat(); + $oTimeFormat = new DateTimeFormat($sTimeFormat); + $sJSLangShort = json_encode(strtolower(substr(Dict::GetUserLanguage(), 0, 2))); + + // Date picker options + $aPickerOptions = array( + 'showOn' => 'button', + 'buttonImage' => '../images/calendar.png', + 'buttonImageOnly' => true, + 'dateFormat' => AttributeDate::GetFormat()->ToDatePicker(), + 'constrainInput' => false, + 'changeMonth' => true, + 'changeYear' => true, + 'dayNamesMin' => $aDaysMin, + 'monthNamesShort' => $aMonthsShort, + 'firstDay' => (int)Dict::S('Calendar-FirstDayOfWeek'), + ); + $sJSDatePickerOptions = json_encode($aPickerOptions); + + // Time picker additional options + $aPickerOptions['showOn'] = ''; + $aPickerOptions['buttonImage'] = null; + $aPickerOptions['timeFormat'] = $oTimeFormat->ToDatePicker(); + $aPickerOptions['controlType'] = 'select'; + $aPickerOptions['closeText'] = Dict::S('UI:Button:Ok'); + $sJSDateTimePickerOptions = json_encode($aPickerOptions); + if ($sJSLangShort != '"en"') { + // More options that cannot be passed via json_encode since they must be evaluated client-side + $aMoreJSOptions = ", + 'timeText': $.timepicker.regional[$sJSLangShort].timeText, + 'hourText': $.timepicker.regional[$sJSLangShort].hourText, + 'minuteText': $.timepicker.regional[$sJSLangShort].minuteText, + 'secondText': $.timepicker.regional[$sJSLangShort].secondText, + 'currentText': $.timepicker.regional[$sJSLangShort].currentText + }"; + $sJSDateTimePickerOptions = substr($sJSDateTimePickerOptions, 0, -1) . $aMoreJSOptions; + } + $this->add_script( + <<< EOF + function PrepareWidgets() + { + // note: each action implemented here must be idempotent, + // because this helper function might be called several times on a given page + + // Note: Trigger image is wrapped in a span so we can display it we want + $(".date-pick").datepicker($sJSDatePickerOptions) + .next("img").wrap(""); + + // Hack for the date and time picker addon issue on Chrome (see #1305) + // The workaround is to instantiate the widget on demand + // It relies on the same markup, thus reverting to the original implementation should be straightforward + $(".datetime-pick:not(.is-widget-ready)").each(function(){ + var oInput = this; + $(oInput).addClass('is-widget-ready'); + $('') + .insertAfter($(this)) + .on('click', function(){ + $(oInput) + .datetimepicker($sJSDateTimePickerOptions) + .datetimepicker('show') + .datetimepicker('option', 'onClose', function(dateText,inst){ + $(oInput).datetimepicker('destroy'); + }) + .on('click keypress', function(){ + $(oInput).datetimepicker('hide'); + }); + }); + }); + } +EOF + ); + + $this->add_init_script( + <<< EOF + try + { + var myLayout; // a var is required because this page utilizes: myLayout.allowOverflow() method + + // Layout + paneSize = GetUserPreference('menu_size', 300); + if ($('body').length > 0) + { + myLayout = $('body').layout({ + west : { + $sInitClosed minSize: 200, size: paneSize, spacing_open: 16, spacing_close: 16, slideTrigger_open: "click", hideTogglerOnSlide: true, enableCursorHotkey: false, + onclose_end: function(name, elt, state, options, layout) + { + if (state.isSliding == false) + { + $('.menu-pane-exclusive').show(); + SetUserPreference('menu_pane', 'closed', true); + } + }, + onresize_end: function(name, elt, state, options, layout) + { + if (state.isSliding == false) + { + SetUserPreference('menu_size', state.size, true); + } + }, + + onopen_end: function(name, elt, state, options, layout) + { + if (state.isSliding == false) + { + $('.menu-pane-exclusive').hide(); + SetUserPreference('menu_pane', 'open', true); + } + } + }, + center: { + onresize_end: function(name, elt, state, options, layout) + { + $('.v-resizable').each( function() { + var fixedWidth = $(this).parent().innerWidth() - 6; + $(this).width(fixedWidth); + // Make sure it cannot be resized horizontally + $(this).resizable('options', { minWidth: fixedWidth, maxWidth: fixedWidth }); + // Now adjust all the child 'items' + var innerWidth = $(this).innerWidth() - 10; + $(this).find('.item').width(innerWidth); + }); + $('.panel-resized').trigger('resized'); + } + + } + }); + } + window.clearTimeout(iPaneVisWatchDog); + //myLayout.open( "west" ); + $('.ui-layout-resizer-west .ui-layout-toggler').css({background: 'transparent'}); + $sConfigureWestPane + if ($('#left-pane').length > 0) + { + $('#left-pane').layout({ resizable: false, spacing_open: 0, south: { size: 94 }, enableCursorHotkey: false }); + } + // Tabs, using JQuery BBQ to store the history + // The "tab widgets" to handle. + var tabs = $('div[id^=tabbedContent]'); + + // This selector will be reused when selecting actual tab widget A elements. + var tab_a_selector = 'ul.ui-tabs-nav a'; + + // Ugly patch for a change in the behavior of jQuery UI: + // Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax) + // when their href was beginning by #. Starting with 1.9, a tag in the page + // is taken into account and causes "local" tabs to be considered as Ajax + // unless their URL is equal to the URL of the page... + $('div[id^=tabbedContent] > ul > li > a').each(function() { + var sHash = location.hash; + var sHref = $(this).attr("href"); + if (sHref.match(/^#/)) + { + var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, ''); + $(this).attr("href", sCleanLocation+$(this).attr("href")); + } + }); + + // Enable tabs on all tab widgets. The `event` property must be overridden so + // that the tabs aren't changed on click, and any custom event name can be + // specified. Note that if you define a callback for the 'select' event, it + // will be executed for the selected tab whenever the hash changes. + tabs.tabs({ + event: 'change', 'show': function(event, ui) { + $('.resizable', ui.panel).resizable(); // Make resizable everything that claims to be resizable ! + }, + beforeLoad: function( event, ui ) { + if ( ui.tab.data('loaded') && (ui.tab.attr('data-cache') == 'true')) { + event.preventDefault(); + return; + } + ui.panel.html('
'); + ui.jqXHR.done(function() { + ui.tab.data( "loaded", true ); + }); + } + }); + } + catch(err) + { + // Do something with the error ! + alert(err); + } +EOF + ); + + $this->add_ready_script( + <<< EOF + + // Adjust initial size + $('.v-resizable').each( function() + { + var parent_id = $(this).parent().id; + // Restore the saved height + var iHeight = GetUserPreference(parent_id+'_'+this.id+'_height', undefined); + if (iHeight != undefined) + { + $(this).height(parseInt(iHeight, 10)); // Parse in base 10 !); + } + // Adjust the child 'item''s height and width to fit + var container = $(this); + var fixedWidth = container.parent().innerWidth() - 6; + // Set the width to fit the parent + $(this).width(fixedWidth); + var headerHeight = $(this).find('.drag_handle').height(); + // Now adjust the width and height of the child 'item' + container.find('.item').height(container.innerHeight() - headerHeight - 12).width(fixedWidth - 10); + } + ); + // Make resizable, vertically only everything that claims to be v-resizable ! + $('.v-resizable').resizable( { handles: 's', minHeight: $(this).find('.drag_handle').height(), minWidth: $(this).parent().innerWidth() - 6, maxWidth: $(this).parent().innerWidth() - 6, stop: function() + { + // Adjust the content + var container = $(this); + var headerHeight = $(this).find('.drag_handle').height(); + container.find('.item').height(container.innerHeight() - headerHeight - 12);//.width(container.innerWidth()); + var parent_id = $(this).parent().id; + SetUserPreference(parent_id+'_'+this.id+'_height', $(this).height(), true); // true => persistent + } + } ); + + // Tabs, using JQuery BBQ to store the history + // The "tab widgets" to handle. + var tabs = $('div[id^=tabbedContent]'); + + // This selector will be reused when selecting actual tab widget A elements. + var tab_a_selector = 'ul.ui-tabs-nav a'; + + // Define our own click handler for the tabs, overriding the default. + tabs.find( tab_a_selector ).click(function() + { + var state = {}; + + // Get the id of this tab widget. + var id = $(this).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); + + // Get the index of this tab. + var idx = $(this).parent().prevAll().length; + + // Set the state! + state[ id ] = idx; + $.bbq.pushState( state ); + }); + + // refresh the hash when the tab is changed (from a JS script) + $('body').on( 'tabsactivate', '.ui-tabs', function(event, ui) { + var state = {}; + + // Get the id of this tab widget. + var id = $(ui.newTab).closest( 'div[id^=tabbedContent]' ).attr( 'id' ); + + // Get the index of this tab. + var idx = $(ui.newTab).prevAll().length; + + // Set the state! + state[ id ] = idx; + $.bbq.pushState( state ); + }); + + // Bind an event to window.onhashchange that, when the history state changes, + // iterates over all tab widgets, changing the current tab as necessary. + $(window).bind( 'hashchange', function(e) + { + // Iterate over all tab widgets. + tabs.each(function() + { + // Get the index for this tab widget from the hash, based on the + // appropriate id property. In jQuery 1.4, you should use e.getState() + // instead of $.bbq.getState(). The second, 'true' argument coerces the + // string value to a number. + var idx = $.bbq.getState( this.id, true ) || 0; + + // Select the appropriate tab for this tab widget by triggering the custom + // event specified in the .tabs() init above (you could keep track of what + // tab each widget is on using .data, and only select a tab if it has + // changed). + $(this).find( tab_a_selector ).eq( idx ).triggerHandler( 'change' ); + }); + + // Iterate over all truncated lists to find whether they are expanded or not + $('a.truncated').each(function() + { + var state = $.bbq.getState( this.id, true ) || 'close'; + if (state == 'open') + { + $(this).trigger('open'); + } + else + { + $(this).trigger('close'); + } + }); + }); + + // Shortcut menu actions + $('.actions_button a').click( function() { + aMatches = /#(.*)$/.exec(window.location.href); + if (aMatches != null) + { + currentHash = aMatches[1]; + if ( /#(.*)$/.test(this.href)) + { + this.href = this.href.replace(/#(.*)$/, '#'+currentHash); + } + } + }); + + // End of Tabs handling + + PrepareWidgets(); + + // Make sortable, everything that claims to be sortable + $('.sortable').sortable( {axis: 'y', cursor: 'move', handle: '.drag_handle', stop: function() + { + if ($(this).hasClass('persistent')) + { + // remember the sort order for next time the page is loaded... + sSerialized = $(this).sortable('serialize', {key: 'menu'}); + var sTemp = sSerialized.replace(/menu=/g, ''); + SetUserPreference(this.id+'_order', sTemp.replace(/&/g, ','), true); // true => persistent ! + } + } + }); + docWidth = $(document).width(); + $('#ModalDlg').dialog({ autoOpen: false, modal: true, width: 0.8*docWidth, height: 'auto', maxHeight: $(window).height() - 50 }); // JQuery UI dialogs + ShowDebug(); + $('#logOffBtn>ul').popupmenu(); + + $('.caselog_header').click( function () { $(this).toggleClass('open').next('.caselog_entry,.caselog_entry_html').toggle(); }); + + $(document).ajaxSend(function(event, jqxhr, options) { + jqxhr.setRequestHeader('X-Combodo-Ajax', 'true'); + }); + $(document).ajaxError(function(event, jqxhr, options) { + if (jqxhr.status == 401) + { + $('
'+$sJSDisconnectedMessage+'
').dialog({ + modal:true, + title: $sJSTitle, + close: function() { $(this).remove(); }, + minWidth: 400, + buttons: [ + { text: $sJSLoginAgain, click: function() { window.location.href= GetAbsoluteUrlAppRoot()+'pages/UI.php' } }, + { text: $sJSStayOnThePage, click: function() { $(this).dialog('close'); } } + ] + }); + } + }); +EOF + ); + $this->add_ready_script(InlineImage::FixImagesWidth()); + /* + * Not used since the sorting of the tables is always performed server-side + AttributeDateTime::InitTableSorter($this, 'custom_date_time'); + AttributeDate::InitTableSorter($this, 'custom_date'); + */ + + $sUserPrefs = appUserPreferences::GetAsJSON(); + $this->add_script( + << 0) + { + sToken = ''; + if (sOwnershipToken != undefined) + { + sToken = '&token='+sOwnershipToken; + } + window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=release_lock_and_details&class='+sClass+'&id='+id+sToken); + } + else + { + window.location.href = sDefaultUrl; // Already contains the context... + } + } + + function BackToList(sClass) + { + window.location.href = AddAppContext(GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=search_oql&oql_class='+sClass+'&oql_clause=WHERE id=0'); + } + + function ShowDebug() + { + if ($('#rawOutput > div').html() != '') + { + $('#rawOutput').dialog( {autoOpen: true, modal:false, width: '80%'}); + } + } + + var oUserPreferences = $sUserPrefs; + + // For disabling the CKEditor at init time when the corresponding textarea is disabled ! + CKEDITOR.plugins.add( 'disabler', + { + init : function( editor ) + { + editor.on( 'instanceReady', function(e) + { + e.removeListener(); + $('#'+ editor.name).trigger('update'); + }); + } + + }); + + + function FixPaneVis() + { + $('.ui-layout-center, .ui-layout-north, .ui-layout-south').css({display: 'block'}); + } +EOF + ); + } + + + /** + * @param string $sId Identifies the item, to search after it in the current breadcrumb + * @param string $sLabel Label of the breadcrumb item + * @param string $sDescription More information, displayed as a tooltip + * @param string $sUrl Specify a URL if the current URL as perceived on the browser side is not relevant + * @param string $sIcon Icon (relative or absolute) path that will be displayed next to the label + */ + public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sIcon = '') + { + $this->bBreadCrumbEnabled = true; + $this->sBreadCrumbEntryId = $sId; + $this->sBreadCrumbEntryLabel = $sLabel; + $this->sBreadCrumbEntryDescription = $sDescription; + $this->sBreadCrumbEntryUrl = $sUrl; + $this->sBreadCrumbEntryIcon = $sIcon; + } + + /** + * State that there will be no breadcrumb item for the current page + */ + public function DisableBreadCrumb() + { + $this->bBreadCrumbEnabled = false; + $this->sBreadCrumbEntryId = null; + $this->sBreadCrumbEntryLabel = null; + $this->sBreadCrumbEntryDescription = null; + $this->sBreadCrumbEntryUrl = null; + $this->sBreadCrumbEntryIcon = null; + } + + public function AddToMenu($sHtml) + { + $this->m_sMenu .= $sHtml; + } + + public function GetSiloSelectionForm() + { + // List of visible Organizations + $iCount = 0; + $oSet = null; + if (MetaModel::IsValidClass('Organization')) { + // Display the list of *favorite* organizations... but keeping in mind what is the real number of organizations + $aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null); + $oSearchFilter = new DBObjectSearch('Organization'); + $oSearchFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true); + $oSet = new CMDBObjectSet($oSearchFilter); + $iCount = $oSet->Count(); // total number of existing Orgs + + // Now get the list of Orgs to be displayed in the menu + $oSearchFilter = DBObjectSearch::FromOQL(ApplicationMenu::GetFavoriteSiloQuery()); + $oSearchFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true); + if (!empty($aFavoriteOrgs)) { + $oSearchFilter->AddCondition('id', $aFavoriteOrgs, 'IN'); + } + $oSet = new CMDBObjectSet($oSearchFilter); // List of favorite orgs + } + switch ($iCount) { + case 0: + // No such dimension/silo => nothing to select + $sHtml = '
'; + break; + + case 1: + // Only one possible choice... no selection, but display the value + $oOrg = $oSet->Fetch(); + $sHtml = '
' . $oOrg->GetName() . '
'; + $sHtml .= ''; + break; + + default: + $sHtml = ''; + $oAppContext = new ApplicationContext(); + $iCurrentOrganization = $oAppContext->GetCurrentValue('org_id'); + $sHtml = '
'; + $sHtml .= '
'; //
'; + $sHtml .= ' '; + $sHtml .= ' ' . self::FilterXSS($sLogOffMenu) . ''; + $sHtml .= ' '; + $sHtml .= ' '; + $sHtml .= ' '; + $sHtml .= ' '; + +// $sHtml .= ' '; +// $sHtml .= '
'; + + $sHtml .= ' '; + $sHtml .= '
'; + $sHtml .= ' '; + $sHtml .= self::FilterXSS($this->s_content); + $sHtml .= ' '; + $sHtml .= '
'; + $sHtml .= ''; + $sHtml .= $sSouthPane; + + // Add the captured output + if (trim($s_captured_output) != "") { + $sHtml .= "
" . self::FilterXSS($s_captured_output) . "
\n"; + } + $sHtml .= "
" . self::FilterXSS($this->s_deferred_content) . "
"; + $sHtml .= "
Please wait...
\n"; // jqModal Window + $sHtml .= "
"; + $sHtml .= "
"; + } else { + $sHtml .= self::FilterXSS($this->s_content); + } + + $sHtml .= "\n"; + $sHtml .= "\n"; + + if ($this->GetOutputFormat() == 'html') { + $oKPI = new ExecutionKPI(); + echo $sHtml; + $oKPI->ComputeAndReport('Echoing (' . round(strlen($sHtml) / 1024) . ' Kb)'); + } else if ($this->GetOutputFormat() == 'pdf' && $this->IsOutputFormatAvailable('pdf')) { + if (@is_readable(APPROOT . 'lib/MPDF/mpdf.php')) { + require_once(APPROOT . 'lib/MPDF/mpdf.php'); + $oMPDF = new mPDF('c'); + $oMPDF->mirroMargins = false; + if ($this->a_base['href'] != '') { + $oMPDF->setBasePath($this->a_base['href']); // Seems that the tag is not recognized by mPDF... + } + $oMPDF->showWatermarkText = true; + if ($this->GetOutputOption('pdf', 'template_path')) { + $oMPDF->setImportUse(); // Allow templates + $oMPDF->SetDocTemplate($this->GetOutputOption('pdf', 'template_path'), 1); + } + $oMPDF->WriteHTML($sHtml); + $sOutputName = $this->s_title . '.pdf'; + if ($this->GetOutputOption('pdf', 'output_name')) { + $sOutputName = $this->GetOutputOption('pdf', 'output_name'); + } + $oMPDF->Output($sOutputName, 'I'); + } + } + DBSearch::RecordQueryTrace(); + ExecutionKPI::ReportStats(); + } + + /** + * Adds init scripts for the collapsible sections + */ + private function outputCollapsibleSectionInit() + { + if (!$this->bHasCollapsibleSection) { + return; + } + + $this->add_script(<<<'EOD' +function initCollapsibleSection(iSectionId, bOpenedByDefault, sSectionStateStorageKey) +{ +var bStoredSectionState = JSON.parse(localStorage.getItem(sSectionStateStorageKey)); +var bIsSectionOpenedInitially = (bStoredSectionState == null) ? bOpenedByDefault : bStoredSectionState; + +if (bIsSectionOpenedInitially) { + $("#LnkCollapse_"+iSectionId).toggleClass("open"); + $("#Collapse_"+iSectionId).toggle(); +} + +$("#LnkCollapse_"+iSectionId).click(function(e) { + localStorage.setItem(sSectionStateStorageKey, !($("#Collapse_"+iSectionId).is(":visible"))); + $("#LnkCollapse_"+iSectionId).toggleClass("open"); + $("#Collapse_"+iSectionId).slideToggle("normal"); + e.preventDefault(); // we don't want to do anything more (see #1030 : a non wanted tab switching was triggered) +}); +} +EOD + ); + } + + public function AddTabContainer($sTabContainer, $sPrefix = '') + { + $this->add($this->m_oTabs->AddTabContainer($sTabContainer, $sPrefix)); + } + + public function AddToTab($sTabContainer, $sTabLabel, $sHtml) + { + $this->add($this->m_oTabs->AddToTab($sTabContainer, $sTabLabel, $sHtml)); + } + + public function SetCurrentTabContainer($sTabContainer = '') + { + return $this->m_oTabs->SetCurrentTabContainer($sTabContainer); + } + + public function SetCurrentTab($sTabLabel = '') + { + return $this->m_oTabs->SetCurrentTab($sTabLabel); + } + + /** + * Add a tab which content will be loaded asynchronously via the supplied URL + * + * Limitations: + * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to pull content from another server. + * Static content cannot be added inside such tabs. + * + * @param string $sTabLabel The (localised) label of the tab + * @param string $sUrl The URL to load (on the same server) + * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause the tab to be reloaded upon each activation. + * + * @since 2.0.3 + */ + public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) + { + $this->add($this->m_oTabs->AddAjaxTab($sTabLabel, $sUrl, $bCache)); + } + + public function GetCurrentTab() + { + return $this->m_oTabs->GetCurrentTab(); + } + + public function RemoveTab($sTabLabel, $sTabContainer = null) + { + $this->m_oTabs->RemoveTab($sTabLabel, $sTabContainer); + } + + /** + * Finds the tab whose title matches a given pattern + * @return mixed The name of the tab as a string or false if not found + */ + public function FindTab($sPattern, $sTabContainer = null) + { + return $this->m_oTabs->FindTab($sPattern, $sTabContainer); + } + + /** + * Make the given tab the active one, as if it were clicked + * DOES NOT WORK: apparently in the *old* version of jquery + * that we are using this is not supported... TO DO upgrade + * the whole jquery bundle... + */ + public function SelectTab($sTabContainer, $sTabLabel) + { + $this->add_ready_script($this->m_oTabs->SelectTab($sTabContainer, $sTabLabel)); + } + + public function StartCollapsibleSection( + $sSectionLabel, $bOpenedByDefault = false, $sSectionStateStorageBusinessKey = '' + ) + { + $this->add($this->GetStartCollapsibleSection($sSectionLabel, $bOpenedByDefault, + $sSectionStateStorageBusinessKey)); + } + + private function GetStartCollapsibleSection( + $sSectionLabel, $bOpenedByDefault = false, $sSectionStateStorageBusinessKey = '' + ) + { + $this->bHasCollapsibleSection = true; + $sHtml = ''; + static $iSectionId = 0; + $sHtml .= '
' . $sSectionLabel . '
' . "\n"; + $sHtml .= '"; + } + + public function add($sHtml) + { + if (($this->m_oTabs->GetCurrentTabContainer() != '') && ($this->m_oTabs->GetCurrentTab() != '')) { + $this->m_oTabs->AddToCurrentTab($sHtml); + } else { + parent::add($sHtml); + } + } + + /** + * Records the current state of the 'html' part of the page output + * @return mixed The current state of the 'html' output + */ + public function start_capture() + { + $sCurrentTabContainer = $this->m_oTabs->GetCurrentTabContainer(); + $sCurrentTab = $this->m_oTabs->GetCurrentTab(); + + if (!empty($sCurrentTabContainer) && !empty($sCurrentTab)) { + $iOffset = $this->m_oTabs->GetCurrentTabLength(); + return array('tc' => $sCurrentTabContainer, 'tab' => $sCurrentTab, 'offset' => $iOffset); + } else { + return parent::start_capture(); + } + } + + /** + * Returns the part of the html output that occurred since the call to start_capture + * and removes this part from the current html output + * + * @param $offset mixed The value returned by start_capture + * + * @return string The part of the html output that was added since the call to start_capture + */ + public function end_capture($offset) + { + if (is_array($offset)) { + if ($this->m_oTabs->TabExists($offset['tc'], $offset['tab'])) { + $sCaptured = $this->m_oTabs->TruncateTab($offset['tc'], $offset['tab'], $offset['offset']); + } else { + $sCaptured = ''; + } + } else { + $sCaptured = parent::end_capture($offset); + } + return $sCaptured; + } + + /** + * Set the message to be displayed in the 'app-banner' section at the top of the page + */ + public function SetMessage($sHtmlMessage) + { + $sHtmlIcon = ''; + $this->AddApplicationMessage($sHtmlMessage, $sHtmlIcon); + } + + /** + * Add message to be displayed in the 'app-banner' section at the top of the page + */ + public function AddApplicationMessage($sHtmlMessage, $sHtmlIcon = null, $sTip = null) + { + if (strlen($sHtmlMessage)) { + $this->m_aMessages[] = array( + 'icon' => $sHtmlIcon, + 'message' => $sHtmlMessage, + 'tip' => $sTip + ); + } + } + /** + * Adds a script to be executed when the DOM is ready (typical JQuery use), right before add_ready_script + * @return void + */ + public function add_init_script($sScript){ + $this->m_aInitScript[] = $sScript; + } +} diff --git a/application/itopwizardwebpage.class.inc.php b/application/itopwizardwebpage.class.inc.php index 24512ab76..c11d5a98a 100644 --- a/application/itopwizardwebpage.class.inc.php +++ b/application/itopwizardwebpage.class.inc.php @@ -1,57 +1,57 @@ - - - -/** - * Class iTopWizardWebPage - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once('itopwebpage.class.inc.php'); -/** - * Web page to display a wizard in the iTop framework - */ -class iTopWizardWebPage extends iTopWebPage -{ - var $m_iCurrentStep; - var $m_aSteps; - public function __construct($sTitle, $currentOrganization, $iCurrentStep, $aSteps) - { - parent::__construct($sTitle." - step $iCurrentStep of ".count($aSteps)." - ".$aSteps[$iCurrentStep - 1], $currentOrganization); - $this->m_iCurrentStep = $iCurrentStep; - $this->m_aSteps = $aSteps; - } - - public function output() - { - $aSteps = array(); - $iIndex = 0; - foreach($this->m_aSteps as $sStepTitle) - { - $iIndex++; - $sStyle = ($iIndex == $this->m_iCurrentStep) ? 'wizActiveStep' : 'wizStep'; - $aSteps[] = "
$sStepTitle
"; - } - $sWizardHeader = "

".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."

\n".implode("
", $aSteps)."
\n"; - $this->s_content = "$sWizardHeader
".$this->s_content."
"; - parent::output(); - } -} -?> + + + +/** + * Class iTopWizardWebPage + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('itopwebpage.class.inc.php'); +/** + * Web page to display a wizard in the iTop framework + */ +class iTopWizardWebPage extends iTopWebPage +{ + var $m_iCurrentStep; + var $m_aSteps; + public function __construct($sTitle, $currentOrganization, $iCurrentStep, $aSteps) + { + parent::__construct($sTitle." - step $iCurrentStep of ".count($aSteps)." - ".$aSteps[$iCurrentStep - 1], $currentOrganization); + $this->m_iCurrentStep = $iCurrentStep; + $this->m_aSteps = $aSteps; + } + + public function output() + { + $aSteps = array(); + $iIndex = 0; + foreach($this->m_aSteps as $sStepTitle) + { + $iIndex++; + $sStyle = ($iIndex == $this->m_iCurrentStep) ? 'wizActiveStep' : 'wizStep'; + $aSteps[] = "
$sStepTitle
"; + } + $sWizardHeader = "

".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."

\n".implode("
", $aSteps)."
\n"; + $this->s_content = "$sWizardHeader
".$this->s_content."
"; + parent::output(); + } +} +?> diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php index 1e4f6fbe8..e77deae08 100644 --- a/application/loginwebpage.class.inc.php +++ b/application/loginwebpage.class.inc.php @@ -1,880 +1,880 @@ - - - -/** - * Class LoginWebPage - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/nicewebpage.class.inc.php"); -require_once(APPROOT.'/application/portaldispatcher.class.inc.php'); -/** - * Web page used for displaying the login form - */ - -class LoginWebPage extends NiceWebPage -{ - const EXIT_PROMPT = 0; - const EXIT_HTTP_401 = 1; - const EXIT_RETURN = 2; - - const EXIT_CODE_OK = 0; - const EXIT_CODE_MISSINGLOGIN = 1; - const EXIT_CODE_MISSINGPASSWORD = 2; - const EXIT_CODE_WRONGCREDENTIALS = 3; - const EXIT_CODE_MUSTBEADMIN = 4; - const EXIT_CODE_PORTALUSERNOTAUTHORIZED = 5; - const EXIT_CODE_NOTAUTHORIZED = 6; - - protected static $sHandlerClass = __class__; - public static function RegisterHandler($sClass) - { - self::$sHandlerClass = $sClass; - } - - /** - * @return \LoginWebPage - */ - public static function NewLoginWebPage() - { - return new self::$sHandlerClass; - } - - protected static $m_sLoginFailedMessage = ''; - - public function __construct($sTitle = null) - { - if($sTitle === null) - { - $sTitle = Dict::S('UI:Login:Title'); - } - - parent::__construct($sTitle); - $this->SetStyleSheet(); - $this->add_header("Cache-control: no-cache"); - } - - public function SetStyleSheet() - { - $this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/login.css'); - } - - public static function SetLoginFailedMessage($sMessage) - { - self::$m_sLoginFailedMessage = $sMessage; - } - - public function EnableResetPassword() - { - return MetaModel::GetConfig()->Get('forgot_password'); - } - - public function DisplayLoginHeader($bMainAppLogo = false) - { - if ($bMainAppLogo) - { - $sLogo = 'itop-logo.png'; - $sBrandingLogo = 'main-logo.png'; - } - else - { - $sLogo = 'itop-logo-external.png'; - $sBrandingLogo = 'login-logo.png'; - } - $sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION); - $sIconUrl = Utils::GetConfig()->Get('app_icon_url'); - $sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/'.$sLogo.'?t='.utils::GetCacheBusterTimestamp(); - if (file_exists(MODULESROOT.'branding/'.$sBrandingLogo)) - { - $sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/'.$sBrandingLogo.'?t='.utils::GetCacheBusterTimestamp(); - } - $this->add("
\n"); - } - - public function DisplayLoginForm($sLoginType, $bFailedLogin = false) - { - switch($sLoginType) - { - case 'cas': - utils::InitCASClient(); - // force CAS authentication - phpCAS::forceAuthentication(); // Will redirect the user and exit since the user is not yet authenticated - break; - - case 'basic': - case 'url': - $this->add_header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION)); - $this->add_header('HTTP/1.0 401 Unauthorized'); - $this->add_header('Content-type: text/html; charset=iso-8859-1'); - // Note: displayed when the user will click on Cancel - $this->add('

'.Dict::S('UI:Login:Error:AccessRestricted').'

'); - break; - - case 'external': - case 'form': - default: // In case the settings get messed up... - $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); - $sAuthPwd = utils::ReadParam('suggest_pwd', '', true, 'raw_data'); - - $this->DisplayLoginHeader(); - $this->add("
\n"); - $this->add("

".Dict::S('UI:Login:Welcome')."

\n"); - if ($bFailedLogin) - { - if (self::$m_sLoginFailedMessage != '') - { - $this->add("

".self::$m_sLoginFailedMessage."

\n"); - } - else - { - $this->add("

".Dict::S('UI:Login:IncorrectLoginPassword')."

\n"); - } - } - else - { - $this->add("

".Dict::S('UI:Login:IdentifyYourself')."

\n"); - } - $this->add("
\n"); - $this->add("\n"); - $sForgotPwd = $this->EnableResetPassword() ? $this->ForgotPwdLink() : ''; - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - if (strlen($sForgotPwd) > 0) - { - $this->add("\n"); - } - $this->add("
$sForgotPwd
\n"); - $this->add("\n"); - - $this->add_ready_script('$("#user").focus();'); - - // Keep the OTHER parameters posted - foreach($_POST as $sPostedKey => $postedValue) - { - if (!in_array($sPostedKey, array('auth_user', 'auth_pwd'))) - { - if (is_array($postedValue)) - { - foreach($postedValue as $sKey => $sValue) - { - $this->add("\n"); - } - } - else - { - $this->add("\n"); - } - } - } - - $this->add("
\n"); - $this->add(Dict::S('UI:Login:About')); - $this->add("
\n"); - break; - } - } - - /** - * Return '' to disable this feature - */ - public function ForgotPwdLink() - { - $sUrl = utils::GetAbsoluteUrlAppRoot() . 'pages/UI.php?loginop=forgot_pwd'; - $sHtml = "".Dict::S('UI:Login:ForgotPwd').""; - return $sHtml; - } - - public function DisplayForgotPwdForm($bFailedToReset = false, $sFailureReason = null) - { - $this->DisplayLoginHeader(); - $this->add("
\n"); - $this->add("

".Dict::S('UI:Login:ForgotPwdForm')."

\n"); - $this->add("

".Dict::S('UI:Login:ForgotPwdForm+')."

\n"); - if ($bFailedToReset) - { - $this->add("

".Dict::Format('UI:Login:ResetPwdFailed', htmlentities($sFailureReason, ENT_QUOTES, 'UTF-8'))."

\n"); - } - $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); - $this->add("
\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("
  
\n"); - $this->add("\n"); - $this->add("
\n"); - $this->add("
\n"); - - $this->add_ready_script('$("#user").focus();'); - } - - protected function ForgotPwdGo() - { - $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); - - try - { - UserRights::Login($sAuthUser); // Set the user's language (if possible!) - /** @var UserInternal $oUser */ - $oUser = UserRights::GetUserObject(); - if ($oUser == null) - { - throw new Exception(Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)); - } - if (!MetaModel::IsValidAttCode(get_class($oUser), 'reset_pwd_token')) - { - throw new Exception(Dict::S('UI:ResetPwd-Error-NotPossible')); - } - if (!$oUser->CanChangePassword()) - { - throw new Exception(Dict::S('UI:ResetPwd-Error-FixedPwd')); - } - - $sTo = $oUser->GetResetPasswordEmail(); // throws Exceptions if not allowed - if ($sTo == '') - { - throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmail')); - } - - // This token allows the user to change the password without knowing the previous one - $sToken = substr(md5(APPROOT.uniqid()), 0, 16); - $oUser->Set('reset_pwd_token', $sToken); - CMDBObject::SetTrackInfo('Reset password'); - $oUser->AllowWrite(true); - $oUser->DBUpdate(); - - $oEmail = new Email(); - $oEmail->SetRecipientTO($sTo); - $sFrom = MetaModel::GetConfig()->Get('forgot_password_from'); - $oEmail->SetRecipientFrom($sFrom); - $oEmail->SetSubject(Dict::S('UI:ResetPwd-EmailSubject')); - $sResetUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?loginop=reset_pwd&auth_user='.urlencode($oUser->Get('login')).'&token='.urlencode($sToken); - $oEmail->SetBody(Dict::Format('UI:ResetPwd-EmailBody', $sResetUrl)); - $iRes = $oEmail->Send($aIssues, true /* force synchronous exec */); - switch ($iRes) - { - //case EMAIL_SEND_PENDING: - case EMAIL_SEND_OK: - break; - - case EMAIL_SEND_ERROR: - default: - IssueLog::Error('Failed to send the email with the NEW password for '.$oUser->Get('friendlyname').': '.implode(', ', $aIssues)); - throw new Exception(Dict::S('UI:ResetPwd-Error-Send')); - } - - $this->DisplayLoginHeader(); - $this->add("
\n"); - $this->add("

".Dict::S('UI:Login:ForgotPwdForm')."

\n"); - $this->add("

".Dict::S('UI:ResetPwd-EmailSent')."

"); - $this->add("
\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("
\n"); - $this->add("
\n"); - $this->add("DisplayForgotPwdForm(true, $e->getMessage()); - } - } - - public function DisplayResetPwdForm() - { - $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); - $sToken = utils::ReadParam('token', '', false, 'raw_data'); - - UserRights::Login($sAuthUser); // Set the user's language - $oUser = UserRights::GetUserObject(); - - $this->DisplayLoginHeader(); - $this->add("
\n"); - $this->add("

".Dict::S('UI:ResetPwd-Title')."

\n"); - if ($oUser == null) - { - $this->add("

".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."

\n"); - } - else - { - $oEncryptedToken = $oUser->Get('reset_pwd_token'); - - if (!$oEncryptedToken->CheckPassword($sToken)) - { - $this->add("

".Dict::S('UI:ResetPwd-Error-InvalidToken')."

\n"); - } - else - { - $this->add("

".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."

\n"); - - $sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch'); - $this->add_script( -<<add("
\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("
\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("
\n"); - $this->add("DisplayLoginHeader(); - $this->add("
\n"); - $this->add("

".Dict::S('UI:ResetPwd-Title')."

\n"); - if ($oUser == null) - { - $this->add("

".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."

\n"); - } - else - { - $oEncryptedPassword = $oUser->Get('reset_pwd_token'); - if (!$oEncryptedPassword->CheckPassword($sToken)) - { - $this->add("

".Dict::S('UI:ResetPwd-Error-InvalidToken')."

\n"); - } - else - { - // Trash the token and change the password - $oUser->Set('reset_pwd_token', ''); - $oUser->SetPassword($sNewPwd); // Does record the change into the DB - - $this->add("

".Dict::S('UI:ResetPwd-Ready')."

"); - $sUrl = utils::GetAbsoluteUrlAppRoot(); - $this->add("

".Dict::S('UI:ResetPwd-Login')."

"); - } - $this->add("add_script(<<DisplayLoginHeader(); - $this->add("
\n"); - $this->add("

".Dict::S('UI:Login:ChangeYourPassword')."

\n"); - if ($bFailedLogin) - { - $this->add("

".Dict::S('UI:Login:IncorrectOldPassword')."

\n"); - } - $this->add("
\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("\n"); - $this->add("
  
\n"); - $this->add("\n"); - $this->add("
\n"); - $this->add("
\n"); - } - - static function ResetSession() - { - // Unset all of the session variables. - unset($_SESSION['auth_user']); - unset($_SESSION['login_mode']); - unset($_SESSION['archive_mode']); - unset($_SESSION['impersonate_user']); - UserRights::_ResetSessionCache(); - // If it's desired to kill the session, also delete the session cookie. - // Note: This will destroy the session, and not just the session data! - } - - static function SecureConnectionRequired() - { - return MetaModel::GetConfig()->GetSecureConnectionRequired(); - } - - /** - * Guess if a string looks like an UTF-8 string based on some ranges of multi-bytes encoding - * @param string $sString - * @return bool True if the string contains some typical UTF-8 multi-byte sequences - */ - static function LooksLikeUTF8($sString) - { - return preg_match('%(?: - [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte - |\xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs - |[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte - |\xED[\x80-\x9F][\x80-\xBF] # excluding surrogates - |\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 - |[\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 - |\xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 - )+%xs', $sString); - } - - /** - * Attempt a login - * - * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) - * @return int One of the class constants EXIT_CODE_... - */ - protected static function Login($iOnExit) - { - if (self::SecureConnectionRequired() && !utils::IsConnectionSecure()) - { - // Non secured URL... request for a secure connection - throw new Exception('Secure connection required!'); - } - - $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); - - if (isset($_SESSION['auth_user'])) - { - //echo "User: ".$_SESSION['auth_user']."\n"; - // Already authentified - $bRet = UserRights::Login($_SESSION['auth_user']); // Login & set the user's language - if ($bRet) - { - return self::EXIT_CODE_OK; - } - // The user account is no longer valid/enabled - static::ResetSession(); - } - - $index = 0; - $sLoginMode = ''; - $sAuthentication = 'internal'; - while(($sLoginMode == '') && ($index < count($aAllowedLoginTypes))) - { - $sLoginType = $aAllowedLoginTypes[$index]; - switch($sLoginType) - { - case 'cas': - utils::InitCASClient(); - // check CAS authentication - if (phpCAS::isAuthenticated()) - { - $sAuthUser = phpCAS::getUser(); - $sAuthPwd = ''; - $sLoginMode = 'cas'; - $sAuthentication = 'external'; - } - break; - - case 'form': - // iTop standard mode: form based authentication - $sAuthUser = utils::ReadPostedParam('auth_user', '', false, 'raw_data'); - $sAuthPwd = utils::ReadPostedParam('auth_pwd', null, false, 'raw_data'); - if (($sAuthUser != '') && ($sAuthPwd !== null)) - { - $sLoginMode = 'form'; - } - break; - - case 'basic': - // Standard PHP authentication method, works with Apache... - // Case 1) Apache running in CGI mode + rewrite rules in .htaccess - if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) - { - list($sAuthUser, $sAuthPwd) = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); - $sLoginMode = 'basic'; - } - else if (isset($_SERVER['PHP_AUTH_USER'])) - { - $sAuthUser = $_SERVER['PHP_AUTH_USER']; - // Unfortunately, the RFC is not clear about the encoding... - // IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8 - // So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base - if (!self::LooksLikeUTF8($sAuthUser)) - { - // Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8 - // Supposed to be harmless in case of a plain ASCII string... - $sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser); - } - $sAuthPwd = $_SERVER['PHP_AUTH_PW']; - if (!self::LooksLikeUTF8($sAuthPwd)) - { - // Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8 - // Supposed to be harmless in case of a plain ASCII string... - $sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd); - } - $sLoginMode = 'basic'; - } - break; - - case 'external': - // Web server supplied authentication - $bExternalAuth = false; - $sExtAuthVar = MetaModel::GetConfig()->GetExternalAuthenticationVariable(); // In which variable is the info passed ? - eval('$sAuthUser = isset('.$sExtAuthVar.') ? '.$sExtAuthVar.' : false;'); // Retrieve the value - if ($sAuthUser && (strlen($sAuthUser) > 0)) - { - $sAuthPwd = ''; // No password in this case the web server already authentified the user... - $sLoginMode = 'external'; - $sAuthentication = 'external'; - } - break; - - case 'url': - // Credentials passed directly in the url - $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); - $sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data'); - if (($sAuthUser != '') && ($sAuthPwd !== null)) - { - $sLoginMode = 'url'; - } - break; - } - $index++; - } - //echo "\nsLoginMode: $sLoginMode (user: $sAuthUser / pwd: $sAuthPwd\n)"; - if ($sLoginMode == '') - { - // First connection - $sDesiredLoginMode = utils::ReadParam('login_mode'); - if (in_array($sDesiredLoginMode, $aAllowedLoginTypes)) - { - $sLoginMode = $sDesiredLoginMode; - } - else - { - $sLoginMode = $aAllowedLoginTypes[0]; // First in the list... - } - if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER)) - { - // X-Combodo-Ajax is a special header automatically added to all ajax requests - // Let's reply that we're currently logged-out - header('HTTP/1.0 401 Unauthorized'); - exit; - } - if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic')) - { - header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION)); - header('HTTP/1.0 401 Unauthorized'); - header('Content-type: text/html; charset=iso-8859-1'); - exit; - } - else if($iOnExit == self::EXIT_RETURN) - { - if (($sAuthUser !== '') && ($sAuthPwd === null)) - { - return self::EXIT_CODE_MISSINGPASSWORD; - } - else - { - return self::EXIT_CODE_MISSINGLOGIN; - } - } - else - { - $oPage = self::NewLoginWebPage(); - $oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */); - $oPage->output(); - exit; - } - } - else - { - if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $sLoginMode, $sAuthentication)) - { - //echo "Check Credentials returned false for user $sAuthUser!"; - self::ResetSession(); - if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic')) - { - header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION)); - header('HTTP/1.0 401 Unauthorized'); - header('Content-type: text/html; charset=iso-8859-1'); - exit; - } - else if($iOnExit == self::EXIT_RETURN) - { - return self::EXIT_CODE_WRONGCREDENTIALS; - } - else - { - $oPage = self::NewLoginWebPage(); - $oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */); - $oPage->output(); - exit; - } - } - else - { - // User is Ok, let's save it in the session and proceed with normal login - UserRights::Login($sAuthUser, $sAuthentication); // Login & set the user's language - - if (MetaModel::GetConfig()->Get('log_usage')) - { - $oLog = new EventLoginUsage(); - $oLog->Set('userinfo', UserRights::GetUser()); - $oLog->Set('user_id', UserRights::GetUserObject()->GetKey()); - $oLog->Set('message', 'Successful login'); - $oLog->DBInsertNoReload(); - } - - $_SESSION['auth_user'] = $sAuthUser; - $_SESSION['login_mode'] = $sLoginMode; - UserRights::_InitSessionCache(); - } - } - return self::EXIT_CODE_OK; - } - - /** - * Overridable: depending on the user, head toward a dedicated portal - * @param string|null $sRequestedPortalId - * @param int $iOnExit How to complete the call: redirect or return a code - */ - protected static function ChangeLocation($sRequestedPortalId = null, $iOnExit = self::EXIT_PROMPT) - { - $fStart = microtime(true); - $ret = call_user_func(array(self::$sHandlerClass, 'Dispatch'), $sRequestedPortalId); - if ($ret === true) - { - return self::EXIT_CODE_OK; - } - else if($ret === false) - { - throw new Exception('Nowhere to go??'); - } - else - { - if ($iOnExit == self::EXIT_RETURN) - { - return self::EXIT_CODE_PORTALUSERNOTAUTHORIZED; - } - else - { - // No rights to be here, redirect to the portal - header('Location: '.$ret); - } - } - } - - /** - * Check if the user is already authentified, if yes, then performs some additional validations: - * - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed - * - if $bIsAllowedToPortalUsers is false and the user has only access to the portal, then the user is redirected - * to the portal - * - * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page - * @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal - * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) - * - * @return int|mixed|string - * @throws \Exception - */ - static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false, $iOnExit = self::EXIT_PROMPT) - { - $sRequestedPortalId = $bIsAllowedToPortalUsers ? 'legacy_portal' : 'backoffice'; - return self::DoLoginEx($sRequestedPortalId, $bMustBeAdmin, $iOnExit); - } - - /** - * Check if the user is already authentified, if yes, then performs some additional validations to redirect towards - * the desired "portal" - * - * @param string|null $sRequestedPortalId The requested "portal" interface, null for any - * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page - * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) - * - * @return int|mixed|string - * @throws \Exception - */ - static function DoLoginEx($sRequestedPortalId = null, $bMustBeAdmin = false, $iOnExit = self::EXIT_PROMPT) - { - $operation = utils::ReadParam('loginop', ''); - - $sMessage = self::HandleOperations($operation); // May exit directly - - $iRet = self::Login($iOnExit); - - if ($iRet == self::EXIT_CODE_OK) - { - if ($bMustBeAdmin && !UserRights::IsAdministrator()) - { - if ($iOnExit == self::EXIT_RETURN) - { - return self::EXIT_CODE_MUSTBEADMIN; - } - else - { - require_once(APPROOT.'/setup/setuppage.class.inc.php'); - $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); - $oP->add("

".Dict::S('UI:Login:Error:AccessAdmin')."

\n"); - $oP->p("".Dict::S('UI:LogOffMenu').""); - $oP->output(); - exit; - } - } - $iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $sRequestedPortalId, $iOnExit); - } - if ($iOnExit == self::EXIT_RETURN) - { - return $iRet; - } - else - { - return $sMessage; - } - } - protected static function HandleOperations($operation) - { - $sMessage = ''; // most of the operations never return, but some can return a message to be displayed - if ($operation == 'logoff') - { - if (isset($_SESSION['login_mode'])) - { - $sLoginMode = $_SESSION['login_mode']; - } - else - { - $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); - if (count($aAllowedLoginTypes) > 0) - { - $sLoginMode = $aAllowedLoginTypes[0]; - } - else - { - $sLoginMode = 'form'; - } - } - self::ResetSession(); - $oPage = self::NewLoginWebPage(); - $oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */); - $oPage->output(); - exit; - } - else if ($operation == 'forgot_pwd') - { - $oPage = self::NewLoginWebPage(); - $oPage->DisplayForgotPwdForm(); - $oPage->output(); - exit; - } - else if ($operation == 'forgot_pwd_go') - { - $oPage = self::NewLoginWebPage(); - $oPage->ForgotPwdGo(); - $oPage->output(); - exit; - } - else if ($operation == 'reset_pwd') - { - $oPage = self::NewLoginWebPage(); - $oPage->DisplayResetPwdForm(); - $oPage->output(); - exit; - } - else if ($operation == 'do_reset_pwd') - { - $oPage = self::NewLoginWebPage(); - $oPage->DoResetPassword(); - $oPage->output(); - exit; - } - else if ($operation == 'change_pwd') - { - $sAuthUser = $_SESSION['auth_user']; - UserRights::Login($sAuthUser); // Set the user's language - $oPage = self::NewLoginWebPage(); - $oPage->DisplayChangePwdForm(); - $oPage->output(); - exit; - } - if ($operation == 'do_change_pwd') - { - $sAuthUser = $_SESSION['auth_user']; - UserRights::Login($sAuthUser); // Set the user's language - $sOldPwd = utils::ReadPostedParam('old_pwd', '', false, 'raw_data'); - $sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data'); - if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd)))) - { - $oPage = self::NewLoginWebPage(); - $oPage->DisplayChangePwdForm(true); // old pwd was wrong - $oPage->output(); - exit; - } - $sMessage = Dict::S('UI:Login:PasswordChanged'); - } - return $sMessage; - } - - protected static function Dispatch($sRequestedPortalId) - { - if ($sRequestedPortalId === null) return true; // allowed to any portal => return true - - $aPortalsConf = PortalDispatcherData::GetData(); - $aDispatchers = array(); - foreach($aPortalsConf as $sPortalId => $aConf) - { - $sHandlerClass = $aConf['handler']; - $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId); - } - - if (array_key_exists($sRequestedPortalId, $aDispatchers) && $aDispatchers[$sRequestedPortalId]->IsUserAllowed()) - { - return true; - } - foreach($aDispatchers as $sPortalId => $oDispatcher) - { - if ($oDispatcher->IsUserAllowed()) return $oDispatcher->GetUrl(); - } - return false; // nothing matched !! - } -} // End of class + + + +/** + * Class LoginWebPage + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/nicewebpage.class.inc.php"); +require_once(APPROOT.'/application/portaldispatcher.class.inc.php'); +/** + * Web page used for displaying the login form + */ + +class LoginWebPage extends NiceWebPage +{ + const EXIT_PROMPT = 0; + const EXIT_HTTP_401 = 1; + const EXIT_RETURN = 2; + + const EXIT_CODE_OK = 0; + const EXIT_CODE_MISSINGLOGIN = 1; + const EXIT_CODE_MISSINGPASSWORD = 2; + const EXIT_CODE_WRONGCREDENTIALS = 3; + const EXIT_CODE_MUSTBEADMIN = 4; + const EXIT_CODE_PORTALUSERNOTAUTHORIZED = 5; + const EXIT_CODE_NOTAUTHORIZED = 6; + + protected static $sHandlerClass = __class__; + public static function RegisterHandler($sClass) + { + self::$sHandlerClass = $sClass; + } + + /** + * @return \LoginWebPage + */ + public static function NewLoginWebPage() + { + return new self::$sHandlerClass; + } + + protected static $m_sLoginFailedMessage = ''; + + public function __construct($sTitle = null) + { + if($sTitle === null) + { + $sTitle = Dict::S('UI:Login:Title'); + } + + parent::__construct($sTitle); + $this->SetStyleSheet(); + $this->add_header("Cache-control: no-cache"); + } + + public function SetStyleSheet() + { + $this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/login.css'); + } + + public static function SetLoginFailedMessage($sMessage) + { + self::$m_sLoginFailedMessage = $sMessage; + } + + public function EnableResetPassword() + { + return MetaModel::GetConfig()->Get('forgot_password'); + } + + public function DisplayLoginHeader($bMainAppLogo = false) + { + if ($bMainAppLogo) + { + $sLogo = 'itop-logo.png'; + $sBrandingLogo = 'main-logo.png'; + } + else + { + $sLogo = 'itop-logo-external.png'; + $sBrandingLogo = 'login-logo.png'; + } + $sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION); + $sIconUrl = Utils::GetConfig()->Get('app_icon_url'); + $sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/'.$sLogo.'?t='.utils::GetCacheBusterTimestamp(); + if (file_exists(MODULESROOT.'branding/'.$sBrandingLogo)) + { + $sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/'.$sBrandingLogo.'?t='.utils::GetCacheBusterTimestamp(); + } + $this->add("
\n"); + } + + public function DisplayLoginForm($sLoginType, $bFailedLogin = false) + { + switch($sLoginType) + { + case 'cas': + utils::InitCASClient(); + // force CAS authentication + phpCAS::forceAuthentication(); // Will redirect the user and exit since the user is not yet authenticated + break; + + case 'basic': + case 'url': + $this->add_header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION)); + $this->add_header('HTTP/1.0 401 Unauthorized'); + $this->add_header('Content-type: text/html; charset=iso-8859-1'); + // Note: displayed when the user will click on Cancel + $this->add('

'.Dict::S('UI:Login:Error:AccessRestricted').'

'); + break; + + case 'external': + case 'form': + default: // In case the settings get messed up... + $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); + $sAuthPwd = utils::ReadParam('suggest_pwd', '', true, 'raw_data'); + + $this->DisplayLoginHeader(); + $this->add("
\n"); + $this->add("

".Dict::S('UI:Login:Welcome')."

\n"); + if ($bFailedLogin) + { + if (self::$m_sLoginFailedMessage != '') + { + $this->add("

".self::$m_sLoginFailedMessage."

\n"); + } + else + { + $this->add("

".Dict::S('UI:Login:IncorrectLoginPassword')."

\n"); + } + } + else + { + $this->add("

".Dict::S('UI:Login:IdentifyYourself')."

\n"); + } + $this->add("
\n"); + $this->add("\n"); + $sForgotPwd = $this->EnableResetPassword() ? $this->ForgotPwdLink() : ''; + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + if (strlen($sForgotPwd) > 0) + { + $this->add("\n"); + } + $this->add("
$sForgotPwd
\n"); + $this->add("\n"); + + $this->add_ready_script('$("#user").focus();'); + + // Keep the OTHER parameters posted + foreach($_POST as $sPostedKey => $postedValue) + { + if (!in_array($sPostedKey, array('auth_user', 'auth_pwd'))) + { + if (is_array($postedValue)) + { + foreach($postedValue as $sKey => $sValue) + { + $this->add("\n"); + } + } + else + { + $this->add("\n"); + } + } + } + + $this->add("
\n"); + $this->add(Dict::S('UI:Login:About')); + $this->add("
\n"); + break; + } + } + + /** + * Return '' to disable this feature + */ + public function ForgotPwdLink() + { + $sUrl = utils::GetAbsoluteUrlAppRoot() . 'pages/UI.php?loginop=forgot_pwd'; + $sHtml = "".Dict::S('UI:Login:ForgotPwd').""; + return $sHtml; + } + + public function DisplayForgotPwdForm($bFailedToReset = false, $sFailureReason = null) + { + $this->DisplayLoginHeader(); + $this->add("
\n"); + $this->add("

".Dict::S('UI:Login:ForgotPwdForm')."

\n"); + $this->add("

".Dict::S('UI:Login:ForgotPwdForm+')."

\n"); + if ($bFailedToReset) + { + $this->add("

".Dict::Format('UI:Login:ResetPwdFailed', htmlentities($sFailureReason, ENT_QUOTES, 'UTF-8'))."

\n"); + } + $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); + $this->add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
  
\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("
\n"); + + $this->add_ready_script('$("#user").focus();'); + } + + protected function ForgotPwdGo() + { + $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); + + try + { + UserRights::Login($sAuthUser); // Set the user's language (if possible!) + /** @var UserInternal $oUser */ + $oUser = UserRights::GetUserObject(); + if ($oUser == null) + { + throw new Exception(Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)); + } + if (!MetaModel::IsValidAttCode(get_class($oUser), 'reset_pwd_token')) + { + throw new Exception(Dict::S('UI:ResetPwd-Error-NotPossible')); + } + if (!$oUser->CanChangePassword()) + { + throw new Exception(Dict::S('UI:ResetPwd-Error-FixedPwd')); + } + + $sTo = $oUser->GetResetPasswordEmail(); // throws Exceptions if not allowed + if ($sTo == '') + { + throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmail')); + } + + // This token allows the user to change the password without knowing the previous one + $sToken = substr(md5(APPROOT.uniqid()), 0, 16); + $oUser->Set('reset_pwd_token', $sToken); + CMDBObject::SetTrackInfo('Reset password'); + $oUser->AllowWrite(true); + $oUser->DBUpdate(); + + $oEmail = new Email(); + $oEmail->SetRecipientTO($sTo); + $sFrom = MetaModel::GetConfig()->Get('forgot_password_from'); + $oEmail->SetRecipientFrom($sFrom); + $oEmail->SetSubject(Dict::S('UI:ResetPwd-EmailSubject')); + $sResetUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?loginop=reset_pwd&auth_user='.urlencode($oUser->Get('login')).'&token='.urlencode($sToken); + $oEmail->SetBody(Dict::Format('UI:ResetPwd-EmailBody', $sResetUrl)); + $iRes = $oEmail->Send($aIssues, true /* force synchronous exec */); + switch ($iRes) + { + //case EMAIL_SEND_PENDING: + case EMAIL_SEND_OK: + break; + + case EMAIL_SEND_ERROR: + default: + IssueLog::Error('Failed to send the email with the NEW password for '.$oUser->Get('friendlyname').': '.implode(', ', $aIssues)); + throw new Exception(Dict::S('UI:ResetPwd-Error-Send')); + } + + $this->DisplayLoginHeader(); + $this->add("
\n"); + $this->add("

".Dict::S('UI:Login:ForgotPwdForm')."

\n"); + $this->add("

".Dict::S('UI:ResetPwd-EmailSent')."

"); + $this->add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("
\n"); + $this->add("DisplayForgotPwdForm(true, $e->getMessage()); + } + } + + public function DisplayResetPwdForm() + { + $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); + $sToken = utils::ReadParam('token', '', false, 'raw_data'); + + UserRights::Login($sAuthUser); // Set the user's language + $oUser = UserRights::GetUserObject(); + + $this->DisplayLoginHeader(); + $this->add("
\n"); + $this->add("

".Dict::S('UI:ResetPwd-Title')."

\n"); + if ($oUser == null) + { + $this->add("

".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."

\n"); + } + else + { + $oEncryptedToken = $oUser->Get('reset_pwd_token'); + + if (!$oEncryptedToken->CheckPassword($sToken)) + { + $this->add("

".Dict::S('UI:ResetPwd-Error-InvalidToken')."

\n"); + } + else + { + $this->add("

".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."

\n"); + + $sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch'); + $this->add_script( +<<add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("DisplayLoginHeader(); + $this->add("
\n"); + $this->add("

".Dict::S('UI:ResetPwd-Title')."

\n"); + if ($oUser == null) + { + $this->add("

".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."

\n"); + } + else + { + $oEncryptedPassword = $oUser->Get('reset_pwd_token'); + if (!$oEncryptedPassword->CheckPassword($sToken)) + { + $this->add("

".Dict::S('UI:ResetPwd-Error-InvalidToken')."

\n"); + } + else + { + // Trash the token and change the password + $oUser->Set('reset_pwd_token', ''); + $oUser->SetPassword($sNewPwd); // Does record the change into the DB + + $this->add("

".Dict::S('UI:ResetPwd-Ready')."

"); + $sUrl = utils::GetAbsoluteUrlAppRoot(); + $this->add("

".Dict::S('UI:ResetPwd-Login')."

"); + } + $this->add("add_script(<<DisplayLoginHeader(); + $this->add("
\n"); + $this->add("

".Dict::S('UI:Login:ChangeYourPassword')."

\n"); + if ($bFailedLogin) + { + $this->add("

".Dict::S('UI:Login:IncorrectOldPassword')."

\n"); + } + $this->add("
\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("\n"); + $this->add("
  
\n"); + $this->add("\n"); + $this->add("
\n"); + $this->add("
\n"); + } + + static function ResetSession() + { + // Unset all of the session variables. + unset($_SESSION['auth_user']); + unset($_SESSION['login_mode']); + unset($_SESSION['archive_mode']); + unset($_SESSION['impersonate_user']); + UserRights::_ResetSessionCache(); + // If it's desired to kill the session, also delete the session cookie. + // Note: This will destroy the session, and not just the session data! + } + + static function SecureConnectionRequired() + { + return MetaModel::GetConfig()->GetSecureConnectionRequired(); + } + + /** + * Guess if a string looks like an UTF-8 string based on some ranges of multi-bytes encoding + * @param string $sString + * @return bool True if the string contains some typical UTF-8 multi-byte sequences + */ + static function LooksLikeUTF8($sString) + { + return preg_match('%(?: + [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte + |\xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs + |[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte + |\xED[\x80-\x9F][\x80-\xBF] # excluding surrogates + |\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + |[\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + |\xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 + )+%xs', $sString); + } + + /** + * Attempt a login + * + * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) + * @return int One of the class constants EXIT_CODE_... + */ + protected static function Login($iOnExit) + { + if (self::SecureConnectionRequired() && !utils::IsConnectionSecure()) + { + // Non secured URL... request for a secure connection + throw new Exception('Secure connection required!'); + } + + $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); + + if (isset($_SESSION['auth_user'])) + { + //echo "User: ".$_SESSION['auth_user']."\n"; + // Already authentified + $bRet = UserRights::Login($_SESSION['auth_user']); // Login & set the user's language + if ($bRet) + { + return self::EXIT_CODE_OK; + } + // The user account is no longer valid/enabled + static::ResetSession(); + } + + $index = 0; + $sLoginMode = ''; + $sAuthentication = 'internal'; + while(($sLoginMode == '') && ($index < count($aAllowedLoginTypes))) + { + $sLoginType = $aAllowedLoginTypes[$index]; + switch($sLoginType) + { + case 'cas': + utils::InitCASClient(); + // check CAS authentication + if (phpCAS::isAuthenticated()) + { + $sAuthUser = phpCAS::getUser(); + $sAuthPwd = ''; + $sLoginMode = 'cas'; + $sAuthentication = 'external'; + } + break; + + case 'form': + // iTop standard mode: form based authentication + $sAuthUser = utils::ReadPostedParam('auth_user', '', false, 'raw_data'); + $sAuthPwd = utils::ReadPostedParam('auth_pwd', null, false, 'raw_data'); + if (($sAuthUser != '') && ($sAuthPwd !== null)) + { + $sLoginMode = 'form'; + } + break; + + case 'basic': + // Standard PHP authentication method, works with Apache... + // Case 1) Apache running in CGI mode + rewrite rules in .htaccess + if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) + { + list($sAuthUser, $sAuthPwd) = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); + $sLoginMode = 'basic'; + } + else if (isset($_SERVER['PHP_AUTH_USER'])) + { + $sAuthUser = $_SERVER['PHP_AUTH_USER']; + // Unfortunately, the RFC is not clear about the encoding... + // IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8 + // So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base + if (!self::LooksLikeUTF8($sAuthUser)) + { + // Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8 + // Supposed to be harmless in case of a plain ASCII string... + $sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser); + } + $sAuthPwd = $_SERVER['PHP_AUTH_PW']; + if (!self::LooksLikeUTF8($sAuthPwd)) + { + // Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8 + // Supposed to be harmless in case of a plain ASCII string... + $sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd); + } + $sLoginMode = 'basic'; + } + break; + + case 'external': + // Web server supplied authentication + $bExternalAuth = false; + $sExtAuthVar = MetaModel::GetConfig()->GetExternalAuthenticationVariable(); // In which variable is the info passed ? + eval('$sAuthUser = isset('.$sExtAuthVar.') ? '.$sExtAuthVar.' : false;'); // Retrieve the value + if ($sAuthUser && (strlen($sAuthUser) > 0)) + { + $sAuthPwd = ''; // No password in this case the web server already authentified the user... + $sLoginMode = 'external'; + $sAuthentication = 'external'; + } + break; + + case 'url': + // Credentials passed directly in the url + $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); + $sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data'); + if (($sAuthUser != '') && ($sAuthPwd !== null)) + { + $sLoginMode = 'url'; + } + break; + } + $index++; + } + //echo "\nsLoginMode: $sLoginMode (user: $sAuthUser / pwd: $sAuthPwd\n)"; + if ($sLoginMode == '') + { + // First connection + $sDesiredLoginMode = utils::ReadParam('login_mode'); + if (in_array($sDesiredLoginMode, $aAllowedLoginTypes)) + { + $sLoginMode = $sDesiredLoginMode; + } + else + { + $sLoginMode = $aAllowedLoginTypes[0]; // First in the list... + } + if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER)) + { + // X-Combodo-Ajax is a special header automatically added to all ajax requests + // Let's reply that we're currently logged-out + header('HTTP/1.0 401 Unauthorized'); + exit; + } + if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic')) + { + header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION)); + header('HTTP/1.0 401 Unauthorized'); + header('Content-type: text/html; charset=iso-8859-1'); + exit; + } + else if($iOnExit == self::EXIT_RETURN) + { + if (($sAuthUser !== '') && ($sAuthPwd === null)) + { + return self::EXIT_CODE_MISSINGPASSWORD; + } + else + { + return self::EXIT_CODE_MISSINGLOGIN; + } + } + else + { + $oPage = self::NewLoginWebPage(); + $oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */); + $oPage->output(); + exit; + } + } + else + { + if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $sLoginMode, $sAuthentication)) + { + //echo "Check Credentials returned false for user $sAuthUser!"; + self::ResetSession(); + if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic')) + { + header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION)); + header('HTTP/1.0 401 Unauthorized'); + header('Content-type: text/html; charset=iso-8859-1'); + exit; + } + else if($iOnExit == self::EXIT_RETURN) + { + return self::EXIT_CODE_WRONGCREDENTIALS; + } + else + { + $oPage = self::NewLoginWebPage(); + $oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */); + $oPage->output(); + exit; + } + } + else + { + // User is Ok, let's save it in the session and proceed with normal login + UserRights::Login($sAuthUser, $sAuthentication); // Login & set the user's language + + if (MetaModel::GetConfig()->Get('log_usage')) + { + $oLog = new EventLoginUsage(); + $oLog->Set('userinfo', UserRights::GetUser()); + $oLog->Set('user_id', UserRights::GetUserObject()->GetKey()); + $oLog->Set('message', 'Successful login'); + $oLog->DBInsertNoReload(); + } + + $_SESSION['auth_user'] = $sAuthUser; + $_SESSION['login_mode'] = $sLoginMode; + UserRights::_InitSessionCache(); + } + } + return self::EXIT_CODE_OK; + } + + /** + * Overridable: depending on the user, head toward a dedicated portal + * @param string|null $sRequestedPortalId + * @param int $iOnExit How to complete the call: redirect or return a code + */ + protected static function ChangeLocation($sRequestedPortalId = null, $iOnExit = self::EXIT_PROMPT) + { + $fStart = microtime(true); + $ret = call_user_func(array(self::$sHandlerClass, 'Dispatch'), $sRequestedPortalId); + if ($ret === true) + { + return self::EXIT_CODE_OK; + } + else if($ret === false) + { + throw new Exception('Nowhere to go??'); + } + else + { + if ($iOnExit == self::EXIT_RETURN) + { + return self::EXIT_CODE_PORTALUSERNOTAUTHORIZED; + } + else + { + // No rights to be here, redirect to the portal + header('Location: '.$ret); + } + } + } + + /** + * Check if the user is already authentified, if yes, then performs some additional validations: + * - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed + * - if $bIsAllowedToPortalUsers is false and the user has only access to the portal, then the user is redirected + * to the portal + * + * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page + * @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal + * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) + * + * @return int|mixed|string + * @throws \Exception + */ + static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false, $iOnExit = self::EXIT_PROMPT) + { + $sRequestedPortalId = $bIsAllowedToPortalUsers ? 'legacy_portal' : 'backoffice'; + return self::DoLoginEx($sRequestedPortalId, $bMustBeAdmin, $iOnExit); + } + + /** + * Check if the user is already authentified, if yes, then performs some additional validations to redirect towards + * the desired "portal" + * + * @param string|null $sRequestedPortalId The requested "portal" interface, null for any + * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page + * @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...) + * + * @return int|mixed|string + * @throws \Exception + */ + static function DoLoginEx($sRequestedPortalId = null, $bMustBeAdmin = false, $iOnExit = self::EXIT_PROMPT) + { + $operation = utils::ReadParam('loginop', ''); + + $sMessage = self::HandleOperations($operation); // May exit directly + + $iRet = self::Login($iOnExit); + + if ($iRet == self::EXIT_CODE_OK) + { + if ($bMustBeAdmin && !UserRights::IsAdministrator()) + { + if ($iOnExit == self::EXIT_RETURN) + { + return self::EXIT_CODE_MUSTBEADMIN; + } + else + { + require_once(APPROOT.'/setup/setuppage.class.inc.php'); + $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); + $oP->add("

".Dict::S('UI:Login:Error:AccessAdmin')."

\n"); + $oP->p("".Dict::S('UI:LogOffMenu').""); + $oP->output(); + exit; + } + } + $iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $sRequestedPortalId, $iOnExit); + } + if ($iOnExit == self::EXIT_RETURN) + { + return $iRet; + } + else + { + return $sMessage; + } + } + protected static function HandleOperations($operation) + { + $sMessage = ''; // most of the operations never return, but some can return a message to be displayed + if ($operation == 'logoff') + { + if (isset($_SESSION['login_mode'])) + { + $sLoginMode = $_SESSION['login_mode']; + } + else + { + $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); + if (count($aAllowedLoginTypes) > 0) + { + $sLoginMode = $aAllowedLoginTypes[0]; + } + else + { + $sLoginMode = 'form'; + } + } + self::ResetSession(); + $oPage = self::NewLoginWebPage(); + $oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */); + $oPage->output(); + exit; + } + else if ($operation == 'forgot_pwd') + { + $oPage = self::NewLoginWebPage(); + $oPage->DisplayForgotPwdForm(); + $oPage->output(); + exit; + } + else if ($operation == 'forgot_pwd_go') + { + $oPage = self::NewLoginWebPage(); + $oPage->ForgotPwdGo(); + $oPage->output(); + exit; + } + else if ($operation == 'reset_pwd') + { + $oPage = self::NewLoginWebPage(); + $oPage->DisplayResetPwdForm(); + $oPage->output(); + exit; + } + else if ($operation == 'do_reset_pwd') + { + $oPage = self::NewLoginWebPage(); + $oPage->DoResetPassword(); + $oPage->output(); + exit; + } + else if ($operation == 'change_pwd') + { + $sAuthUser = $_SESSION['auth_user']; + UserRights::Login($sAuthUser); // Set the user's language + $oPage = self::NewLoginWebPage(); + $oPage->DisplayChangePwdForm(); + $oPage->output(); + exit; + } + if ($operation == 'do_change_pwd') + { + $sAuthUser = $_SESSION['auth_user']; + UserRights::Login($sAuthUser); // Set the user's language + $sOldPwd = utils::ReadPostedParam('old_pwd', '', false, 'raw_data'); + $sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data'); + if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd)))) + { + $oPage = self::NewLoginWebPage(); + $oPage->DisplayChangePwdForm(true); // old pwd was wrong + $oPage->output(); + exit; + } + $sMessage = Dict::S('UI:Login:PasswordChanged'); + } + return $sMessage; + } + + protected static function Dispatch($sRequestedPortalId) + { + if ($sRequestedPortalId === null) return true; // allowed to any portal => return true + + $aPortalsConf = PortalDispatcherData::GetData(); + $aDispatchers = array(); + foreach($aPortalsConf as $sPortalId => $aConf) + { + $sHandlerClass = $aConf['handler']; + $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId); + } + + if (array_key_exists($sRequestedPortalId, $aDispatchers) && $aDispatchers[$sRequestedPortalId]->IsUserAllowed()) + { + return true; + } + foreach($aDispatchers as $sPortalId => $oDispatcher) + { + if ($oDispatcher->IsUserAllowed()) return $oDispatcher->GetUrl(); + } + return false; // nothing matched !! + } +} // End of class diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php index 791571d17..db5e46562 100644 --- a/application/menunode.class.inc.php +++ b/application/menunode.class.inc.php @@ -1,1419 +1,1419 @@ - - - -/** - * Construction and display of the application's main menu - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/utils.inc.php'); -require_once(APPROOT.'/application/template.class.inc.php'); -require_once(APPROOT."/application/user.dashboard.class.inc.php"); - - -/** - * This class manipulates, stores and displays the navigation menu used in the application - * In order to improve the modularity of the data model and to ease the update/migration - * between evolving data models, the menus are no longer stored in the database, but are instead - * built on the fly each time a page is loaded. - * The application's menu is organized into top-level groups with, inside each group, a tree of menu items. - * Top level groups do not display any content, they just expand/collapse. - * Sub-items drive the actual content of the page, they are based either on templates, OQL queries or full (external?) web pages. - * - * Example: - * Here is how to insert the following items in the application's menu: - * +----------------------------------------+ - * | Configuration Management Group | >> Top level group - * +----------------------------------------+ - * + Configuration Management Overview >> Template based menu item - * + Contacts >> Template based menu item - * + Persons >> Plain list (OQL based) - * + Teams >> Plain list (OQL based) - * - * // Create the top-level group. fRank = 1, means it will be inserted after the group '0', which is usually 'Welcome' - * $oConfigMgmtMenu = new MenuGroup('ConfigurationManagementMenu', 1); - * // Create an entry, based on a custom template, for the Configuration management overview, under the top-level group - * new TemplateMenuNode('ConfigurationManagementMenu', '../somedirectory/configuration_management_menu.html', $oConfigMgmtMenu->GetIndex(), 0); - * // Create an entry (template based) for the overview of contacts - * $oContactsMenu = new TemplateMenuNode('ContactsMenu', '../somedirectory/configuration_management_menu.html',$oConfigMgmtMenu->GetIndex(), 1); - * // Plain list of persons - * new OQLMenuNode('PersonsMenu', 'SELECT bizPerson', $oContactsMenu->GetIndex(), 0); - * - */ - -class ApplicationMenu -{ - /** - * @var bool - */ - static $bAdditionalMenusLoaded = false; - /** - * @var array - */ - static $aRootMenus = array(); - /** - * @var array - */ - static $aMenusIndex = array(); - /** - * @var string - */ - static $sFavoriteSiloQuery = 'SELECT Organization'; - - static public function LoadAdditionalMenus() - { - if (!self::$bAdditionalMenusLoaded) - { - // Build menus from module handlers - // - foreach(MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) - { - $oPHPClass::OnMenuCreation(); - } - - // Build menus from the menus themselves (e.g. the ShortcutContainerMenuNode will do that) - // - foreach(self::$aRootMenus as $aMenu) - { - $oMenuNode = self::GetMenuNode($aMenu['index']); - $oMenuNode->PopulateChildMenus(); - } - - self::$bAdditionalMenusLoaded = true; - } - } - - /** - * Set the query used to limit the list of displayed organizations in the drop-down menu - * @param $sOQL string The OQL query returning a list of Organization objects - * @return void - */ - static public function SetFavoriteSiloQuery($sOQL) - { - self::$sFavoriteSiloQuery = $sOQL; - } - - /** - * Get the query used to limit the list of displayed organizations in the drop-down menu - * @return string The OQL query returning a list of Organization objects - */ - static public function GetFavoriteSiloQuery() - { - return self::$sFavoriteSiloQuery; - } - - /** - * Check wether a menu Id is enabled or not - * @param $sMenuId - * @throws DictExceptionMissingString - */ - static public function CheckMenuIdEnabled($sMenuId) - { - self::LoadAdditionalMenus(); - $oMenuNode = self::GetMenuNode(self::GetMenuIndexById($sMenuId)); - if (is_null($oMenuNode) || !$oMenuNode->IsEnabled()) - { - require_once(APPROOT.'/setup/setuppage.class.inc.php'); - $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); - $oP->add("

".Dict::S('UI:Login:Error:AccessRestricted')."

\n"); - $oP->p("".Dict::S('UI:LogOffMenu').""); - $oP->output(); - exit; - } - } - - /** - * Main function to add a menu entry into the application, can be called during the definition - * of the data model objects - * @param MenuNode $oMenuNode - * @param $iParentIndex - * @param $fRank - * @return int - */ - static public function InsertMenu(MenuNode $oMenuNode, $iParentIndex, $fRank) - { - $index = self::GetMenuIndexById($oMenuNode->GetMenuId()); - if ($index == -1) - { - // The menu does not already exist, insert it - $index = count(self::$aMenusIndex); - - if ($iParentIndex == -1) - { - $sParentId = ''; - self::$aRootMenus[] = array ('rank' => $fRank, 'index' => $index); - } - else - { - $sParentId = self::$aMenusIndex[$iParentIndex]['node']->GetMenuId(); - self::$aMenusIndex[$iParentIndex]['children'][] = array ('rank' => $fRank, 'index' => $index); - } - - // Note: At the time when 'parent', 'rank' and 'source_file' have been added for the reflection API, - // they were not used to display the menus (redundant or unused) - // - $aBacktrace = debug_backtrace(); - $sFile = isset($aBacktrace[2]["file"]) ? $aBacktrace[2]["file"] : $aBacktrace[1]["file"]; - self::$aMenusIndex[$index] = array('node' => $oMenuNode, 'children' => array(), 'parent' => $sParentId, 'rank' => $fRank, 'source_file' => $sFile); - } - else - { - // the menu already exists, let's combine the conditions that make it visible - self::$aMenusIndex[$index]['node']->AddCondition($oMenuNode); - } - - return $index; - } - - /** - * Reflection API - Get menu entries - */ - static public function ReflectionMenuNodes() - { - self::LoadAdditionalMenus(); - return self::$aMenusIndex; - } - - /** - * Entry point to display the whole menu into the web page, used by iTopWebPage - * @param $oPage - * @param $aExtraParams - * @throws DictExceptionMissingString - */ - static public function DisplayMenu($oPage, $aExtraParams) - { - self::LoadAdditionalMenus(); - // Sort the root menu based on the rank - usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank')); - $iAccordion = 0; - $iActiveMenu = self::GetMenuIndexById(self::GetActiveNodeId()); - foreach(self::$aRootMenus as $aMenu) - { - if (!self::CanDisplayMenu($aMenu)) { continue; } - $oMenuNode = self::GetMenuNode($aMenu['index']); - $oPage->AddToMenu('

'.$oMenuNode->GetTitle().'

'); - $oPage->AddToMenu('
'); - $oPage->AddToMenu('
    '); - $aChildren = self::GetChildren($aMenu['index']); - $bActive = self::DisplaySubMenu($oPage, $aChildren, $aExtraParams, $iActiveMenu); - $oPage->AddToMenu('
'); - if ($bActive) - { -$oPage->add_ready_script( -<<AddToMenu('
'); - $iAccordion++; - } - } - - /** - * Recursively check if the menu and at least one of his sub-menu is enabled - * @param array $aMenu menu entry - * @return bool true if at least one menu is enabled - */ - static private function CanDisplayMenu($aMenu) - { - $oMenuNode = self::GetMenuNode($aMenu['index']); - if ($oMenuNode->IsEnabled()) - { - $aChildren = self::GetChildren($aMenu['index']); - if (count($aChildren) > 0) - { - foreach($aChildren as $aSubMenu) - { - if (self::CanDisplayMenu($aSubMenu)) - { - return true; - } - } - } - else - { - return true; - } - } - return false; - } - - /** - * Handles the display of the sub-menus (called recursively if necessary) - * @param WebPage $oPage - * @param array $aMenus - * @param array $aExtraParams - * @param int $iActiveMenu - * @return true if the currently selected menu is one of the submenus - * @throws DictExceptionMissingString - */ - static protected function DisplaySubMenu($oPage, $aMenus, $aExtraParams, $iActiveMenu = -1) - { - // Sort the menu based on the rank - $bActive = false; - usort($aMenus, array('ApplicationMenu', 'CompareOnRank')); - foreach($aMenus as $aMenu) - { - $index = $aMenu['index']; - $oMenu = self::GetMenuNode($index); - if ($oMenu->IsEnabled()) - { - $aChildren = self::GetChildren($index); - $sCSSClass = (count($aChildren) > 0) ? ' class="submenu"' : ''; - $sHyperlink = $oMenu->GetHyperlink($aExtraParams); - if ($sHyperlink != '') - { - $oPage->AddToMenu('
  • '.$oMenu->GetTitle().'
  • '); - } - else - { - $oPage->AddToMenu('
  • '.$oMenu->GetTitle().'
  • '); - } - if ($iActiveMenu == $index) - { - $bActive = true; - } - if (count($aChildren) > 0) - { - $oPage->AddToMenu('
      '); - $bActive |= self::DisplaySubMenu($oPage, $aChildren, $aExtraParams, $iActiveMenu); - $oPage->AddToMenu('
    '); - } - } - } - return $bActive; - } - - /** - * Helper function to sort the menus based on their rank - * @param $a - * @param $b - * @return int - */ - static public function CompareOnRank($a, $b) - { - $result = 1; - if ($a['rank'] == $b['rank']) - { - $result = 0; - } - if ($a['rank'] < $b['rank']) - { - $result = -1; - } - return $result; - } - - /** - * Helper function to retrieve the MenuNode Object based on its ID - * @param int $index - * @return MenuNode|null - */ - static public function GetMenuNode($index) - { - return isset(self::$aMenusIndex[$index]) ? self::$aMenusIndex[$index]['node'] : null; - } - - /** - * Helper function to get the list of child(ren) of a menu - * @param int $index - * @return array - */ - static public function GetChildren($index) - { - return self::$aMenusIndex[$index]['children']; - } - - /** - * Helper function to get the ID of a menu based on its name - * @param string $sTitle Title of the menu (as passed when creating the menu) - * @return integer ID of the menu, or -1 if not found - */ - static public function GetMenuIndexById($sTitle) - { - $index = -1; - foreach(self::$aMenusIndex as $aMenu) - { - if ($aMenu['node']->GetMenuId() == $sTitle) - { - $index = $aMenu['node']->GetIndex(); - break; - } - } - return $index; - } - - /** - * Retrieves the currently active menu (if any, otherwise the first menu is the default) - * @return string The Id of the currently active menu - */ - static public function GetActiveNodeId() - { - $oAppContext = new ApplicationContext(); - $sMenuId = $oAppContext->GetCurrentValue('menu', null); - if ($sMenuId === null) - { - $sMenuId = self::GetDefaultMenuId(); - } - return $sMenuId; - } - - /** - * @return null|string - */ - static public function GetDefaultMenuId() - { - static $sDefaultMenuId = null; - if (is_null($sDefaultMenuId)) - { - // Make sure the root menu is sorted on 'rank' - usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank')); - $oFirstGroup = self::GetMenuNode(self::$aRootMenus[0]['index']); - $aChildren = self::$aMenusIndex[$oFirstGroup->GetIndex()]['children']; - usort($aChildren, array('ApplicationMenu', 'CompareOnRank')); - $oMenuNode = self::GetMenuNode($aChildren[0]['index']); - $sDefaultMenuId = $oMenuNode->GetMenuId(); - } - return $sDefaultMenuId; - } - - /** - * @param $sMenuId - * @return string - */ - static public function GetRootMenuId($sMenuId) - { - $iMenuIndex = self::GetMenuIndexById($sMenuId); - if ($iMenuIndex == -1) - { - return ''; - } - $oMenu = ApplicationMenu::GetMenuNode($iMenuIndex); - while ($oMenu->GetParentIndex() != -1) - { - $oMenu = ApplicationMenu::GetMenuNode($oMenu->GetParentIndex()); - } - return $oMenu->GetMenuId(); - } -} - -/** - * Root class for all the kind of node in the menu tree, data model providers are responsible for instantiating - * MenuNodes (i.e instances from derived classes) in order to populate the application's menu. Creating an objet - * derived from MenuNode is enough to have it inserted in the application's main menu. - * The class iTopWebPage, takes care of 3 items: - * +--------------------+ - * | Welcome | - * +--------------------+ - * Welcome To iTop - * +--------------------+ - * | Tools | - * +--------------------+ - * CSV Import - * +--------------------+ - * | Admin Tools | - * +--------------------+ - * User Accounts - * Profiles - * Notifications - * Run Queries - * Export - * Data Model - * Universal Search - * - * All the other menu items must constructed along with the various data model modules - */ -abstract class MenuNode -{ - /** - * @var string - */ - protected $sMenuId; - /** - * @var int - */ - protected $index; - /** - * @var int - */ - protected $iParentIndex; - - /** - * Properties reflecting how the node has been declared - */ - protected $aReflectionProperties; - - /** - * Class of objects to check if the menu is enabled, null if none - */ - protected $m_aEnableClasses; - - /** - * User Rights Action code to check if the menu is enabled, null if none - */ - protected $m_aEnableActions; - - /** - * User Rights allowed results (actually a bitmask) to check if the menu is enabled, null if none - */ - protected $m_aEnableActionResults; - - /** - * Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu - */ - protected $m_aEnableStimuli; - - /** - * Create a menu item, sets the condition to have it displayed and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param integer $iParentIndex ID of the parent menu, pass -1 for top level (group) items - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param string $sEnableClass Name of class of object - * @param mixed $iActionCode UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus The user can see this menu if she/he has enough rights to apply this stimulus - */ - public function __construct($sMenuId, $iParentIndex = -1, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - $this->sMenuId = $sMenuId; - $this->iParentIndex = $iParentIndex; - $this->aReflectionProperties = array(); - if (strlen($sEnableClass) > 0) - { - $this->aReflectionProperties['enable_class'] = $sEnableClass; - $this->aReflectionProperties['enable_action'] = $iActionCode; - $this->aReflectionProperties['enable_permission'] = $iAllowedResults; - $this->aReflectionProperties['enable_stimulus'] = $sEnableStimulus; - } - $this->m_aEnableClasses = array($sEnableClass); - $this->m_aEnableActions = array($iActionCode); - $this->m_aEnableActionResults = array($iAllowedResults); - $this->m_aEnableStimuli = array($sEnableStimulus); - $this->index = ApplicationMenu::InsertMenu($this, $iParentIndex, $fRank); - } - - /** - * @return array - */ - public function ReflectionProperties() - { - return $this->aReflectionProperties; - } - - /** - * @return string - */ - public function GetMenuId() - { - return $this->sMenuId; - } - - /** - * @return int - */ - public function GetParentIndex() - { - return $this->iParentIndex; - } - - /** - * @return string - * @throws DictExceptionMissingString - */ - public function GetTitle() - { - return Dict::S("Menu:$this->sMenuId", str_replace('_', ' ', $this->sMenuId)); - } - - /** - * @return string - * @throws DictExceptionMissingString - */ - public function GetLabel() - { - $sRet = Dict::S("Menu:$this->sMenuId+", ""); - if ($sRet === '') - { - if ($this->iParentIndex != -1) - { - $oParentMenu = ApplicationMenu::GetMenuNode($this->iParentIndex); - $sRet = $oParentMenu->GetTitle().' / '.$this->GetTitle(); - } - else - { - $sRet = $this->GetTitle(); - } - //$sRet = $this->GetTitle(); - } - return $sRet; - } - - /** - * @return int - */ - public function GetIndex() - { - return $this->index; - } - - public function PopulateChildMenus() - { - foreach (ApplicationMenu::GetChildren($this->GetIndex()) as $aMenu) - { - $index = $aMenu['index']; - $oMenu = ApplicationMenu::GetMenuNode($index); - $oMenu->PopulateChildMenus(); - } - } - - /** - * @param $aExtraParams - * @return string - */ - public function GetHyperlink($aExtraParams) - { - $aExtraParams['c[menu]'] = $this->GetMenuId(); - return $this->AddParams(utils::GetAbsoluteUrlAppRoot().'pages/UI.php', $aExtraParams); - } - - /** - * Add a limiting display condition for the same menu node. The conditions will be combined with a AND - * @param $oMenuNode MenuNode Another definition of the same menu node, with potentially different access restriction - * @return void - */ - public function AddCondition(MenuNode $oMenuNode) - { - foreach($oMenuNode->m_aEnableClasses as $index => $sClass ) - { - $this->m_aEnableClasses[] = $sClass; - $this->m_aEnableActions[] = $oMenuNode->m_aEnableActions[$index]; - $this->m_aEnableActionResults[] = $oMenuNode->m_aEnableActionResults[$index]; - $this->m_aEnableStimuli[] = $oMenuNode->m_aEnableStimuli[$index]; - } - } - /** - * Tells whether the menu is enabled (i.e. displayed) for the current user - * @return bool True if enabled, false otherwise - */ - public function IsEnabled() - { - foreach($this->m_aEnableClasses as $index => $sClass) - { - if ($sClass != null) - { - if (MetaModel::IsValidClass($sClass)) - { - if ($this->m_aEnableStimuli[$index] != null) - { - if (!UserRights::IsStimulusAllowed($sClass, $this->m_aEnableStimuli[$index])) - { - return false; - } - } - if ($this->m_aEnableActions[$index] != null) - { - // Menus access rights ignore the archive mode - utils::PushArchiveMode(false); - $iResult = UserRights::IsActionAllowed($sClass, $this->m_aEnableActions[$index]); - utils::PopArchiveMode(); - if (!($iResult & $this->m_aEnableActionResults[$index])) - { - return false; - } - } - } - else - { - return false; - } - } - } - return true; - } - - /** - * @param WebPage $oPage - * @param array $aExtraParams - * @return mixed - */ - public abstract function RenderContent(WebPage $oPage, $aExtraParams = array()); - - /** - * @param $sHyperlink - * @param $aExtraParams - * @return string - */ - protected function AddParams($sHyperlink, $aExtraParams) - { - if (count($aExtraParams) > 0) - { - $aQuery = array(); - $sSeparator = '?'; - if (strpos($sHyperlink, '?') !== false) - { - $sSeparator = '&'; - } - foreach($aExtraParams as $sName => $sValue) - { - $aQuery[] = urlencode($sName).'='.urlencode($sValue); - } - $sHyperlink .= $sSeparator.implode('&', $aQuery); - } - return $sHyperlink; - } -} - -/** - * This class implements a top-level menu group. A group is just a container for sub-items - * it does not display a page by itself - */ -class MenuGroup extends MenuNode -{ - /** - * Create a top-level menu group and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param float $fRank Number used to order the list, the groups are sorted based on this value - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $fRank, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, -1 /* no parent, groups are at root level */, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - } - - /** - * @param WebPage $oPage - * @param array $aExtraParams - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - assert(false); // Shall never be called, groups do not display any content - } -} - -/** - * This class defines a menu item which content is based on a custom template. - * Note the template can be either a local file or an URL ! - */ -class TemplateMenuNode extends MenuNode -{ - /** - * @var string - */ - protected $sTemplateFile; - - /** - * Create a menu item based on a custom template and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param string $sTemplateFile Path (or URL) to the file that will be used as a template for displaying the page's content - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $sTemplateFile, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->sTemplateFile = $sTemplateFile; - $this->aReflectionProperties['template_file'] = $sTemplateFile; - } - - /** - * @param $aExtraParams - * @return string - */ - public function GetHyperlink($aExtraParams) - { - if ($this->sTemplateFile == '') return ''; - return parent::GetHyperlink($aExtraParams); - } - - /** - * @param WebPage $oPage - * @param array $aExtraParams - * @return mixed|void - * @throws DictExceptionMissingString - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); - $sTemplate = @file_get_contents($this->sTemplateFile); - if ($sTemplate !== false) - { - $aExtraParams['table_id'] = 'Menu_'.$this->GetMenuId(); - $oTemplate = new DisplayTemplate($sTemplate); - $oTemplate->Render($oPage, $aExtraParams); - } - else - { - $oPage->p("Error: failed to load template file: '{$this->sTemplateFile}'"); // No need to translate ? - } - } -} - -/** - * This class defines a menu item that uses a standard template to display a list of items therefore it allows - * only two parameters: the page's title and the OQL expression defining the list of items to be displayed - */ -class OQLMenuNode extends MenuNode -{ - /** - * @var string - */ - protected $sPageTitle; - /** - * @var string - */ - protected $sOQL; - /** - * @var bool - */ - protected $bSearch; - /** - * @var bool|null - */ - protected $bSearchFormOpen; - - /** - * Extra parameters to be passed to the display block to fine tune its appearence - */ - protected $m_aParams; - - - /** - * Create a menu item based on an OQL query and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param string $sOQL OQL query defining the set of objects to be displayed - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param bool $bSearch Whether or not to display a (collapsed) search frame at the top of the page - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus - * @param bool $bSearchFormOpen - */ - public function __construct($sMenuId, $sOQL, $iParentIndex, $fRank = 0.0, $bSearch = false, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null, $bSearchFormOpen = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->sPageTitle = "Menu:$sMenuId+"; - $this->sOQL = $sOQL; - $this->bSearch = $bSearch; - $this->bSearchFormOpen = $bSearchFormOpen; - $this->m_aParams = array(); - $this->aReflectionProperties['oql'] = $sOQL; - $this->aReflectionProperties['do_search'] = $bSearch; - // Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects - // of the class specified by the OQL... - } - - /** - * Set some extra parameters to be passed to the display block to fine tune its appearence - * @param Hash $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters - */ - public function SetParameters($aParams) - { - $this->m_aParams = $aParams; - foreach($aParams as $sKey => $value) - { - $this->aReflectionProperties[$sKey] = $value; - } - } - - /** - * @param WebPage $oPage - * @param array $aExtraParams - * @return mixed|void - * @throws CoreException - * @throws DictExceptionMissingString - * @throws OQLException - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); - OQLMenuNode::RenderOQLSearch - ( - $this->sOQL, - Dict::S($this->sPageTitle), - 'Menu_'.$this->GetMenuId(), - $this->bSearch, // Search pane - $this->bSearchFormOpen, // Search open - $oPage, - array_merge($this->m_aParams, $aExtraParams), - true - ); - } - - /** - * @param $sOql - * @param $sTitle - * @param $sUsageId - * @param $bSearchPane - * @param $bSearchOpen - * @param WebPage $oPage - * @param array $aExtraParams - * @param bool $bEnableBreadcrumb - * @throws CoreException - * @throws DictExceptionMissingString - * @throws OQLException - */ - public static function RenderOQLSearch($sOql, $sTitle, $sUsageId, $bSearchPane, $bSearchOpen, WebPage $oPage, $aExtraParams = array(), $bEnableBreadcrumb = false) - { - $sUsageId = utils::GetSafeId($sUsageId); - $oSearch = DBObjectSearch::FromOQL($sOql); - $sIcon = MetaModel::GetClassIcon($oSearch->GetClass()); - - if ($bSearchPane) - { - $aParams = array_merge(array('open' => $bSearchOpen, 'table_id' => $sUsageId), $aExtraParams); - $oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams); - $oBlock->Display($oPage, 0); - } - - $oPage->add("

    $sIcon ".Dict::S($sTitle)."

    "); - - $aParams = array_merge(array('table_id' => $sUsageId), $aExtraParams); - $oBlock = new DisplayBlock($oSearch, 'list', false /* Asynchronous */, $aParams); - $oBlock->Display($oPage, $sUsageId); - - if ($bEnableBreadcrumb && ($oPage instanceof iTopWebPage)) - { - // Breadcrumb - //$iCount = $oBlock->GetDisplayedCount(); - $sPageId = "ui-search-".$oSearch->GetClass(); - $sLabel = MetaModel::GetName($oSearch->GetClass()); - $oPage->SetBreadCrumbEntry($sPageId, $sLabel, $sTitle, '', '../images/breadcrumb-search.png'); - } - } -} - -/** - * This class defines a menu item that displays a search form for the given class of objects - */ -class SearchMenuNode extends MenuNode -{ - /** - * @var string - */ - protected $sPageTitle; - /** - * @var string - */ - protected $sClass; - - /** - * Create a menu item based on an OQL query and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param string $sClass The class of objects to search for - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param bool $bSearch (not used) - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $sClass, $iParentIndex, $fRank = 0.0, $bSearch = false, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->sPageTitle = "Menu:$sMenuId+"; - $this->sClass = $sClass; - $this->aReflectionProperties['class'] = $sClass; - } - - /** - * @param WebPage $oPage - * @param array $aExtraParams - * @return mixed|void - * @throws DictExceptionMissingString - * @throws Exception - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); - $oPage->SetBreadCrumbEntry("menu-".$this->sMenuId, $this->GetTitle(), '', '', utils::GetAbsoluteUrlAppRoot().'images/search.png'); - - $oSearch = new DBObjectSearch($this->sClass); - $aParams = array_merge(array('table_id' => 'Menu_'.utils::GetSafeId($this->GetMenuId())), $aExtraParams); - $oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams); - $oBlock->Display($oPage, 0); - } -} - -/** - * This class defines a menu that points to any web page. It takes only two parameters: - * - The hyperlink to point to - * - The name of the menu - * Note: the parameter menu=xxx (where xxx is the id of the menu itself) will be added to the hyperlink - * in order to make it the active one, if the target page is based on iTopWebPage and therefore displays the menu - */ -class WebPageMenuNode extends MenuNode -{ - /** - * @var string - */ - protected $sHyperlink; - - /** - * Create a menu item that points to any web page (not only UI.php) - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param string $sHyperlink URL to the page to load. Use relative URL if you want to keep the application portable ! - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $sHyperlink, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->sHyperlink = $sHyperlink; - $this->aReflectionProperties['url'] = $sHyperlink; - } - - /** - * @param array $aExtraParams - * @return string - */ - public function GetHyperlink($aExtraParams) - { - $aExtraParams['c[menu]'] = $this->GetMenuId(); - return $this->AddParams( $this->sHyperlink, $aExtraParams); - } - - /** - * @param WebPage $oPage - * @param array $aExtraParams - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - assert(false); // Shall never be called, the external web page will handle the display by itself - } -} - -/** - * This class defines a menu that points to the page for creating a new object of the specified class. - * It take only one parameter: the name of the class - * Note: the parameter menu=xxx (where xxx is the id of the menu itself) will be added to the hyperlink - * in order to make it the active one - */ -class NewObjectMenuNode extends MenuNode -{ - /** - * @var string - */ - protected $sClass; - - /** - * Create a menu item that points to the URL for creating a new object, the menu will be added only if the current user has enough - * rights to create such an object (or an object of a child class) - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param string $sClass URL to the page to load. Use relative URL if you want to keep the application portable ! - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param string $sEnableClass - * @param int|null $iActionCode - * @param int $iAllowedResults - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $sClass, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->sClass = $sClass; - $this->aReflectionProperties['class'] = $sClass; - } - - /** - * @param string[] $aExtraParams - * @return string - */ - public function GetHyperlink($aExtraParams) - { - $sHyperlink = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=new&class='.$this->sClass; - $aExtraParams['c[menu]'] = $this->GetMenuId(); - return $this->AddParams($sHyperlink, $aExtraParams); - } - - /** - * Overload the check of the "enable" state of this menu to take into account - * derived classes of objects - * @throws CoreException - */ - public function IsEnabled() - { - // Enable this menu, only if the current user has enough rights to create such an object, or an object of - // any child class - - $aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself - $bActionIsAllowed = false; - - foreach($aSubClasses as $sCandidateClass) - { - if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) - { - $bActionIsAllowed = true; - break; // Enough for now - } - } - return $bActionIsAllowed; - } - - /** - * @param WebPage $oPage - * @param string[] $aExtraParams - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - assert(false); // Shall never be called, the external web page will handle the display by itself - } -} - -require_once(APPROOT.'application/dashboard.class.inc.php'); -/** - * This class defines a menu item which content is based on XML dashboard. - */ -class DashboardMenuNode extends MenuNode -{ - /** - * @var string - */ - protected $sDashboardFile; - - /** - * Create a menu item based on a custom template and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param string $sDashboardFile - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $sDashboardFile, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->sDashboardFile = $sDashboardFile; - $this->aReflectionProperties['definition_file'] = $sDashboardFile; - } - - /** - * @param string[] $aExtraParams - * @return string - */ - public function GetHyperlink($aExtraParams) - { - if ($this->sDashboardFile == '') return ''; - return parent::GetHyperlink($aExtraParams); - } - - /** - * @return null|RuntimeDashboard - * @throws CoreException - * @throws Exception - */ - public function GetDashboard() - { - $sDashboardDefinition = @file_get_contents($this->sDashboardFile); - if ($sDashboardDefinition !== false) - { - $bCustomized = false; - - // Search for an eventual user defined dashboard, overloading the existing one - $oUDSearch = new DBObjectSearch('UserDashboard'); - $oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '='); - $oUDSearch->AddCondition('menu_code', $this->sMenuId, '='); - $oUDSet = new DBObjectSet($oUDSearch); - if ($oUDSet->Count() > 0) - { - // Assuming there is at most one couple {user, menu}! - $oUserDashboard = $oUDSet->Fetch(); - $sDashboardDefinition = $oUserDashboard->Get('contents'); - $bCustomized = true; - - } - $oDashboard = new RuntimeDashboard($this->sMenuId); - $oDashboard->FromXml($sDashboardDefinition); - $oDashboard->SetCustomFlag($bCustomized); - } - else - { - $oDashboard = null; - } - return $oDashboard; - } - - /** - * @param WebPage $oPage - * @param string[] $aExtraParams - * @throws CoreException - * @throws Exception - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); - $oDashboard = $this->GetDashboard(); - if ($oDashboard != null) - { - $sDivId = preg_replace('/[^a-zA-Z0-9_]/', '', $this->sMenuId); - $oPage->add('
    '); - $oDashboard->Render($oPage, false, $aExtraParams); - $oPage->add('
    '); - $oDashboard->RenderEditionTools($oPage); - - if ($oDashboard->GetAutoReload()) - { - $sId = $this->sMenuId; - $sExtraParams = json_encode($aExtraParams); - $iReloadInterval = 1000 * $oDashboard->GetAutoReloadInterval(); - $oPage->add_script( -<< 0)) - { - $('.dashboard_contents#'+sDivId).block(); - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', - { operation: 'reload_dashboard', dashboard_id: '$sId', extra_params: oExtraParams}, - function(data){ - $('.dashboard_contents#'+sDivId).html(data); - $('.dashboard_contents#'+sDivId).unblock(); - } - ); - } - } -EOF - ); - } - - $bEdit = utils::ReadParam('edit', false); - if ($bEdit) - { - $sId = addslashes($this->sMenuId); - $oPage->add_ready_script("EditDashboard('$sId');"); - } - else - { - $oParentMenu = ApplicationMenu::GetMenuNode($this->iParentIndex); - $sParentTitle = $oParentMenu->GetTitle(); - $sThisTitle = $this->GetTitle(); - if ($sParentTitle != $sThisTitle) - { - $sDescription = $sParentTitle.' / '.$sThisTitle; - } - else - { - $sDescription = $sThisTitle; - } - if ($this->sMenuId == ApplicationMenu::GetDefaultMenuId()) - { - $sIcon = '../images/breadcrumb_home.png'; - } - else - { - $sIcon = '../images/breadcrumb-dashboard.png'; - } - $oPage->SetBreadCrumbEntry("ui-dashboard-".$this->sMenuId, $this->GetTitle(), $sDescription, '', $sIcon); - } - } - else - { - $oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'"); - } - } - - /** - * @param WebPage $oPage - * @throws CoreException - * @throws Exception - */ - public function RenderEditor(WebPage $oPage) - { - $oDashboard = $this->GetDashboard(); - if ($oDashboard != null) - { - $oDashboard->RenderEditor($oPage); - } - else - { - $oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'"); - } - } - - /** - * @param $oDashlet - * @throws Exception - */ - public function AddDashlet($oDashlet) - { - $oDashboard = $this->GetDashboard(); - if ($oDashboard != null) - { - $oDashboard->AddDashlet($oDashlet); - $oDashboard->Save(); - } - else - { - throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'"); - } - } - -} - -/** - * A shortcut container is the preferred destination of newly created shortcuts - */ -class ShortcutContainerMenuNode extends MenuNode -{ - /** - * @param string[] $aExtraParams - * @return string - */ - public function GetHyperlink($aExtraParams) - { - return ''; - } - - /** - * @param WebPage $oPage - * @param string[] $aExtraParams - * @return mixed|void - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - } - - /** - * @throws CoreException - * @throws Exception - */ - public function PopulateChildMenus() - { - // Load user shortcuts in DB - // - $oBMSearch = new DBObjectSearch('Shortcut'); - $oBMSearch->AddCondition('user_id', UserRights::GetUserId(), '='); - $oBMSet = new DBObjectSet($oBMSearch, array('friendlyname' => true)); // ascending on friendlyname - $fRank = 1; - while ($oShortcut = $oBMSet->Fetch()) - { - $sName = $this->GetMenuId().'_'.$oShortcut->GetKey(); - new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++); - } - - // Complete the tree - // - parent::PopulateChildMenus(); - } -} - - -require_once(APPROOT.'application/shortcut.class.inc.php'); -/** - * This class defines a menu item which content is a shortcut. - */ -class ShortcutMenuNode extends MenuNode -{ - /** - * @var Shortcut - */ - protected $oShortcut; - - /** - * Create a menu item based on a custom template and inserts it into the application's main menu - * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) - * @param object $oShortcut Shortcut object - * @param integer $iParentIndex ID of the parent menu - * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value - * @param string $sEnableClass Name of class of object - * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE - * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... - * @param string $sEnableStimulus - */ - public function __construct($sMenuId, $oShortcut, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) - { - parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); - $this->oShortcut = $oShortcut; - $this->aReflectionProperties['shortcut'] = $oShortcut->GetKey(); - } - - /** - * @param string[] $aExtraParams - * @return string - * @throws CoreException - */ - public function GetHyperlink($aExtraParams) - { - $sContext = $this->oShortcut->Get('context'); - $aContext = unserialize($sContext); - if (isset($aContext['menu'])) - { - unset($aContext['menu']); - } - foreach ($aContext as $sArgName => $sArgValue) - { - $aExtraParams[$sArgName] = $sArgValue; - } - return parent::GetHyperlink($aExtraParams); - } - - /** - * @param WebPage $oPage - * @param string[] $aExtraParams - * @return mixed|void - * @throws DictExceptionMissingString - */ - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); - $this->oShortcut->RenderContent($oPage, $aExtraParams); - } - - /** - * @return string - * @throws CoreException - */ - public function GetTitle() - { - return $this->oShortcut->Get('name'); - } - - /** - * @return string - * @throws CoreException - */ - public function GetLabel() - { - return $this->oShortcut->Get('name'); - } -} - + + + +/** + * Construction and display of the application's main menu + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/utils.inc.php'); +require_once(APPROOT.'/application/template.class.inc.php'); +require_once(APPROOT."/application/user.dashboard.class.inc.php"); + + +/** + * This class manipulates, stores and displays the navigation menu used in the application + * In order to improve the modularity of the data model and to ease the update/migration + * between evolving data models, the menus are no longer stored in the database, but are instead + * built on the fly each time a page is loaded. + * The application's menu is organized into top-level groups with, inside each group, a tree of menu items. + * Top level groups do not display any content, they just expand/collapse. + * Sub-items drive the actual content of the page, they are based either on templates, OQL queries or full (external?) web pages. + * + * Example: + * Here is how to insert the following items in the application's menu: + * +----------------------------------------+ + * | Configuration Management Group | >> Top level group + * +----------------------------------------+ + * + Configuration Management Overview >> Template based menu item + * + Contacts >> Template based menu item + * + Persons >> Plain list (OQL based) + * + Teams >> Plain list (OQL based) + * + * // Create the top-level group. fRank = 1, means it will be inserted after the group '0', which is usually 'Welcome' + * $oConfigMgmtMenu = new MenuGroup('ConfigurationManagementMenu', 1); + * // Create an entry, based on a custom template, for the Configuration management overview, under the top-level group + * new TemplateMenuNode('ConfigurationManagementMenu', '../somedirectory/configuration_management_menu.html', $oConfigMgmtMenu->GetIndex(), 0); + * // Create an entry (template based) for the overview of contacts + * $oContactsMenu = new TemplateMenuNode('ContactsMenu', '../somedirectory/configuration_management_menu.html',$oConfigMgmtMenu->GetIndex(), 1); + * // Plain list of persons + * new OQLMenuNode('PersonsMenu', 'SELECT bizPerson', $oContactsMenu->GetIndex(), 0); + * + */ + +class ApplicationMenu +{ + /** + * @var bool + */ + static $bAdditionalMenusLoaded = false; + /** + * @var array + */ + static $aRootMenus = array(); + /** + * @var array + */ + static $aMenusIndex = array(); + /** + * @var string + */ + static $sFavoriteSiloQuery = 'SELECT Organization'; + + static public function LoadAdditionalMenus() + { + if (!self::$bAdditionalMenusLoaded) + { + // Build menus from module handlers + // + foreach(MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) + { + $oPHPClass::OnMenuCreation(); + } + + // Build menus from the menus themselves (e.g. the ShortcutContainerMenuNode will do that) + // + foreach(self::$aRootMenus as $aMenu) + { + $oMenuNode = self::GetMenuNode($aMenu['index']); + $oMenuNode->PopulateChildMenus(); + } + + self::$bAdditionalMenusLoaded = true; + } + } + + /** + * Set the query used to limit the list of displayed organizations in the drop-down menu + * @param $sOQL string The OQL query returning a list of Organization objects + * @return void + */ + static public function SetFavoriteSiloQuery($sOQL) + { + self::$sFavoriteSiloQuery = $sOQL; + } + + /** + * Get the query used to limit the list of displayed organizations in the drop-down menu + * @return string The OQL query returning a list of Organization objects + */ + static public function GetFavoriteSiloQuery() + { + return self::$sFavoriteSiloQuery; + } + + /** + * Check wether a menu Id is enabled or not + * @param $sMenuId + * @throws DictExceptionMissingString + */ + static public function CheckMenuIdEnabled($sMenuId) + { + self::LoadAdditionalMenus(); + $oMenuNode = self::GetMenuNode(self::GetMenuIndexById($sMenuId)); + if (is_null($oMenuNode) || !$oMenuNode->IsEnabled()) + { + require_once(APPROOT.'/setup/setuppage.class.inc.php'); + $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); + $oP->add("

    ".Dict::S('UI:Login:Error:AccessRestricted')."

    \n"); + $oP->p("".Dict::S('UI:LogOffMenu').""); + $oP->output(); + exit; + } + } + + /** + * Main function to add a menu entry into the application, can be called during the definition + * of the data model objects + * @param MenuNode $oMenuNode + * @param $iParentIndex + * @param $fRank + * @return int + */ + static public function InsertMenu(MenuNode $oMenuNode, $iParentIndex, $fRank) + { + $index = self::GetMenuIndexById($oMenuNode->GetMenuId()); + if ($index == -1) + { + // The menu does not already exist, insert it + $index = count(self::$aMenusIndex); + + if ($iParentIndex == -1) + { + $sParentId = ''; + self::$aRootMenus[] = array ('rank' => $fRank, 'index' => $index); + } + else + { + $sParentId = self::$aMenusIndex[$iParentIndex]['node']->GetMenuId(); + self::$aMenusIndex[$iParentIndex]['children'][] = array ('rank' => $fRank, 'index' => $index); + } + + // Note: At the time when 'parent', 'rank' and 'source_file' have been added for the reflection API, + // they were not used to display the menus (redundant or unused) + // + $aBacktrace = debug_backtrace(); + $sFile = isset($aBacktrace[2]["file"]) ? $aBacktrace[2]["file"] : $aBacktrace[1]["file"]; + self::$aMenusIndex[$index] = array('node' => $oMenuNode, 'children' => array(), 'parent' => $sParentId, 'rank' => $fRank, 'source_file' => $sFile); + } + else + { + // the menu already exists, let's combine the conditions that make it visible + self::$aMenusIndex[$index]['node']->AddCondition($oMenuNode); + } + + return $index; + } + + /** + * Reflection API - Get menu entries + */ + static public function ReflectionMenuNodes() + { + self::LoadAdditionalMenus(); + return self::$aMenusIndex; + } + + /** + * Entry point to display the whole menu into the web page, used by iTopWebPage + * @param $oPage + * @param $aExtraParams + * @throws DictExceptionMissingString + */ + static public function DisplayMenu($oPage, $aExtraParams) + { + self::LoadAdditionalMenus(); + // Sort the root menu based on the rank + usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank')); + $iAccordion = 0; + $iActiveMenu = self::GetMenuIndexById(self::GetActiveNodeId()); + foreach(self::$aRootMenus as $aMenu) + { + if (!self::CanDisplayMenu($aMenu)) { continue; } + $oMenuNode = self::GetMenuNode($aMenu['index']); + $oPage->AddToMenu('

    '.$oMenuNode->GetTitle().'

    '); + $oPage->AddToMenu('
    '); + $oPage->AddToMenu('
      '); + $aChildren = self::GetChildren($aMenu['index']); + $bActive = self::DisplaySubMenu($oPage, $aChildren, $aExtraParams, $iActiveMenu); + $oPage->AddToMenu('
    '); + if ($bActive) + { +$oPage->add_ready_script( +<<AddToMenu('
    '); + $iAccordion++; + } + } + + /** + * Recursively check if the menu and at least one of his sub-menu is enabled + * @param array $aMenu menu entry + * @return bool true if at least one menu is enabled + */ + static private function CanDisplayMenu($aMenu) + { + $oMenuNode = self::GetMenuNode($aMenu['index']); + if ($oMenuNode->IsEnabled()) + { + $aChildren = self::GetChildren($aMenu['index']); + if (count($aChildren) > 0) + { + foreach($aChildren as $aSubMenu) + { + if (self::CanDisplayMenu($aSubMenu)) + { + return true; + } + } + } + else + { + return true; + } + } + return false; + } + + /** + * Handles the display of the sub-menus (called recursively if necessary) + * @param WebPage $oPage + * @param array $aMenus + * @param array $aExtraParams + * @param int $iActiveMenu + * @return true if the currently selected menu is one of the submenus + * @throws DictExceptionMissingString + */ + static protected function DisplaySubMenu($oPage, $aMenus, $aExtraParams, $iActiveMenu = -1) + { + // Sort the menu based on the rank + $bActive = false; + usort($aMenus, array('ApplicationMenu', 'CompareOnRank')); + foreach($aMenus as $aMenu) + { + $index = $aMenu['index']; + $oMenu = self::GetMenuNode($index); + if ($oMenu->IsEnabled()) + { + $aChildren = self::GetChildren($index); + $sCSSClass = (count($aChildren) > 0) ? ' class="submenu"' : ''; + $sHyperlink = $oMenu->GetHyperlink($aExtraParams); + if ($sHyperlink != '') + { + $oPage->AddToMenu('
  • '.$oMenu->GetTitle().'
  • '); + } + else + { + $oPage->AddToMenu('
  • '.$oMenu->GetTitle().'
  • '); + } + if ($iActiveMenu == $index) + { + $bActive = true; + } + if (count($aChildren) > 0) + { + $oPage->AddToMenu('
      '); + $bActive |= self::DisplaySubMenu($oPage, $aChildren, $aExtraParams, $iActiveMenu); + $oPage->AddToMenu('
    '); + } + } + } + return $bActive; + } + + /** + * Helper function to sort the menus based on their rank + * @param $a + * @param $b + * @return int + */ + static public function CompareOnRank($a, $b) + { + $result = 1; + if ($a['rank'] == $b['rank']) + { + $result = 0; + } + if ($a['rank'] < $b['rank']) + { + $result = -1; + } + return $result; + } + + /** + * Helper function to retrieve the MenuNode Object based on its ID + * @param int $index + * @return MenuNode|null + */ + static public function GetMenuNode($index) + { + return isset(self::$aMenusIndex[$index]) ? self::$aMenusIndex[$index]['node'] : null; + } + + /** + * Helper function to get the list of child(ren) of a menu + * @param int $index + * @return array + */ + static public function GetChildren($index) + { + return self::$aMenusIndex[$index]['children']; + } + + /** + * Helper function to get the ID of a menu based on its name + * @param string $sTitle Title of the menu (as passed when creating the menu) + * @return integer ID of the menu, or -1 if not found + */ + static public function GetMenuIndexById($sTitle) + { + $index = -1; + foreach(self::$aMenusIndex as $aMenu) + { + if ($aMenu['node']->GetMenuId() == $sTitle) + { + $index = $aMenu['node']->GetIndex(); + break; + } + } + return $index; + } + + /** + * Retrieves the currently active menu (if any, otherwise the first menu is the default) + * @return string The Id of the currently active menu + */ + static public function GetActiveNodeId() + { + $oAppContext = new ApplicationContext(); + $sMenuId = $oAppContext->GetCurrentValue('menu', null); + if ($sMenuId === null) + { + $sMenuId = self::GetDefaultMenuId(); + } + return $sMenuId; + } + + /** + * @return null|string + */ + static public function GetDefaultMenuId() + { + static $sDefaultMenuId = null; + if (is_null($sDefaultMenuId)) + { + // Make sure the root menu is sorted on 'rank' + usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank')); + $oFirstGroup = self::GetMenuNode(self::$aRootMenus[0]['index']); + $aChildren = self::$aMenusIndex[$oFirstGroup->GetIndex()]['children']; + usort($aChildren, array('ApplicationMenu', 'CompareOnRank')); + $oMenuNode = self::GetMenuNode($aChildren[0]['index']); + $sDefaultMenuId = $oMenuNode->GetMenuId(); + } + return $sDefaultMenuId; + } + + /** + * @param $sMenuId + * @return string + */ + static public function GetRootMenuId($sMenuId) + { + $iMenuIndex = self::GetMenuIndexById($sMenuId); + if ($iMenuIndex == -1) + { + return ''; + } + $oMenu = ApplicationMenu::GetMenuNode($iMenuIndex); + while ($oMenu->GetParentIndex() != -1) + { + $oMenu = ApplicationMenu::GetMenuNode($oMenu->GetParentIndex()); + } + return $oMenu->GetMenuId(); + } +} + +/** + * Root class for all the kind of node in the menu tree, data model providers are responsible for instantiating + * MenuNodes (i.e instances from derived classes) in order to populate the application's menu. Creating an objet + * derived from MenuNode is enough to have it inserted in the application's main menu. + * The class iTopWebPage, takes care of 3 items: + * +--------------------+ + * | Welcome | + * +--------------------+ + * Welcome To iTop + * +--------------------+ + * | Tools | + * +--------------------+ + * CSV Import + * +--------------------+ + * | Admin Tools | + * +--------------------+ + * User Accounts + * Profiles + * Notifications + * Run Queries + * Export + * Data Model + * Universal Search + * + * All the other menu items must constructed along with the various data model modules + */ +abstract class MenuNode +{ + /** + * @var string + */ + protected $sMenuId; + /** + * @var int + */ + protected $index; + /** + * @var int + */ + protected $iParentIndex; + + /** + * Properties reflecting how the node has been declared + */ + protected $aReflectionProperties; + + /** + * Class of objects to check if the menu is enabled, null if none + */ + protected $m_aEnableClasses; + + /** + * User Rights Action code to check if the menu is enabled, null if none + */ + protected $m_aEnableActions; + + /** + * User Rights allowed results (actually a bitmask) to check if the menu is enabled, null if none + */ + protected $m_aEnableActionResults; + + /** + * Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu + */ + protected $m_aEnableStimuli; + + /** + * Create a menu item, sets the condition to have it displayed and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param integer $iParentIndex ID of the parent menu, pass -1 for top level (group) items + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param mixed $iActionCode UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus The user can see this menu if she/he has enough rights to apply this stimulus + */ + public function __construct($sMenuId, $iParentIndex = -1, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + $this->sMenuId = $sMenuId; + $this->iParentIndex = $iParentIndex; + $this->aReflectionProperties = array(); + if (strlen($sEnableClass) > 0) + { + $this->aReflectionProperties['enable_class'] = $sEnableClass; + $this->aReflectionProperties['enable_action'] = $iActionCode; + $this->aReflectionProperties['enable_permission'] = $iAllowedResults; + $this->aReflectionProperties['enable_stimulus'] = $sEnableStimulus; + } + $this->m_aEnableClasses = array($sEnableClass); + $this->m_aEnableActions = array($iActionCode); + $this->m_aEnableActionResults = array($iAllowedResults); + $this->m_aEnableStimuli = array($sEnableStimulus); + $this->index = ApplicationMenu::InsertMenu($this, $iParentIndex, $fRank); + } + + /** + * @return array + */ + public function ReflectionProperties() + { + return $this->aReflectionProperties; + } + + /** + * @return string + */ + public function GetMenuId() + { + return $this->sMenuId; + } + + /** + * @return int + */ + public function GetParentIndex() + { + return $this->iParentIndex; + } + + /** + * @return string + * @throws DictExceptionMissingString + */ + public function GetTitle() + { + return Dict::S("Menu:$this->sMenuId", str_replace('_', ' ', $this->sMenuId)); + } + + /** + * @return string + * @throws DictExceptionMissingString + */ + public function GetLabel() + { + $sRet = Dict::S("Menu:$this->sMenuId+", ""); + if ($sRet === '') + { + if ($this->iParentIndex != -1) + { + $oParentMenu = ApplicationMenu::GetMenuNode($this->iParentIndex); + $sRet = $oParentMenu->GetTitle().' / '.$this->GetTitle(); + } + else + { + $sRet = $this->GetTitle(); + } + //$sRet = $this->GetTitle(); + } + return $sRet; + } + + /** + * @return int + */ + public function GetIndex() + { + return $this->index; + } + + public function PopulateChildMenus() + { + foreach (ApplicationMenu::GetChildren($this->GetIndex()) as $aMenu) + { + $index = $aMenu['index']; + $oMenu = ApplicationMenu::GetMenuNode($index); + $oMenu->PopulateChildMenus(); + } + } + + /** + * @param $aExtraParams + * @return string + */ + public function GetHyperlink($aExtraParams) + { + $aExtraParams['c[menu]'] = $this->GetMenuId(); + return $this->AddParams(utils::GetAbsoluteUrlAppRoot().'pages/UI.php', $aExtraParams); + } + + /** + * Add a limiting display condition for the same menu node. The conditions will be combined with a AND + * @param $oMenuNode MenuNode Another definition of the same menu node, with potentially different access restriction + * @return void + */ + public function AddCondition(MenuNode $oMenuNode) + { + foreach($oMenuNode->m_aEnableClasses as $index => $sClass ) + { + $this->m_aEnableClasses[] = $sClass; + $this->m_aEnableActions[] = $oMenuNode->m_aEnableActions[$index]; + $this->m_aEnableActionResults[] = $oMenuNode->m_aEnableActionResults[$index]; + $this->m_aEnableStimuli[] = $oMenuNode->m_aEnableStimuli[$index]; + } + } + /** + * Tells whether the menu is enabled (i.e. displayed) for the current user + * @return bool True if enabled, false otherwise + */ + public function IsEnabled() + { + foreach($this->m_aEnableClasses as $index => $sClass) + { + if ($sClass != null) + { + if (MetaModel::IsValidClass($sClass)) + { + if ($this->m_aEnableStimuli[$index] != null) + { + if (!UserRights::IsStimulusAllowed($sClass, $this->m_aEnableStimuli[$index])) + { + return false; + } + } + if ($this->m_aEnableActions[$index] != null) + { + // Menus access rights ignore the archive mode + utils::PushArchiveMode(false); + $iResult = UserRights::IsActionAllowed($sClass, $this->m_aEnableActions[$index]); + utils::PopArchiveMode(); + if (!($iResult & $this->m_aEnableActionResults[$index])) + { + return false; + } + } + } + else + { + return false; + } + } + } + return true; + } + + /** + * @param WebPage $oPage + * @param array $aExtraParams + * @return mixed + */ + public abstract function RenderContent(WebPage $oPage, $aExtraParams = array()); + + /** + * @param $sHyperlink + * @param $aExtraParams + * @return string + */ + protected function AddParams($sHyperlink, $aExtraParams) + { + if (count($aExtraParams) > 0) + { + $aQuery = array(); + $sSeparator = '?'; + if (strpos($sHyperlink, '?') !== false) + { + $sSeparator = '&'; + } + foreach($aExtraParams as $sName => $sValue) + { + $aQuery[] = urlencode($sName).'='.urlencode($sValue); + } + $sHyperlink .= $sSeparator.implode('&', $aQuery); + } + return $sHyperlink; + } +} + +/** + * This class implements a top-level menu group. A group is just a container for sub-items + * it does not display a page by itself + */ +class MenuGroup extends MenuNode +{ + /** + * Create a top-level menu group and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param float $fRank Number used to order the list, the groups are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $fRank, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, -1 /* no parent, groups are at root level */, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + } + + /** + * @param WebPage $oPage + * @param array $aExtraParams + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + assert(false); // Shall never be called, groups do not display any content + } +} + +/** + * This class defines a menu item which content is based on a custom template. + * Note the template can be either a local file or an URL ! + */ +class TemplateMenuNode extends MenuNode +{ + /** + * @var string + */ + protected $sTemplateFile; + + /** + * Create a menu item based on a custom template and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sTemplateFile Path (or URL) to the file that will be used as a template for displaying the page's content + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $sTemplateFile, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sTemplateFile = $sTemplateFile; + $this->aReflectionProperties['template_file'] = $sTemplateFile; + } + + /** + * @param $aExtraParams + * @return string + */ + public function GetHyperlink($aExtraParams) + { + if ($this->sTemplateFile == '') return ''; + return parent::GetHyperlink($aExtraParams); + } + + /** + * @param WebPage $oPage + * @param array $aExtraParams + * @return mixed|void + * @throws DictExceptionMissingString + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); + $sTemplate = @file_get_contents($this->sTemplateFile); + if ($sTemplate !== false) + { + $aExtraParams['table_id'] = 'Menu_'.$this->GetMenuId(); + $oTemplate = new DisplayTemplate($sTemplate); + $oTemplate->Render($oPage, $aExtraParams); + } + else + { + $oPage->p("Error: failed to load template file: '{$this->sTemplateFile}'"); // No need to translate ? + } + } +} + +/** + * This class defines a menu item that uses a standard template to display a list of items therefore it allows + * only two parameters: the page's title and the OQL expression defining the list of items to be displayed + */ +class OQLMenuNode extends MenuNode +{ + /** + * @var string + */ + protected $sPageTitle; + /** + * @var string + */ + protected $sOQL; + /** + * @var bool + */ + protected $bSearch; + /** + * @var bool|null + */ + protected $bSearchFormOpen; + + /** + * Extra parameters to be passed to the display block to fine tune its appearence + */ + protected $m_aParams; + + + /** + * Create a menu item based on an OQL query and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sOQL OQL query defining the set of objects to be displayed + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param bool $bSearch Whether or not to display a (collapsed) search frame at the top of the page + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus + * @param bool $bSearchFormOpen + */ + public function __construct($sMenuId, $sOQL, $iParentIndex, $fRank = 0.0, $bSearch = false, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null, $bSearchFormOpen = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sPageTitle = "Menu:$sMenuId+"; + $this->sOQL = $sOQL; + $this->bSearch = $bSearch; + $this->bSearchFormOpen = $bSearchFormOpen; + $this->m_aParams = array(); + $this->aReflectionProperties['oql'] = $sOQL; + $this->aReflectionProperties['do_search'] = $bSearch; + // Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects + // of the class specified by the OQL... + } + + /** + * Set some extra parameters to be passed to the display block to fine tune its appearence + * @param Hash $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters + */ + public function SetParameters($aParams) + { + $this->m_aParams = $aParams; + foreach($aParams as $sKey => $value) + { + $this->aReflectionProperties[$sKey] = $value; + } + } + + /** + * @param WebPage $oPage + * @param array $aExtraParams + * @return mixed|void + * @throws CoreException + * @throws DictExceptionMissingString + * @throws OQLException + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); + OQLMenuNode::RenderOQLSearch + ( + $this->sOQL, + Dict::S($this->sPageTitle), + 'Menu_'.$this->GetMenuId(), + $this->bSearch, // Search pane + $this->bSearchFormOpen, // Search open + $oPage, + array_merge($this->m_aParams, $aExtraParams), + true + ); + } + + /** + * @param $sOql + * @param $sTitle + * @param $sUsageId + * @param $bSearchPane + * @param $bSearchOpen + * @param WebPage $oPage + * @param array $aExtraParams + * @param bool $bEnableBreadcrumb + * @throws CoreException + * @throws DictExceptionMissingString + * @throws OQLException + */ + public static function RenderOQLSearch($sOql, $sTitle, $sUsageId, $bSearchPane, $bSearchOpen, WebPage $oPage, $aExtraParams = array(), $bEnableBreadcrumb = false) + { + $sUsageId = utils::GetSafeId($sUsageId); + $oSearch = DBObjectSearch::FromOQL($sOql); + $sIcon = MetaModel::GetClassIcon($oSearch->GetClass()); + + if ($bSearchPane) + { + $aParams = array_merge(array('open' => $bSearchOpen, 'table_id' => $sUsageId), $aExtraParams); + $oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams); + $oBlock->Display($oPage, 0); + } + + $oPage->add("

    $sIcon ".Dict::S($sTitle)."

    "); + + $aParams = array_merge(array('table_id' => $sUsageId), $aExtraParams); + $oBlock = new DisplayBlock($oSearch, 'list', false /* Asynchronous */, $aParams); + $oBlock->Display($oPage, $sUsageId); + + if ($bEnableBreadcrumb && ($oPage instanceof iTopWebPage)) + { + // Breadcrumb + //$iCount = $oBlock->GetDisplayedCount(); + $sPageId = "ui-search-".$oSearch->GetClass(); + $sLabel = MetaModel::GetName($oSearch->GetClass()); + $oPage->SetBreadCrumbEntry($sPageId, $sLabel, $sTitle, '', '../images/breadcrumb-search.png'); + } + } +} + +/** + * This class defines a menu item that displays a search form for the given class of objects + */ +class SearchMenuNode extends MenuNode +{ + /** + * @var string + */ + protected $sPageTitle; + /** + * @var string + */ + protected $sClass; + + /** + * Create a menu item based on an OQL query and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sClass The class of objects to search for + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param bool $bSearch (not used) + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $sClass, $iParentIndex, $fRank = 0.0, $bSearch = false, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sPageTitle = "Menu:$sMenuId+"; + $this->sClass = $sClass; + $this->aReflectionProperties['class'] = $sClass; + } + + /** + * @param WebPage $oPage + * @param array $aExtraParams + * @return mixed|void + * @throws DictExceptionMissingString + * @throws Exception + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); + $oPage->SetBreadCrumbEntry("menu-".$this->sMenuId, $this->GetTitle(), '', '', utils::GetAbsoluteUrlAppRoot().'images/search.png'); + + $oSearch = new DBObjectSearch($this->sClass); + $aParams = array_merge(array('table_id' => 'Menu_'.utils::GetSafeId($this->GetMenuId())), $aExtraParams); + $oBlock = new DisplayBlock($oSearch, 'search', false /* Asynchronous */, $aParams); + $oBlock->Display($oPage, 0); + } +} + +/** + * This class defines a menu that points to any web page. It takes only two parameters: + * - The hyperlink to point to + * - The name of the menu + * Note: the parameter menu=xxx (where xxx is the id of the menu itself) will be added to the hyperlink + * in order to make it the active one, if the target page is based on iTopWebPage and therefore displays the menu + */ +class WebPageMenuNode extends MenuNode +{ + /** + * @var string + */ + protected $sHyperlink; + + /** + * Create a menu item that points to any web page (not only UI.php) + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sHyperlink URL to the page to load. Use relative URL if you want to keep the application portable ! + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $sHyperlink, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sHyperlink = $sHyperlink; + $this->aReflectionProperties['url'] = $sHyperlink; + } + + /** + * @param array $aExtraParams + * @return string + */ + public function GetHyperlink($aExtraParams) + { + $aExtraParams['c[menu]'] = $this->GetMenuId(); + return $this->AddParams( $this->sHyperlink, $aExtraParams); + } + + /** + * @param WebPage $oPage + * @param array $aExtraParams + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + assert(false); // Shall never be called, the external web page will handle the display by itself + } +} + +/** + * This class defines a menu that points to the page for creating a new object of the specified class. + * It take only one parameter: the name of the class + * Note: the parameter menu=xxx (where xxx is the id of the menu itself) will be added to the hyperlink + * in order to make it the active one + */ +class NewObjectMenuNode extends MenuNode +{ + /** + * @var string + */ + protected $sClass; + + /** + * Create a menu item that points to the URL for creating a new object, the menu will be added only if the current user has enough + * rights to create such an object (or an object of a child class) + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sClass URL to the page to load. Use relative URL if you want to keep the application portable ! + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass + * @param int|null $iActionCode + * @param int $iAllowedResults + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $sClass, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sClass = $sClass; + $this->aReflectionProperties['class'] = $sClass; + } + + /** + * @param string[] $aExtraParams + * @return string + */ + public function GetHyperlink($aExtraParams) + { + $sHyperlink = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=new&class='.$this->sClass; + $aExtraParams['c[menu]'] = $this->GetMenuId(); + return $this->AddParams($sHyperlink, $aExtraParams); + } + + /** + * Overload the check of the "enable" state of this menu to take into account + * derived classes of objects + * @throws CoreException + */ + public function IsEnabled() + { + // Enable this menu, only if the current user has enough rights to create such an object, or an object of + // any child class + + $aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself + $bActionIsAllowed = false; + + foreach($aSubClasses as $sCandidateClass) + { + if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) + { + $bActionIsAllowed = true; + break; // Enough for now + } + } + return $bActionIsAllowed; + } + + /** + * @param WebPage $oPage + * @param string[] $aExtraParams + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + assert(false); // Shall never be called, the external web page will handle the display by itself + } +} + +require_once(APPROOT.'application/dashboard.class.inc.php'); +/** + * This class defines a menu item which content is based on XML dashboard. + */ +class DashboardMenuNode extends MenuNode +{ + /** + * @var string + */ + protected $sDashboardFile; + + /** + * Create a menu item based on a custom template and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param string $sDashboardFile + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $sDashboardFile, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->sDashboardFile = $sDashboardFile; + $this->aReflectionProperties['definition_file'] = $sDashboardFile; + } + + /** + * @param string[] $aExtraParams + * @return string + */ + public function GetHyperlink($aExtraParams) + { + if ($this->sDashboardFile == '') return ''; + return parent::GetHyperlink($aExtraParams); + } + + /** + * @return null|RuntimeDashboard + * @throws CoreException + * @throws Exception + */ + public function GetDashboard() + { + $sDashboardDefinition = @file_get_contents($this->sDashboardFile); + if ($sDashboardDefinition !== false) + { + $bCustomized = false; + + // Search for an eventual user defined dashboard, overloading the existing one + $oUDSearch = new DBObjectSearch('UserDashboard'); + $oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '='); + $oUDSearch->AddCondition('menu_code', $this->sMenuId, '='); + $oUDSet = new DBObjectSet($oUDSearch); + if ($oUDSet->Count() > 0) + { + // Assuming there is at most one couple {user, menu}! + $oUserDashboard = $oUDSet->Fetch(); + $sDashboardDefinition = $oUserDashboard->Get('contents'); + $bCustomized = true; + + } + $oDashboard = new RuntimeDashboard($this->sMenuId); + $oDashboard->FromXml($sDashboardDefinition); + $oDashboard->SetCustomFlag($bCustomized); + } + else + { + $oDashboard = null; + } + return $oDashboard; + } + + /** + * @param WebPage $oPage + * @param string[] $aExtraParams + * @throws CoreException + * @throws Exception + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); + $oDashboard = $this->GetDashboard(); + if ($oDashboard != null) + { + $sDivId = preg_replace('/[^a-zA-Z0-9_]/', '', $this->sMenuId); + $oPage->add('
    '); + $oDashboard->Render($oPage, false, $aExtraParams); + $oPage->add('
    '); + $oDashboard->RenderEditionTools($oPage); + + if ($oDashboard->GetAutoReload()) + { + $sId = $this->sMenuId; + $sExtraParams = json_encode($aExtraParams); + $iReloadInterval = 1000 * $oDashboard->GetAutoReloadInterval(); + $oPage->add_script( +<< 0)) + { + $('.dashboard_contents#'+sDivId).block(); + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', + { operation: 'reload_dashboard', dashboard_id: '$sId', extra_params: oExtraParams}, + function(data){ + $('.dashboard_contents#'+sDivId).html(data); + $('.dashboard_contents#'+sDivId).unblock(); + } + ); + } + } +EOF + ); + } + + $bEdit = utils::ReadParam('edit', false); + if ($bEdit) + { + $sId = addslashes($this->sMenuId); + $oPage->add_ready_script("EditDashboard('$sId');"); + } + else + { + $oParentMenu = ApplicationMenu::GetMenuNode($this->iParentIndex); + $sParentTitle = $oParentMenu->GetTitle(); + $sThisTitle = $this->GetTitle(); + if ($sParentTitle != $sThisTitle) + { + $sDescription = $sParentTitle.' / '.$sThisTitle; + } + else + { + $sDescription = $sThisTitle; + } + if ($this->sMenuId == ApplicationMenu::GetDefaultMenuId()) + { + $sIcon = '../images/breadcrumb_home.png'; + } + else + { + $sIcon = '../images/breadcrumb-dashboard.png'; + } + $oPage->SetBreadCrumbEntry("ui-dashboard-".$this->sMenuId, $this->GetTitle(), $sDescription, '', $sIcon); + } + } + else + { + $oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'"); + } + } + + /** + * @param WebPage $oPage + * @throws CoreException + * @throws Exception + */ + public function RenderEditor(WebPage $oPage) + { + $oDashboard = $this->GetDashboard(); + if ($oDashboard != null) + { + $oDashboard->RenderEditor($oPage); + } + else + { + $oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'"); + } + } + + /** + * @param $oDashlet + * @throws Exception + */ + public function AddDashlet($oDashlet) + { + $oDashboard = $this->GetDashboard(); + if ($oDashboard != null) + { + $oDashboard->AddDashlet($oDashlet); + $oDashboard->Save(); + } + else + { + throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'"); + } + } + +} + +/** + * A shortcut container is the preferred destination of newly created shortcuts + */ +class ShortcutContainerMenuNode extends MenuNode +{ + /** + * @param string[] $aExtraParams + * @return string + */ + public function GetHyperlink($aExtraParams) + { + return ''; + } + + /** + * @param WebPage $oPage + * @param string[] $aExtraParams + * @return mixed|void + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + } + + /** + * @throws CoreException + * @throws Exception + */ + public function PopulateChildMenus() + { + // Load user shortcuts in DB + // + $oBMSearch = new DBObjectSearch('Shortcut'); + $oBMSearch->AddCondition('user_id', UserRights::GetUserId(), '='); + $oBMSet = new DBObjectSet($oBMSearch, array('friendlyname' => true)); // ascending on friendlyname + $fRank = 1; + while ($oShortcut = $oBMSet->Fetch()) + { + $sName = $this->GetMenuId().'_'.$oShortcut->GetKey(); + new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++); + } + + // Complete the tree + // + parent::PopulateChildMenus(); + } +} + + +require_once(APPROOT.'application/shortcut.class.inc.php'); +/** + * This class defines a menu item which content is a shortcut. + */ +class ShortcutMenuNode extends MenuNode +{ + /** + * @var Shortcut + */ + protected $oShortcut; + + /** + * Create a menu item based on a custom template and inserts it into the application's main menu + * @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary) + * @param object $oShortcut Shortcut object + * @param integer $iParentIndex ID of the parent menu + * @param float $fRank Number used to order the list, any number will do, but for a given level (i.e same parent) all menus are sorted based on this value + * @param string $sEnableClass Name of class of object + * @param integer $iActionCode Either UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_DELETE, UR_ACTION_BULKREAD, UR_ACTION_BULKMODIFY or UR_ACTION_BULKDELETE + * @param integer $iAllowedResults Expected "rights" for the action: either UR_ALLOWED_YES, UR_ALLOWED_NO, UR_ALLOWED_DEPENDS or a mix of them... + * @param string $sEnableStimulus + */ + public function __construct($sMenuId, $oShortcut, $iParentIndex, $fRank = 0.0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null) + { + parent::__construct($sMenuId, $iParentIndex, $fRank, $sEnableClass, $iActionCode, $iAllowedResults, $sEnableStimulus); + $this->oShortcut = $oShortcut; + $this->aReflectionProperties['shortcut'] = $oShortcut->GetKey(); + } + + /** + * @param string[] $aExtraParams + * @return string + * @throws CoreException + */ + public function GetHyperlink($aExtraParams) + { + $sContext = $this->oShortcut->Get('context'); + $aContext = unserialize($sContext); + if (isset($aContext['menu'])) + { + unset($aContext['menu']); + } + foreach ($aContext as $sArgName => $sArgValue) + { + $aExtraParams[$sArgName] = $sArgValue; + } + return parent::GetHyperlink($aExtraParams); + } + + /** + * @param WebPage $oPage + * @param string[] $aExtraParams + * @return mixed|void + * @throws DictExceptionMissingString + */ + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + ApplicationMenu::CheckMenuIdEnabled($this->GetMenuId()); + $this->oShortcut->RenderContent($oPage, $aExtraParams); + } + + /** + * @return string + * @throws CoreException + */ + public function GetTitle() + { + return $this->oShortcut->Get('name'); + } + + /** + * @return string + * @throws CoreException + */ + public function GetLabel() + { + return $this->oShortcut->Get('name'); + } +} + diff --git a/application/nicewebpage.class.inc.php b/application/nicewebpage.class.inc.php index 60d1229eb..bc7056ea7 100644 --- a/application/nicewebpage.class.inc.php +++ b/application/nicewebpage.class.inc.php @@ -1,251 +1,251 @@ - - - -/** - * Class NiceWebPage - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/webpage.class.inc.php"); -/** - * Web page with some associated CSS and scripts (jquery) for a fancier display - */ -class NiceWebPage extends WebPage -{ - var $m_aReadyScripts; - var $m_sRootUrl; - - public function __construct($s_title, $bPrintable = false) - { - parent::__construct($s_title, $bPrintable); - $this->m_aReadyScripts = array(); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-3.3.1.min.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-migrate-3.0.1.min.js'); // Needed since many other plugins still rely on oldies like $.browser - $this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/ui-lightness/jquery-ui-1.11.4.custom.css'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-ui-1.11.4.custom.min.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/utils.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/hovertip.js'); - // table sorting - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.pager.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablehover.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/table-selectable-lines.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/field_sorter.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/datatable.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.positionBy.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.popupmenu.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/searchformforeignkeys.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/latinise/latinise.min.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler_history.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_raw.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_string.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_field.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_numeric.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_enum.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_key.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_hierarchical_key.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_abstract.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date.js'); - $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_time.js'); - - $this->add_dict_entries('UI:Combo'); - - $this->add_ready_script( -<<< EOF - //add new widget called TruncatedList to properly display truncated lists when they are sorted - $.tablesorter.addWidget({ - // give the widget a id - id: "truncatedList", - // format is called when the on init and when a sorting has finished - format: function(table) - { - // Check if there is a "truncated" line - this.truncatedList = false; - if ($("tr td.truncated",table).length > 0) - { - this.truncatedList = true; - } - if (this.truncatedList) - { - $("tr td",table).removeClass('truncated'); - $("tr:last td",table).addClass('truncated'); - } - } - }); - - $.tablesorter.addWidget({ - // give the widget a id - id: "myZebra", - // format is called when the on init and when a sorting has finished - format: function(table) - { - // Replace the 'red even' lines by 'red_even' since most browser do not support 2 classes selector in CSS, etc.. - $("tbody tr:even",table).addClass('even'); - $("tbody tr.red:even",table).removeClass('red').removeClass('even').addClass('red_even'); - $("tbody tr.orange:even",table).removeClass('orange').removeClass('even').addClass('orange_even'); - $("tbody tr.green:even",table).removeClass('green').removeClass('even').addClass('green_even'); - // In case we sort again the table, we need to remove the added 'even' classes on odd rows - $("tbody tr:odd",table).removeClass('even'); - $("tbody tr.red_even:odd",table).removeClass('even').removeClass('red_even').addClass('red'); - $("tbody tr.orange_even:odd",table).removeClass('even').removeClass('orange_even').addClass('orange'); - $("tbody tr.green_even:odd",table).removeClass('even').removeClass('green_even').addClass('green'); - } - }); - $("table.listResults").tableHover(); // hover tables -EOF - ); - $this->add_saas("css/light-grey.scss"); - - $this->m_sRootUrl = $this->GetAbsoluteUrlAppRoot(); - $sAbsURLAppRoot = addslashes($this->m_sRootUrl); - $sAbsURLModulesRoot = addslashes($this->GetAbsoluteUrlModulesRoot()); - $sEnvironment = addslashes(utils::GetCurrentEnvironment()); - - $sAppContext = addslashes($this->GetApplicationContext()); - - $this->add_script( -<< 0) - { - if (sURL.indexOf('?') == -1) - { - return sURL+'?'+sContext; - } - return sURL+'&'+sContext; - } - return sURL; -} -EOF - ); - } - - public function SetRootUrl($sRootUrl) - { - $this->m_sRootUrl = $sRootUrl; - } - - public function small_p($sText) - { - $this->add("

    $sText

    \n"); - } - - public function GetAbsoluteUrlAppRoot() - { - return utils::GetAbsoluteUrlAppRoot(); - } - - public function GetAbsoluteUrlModulesRoot() - { - return utils::GetAbsoluteUrlModulesRoot(); - } - - function GetApplicationContext() - { - $oAppContext = new ApplicationContext(); - return $oAppContext->GetForLink(); - } - - // By Rom, used by CSVImport and Advanced search - public function MakeClassesSelect($sName, $sDefaultValue, $iWidthPx, $iActionCode = null) - { - // $aTopLevelClasses = array('bizService', 'bizContact', 'logInfra', 'bizDocument'); - // These are classes wich root class is cmdbAbstractObject ! - $this->add(""); - } - - // By Rom, used by Advanced search - public function add_select($aChoices, $sName, $sDefaultValue, $iWidthPx) - { - $this->add(""); - } - - public function add_ready_script($sScript) - { - $this->m_aReadyScripts[] = $sScript; - } - - /** - * Outputs (via some echo) the complete HTML page by assembling all its elements - */ - public function output() - { - //$this->set_base($this->m_sRootUrl.'pages/'); - if (count($this->m_aReadyScripts)>0) - { - $this->add_script("\$(document).ready(function() {\n".implode("\n", $this->m_aReadyScripts)."\n});"); - } - parent::output(); - } -} - -?> + + + +/** + * Class NiceWebPage + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); +/** + * Web page with some associated CSS and scripts (jquery) for a fancier display + */ +class NiceWebPage extends WebPage +{ + var $m_aReadyScripts; + var $m_sRootUrl; + + public function __construct($s_title, $bPrintable = false) + { + parent::__construct($s_title, $bPrintable); + $this->m_aReadyScripts = array(); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-3.3.1.min.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-migrate-3.0.1.min.js'); // Needed since many other plugins still rely on oldies like $.browser + $this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/ui-lightness/jquery-ui-1.11.4.custom.css'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-ui-1.11.4.custom.min.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/utils.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/hovertip.js'); + // table sorting + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.pager.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablehover.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/table-selectable-lines.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/field_sorter.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/datatable.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.positionBy.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.popupmenu.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/searchformforeignkeys.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/latinise/latinise.min.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler_history.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_raw.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_string.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_field.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_numeric.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_enum.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_key.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_hierarchical_key.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_abstract.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date.js'); + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_time.js'); + + $this->add_dict_entries('UI:Combo'); + + $this->add_ready_script( +<<< EOF + //add new widget called TruncatedList to properly display truncated lists when they are sorted + $.tablesorter.addWidget({ + // give the widget a id + id: "truncatedList", + // format is called when the on init and when a sorting has finished + format: function(table) + { + // Check if there is a "truncated" line + this.truncatedList = false; + if ($("tr td.truncated",table).length > 0) + { + this.truncatedList = true; + } + if (this.truncatedList) + { + $("tr td",table).removeClass('truncated'); + $("tr:last td",table).addClass('truncated'); + } + } + }); + + $.tablesorter.addWidget({ + // give the widget a id + id: "myZebra", + // format is called when the on init and when a sorting has finished + format: function(table) + { + // Replace the 'red even' lines by 'red_even' since most browser do not support 2 classes selector in CSS, etc.. + $("tbody tr:even",table).addClass('even'); + $("tbody tr.red:even",table).removeClass('red').removeClass('even').addClass('red_even'); + $("tbody tr.orange:even",table).removeClass('orange').removeClass('even').addClass('orange_even'); + $("tbody tr.green:even",table).removeClass('green').removeClass('even').addClass('green_even'); + // In case we sort again the table, we need to remove the added 'even' classes on odd rows + $("tbody tr:odd",table).removeClass('even'); + $("tbody tr.red_even:odd",table).removeClass('even').removeClass('red_even').addClass('red'); + $("tbody tr.orange_even:odd",table).removeClass('even').removeClass('orange_even').addClass('orange'); + $("tbody tr.green_even:odd",table).removeClass('even').removeClass('green_even').addClass('green'); + } + }); + $("table.listResults").tableHover(); // hover tables +EOF + ); + $this->add_saas("css/light-grey.scss"); + + $this->m_sRootUrl = $this->GetAbsoluteUrlAppRoot(); + $sAbsURLAppRoot = addslashes($this->m_sRootUrl); + $sAbsURLModulesRoot = addslashes($this->GetAbsoluteUrlModulesRoot()); + $sEnvironment = addslashes(utils::GetCurrentEnvironment()); + + $sAppContext = addslashes($this->GetApplicationContext()); + + $this->add_script( +<< 0) + { + if (sURL.indexOf('?') == -1) + { + return sURL+'?'+sContext; + } + return sURL+'&'+sContext; + } + return sURL; +} +EOF + ); + } + + public function SetRootUrl($sRootUrl) + { + $this->m_sRootUrl = $sRootUrl; + } + + public function small_p($sText) + { + $this->add("

    $sText

    \n"); + } + + public function GetAbsoluteUrlAppRoot() + { + return utils::GetAbsoluteUrlAppRoot(); + } + + public function GetAbsoluteUrlModulesRoot() + { + return utils::GetAbsoluteUrlModulesRoot(); + } + + function GetApplicationContext() + { + $oAppContext = new ApplicationContext(); + return $oAppContext->GetForLink(); + } + + // By Rom, used by CSVImport and Advanced search + public function MakeClassesSelect($sName, $sDefaultValue, $iWidthPx, $iActionCode = null) + { + // $aTopLevelClasses = array('bizService', 'bizContact', 'logInfra', 'bizDocument'); + // These are classes wich root class is cmdbAbstractObject ! + $this->add(""); + } + + // By Rom, used by Advanced search + public function add_select($aChoices, $sName, $sDefaultValue, $iWidthPx) + { + $this->add(""); + } + + public function add_ready_script($sScript) + { + $this->m_aReadyScripts[] = $sScript; + } + + /** + * Outputs (via some echo) the complete HTML page by assembling all its elements + */ + public function output() + { + //$this->set_base($this->m_sRootUrl.'pages/'); + if (count($this->m_aReadyScripts)>0) + { + $this->add_script("\$(document).ready(function() {\n".implode("\n", $this->m_aReadyScripts)."\n});"); + } + parent::output(); + } +} + +?> diff --git a/application/query.class.inc.php b/application/query.class.inc.php index 91cc08d0b..cfe8f640b 100644 --- a/application/query.class.inc.php +++ b/application/query.class.inc.php @@ -1,141 +1,141 @@ - - - -/** - * Persistent class Event and derived - * Application internal events - * There is also a file log - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -abstract class Query extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui,application,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_query", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeText("fields", array("allowed_values"=>null, "sql"=>"fields", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'fields')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'fields')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array('name', 'description')); // Criteria of the default search form - // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class QueryOQL extends Query -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui,application,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_query_oql", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeOQL("oql", array("allowed_values"=>null, "sql"=>"oql", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'oql', 'fields')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'fields', 'oql')); // Criteria of the std search form - } - - function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) - { - $aFieldsMap = parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams); - - if (!$bEditMode) - { - $sFields = trim($this->Get('fields')); - $bExportV1Recommended = ($sFields == ''); - if ($bExportV1Recommended) - { - $oFieldAttDef = MetaModel::GetAttributeDef('QueryOQL', 'fields'); - $oPage->add('
    '.Dict::Format('UI:Query:UrlV1', $oFieldAttDef->GetLabel()).'
    '); - $sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?format=spreadsheet&login_mode=basic&query='.$this->GetKey(); - } - else - { - $sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?format=spreadsheet&login_mode=basic&date_format='.urlencode((string)AttributeDateTime::GetFormat()).'&query='.$this->GetKey(); - } - $sOql = $this->Get('oql'); - $sMessage = null; - try - { - $oSearch = DBObjectSearch::FromOQL($sOql); - $aParameters = $oSearch->GetQueryParams(); - foreach($aParameters as $sParam => $val) - { - $sUrl .= '&arg_'.$sParam.'=["'.$sParam.'"]'; - } - - $oPage->p(Dict::S('UI:Query:UrlForExcel').':
    '); - - if (count($aParameters) == 0) - { - $oBlock = new DisplayBlock($oSearch, 'list'); - $aExtraParams = array( - //'menu' => $sShowMenu, - 'table_id' => 'query_preview_'.$this->getKey(), - ); - $sBlockId = 'block_query_preview_'.$this->GetKey(); // make a unique id (edition occuring in the same DOM) - $oBlock->Display($oPage, $sBlockId, $aExtraParams); - } - } - catch (OQLException $e) - { - $sMessage = '
    '.Dict::Format('UI:RunQuery:Error', $e->getHtmlDesc()).'
    '; - $oPage->p($sMessage); - } - } - return $aFieldsMap; - } -} - -?> + + + +/** + * Persistent class Event and derived + * Application internal events + * There is also a file log + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +abstract class Query extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui,application,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_query", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeText("fields", array("allowed_values"=>null, "sql"=>"fields", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'fields')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'fields')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array('name', 'description')); // Criteria of the default search form + // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class QueryOQL extends Query +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui,application,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_query_oql", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeOQL("oql", array("allowed_values"=>null, "sql"=>"oql", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'oql', 'fields')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'fields', 'oql')); // Criteria of the std search form + } + + function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) + { + $aFieldsMap = parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams); + + if (!$bEditMode) + { + $sFields = trim($this->Get('fields')); + $bExportV1Recommended = ($sFields == ''); + if ($bExportV1Recommended) + { + $oFieldAttDef = MetaModel::GetAttributeDef('QueryOQL', 'fields'); + $oPage->add('
    '.Dict::Format('UI:Query:UrlV1', $oFieldAttDef->GetLabel()).'
    '); + $sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?format=spreadsheet&login_mode=basic&query='.$this->GetKey(); + } + else + { + $sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?format=spreadsheet&login_mode=basic&date_format='.urlencode((string)AttributeDateTime::GetFormat()).'&query='.$this->GetKey(); + } + $sOql = $this->Get('oql'); + $sMessage = null; + try + { + $oSearch = DBObjectSearch::FromOQL($sOql); + $aParameters = $oSearch->GetQueryParams(); + foreach($aParameters as $sParam => $val) + { + $sUrl .= '&arg_'.$sParam.'=["'.$sParam.'"]'; + } + + $oPage->p(Dict::S('UI:Query:UrlForExcel').':
    '); + + if (count($aParameters) == 0) + { + $oBlock = new DisplayBlock($oSearch, 'list'); + $aExtraParams = array( + //'menu' => $sShowMenu, + 'table_id' => 'query_preview_'.$this->getKey(), + ); + $sBlockId = 'block_query_preview_'.$this->GetKey(); // make a unique id (edition occuring in the same DOM) + $oBlock->Display($oPage, $sBlockId, $aExtraParams); + } + } + catch (OQLException $e) + { + $sMessage = '
    '.Dict::Format('UI:RunQuery:Error', $e->getHtmlDesc()).'
    '; + $oPage->p($sMessage); + } + } + return $aFieldsMap; + } +} + +?> diff --git a/application/shortcut.class.inc.php b/application/shortcut.class.inc.php index 282fae897..a92e6ba75 100644 --- a/application/shortcut.class.inc.php +++ b/application/shortcut.class.inc.php @@ -1,338 +1,338 @@ - - - -/** - * Persistent class Shortcut and derived - * Shortcuts of any kind - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -abstract class Shortcut extends DBObject implements iDisplay -{ - public static function Init() - { - $aParams = array - ( - "category" => "gui,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_shortcut", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("context", array("allowed_values"=>null, "sql"=>"context", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'context')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('name')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - abstract public function RenderContent(WebPage $oPage, $aExtraParams = array()); - - protected function OnInsert() - { - $this->Set('user_id', UserRights::GetUserId()); - } - - public function StartRenameDialog($oPage) - { - $oPage->add('
    '); - - $oForm = new DesignerForm(); - $sDefault = $this->Get('name'); - $oField = new DesignerTextField('name', Dict::S('Class:Shortcut/Attribute:name'), $sDefault); - $oField->SetMandatory(true); - $oForm->AddField($oField); - $oForm->Render($oPage); - $oPage->add('
    '); - - $sDialogTitle = Dict::S('UI:ShortcutRenameDlg:Title'); - $sOkButtonLabel = Dict::S('UI:Button:Ok'); - $sCancelButtonLabel = Dict::S('UI:Button:Cancel'); - $iShortcut = $this->GetKey(); - - $oPage->add_ready_script( -<< "gui,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_shortcut_oql", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeOQL("oql", array("allowed_values"=>null, "sql"=>"oql", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("auto_reload", array("allowed_values"=>new ValueSetEnum('none,custom'), "sql"=>"auto_reload", "default_value"=>"none", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("auto_reload_sec", array("allowed_values"=>null, "sql"=>"auto_reload_sec", "default_value"=>60, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'context', 'oql')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('name')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - public function RenderContent(WebPage $oPage, $aExtraParams = array()) - { - $oPage->set_title($this->Get('name')); - - switch($this->Get('auto_reload')) - { - case 'custom': - $iRate = (int)$this->Get('auto_reload_sec'); - if ($iRate > 0) - { - // Must a string otherwise it can be evaluated to 'true' and defaults to "standard" refresh rate! - $aExtraParams['auto_reload'] = (string)$iRate; - } - break; - - default: - case 'none': - } - - $bSearchPane = true; - $bSearchOpen = true; - try - { - OQLMenuNode::RenderOQLSearch($this->Get('oql'), $this->Get('name'), 'shortcut_'.$this->GetKey(), $bSearchPane, $bSearchOpen, $oPage, $aExtraParams, true); - } - catch (Exception $e) - { - throw new Exception("The OQL shortcut '".$this->Get('name')."' (id: ".$this->GetKey().") could not be displayed: ".$e->getMessage()); - } - - } - - public function CloneTableSettings($sTableSettings) - { - $aTableSettings = json_decode($sTableSettings, true); - - $oFilter = DBObjectSearch::FromOQL($this->Get('oql')); - $oCustomSettings = new DataTableSettings($oFilter->GetSelectedClasses()); - $oCustomSettings->iDefaultPageSize = $aTableSettings['iPageSize']; - $oCustomSettings->aColumns = $aTableSettings['oColumns']; - $oCustomSettings->Save('shortcut_'.$this->GetKey()); - } - - public static function GetCreationForm($sOQL = null, $sTableSettings = null) - { - $oForm = new DesignerForm(); - - // Find a unique default name - // -> The class of the query + an index if necessary - if ($sOQL == null) - { - $sDefault = ''; - } - else - { - $oBMSearch = new DBObjectSearch('Shortcut'); - $oBMSearch->AddCondition('user_id', UserRights::GetUserId(), '='); - $oBMSet = new DBObjectSet($oBMSearch); - $aNames = $oBMSet->GetColumnAsArray('name'); - $oSearch = DBObjectSearch::FromOQL($sOQL); - $sDefault = utils::MakeUniqueName($oSearch->GetClass(), $aNames); - } - - $oField = new DesignerTextField('name', Dict::S('Class:Shortcut/Attribute:name'), $sDefault); - $oField->SetMandatory(true); - $oForm->AddField($oField); - - /* - $oField = new DesignerComboField('auto_reload', Dict::S('Class:ShortcutOQL/Attribute:auto_reload'), 'none'); - $oAttDef = MetaModel::GetAttributeDef(__class__, 'auto_reload'); - $oField->SetAllowedValues($oAttDef->GetAllowedValues()); - $oField->SetMandatory(true); - $oForm->AddField($oField); - */ - $oField = new DesignerBooleanField('auto_reload', Dict::S('Class:ShortcutOQL/Attribute:auto_reload'), false); - $oForm->AddField($oField); - - $oField = new DesignerIntegerField('auto_reload_sec', Dict::S('Class:ShortcutOQL/Attribute:auto_reload_sec'), MetaModel::GetConfig()->GetStandardReloadInterval()); - $oField->SetBoundaries(MetaModel::GetConfig()->Get('min_reload_interval'), null); // no upper limit - $oField->SetMandatory(false); - $oForm->AddField($oField); - - $oField = new DesignerHiddenField('oql', '', $sOQL); - $oForm->AddField($oField); - - $oField = new DesignerHiddenField('table_settings', '', $sTableSettings); - $oForm->AddField($oField); - - return $oForm; - } - - public static function GetCreationDlgFromOQL($oPage, $sOQL, $sTableSettings) - { - $oPage->add('
    '); - - $oForm = self::GetCreationForm($sOQL, $sTableSettings); - - $oForm->Render($oPage); - $oPage->add('
    '); - - $sDialogTitle = Dict::S('UI:ShortcutListDlg:Title'); - $sOkButtonLabel = Dict::S('UI:Button:Ok'); - $sCancelButtonLabel = Dict::S('UI:Button:Cancel'); - - $oAppContext = new ApplicationContext(); - $sContext = $oAppContext->GetForLink(); - - $sRateTitle = addslashes(Dict::Format('Class:ShortcutOQL/Attribute:auto_reload_sec/tip', MetaModel::GetConfig()->Get('min_reload_interval'))); - - $oPage->add_ready_script( -<< + + + +/** + * Persistent class Shortcut and derived + * Shortcuts of any kind + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +abstract class Shortcut extends DBObject implements iDisplay +{ + public static function Init() + { + $aParams = array + ( + "category" => "gui,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_shortcut", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("context", array("allowed_values"=>null, "sql"=>"context", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'context')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('name')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + abstract public function RenderContent(WebPage $oPage, $aExtraParams = array()); + + protected function OnInsert() + { + $this->Set('user_id', UserRights::GetUserId()); + } + + public function StartRenameDialog($oPage) + { + $oPage->add('
    '); + + $oForm = new DesignerForm(); + $sDefault = $this->Get('name'); + $oField = new DesignerTextField('name', Dict::S('Class:Shortcut/Attribute:name'), $sDefault); + $oField->SetMandatory(true); + $oForm->AddField($oField); + $oForm->Render($oPage); + $oPage->add('
    '); + + $sDialogTitle = Dict::S('UI:ShortcutRenameDlg:Title'); + $sOkButtonLabel = Dict::S('UI:Button:Ok'); + $sCancelButtonLabel = Dict::S('UI:Button:Cancel'); + $iShortcut = $this->GetKey(); + + $oPage->add_ready_script( +<< "gui,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_shortcut_oql", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeOQL("oql", array("allowed_values"=>null, "sql"=>"oql", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("auto_reload", array("allowed_values"=>new ValueSetEnum('none,custom'), "sql"=>"auto_reload", "default_value"=>"none", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("auto_reload_sec", array("allowed_values"=>null, "sql"=>"auto_reload_sec", "default_value"=>60, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'context', 'oql')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('name')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + public function RenderContent(WebPage $oPage, $aExtraParams = array()) + { + $oPage->set_title($this->Get('name')); + + switch($this->Get('auto_reload')) + { + case 'custom': + $iRate = (int)$this->Get('auto_reload_sec'); + if ($iRate > 0) + { + // Must a string otherwise it can be evaluated to 'true' and defaults to "standard" refresh rate! + $aExtraParams['auto_reload'] = (string)$iRate; + } + break; + + default: + case 'none': + } + + $bSearchPane = true; + $bSearchOpen = true; + try + { + OQLMenuNode::RenderOQLSearch($this->Get('oql'), $this->Get('name'), 'shortcut_'.$this->GetKey(), $bSearchPane, $bSearchOpen, $oPage, $aExtraParams, true); + } + catch (Exception $e) + { + throw new Exception("The OQL shortcut '".$this->Get('name')."' (id: ".$this->GetKey().") could not be displayed: ".$e->getMessage()); + } + + } + + public function CloneTableSettings($sTableSettings) + { + $aTableSettings = json_decode($sTableSettings, true); + + $oFilter = DBObjectSearch::FromOQL($this->Get('oql')); + $oCustomSettings = new DataTableSettings($oFilter->GetSelectedClasses()); + $oCustomSettings->iDefaultPageSize = $aTableSettings['iPageSize']; + $oCustomSettings->aColumns = $aTableSettings['oColumns']; + $oCustomSettings->Save('shortcut_'.$this->GetKey()); + } + + public static function GetCreationForm($sOQL = null, $sTableSettings = null) + { + $oForm = new DesignerForm(); + + // Find a unique default name + // -> The class of the query + an index if necessary + if ($sOQL == null) + { + $sDefault = ''; + } + else + { + $oBMSearch = new DBObjectSearch('Shortcut'); + $oBMSearch->AddCondition('user_id', UserRights::GetUserId(), '='); + $oBMSet = new DBObjectSet($oBMSearch); + $aNames = $oBMSet->GetColumnAsArray('name'); + $oSearch = DBObjectSearch::FromOQL($sOQL); + $sDefault = utils::MakeUniqueName($oSearch->GetClass(), $aNames); + } + + $oField = new DesignerTextField('name', Dict::S('Class:Shortcut/Attribute:name'), $sDefault); + $oField->SetMandatory(true); + $oForm->AddField($oField); + + /* + $oField = new DesignerComboField('auto_reload', Dict::S('Class:ShortcutOQL/Attribute:auto_reload'), 'none'); + $oAttDef = MetaModel::GetAttributeDef(__class__, 'auto_reload'); + $oField->SetAllowedValues($oAttDef->GetAllowedValues()); + $oField->SetMandatory(true); + $oForm->AddField($oField); + */ + $oField = new DesignerBooleanField('auto_reload', Dict::S('Class:ShortcutOQL/Attribute:auto_reload'), false); + $oForm->AddField($oField); + + $oField = new DesignerIntegerField('auto_reload_sec', Dict::S('Class:ShortcutOQL/Attribute:auto_reload_sec'), MetaModel::GetConfig()->GetStandardReloadInterval()); + $oField->SetBoundaries(MetaModel::GetConfig()->Get('min_reload_interval'), null); // no upper limit + $oField->SetMandatory(false); + $oForm->AddField($oField); + + $oField = new DesignerHiddenField('oql', '', $sOQL); + $oForm->AddField($oField); + + $oField = new DesignerHiddenField('table_settings', '', $sTableSettings); + $oForm->AddField($oField); + + return $oForm; + } + + public static function GetCreationDlgFromOQL($oPage, $sOQL, $sTableSettings) + { + $oPage->add('
    '); + + $oForm = self::GetCreationForm($sOQL, $sTableSettings); + + $oForm->Render($oPage); + $oPage->add('
    '); + + $sDialogTitle = Dict::S('UI:ShortcutListDlg:Title'); + $sOkButtonLabel = Dict::S('UI:Button:Ok'); + $sCancelButtonLabel = Dict::S('UI:Button:Cancel'); + + $oAppContext = new ApplicationContext(); + $sContext = $oAppContext->GetForLink(); + + $sRateTitle = addslashes(Dict::Format('Class:ShortcutOQL/Attribute:auto_reload_sec/tip', MetaModel::GetConfig()->Get('min_reload_interval'))); + + $oPage->add_ready_script( +<< diff --git a/application/startup.inc.php b/application/startup.inc.php index 55654924d..e78da1f95 100644 --- a/application/startup.inc.php +++ b/application/startup.inc.php @@ -1,69 +1,69 @@ - - - -/** - * File to include to initialize the datamodel in memory - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -// This storage is freed on error (case of allowed memory exhausted) -$sReservedMemory = str_repeat('*', 1024 * 1024); -register_shutdown_function(function() -{ - global $sReservedMemory; - $sReservedMemory = null; - if (!is_null($err = error_get_last()) && ($err['type'] == E_ERROR)) - { - if (strpos($err['message'], 'Allowed memory size of') !== false) - { - $sLimit = ini_get('memory_limit'); - echo "

    iTop: Allowed memory size of $sLimit exhausted, contact your administrator to increase memory_limit in php.ini

    \n"; - } - else - { - echo "

    iTop: An error occurred, check server error log for more information.

    \n"; - } - } -}); - -require_once(APPROOT.'/core/cmdbobject.class.inc.php'); -require_once(APPROOT.'/application/utils.inc.php'); -require_once(APPROOT.'/core/contexttag.class.inc.php'); -session_name('itop-'.md5(APPROOT)); -session_start(); -$sSwitchEnv = utils::ReadParam('switch_env', null); -if (($sSwitchEnv != null) && (file_exists(APPCONF.$sSwitchEnv.'/'.ITOP_CONFIG_FILE))) -{ - $_SESSION['itop_env'] = $sSwitchEnv; - $sEnv = $sSwitchEnv; - // TODO: reset the credentials as well ?? -} -else if (isset($_SESSION['itop_env'])) -{ - $sEnv = $_SESSION['itop_env']; -} -else -{ - $sEnv = ITOP_DEFAULT_ENV; - $_SESSION['itop_env'] = ITOP_DEFAULT_ENV; -} -$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE; + + + +/** + * File to include to initialize the datamodel in memory + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +// This storage is freed on error (case of allowed memory exhausted) +$sReservedMemory = str_repeat('*', 1024 * 1024); +register_shutdown_function(function() +{ + global $sReservedMemory; + $sReservedMemory = null; + if (!is_null($err = error_get_last()) && ($err['type'] == E_ERROR)) + { + if (strpos($err['message'], 'Allowed memory size of') !== false) + { + $sLimit = ini_get('memory_limit'); + echo "

    iTop: Allowed memory size of $sLimit exhausted, contact your administrator to increase memory_limit in php.ini

    \n"; + } + else + { + echo "

    iTop: An error occurred, check server error log for more information.

    \n"; + } + } +}); + +require_once(APPROOT.'/core/cmdbobject.class.inc.php'); +require_once(APPROOT.'/application/utils.inc.php'); +require_once(APPROOT.'/core/contexttag.class.inc.php'); +session_name('itop-'.md5(APPROOT)); +session_start(); +$sSwitchEnv = utils::ReadParam('switch_env', null); +if (($sSwitchEnv != null) && (file_exists(APPCONF.$sSwitchEnv.'/'.ITOP_CONFIG_FILE))) +{ + $_SESSION['itop_env'] = $sSwitchEnv; + $sEnv = $sSwitchEnv; + // TODO: reset the credentials as well ?? +} +else if (isset($_SESSION['itop_env'])) +{ + $sEnv = $_SESSION['itop_env']; +} +else +{ + $sEnv = ITOP_DEFAULT_ENV; + $_SESSION['itop_env'] = ITOP_DEFAULT_ENV; +} +$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE; MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv); \ No newline at end of file diff --git a/application/template.class.inc.php b/application/template.class.inc.php index 6c15355b4..99544644b 100644 --- a/application/template.class.inc.php +++ b/application/template.class.inc.php @@ -1,427 +1,427 @@ - - - -/** - * Class DisplayTemplate - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/displayblock.class.inc.php'); -/** - * This class manages the special template format used internally to build the iTop web pages - */ -class DisplayTemplate -{ - protected $m_sTemplate; - protected $m_aTags; - static protected $iBlockCount = 0; - - public function __construct($sTemplate) - { - $this->m_aTags = array ( - 'itopblock', - 'itopcheck', - 'itoptabs', - 'itoptab', - 'itoptoggle', - 'itopstring', - 'sqlblock' - ); - $this->m_sTemplate = $sTemplate; - } - - public function Render(WebPage $oPage, $aParams = array()) - { - $this->m_sTemplate = MetaModel::ApplyParams($this->m_sTemplate, $aParams); - $iStart = 0; - $iEnd = strlen($this->m_sTemplate); - $iCount = 0; - $iBeforeTagPos = $iStart; - $iAfterTagPos = $iStart; - while($sTag = $this->GetNextTag($iStart, $iEnd)) - { - $sContent = $this->GetTagContent($sTag, $iStart, $iEnd); - $iAfterTagPos = $iEnd + strlen(''); - $sOuterTag = substr($this->m_sTemplate, $iStart, $iAfterTagPos - $iStart); - $oPage->add(substr($this->m_sTemplate, $iBeforeTagPos, $iStart - $iBeforeTagPos)); - if ($sTag == DisplayBlock::TAG_BLOCK) - { - try - { - $oBlock = DisplayBlock::FromTemplate($sOuterTag); - if (is_object($oBlock)) - { - $oBlock->Display($oPage, 'block_'.self::$iBlockCount, $aParams); - } - } - catch(OQLException $e) - { - $oPage->p('Error in template (please contact your administrator) - Invalid query'); - } - catch(Exception $e) - { - $oPage->p('Error in template (please contact your administrator)'); - } - - self::$iBlockCount++; - } - else - { - $aAttributes = $this->GetTagAttributes($sTag, $iStart, $iEnd); - //$oPage->p("Tag: $sTag - ($iStart, $iEnd)"); - $this->RenderTag($oPage, $sTag, $aAttributes, $sContent); - - } - $iAfterTagPos = $iEnd + strlen(''); - $iBeforeTagPos = $iAfterTagPos; - $iStart = $iEnd; - $iEnd = strlen($this->m_sTemplate); - $iCount++; - } - $oPage->add(substr($this->m_sTemplate, $iAfterTagPos)); - } - - public function GetNextTag(&$iStartPos, &$iEndPos) - { - $iChunkStartPos = $iStartPos; - $sNextTag = null; - $iStartPos = $iEndPos; - foreach($this->m_aTags as $sTag) - { - // Search for the opening tag - $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.' ', $iChunkStartPos); - if ($iOpeningPos === false) - { - $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.'>', $iChunkStartPos); - } - if ($iOpeningPos !== false) - { - $iClosingPos = stripos($this->m_sTemplate, '', $iOpeningPos); - } - if ( ($iOpeningPos !== false) && ($iClosingPos !== false)) - { - if ($iOpeningPos < $iStartPos) - { - // This is the next tag - $iStartPos = $iOpeningPos; - $iEndPos = $iClosingPos; - $sNextTag = $sTag; - } - } - } - return $sNextTag; - } - - public function GetTagContent($sTag, $iStartPos, $iEndPos) - { - $sContent = ""; - $iContentStart = strpos($this->m_sTemplate, '>', $iStartPos); // Content of tag start immediatly after the first closing bracket - if ($iContentStart !== false) - { - $sContent = substr($this->m_sTemplate, 1+$iContentStart, $iEndPos - $iContentStart - 1); - } - return $sContent; - } - - public function GetTagAttributes($sTag, $iStartPos, $iEndPos) - { - $aAttr = array(); - $iAttrStart = strpos($this->m_sTemplate, ' ', $iStartPos); // Attributes start just after the first space - $iAttrEnd = strpos($this->m_sTemplate, '>', $iStartPos); // Attributes end just before the first closing bracket - if ( ($iAttrStart !== false) && ($iAttrEnd !== false) && ($iAttrEnd > $iAttrStart)) - { - $sAttributes = substr($this->m_sTemplate, 1+$iAttrStart, $iAttrEnd - $iAttrStart - 1); - $aAttributes = explode(' ', $sAttributes); - foreach($aAttributes as $sAttr) - { - if ( preg_match('/(.+) *= *"(.+)"$/', $sAttr, $aMatches) ) - { - $aAttr[strtolower($aMatches[1])] = $aMatches[2]; - } - } - } - return $aAttr; - } - - protected function RenderTag($oPage, $sTag, $aAttributes, $sContent) - { - static $iTabContainerCount = 0; - switch($sTag) - { - case 'itoptabs': - $oPage->AddTabContainer('Tabs_'.$iTabContainerCount); - $oPage->SetCurrentTabContainer('Tabs_'.$iTabContainerCount); - $iTabContainerCount++; - //$oPage->p('Content:
    '.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'
    '); - $oTemplate = new DisplayTemplate($sContent); - $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied - $oPage->SetCurrentTabContainer(''); - break; - - case 'itopcheck': - $sClassName = $aAttributes['class']; - if (MetaModel::IsValidClass($sClassName) && UserRights::IsActionAllowed($sClassName, UR_ACTION_READ)) - { - $oTemplate = new DisplayTemplate($sContent); - $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied - } - else - { - // Leave a trace for those who'd like to understand why nothing is displayed - $oPage->add("\n"); - } - break; - - case 'itoptab': - $oPage->SetCurrentTab(Dict::S(str_replace('_', ' ', $aAttributes['name']))); - $oTemplate = new DisplayTemplate($sContent); - $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied - //$oPage->p('iTop Tab Content:
    '.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'
    '); - $oPage->SetCurrentTab(''); - break; - - case 'itoptoggle': - $sName = isset($aAttributes['name']) ? $aAttributes['name'] : 'Tagada'; - $bOpen = isset($aAttributes['open']) ? $aAttributes['open'] : true; - $oPage->StartCollapsibleSection(Dict::S($sName), $bOpen); - $oTemplate = new DisplayTemplate($sContent); - $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied - //$oPage->p('iTop Tab Content:
    '.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'
    '); - $oPage->EndCollapsibleSection(); - break; - - case 'itopstring': - $oPage->add(Dict::S($sContent)); - break; - - case 'sqlblock': - $oBlock = SqlBlock::FromTemplate($sContent); - $oBlock->RenderContent($oPage); - break; - - case 'itopblock': // No longer used, handled by DisplayBlock::FromTemplate see above - $oPage->add(""); - break; - - default: - // Unknown tag, just ignore it or now -- output an HTML comment - $oPage->add(""); - } - } - - /** - * Unit test - */ - static public function UnitTest() - { - require_once(APPROOT.'/application/startup.inc.php'); - require_once(APPROOT."/application/itopwebpage.class.inc.php"); - - $sTemplate = ' - - - - SELECT Interface AS i WHERE i.device_id = $id$ - - - SELECT Contact AS c JOIN lnkContactToCI AS l ON l.contact_id = c.id WHERE l.ci_id = $id$ - - - SELECT Document AS d JOIN lnkDocumentToCI as l ON l.document_id = d.id WHERE l.ci_id = $id$) - - '; - - $oPage = new iTopWebPage('Unit Test'); - //$oPage->add("Template content:
    ".htmlentities($sTemplate, ENT_QUOTES, 'UTF-8')."
    \n"); - $oTemplate = new DisplayTemplate($sTemplate); - $oTemplate->Render($oPage, array('class'=>'Network device','pkey'=> 271, 'name' => 'deliversw01.mecanorama.fr', 'org_id' => 3)); - $oPage->output(); - } -} - -/** - * Special type of template for displaying the details of an object - * On top of the defaut 'blocks' managed by the parent class, the following placeholders - * are available in such a template: - * $attribute_code$ An attribute of the object (in edit mode this is the input for the attribute) - * $attribute_code->label()$ The label of an attribute - * $PlugIn:plugInClass->properties()$ The ouput of OnDisplayProperties of the specified plugInClass - */ -class ObjectDetailsTemplate extends DisplayTemplate -{ - public function __construct($sTemplate, $oObj, $sFormPrefix = '') - { - parent::__construct($sTemplate); - $this->m_oObj = $oObj; - $this->m_sPrefix = $sFormPrefix; - } - - public function Render(WebPage $oPage, $aParams = array(), $bEditMode = false) - { - $sStateAttCode = MetaModel :: GetStateAttributeCode(get_class($this->m_oObj)); - $aTemplateFields = array(); - preg_match_all('/\\$this->([a-z0-9_]+)\\$/', $this->m_sTemplate, $aMatches); - foreach ($aMatches[1] as $sAttCode) - { - if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode)) - { - $aTemplateFields[] = $sAttCode; - } - else - { - $aParams['this->'.$sAttCode] = ""; - } - } - preg_match_all('/\\$this->field\\(([a-z0-9_]+)\\)\\$/', $this->m_sTemplate, $aMatches); - foreach ($aMatches[1] as $sAttCode) - { - if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode)) - { - $aTemplateFields[] = $sAttCode; - } - else - { - $aParams['this->field('.$sAttCode.')'] = ""; - } - } - $aFieldsComments = (isset($aParams['fieldsComments'])) ? $aParams['fieldsComments'] : array(); - $aFieldsMap = array(); - - $sClass = get_class($this->m_oObj); - // Renders the fields used in the template - foreach(MetaModel::ListAttributeDefs(get_class($this->m_oObj)) as $sAttCode => $oAttDef) - { - $aParams['this->label('.$sAttCode.')'] = $oAttDef->GetLabel(); - $aParams['this->comments('.$sAttCode.')'] = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ''; - $iInputId = '2_'.$sAttCode; // TODO: generate a real/unique prefix... - if (in_array($sAttCode, $aTemplateFields)) - { - if ($this->m_oObj->IsNew()) - { - $iFlags = $this->m_oObj->GetInitialStateAttributeFlags($sAttCode); - } - else - { - $iFlags = $this->m_oObj->GetAttributeFlags($sAttCode); - } - if (($iFlags & OPT_ATT_MANDATORY) && $this->m_oObj->IsNew()) - { - $iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object - } - - if ((!$oAttDef->IsWritable()) || ($sStateAttCode == $sAttCode)) - { - $iFlags = $iFlags | OPT_ATT_READONLY; - } - - if ($iFlags & OPT_ATT_HIDDEN) - { - $aParams['this->label('.$sAttCode.')'] = ''; - $aParams['this->field('.$sAttCode.')'] = ''; - $aParams['this->comments('.$sAttCode.')'] = ''; - $aParams['this->'.$sAttCode] = ''; - } - else - { - if ($bEditMode && ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE))) - { - // Check if the attribute is not read-only because of a synchro... - $aReasons = array(); - $sSynchroIcon = ''; - if ($iFlags & OPT_ATT_SLAVE) - { - $iSynchroFlags = $this->m_oObj->GetSynchroReplicaFlags($sAttCode, $aReasons); - $sSynchroIcon = " "; - $sTip = ''; - foreach($aReasons as $aRow) - { - $sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8'); - $sDescription = str_replace(array("\r\n", "\n"), "
    ", $sDescription); - $sTip .= "
    "; - $sTip .= "
    Synchronized with {$aRow['name']}
    "; - $sTip .= "
    $sDescription
    "; - } - $oPage->add_ready_script("$('#synchro_$iInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); - } - - // Attribute is read-only - $sHTMLValue = "".$this->m_oObj->GetAsHTML($sAttCode); - $sHTMLValue .= ''; - $aFieldsMap[$sAttCode] = $iInputId; - $aParams['this->comments('.$sAttCode.')'] = $sSynchroIcon; - } - - if ($bEditMode && !($iFlags & OPT_ATT_READONLY)) //TODO: check the data synchro status... - { - $aParams['this->field('.$sAttCode.')'] = "".$this->m_oObj->GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, - $this->m_oObj->Get($sAttCode), - $this->m_oObj->GetEditValue($sAttCode), - $iInputId, // InputID - '', - $iFlags, - array('this' => $this->m_oObj) // aArgs - ).''; - $aFieldsMap[$sAttCode] = $iInputId; - } - else - { - $aParams['this->field('.$sAttCode.')'] = $this->m_oObj->GetAsHTML($sAttCode); - } - $aParams['this->'.$sAttCode] = "
    ".$aParams['this->label('.$sAttCode.')'].":".$aParams['this->field('.$sAttCode.')']."".$aParams['this->comments('.$sAttCode.')']."
    "; - } - } - } - - // Renders the PlugIns used in the template - preg_match_all('/\\$PlugIn:([A-Za-z0-9_]+)->properties\\(\\)\\$/', $this->m_sTemplate, $aMatches); - $aPlugInProperties = $aMatches[1]; - foreach($aPlugInProperties as $sPlugInClass) - { - $oInstance = MetaModel::GetPlugins('iApplicationUIExtension', $sPlugInClass); - if ($oInstance != null) // Safety check... - { - $offset = $oPage->start_capture(); - $oInstance->OnDisplayProperties($this->m_oObj, $oPage, $bEditMode); - $sContent = $oPage->end_capture($offset); - $aParams["PlugIn:{$sPlugInClass}->properties()"]= $sContent; - } - else - { - $aParams["PlugIn:{$sPlugInClass}->properties()"]= "Missing PlugIn: $sPlugInClass"; - } - } - - $offset = $oPage->start_capture(); - parent::Render($oPage, $aParams); - $sContent = $oPage->end_capture($offset); - // Remove empty table rows in case some attributes are hidden... - $sContent = preg_replace('/]*>\s*(]*>\s*<\\/td>)+\s*<\\/tr>/im', '', $sContent); - $oPage->add($sContent); - return $aFieldsMap; - } -} - -//DisplayTemplate::UnitTest(); -?> + + + +/** + * Class DisplayTemplate + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/displayblock.class.inc.php'); +/** + * This class manages the special template format used internally to build the iTop web pages + */ +class DisplayTemplate +{ + protected $m_sTemplate; + protected $m_aTags; + static protected $iBlockCount = 0; + + public function __construct($sTemplate) + { + $this->m_aTags = array ( + 'itopblock', + 'itopcheck', + 'itoptabs', + 'itoptab', + 'itoptoggle', + 'itopstring', + 'sqlblock' + ); + $this->m_sTemplate = $sTemplate; + } + + public function Render(WebPage $oPage, $aParams = array()) + { + $this->m_sTemplate = MetaModel::ApplyParams($this->m_sTemplate, $aParams); + $iStart = 0; + $iEnd = strlen($this->m_sTemplate); + $iCount = 0; + $iBeforeTagPos = $iStart; + $iAfterTagPos = $iStart; + while($sTag = $this->GetNextTag($iStart, $iEnd)) + { + $sContent = $this->GetTagContent($sTag, $iStart, $iEnd); + $iAfterTagPos = $iEnd + strlen(''); + $sOuterTag = substr($this->m_sTemplate, $iStart, $iAfterTagPos - $iStart); + $oPage->add(substr($this->m_sTemplate, $iBeforeTagPos, $iStart - $iBeforeTagPos)); + if ($sTag == DisplayBlock::TAG_BLOCK) + { + try + { + $oBlock = DisplayBlock::FromTemplate($sOuterTag); + if (is_object($oBlock)) + { + $oBlock->Display($oPage, 'block_'.self::$iBlockCount, $aParams); + } + } + catch(OQLException $e) + { + $oPage->p('Error in template (please contact your administrator) - Invalid query'); + } + catch(Exception $e) + { + $oPage->p('Error in template (please contact your administrator)'); + } + + self::$iBlockCount++; + } + else + { + $aAttributes = $this->GetTagAttributes($sTag, $iStart, $iEnd); + //$oPage->p("Tag: $sTag - ($iStart, $iEnd)"); + $this->RenderTag($oPage, $sTag, $aAttributes, $sContent); + + } + $iAfterTagPos = $iEnd + strlen(''); + $iBeforeTagPos = $iAfterTagPos; + $iStart = $iEnd; + $iEnd = strlen($this->m_sTemplate); + $iCount++; + } + $oPage->add(substr($this->m_sTemplate, $iAfterTagPos)); + } + + public function GetNextTag(&$iStartPos, &$iEndPos) + { + $iChunkStartPos = $iStartPos; + $sNextTag = null; + $iStartPos = $iEndPos; + foreach($this->m_aTags as $sTag) + { + // Search for the opening tag + $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.' ', $iChunkStartPos); + if ($iOpeningPos === false) + { + $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.'>', $iChunkStartPos); + } + if ($iOpeningPos !== false) + { + $iClosingPos = stripos($this->m_sTemplate, '', $iOpeningPos); + } + if ( ($iOpeningPos !== false) && ($iClosingPos !== false)) + { + if ($iOpeningPos < $iStartPos) + { + // This is the next tag + $iStartPos = $iOpeningPos; + $iEndPos = $iClosingPos; + $sNextTag = $sTag; + } + } + } + return $sNextTag; + } + + public function GetTagContent($sTag, $iStartPos, $iEndPos) + { + $sContent = ""; + $iContentStart = strpos($this->m_sTemplate, '>', $iStartPos); // Content of tag start immediatly after the first closing bracket + if ($iContentStart !== false) + { + $sContent = substr($this->m_sTemplate, 1+$iContentStart, $iEndPos - $iContentStart - 1); + } + return $sContent; + } + + public function GetTagAttributes($sTag, $iStartPos, $iEndPos) + { + $aAttr = array(); + $iAttrStart = strpos($this->m_sTemplate, ' ', $iStartPos); // Attributes start just after the first space + $iAttrEnd = strpos($this->m_sTemplate, '>', $iStartPos); // Attributes end just before the first closing bracket + if ( ($iAttrStart !== false) && ($iAttrEnd !== false) && ($iAttrEnd > $iAttrStart)) + { + $sAttributes = substr($this->m_sTemplate, 1+$iAttrStart, $iAttrEnd - $iAttrStart - 1); + $aAttributes = explode(' ', $sAttributes); + foreach($aAttributes as $sAttr) + { + if ( preg_match('/(.+) *= *"(.+)"$/', $sAttr, $aMatches) ) + { + $aAttr[strtolower($aMatches[1])] = $aMatches[2]; + } + } + } + return $aAttr; + } + + protected function RenderTag($oPage, $sTag, $aAttributes, $sContent) + { + static $iTabContainerCount = 0; + switch($sTag) + { + case 'itoptabs': + $oPage->AddTabContainer('Tabs_'.$iTabContainerCount); + $oPage->SetCurrentTabContainer('Tabs_'.$iTabContainerCount); + $iTabContainerCount++; + //$oPage->p('Content:
    '.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'
    '); + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + $oPage->SetCurrentTabContainer(''); + break; + + case 'itopcheck': + $sClassName = $aAttributes['class']; + if (MetaModel::IsValidClass($sClassName) && UserRights::IsActionAllowed($sClassName, UR_ACTION_READ)) + { + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + } + else + { + // Leave a trace for those who'd like to understand why nothing is displayed + $oPage->add("\n"); + } + break; + + case 'itoptab': + $oPage->SetCurrentTab(Dict::S(str_replace('_', ' ', $aAttributes['name']))); + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + //$oPage->p('iTop Tab Content:
    '.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'
    '); + $oPage->SetCurrentTab(''); + break; + + case 'itoptoggle': + $sName = isset($aAttributes['name']) ? $aAttributes['name'] : 'Tagada'; + $bOpen = isset($aAttributes['open']) ? $aAttributes['open'] : true; + $oPage->StartCollapsibleSection(Dict::S($sName), $bOpen); + $oTemplate = new DisplayTemplate($sContent); + $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied + //$oPage->p('iTop Tab Content:
    '.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'
    '); + $oPage->EndCollapsibleSection(); + break; + + case 'itopstring': + $oPage->add(Dict::S($sContent)); + break; + + case 'sqlblock': + $oBlock = SqlBlock::FromTemplate($sContent); + $oBlock->RenderContent($oPage); + break; + + case 'itopblock': // No longer used, handled by DisplayBlock::FromTemplate see above + $oPage->add(""); + break; + + default: + // Unknown tag, just ignore it or now -- output an HTML comment + $oPage->add(""); + } + } + + /** + * Unit test + */ + static public function UnitTest() + { + require_once(APPROOT.'/application/startup.inc.php'); + require_once(APPROOT."/application/itopwebpage.class.inc.php"); + + $sTemplate = ' + + + + SELECT Interface AS i WHERE i.device_id = $id$ + + + SELECT Contact AS c JOIN lnkContactToCI AS l ON l.contact_id = c.id WHERE l.ci_id = $id$ + + + SELECT Document AS d JOIN lnkDocumentToCI as l ON l.document_id = d.id WHERE l.ci_id = $id$) + + '; + + $oPage = new iTopWebPage('Unit Test'); + //$oPage->add("Template content:
    ".htmlentities($sTemplate, ENT_QUOTES, 'UTF-8')."
    \n"); + $oTemplate = new DisplayTemplate($sTemplate); + $oTemplate->Render($oPage, array('class'=>'Network device','pkey'=> 271, 'name' => 'deliversw01.mecanorama.fr', 'org_id' => 3)); + $oPage->output(); + } +} + +/** + * Special type of template for displaying the details of an object + * On top of the defaut 'blocks' managed by the parent class, the following placeholders + * are available in such a template: + * $attribute_code$ An attribute of the object (in edit mode this is the input for the attribute) + * $attribute_code->label()$ The label of an attribute + * $PlugIn:plugInClass->properties()$ The ouput of OnDisplayProperties of the specified plugInClass + */ +class ObjectDetailsTemplate extends DisplayTemplate +{ + public function __construct($sTemplate, $oObj, $sFormPrefix = '') + { + parent::__construct($sTemplate); + $this->m_oObj = $oObj; + $this->m_sPrefix = $sFormPrefix; + } + + public function Render(WebPage $oPage, $aParams = array(), $bEditMode = false) + { + $sStateAttCode = MetaModel :: GetStateAttributeCode(get_class($this->m_oObj)); + $aTemplateFields = array(); + preg_match_all('/\\$this->([a-z0-9_]+)\\$/', $this->m_sTemplate, $aMatches); + foreach ($aMatches[1] as $sAttCode) + { + if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode)) + { + $aTemplateFields[] = $sAttCode; + } + else + { + $aParams['this->'.$sAttCode] = ""; + } + } + preg_match_all('/\\$this->field\\(([a-z0-9_]+)\\)\\$/', $this->m_sTemplate, $aMatches); + foreach ($aMatches[1] as $sAttCode) + { + if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode)) + { + $aTemplateFields[] = $sAttCode; + } + else + { + $aParams['this->field('.$sAttCode.')'] = ""; + } + } + $aFieldsComments = (isset($aParams['fieldsComments'])) ? $aParams['fieldsComments'] : array(); + $aFieldsMap = array(); + + $sClass = get_class($this->m_oObj); + // Renders the fields used in the template + foreach(MetaModel::ListAttributeDefs(get_class($this->m_oObj)) as $sAttCode => $oAttDef) + { + $aParams['this->label('.$sAttCode.')'] = $oAttDef->GetLabel(); + $aParams['this->comments('.$sAttCode.')'] = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ''; + $iInputId = '2_'.$sAttCode; // TODO: generate a real/unique prefix... + if (in_array($sAttCode, $aTemplateFields)) + { + if ($this->m_oObj->IsNew()) + { + $iFlags = $this->m_oObj->GetInitialStateAttributeFlags($sAttCode); + } + else + { + $iFlags = $this->m_oObj->GetAttributeFlags($sAttCode); + } + if (($iFlags & OPT_ATT_MANDATORY) && $this->m_oObj->IsNew()) + { + $iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object + } + + if ((!$oAttDef->IsWritable()) || ($sStateAttCode == $sAttCode)) + { + $iFlags = $iFlags | OPT_ATT_READONLY; + } + + if ($iFlags & OPT_ATT_HIDDEN) + { + $aParams['this->label('.$sAttCode.')'] = ''; + $aParams['this->field('.$sAttCode.')'] = ''; + $aParams['this->comments('.$sAttCode.')'] = ''; + $aParams['this->'.$sAttCode] = ''; + } + else + { + if ($bEditMode && ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE))) + { + // Check if the attribute is not read-only because of a synchro... + $aReasons = array(); + $sSynchroIcon = ''; + if ($iFlags & OPT_ATT_SLAVE) + { + $iSynchroFlags = $this->m_oObj->GetSynchroReplicaFlags($sAttCode, $aReasons); + $sSynchroIcon = " "; + $sTip = ''; + foreach($aReasons as $aRow) + { + $sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8'); + $sDescription = str_replace(array("\r\n", "\n"), "
    ", $sDescription); + $sTip .= "
    "; + $sTip .= "
    Synchronized with {$aRow['name']}
    "; + $sTip .= "
    $sDescription
    "; + } + $oPage->add_ready_script("$('#synchro_$iInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); + } + + // Attribute is read-only + $sHTMLValue = "".$this->m_oObj->GetAsHTML($sAttCode); + $sHTMLValue .= ''; + $aFieldsMap[$sAttCode] = $iInputId; + $aParams['this->comments('.$sAttCode.')'] = $sSynchroIcon; + } + + if ($bEditMode && !($iFlags & OPT_ATT_READONLY)) //TODO: check the data synchro status... + { + $aParams['this->field('.$sAttCode.')'] = "".$this->m_oObj->GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, + $this->m_oObj->Get($sAttCode), + $this->m_oObj->GetEditValue($sAttCode), + $iInputId, // InputID + '', + $iFlags, + array('this' => $this->m_oObj) // aArgs + ).''; + $aFieldsMap[$sAttCode] = $iInputId; + } + else + { + $aParams['this->field('.$sAttCode.')'] = $this->m_oObj->GetAsHTML($sAttCode); + } + $aParams['this->'.$sAttCode] = "
    ".$aParams['this->label('.$sAttCode.')'].":".$aParams['this->field('.$sAttCode.')']."".$aParams['this->comments('.$sAttCode.')']."
    "; + } + } + } + + // Renders the PlugIns used in the template + preg_match_all('/\\$PlugIn:([A-Za-z0-9_]+)->properties\\(\\)\\$/', $this->m_sTemplate, $aMatches); + $aPlugInProperties = $aMatches[1]; + foreach($aPlugInProperties as $sPlugInClass) + { + $oInstance = MetaModel::GetPlugins('iApplicationUIExtension', $sPlugInClass); + if ($oInstance != null) // Safety check... + { + $offset = $oPage->start_capture(); + $oInstance->OnDisplayProperties($this->m_oObj, $oPage, $bEditMode); + $sContent = $oPage->end_capture($offset); + $aParams["PlugIn:{$sPlugInClass}->properties()"]= $sContent; + } + else + { + $aParams["PlugIn:{$sPlugInClass}->properties()"]= "Missing PlugIn: $sPlugInClass"; + } + } + + $offset = $oPage->start_capture(); + parent::Render($oPage, $aParams); + $sContent = $oPage->end_capture($offset); + // Remove empty table rows in case some attributes are hidden... + $sContent = preg_replace('/]*>\s*(]*>\s*<\\/td>)+\s*<\\/tr>/im', '', $sContent); + $oPage->add($sContent); + return $aFieldsMap; + } +} + +//DisplayTemplate::UnitTest(); +?> diff --git a/application/templates/audit_category.html b/application/templates/audit_category.html index 415555402..379c506c2 100644 --- a/application/templates/audit_category.html +++ b/application/templates/audit_category.html @@ -1,12 +1,12 @@ - - -SELECT $class$ WHERE id = $id$ - - - SELECT AuditRule WHERE category_id = $id$ - - + + +SELECT $class$ WHERE id = $id$ + + + SELECT AuditRule WHERE category_id = $id$ + + diff --git a/application/ui.linkswidget.class.inc.php b/application/ui.linkswidget.class.inc.php index ff7ca2e36..6ad11c308 100644 --- a/application/ui.linkswidget.class.inc.php +++ b/application/ui.linkswidget.class.inc.php @@ -1,608 +1,608 @@ - - - -/** - * Class UILinksWidget - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'application/webpage.class.inc.php'); -require_once(APPROOT.'application/displayblock.class.inc.php'); - -class UILinksWidget -{ - protected $m_sClass; - protected $m_sAttCode; - protected $m_sNameSuffix; - protected $m_iInputId; - protected $m_aAttributes; - protected $m_sExtKeyToRemote; - protected $m_sExtKeyToMe; - protected $m_sLinkedClass; - protected $m_sRemoteClass; - protected $m_bDuplicatesAllowed; - protected $m_aEditableFields; - protected $m_aTableConfig; - - /** - * UILinksWidget constructor. - * - * @param string $sClass - * @param string $sAttCode - * @param int $iInputId - * @param string $sNameSuffix - * @param bool $bDuplicatesAllowed - * - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \Exception - */ - public function __construct($sClass, $sAttCode, $iInputId, $sNameSuffix = '', $bDuplicatesAllowed = false) - { - $this->m_sClass = $sClass; - $this->m_sAttCode = $sAttCode; - $this->m_sNameSuffix = $sNameSuffix; - $this->m_iInputId = $iInputId; - $this->m_bDuplicatesAllowed = $bDuplicatesAllowed; - $this->m_aEditableFields = array(); - - /** @var AttributeLinkedSetIndirect $oAttDef */ - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sAttCode); - $this->m_sLinkedClass = $oAttDef->GetLinkedClass(); - $this->m_sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); - $this->m_sExtKeyToMe = $oAttDef->GetExtKeyToMe(); - - /** @var AttributeExternalKey $oLinkingAttDef */ - $oLinkingAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $this->m_sExtKeyToRemote); - $this->m_sRemoteClass = $oLinkingAttDef->GetTargetClass(); - $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); - $sStateAttCode = MetaModel::GetStateAttributeCode($this->m_sClass); - $sDefaultState = MetaModel::GetDefaultState($this->m_sClass); - - $this->m_aEditableFields = array(); - $this->m_aTableConfig = array(); - $this->m_aTableConfig['form::checkbox'] = array( 'label' => "m_sAttCode}{$this->m_sNameSuffix} .selection', this.checked); oWidget".$this->m_iInputId.".OnSelectChange();\">", 'description' => Dict::S('UI:SelectAllToggle+')); - - foreach(MetaModel::FlattenZList(MetaModel::GetZListItems($this->m_sLinkedClass, 'list')) as $sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sAttCode); - if ($sStateAttCode == $sAttCode) - { - // State attribute is always hidden from the UI - } - else if ($oAttDef->IsWritable() && ($sAttCode != $sExtKeyToMe) && ($sAttCode != $this->m_sExtKeyToRemote) && ($sAttCode != 'finalclass')) - { - $iFlags = MetaModel::GetAttributeFlags($this->m_sLinkedClass, $sDefaultState, $sAttCode); - if ( !($iFlags & OPT_ATT_HIDDEN) && !($iFlags & OPT_ATT_READONLY) ) - { - $this->m_aEditableFields[] = $sAttCode; - $this->m_aTableConfig[$sAttCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); - } - } - } - - $this->m_aTableConfig['static::key'] = array( 'label' => MetaModel::GetName($this->m_sRemoteClass), 'description' => MetaModel::GetClassDescription($this->m_sRemoteClass)); - foreach(MetaModel::GetZListItems($this->m_sRemoteClass, 'list') as $sFieldCode) - { - // TO DO: check the state of the attribute: hidden or visible ? - if ($sFieldCode != 'finalclass') - { - $oAttDef = MetaModel::GetAttributeDef($this->m_sRemoteClass, $sFieldCode); - $this->m_aTableConfig['static::'.$sFieldCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); - } - } - } - - /** - * A one-row form for editing a link record - * - * @param WebPage $oP Web page used for the ouput - * @param DBObject $oLinkedObj Remote object - * @param mixed $linkObjOrId Either the object linked or a unique number for new link records to add - * @param array $aArgs Extra context arguments - * @param DBObject $oCurrentObj The object to which all the elements of the linked set refer to - * @param int $iUniqueId A unique identifier of new links - * @param boolean $bReadOnly Display link as editable or read-only. Default is false (editable) - * - * @return array The HTML fragment of the one-row form - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \Exception - */ - protected function GetFormRow(WebPage $oP, DBObject $oLinkedObj, $linkObjOrId, $aArgs, $oCurrentObj, $iUniqueId, $bReadOnly = false) - { - $sPrefix = "$this->m_sAttCode{$this->m_sNameSuffix}"; - $aRow = array(); - $aFieldsMap = array(); - if(is_object($linkObjOrId) && (!$linkObjOrId->IsNew())) - { - $key = $linkObjOrId->GetKey(); - $iRemoteObjKey = $linkObjOrId->Get($this->m_sExtKeyToRemote); - $sPrefix .= "[$key]["; - $sNameSuffix = "]"; // To make a tabular form - $aArgs['prefix'] = $sPrefix; - $aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}{$key}"; - $aArgs['this'] = $linkObjOrId; - - if($bReadOnly) - { - $aRow['form::checkbox'] = ""; - foreach($this->m_aEditableFields as $sFieldCode) - { - $sDisplayValue = $linkObjOrId->GetEditValue($sFieldCode); - $aRow[$sFieldCode] = $sDisplayValue; - } - } - else - { - $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"$key\">"; - foreach($this->m_aEditableFields as $sFieldCode) - { - $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.$linkObjOrId->GetKey().']'; - $sSafeId = utils::GetSafeId($sFieldId); - $sValue = $linkObjOrId->Get($sFieldCode); - $sDisplayValue = $linkObjOrId->GetEditValue($sFieldCode); - $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); - $aRow[$sFieldCode] = '
    '. - cmdbAbstractObject::GetFormElementForField($oP, $this->m_sLinkedClass, $sFieldCode, $oAttDef, $sValue, $sDisplayValue, $sSafeId, $sNameSuffix, 0, $aArgs). - '
    '; - $aFieldsMap[$sFieldCode] = $sSafeId; - } - } - - $sState = $linkObjOrId->GetState(); - } - else - { - // form for creating a new record - if (is_object($linkObjOrId)) - { - // New link existing only in memory - $oNewLinkObj = $linkObjOrId; - $iRemoteObjKey = $oNewLinkObj->Get($this->m_sExtKeyToRemote); - $oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields - } - else - { - $iRemoteObjKey = $linkObjOrId; - $oNewLinkObj = MetaModel::NewObject($this->m_sLinkedClass); - $oRemoteObj = MetaModel::GetObject($this->m_sRemoteClass, $iRemoteObjKey); - $oNewLinkObj->Set($this->m_sExtKeyToRemote, $oRemoteObj); // Setting the extkey with the object alsoo fills the related external fields - $oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields - } - $sPrefix .= "[-$iUniqueId]["; - $sNameSuffix = "]"; // To make a tabular form - $aArgs['prefix'] = $sPrefix; - $aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}_".($iUniqueId < 0 ? -$iUniqueId : $iUniqueId); - $aArgs['this'] = $oNewLinkObj; - $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"-$iUniqueId\">"; - foreach($this->m_aEditableFields as $sFieldCode) - { - $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.-$iUniqueId.']'; - $sSafeId = utils::GetSafeId($sFieldId); - $sValue = $oNewLinkObj->Get($sFieldCode); - $sDisplayValue = $oNewLinkObj->GetEditValue($sFieldCode); - $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); - $aRow[$sFieldCode] = '
    '. - cmdbAbstractObject::GetFormElementForField($oP, $this->m_sLinkedClass, $sFieldCode, $oAttDef, $sValue, $sDisplayValue, $sSafeId /* id */, $sNameSuffix, 0, $aArgs). - '
    '; - $aFieldsMap[$sFieldCode] = $sSafeId; - } - $sState = ''; - - // Rows created with ajax call need OnLinkAdded call. - // Rows added before loading the form cannot call OnLinkAdded. - if ($iUniqueId > 0) - { - $oP->add_script( - <<m_iInputId}.OnLinkAdded($iUniqueId, $iRemoteObjKey); -EOF - ); - } - } - - if(!$bReadOnly) - { - $sExtKeyToMeId = utils::GetSafeId($sPrefix.$this->m_sExtKeyToMe); - $aFieldsMap[$this->m_sExtKeyToMe] = $sExtKeyToMeId; - $aRow['form::checkbox'] .= "GetKey()."\">"; - - $sExtKeyToRemoteId = utils::GetSafeId($sPrefix.$this->m_sExtKeyToRemote); - $aFieldsMap[$this->m_sExtKeyToRemote] = $sExtKeyToRemoteId; - $aRow['form::checkbox'] .= ""; - } - - $iFieldsCount = count($aFieldsMap); - $sJsonFieldsMap = json_encode($aFieldsMap); - - $oP->add_script( -<<m_sLinkedClass}', '', '$sState'); -{$aArgs['wizHelper']}.SetFieldsMap($sJsonFieldsMap); -{$aArgs['wizHelper']}.SetFieldsCount($iFieldsCount); -EOF - ); - $aRow['static::key'] = $oLinkedObj->GetHyperLink(); - foreach(MetaModel::GetZListItems($this->m_sRemoteClass, 'list') as $sFieldCode) - { - $aRow['static::'.$sFieldCode] = $oLinkedObj->GetAsHTML($sFieldCode); - } - return $aRow; - } - - /** - * Display one row of the whole form - * @param WebPage $oP - * @param array $aConfig - * @param array $aRow - * @param int $iRowId - * @return string - */ - protected function DisplayFormRow(WebPage $oP, $aConfig, $aRow, $iRowId) - { - $sHtml = ''; - $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}_row_$iRowId\">\n"; - foreach($aConfig as $sName=>$void) - { - $sHtml .= "".$aRow[$sName]."\n"; - } - $sHtml .= "\n"; - - return $sHtml; - } - - /** - * Display the table with the form for editing all the links at once - * @param WebPage $oP The web page used for the output - * @param array $aConfig The table's header configuration - * @param array $aData The tabular data to be displayed - * @return string Html fragment representing the form table - */ - protected function DisplayFormTable(WebPage $oP, $aConfig, $aData) - { - $sHtml = "m_sAttCode}{$this->m_sNameSuffix}\" value=\"\">"; - $sHtml .= "\n"; - // Header - $sHtml .= "\n"; - $sHtml .= "\n"; - foreach($aConfig as $sName=>$aDef) - { - $sHtml .= "\n"; - } - $sHtml .= "\n"; - $sHtml .= "\n"; - - // Content - $sHtml .= "\n"; - $sEmptyRowStyle = ''; - if (count($aData) != 0) - { - $sEmptyRowStyle = 'style="display:none;"'; - } - - foreach($aData as $iRowId => $aRow) - { - $sHtml .= $this->DisplayFormRow($oP, $aConfig, $aRow, $iRowId); - } - $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}_empty_row\">"; - $sHtml .= "\n"; - - // Footer - $sHtml .= "
    ".$aDef['label']."
    ".Dict::S('UI:Message:EmptyList:UseAdd')."
    \n"; - - return $sHtml; - } - - - /** - * Get the HTML fragment corresponding to the linkset editing widget - * - * @param WebPage $oPage - * @param DBObject|ormLinkSet $oValue - * @param array $aArgs Extra context arguments - * @param string $sFormPrefix prefix of the fields in the current form - * @param DBObject $oCurrentObj the current object to which the linkset is related - * - * @return string The HTML fragment to be inserted into the page - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function Display(WebPage $oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj) - { - $sHtmlValue = ''; - $sHtmlValue .= "
    m_sAttCode}{$this->m_sNameSuffix}\">\n"; - $sHtmlValue .= "m_iInputId}\">\n"; - $oValue->Rewind(); - $aForm = array(); - $iAddedId = 1; // Unique id for new links - while($oCurrentLink = $oValue->Fetch()) - { - // We try to retrieve the remote object as usual - $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote), false /* Must not be found */); - // If successful, it means that we can edit its link - if($oLinkedObj !== null) - { - $bReadOnly = false; - } - // Else we retrieve it without restrictions (silos) and will display its link as readonly - else - { - $bReadOnly = true; - $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote), false /* Must not be found */, true); - } - - if ($oCurrentLink->IsNew()) - { - $key = -($iAddedId++); - } - else - { - $key = $oCurrentLink->GetKey(); - } - $aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $oCurrentLink, $aArgs, $oCurrentObj, $key, $bReadOnly); - } - $sHtmlValue .= $this->DisplayFormTable($oPage, $this->m_aTableConfig, $aForm); - $sDuplicates = ($this->m_bDuplicatesAllowed) ? 'true' : 'false'; - // Don't automatically launch the search if the table is huge - $bDoSearch = !utils::IsHighCardinality($this->m_sRemoteClass); - $sJSDoSearch = $bDoSearch ? 'true' : 'false'; - $sWizHelper = 'oWizardHelper'.$sFormPrefix; - $oPage->add_ready_script(<<m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates, $sWizHelper, '{$this->m_sExtKeyToRemote}', $sJSDoSearch); - oWidget{$this->m_iInputId}.Init(); -EOF -); - $sHtmlValue .= "     m_sAttCode}{$this->m_sNameSuffix}_btnRemove\" type=\"button\" value=\"".Dict::S('UI:RemoveLinkedObjectsOf_Class')."\" onClick=\"oWidget{$this->m_iInputId}.RemoveSelected();\" >"; - $sHtmlValue .= "   m_sAttCode}{$this->m_sNameSuffix}_btnAdd\" type=\"button\" value=\"".Dict::Format('UI:AddLinkedObjectsOf_Class', MetaModel::GetName($this->m_sRemoteClass))."\" onClick=\"oWidget{$this->m_iInputId}.AddObjects();\">m_sAttCode}{$this->m_sNameSuffix}_indicatorAdd\">\n"; - $sHtmlValue .= "

     

    \n"; - $sHtmlValue .= "
    \n"; - $oPage->add_at_the_end("
    m_sAttCode}{$this->m_sNameSuffix}\">
    "); // To prevent adding forms inside the main form - return $sHtmlValue; - } - - /** - * @param string $sClass - * @param string $sAttCode - * - * @return string - * @throws \Exception - */ - protected static function GetTargetClass($sClass, $sAttCode) - { - /** @var AttributeLinkedSet $oAttDef */ - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $sLinkedClass = $oAttDef->GetLinkedClass(); - $sTargetClass = ''; - switch(get_class($oAttDef)) - { - case 'AttributeLinkedSetIndirect': - /** @var AttributeExternalKey $oLinkingAttDef */ - /** @var AttributeLinkedSetIndirect $oAttDef */ - $oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote()); - $sTargetClass = $oLinkingAttDef->GetTargetClass(); - break; - - case 'AttributeLinkedSet': - $sTargetClass = $sLinkedClass; - break; - } - - return $sTargetClass; - } - - /** - * @param WebPage $oPage - * @param DBObject $oCurrentObj - * @param $sJson - * @param array $aAlreadyLinkedIds - * - * @throws DictExceptionMissingString - * @throws Exception - */ - public function GetObjectPickerDialog($oPage, $oCurrentObj, $sJson, $aAlreadyLinkedIds = array(), $aPrefillFormParam = array()) - { - $sHtml = "
    \n"; - - $oAlreadyLinkedFilter = new DBObjectSearch($this->m_sRemoteClass); - if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0) - { - $oAlreadyLinkedFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN'); - $oAlreadyLinkedExpression = $oAlreadyLinkedFilter->GetCriteria(); - $sAlreadyLinkedExpression = $oAlreadyLinkedExpression->Render(); - } - else - { - $sAlreadyLinkedExpression = ''; - } - - $oFilter = new DBObjectSearch($this->m_sRemoteClass); - - if(!empty($oCurrentObj)) - { - $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); - $aPrefillFormParam['filter'] = $oFilter; - $aPrefillFormParam['dest_class'] = $this->m_sRemoteClass; - $oCurrentObj->PrefillForm('search', $aPrefillFormParam); - } - $oBlock = new DisplayBlock($oFilter, 'search', false); - $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", - array( - 'menu' => false, - 'result_list_outer_selector' => "SearchResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", - 'table_id' => 'add_'.$this->m_sAttCode, - 'table_inner_id' => "ResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", - 'selection_mode' => true, - 'json' => $sJson, - 'cssCount' => '#count_'.$this->m_sAttCode.$this->m_sNameSuffix, - 'query_params' => $oFilter->GetInternalParams(), - 'hidden_criteria' => $sAlreadyLinkedExpression, - )); - $sHtml .= "
    m_sAttCode}{$this->m_sNameSuffix}\">\n"; - $sHtml .= "
    m_sAttCode}{$this->m_sNameSuffix}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; - $sHtml .= "

    ".Dict::S('UI:Message:EmptyList:UseSearchForm')."

    \n"; - $sHtml .= "
    \n"; - $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}\" value=\"0\"/>"; - $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">  m_sAttCode}{$this->m_sNameSuffix}\" disabled=\"disabled\" type=\"button\" onclick=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\" value=\"".Dict::S('UI:Button:Add')."\">"; - $sHtml .= "
    \n"; - $sHtml .= "\n"; - $oPage->add($sHtml); - $oPage->add_ready_script("$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, resizeStop: oWidget{$this->m_iInputId}.UpdateSizes });"); - $oPage->add_ready_script("$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog('option', {title:'".addslashes(Dict::Format('UI:AddObjectsOf_Class_LinkedWith_Class', MetaModel::GetName($this->m_sLinkedClass), MetaModel::GetName($this->m_sClass)))."'});"); - $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix} form').bind('submit.uilinksWizard', oWidget{$this->m_iInputId}.SearchObjectsToAdd);"); - $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}').resize(oWidget{$this->m_iInputId}.UpdateSizes);"); - } - - /** - * Search for objects to be linked to the current object (i.e "remote" objects) - * - * @param WebPage $oP The page used for the output (usually an AjaxWebPage) - * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of - * m_sRemoteClass - * @param array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of - * the search - * - * @throws \CoreException - * @throws \Exception - */ - public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array(), $oCurrentObj = null) - { - if ($sRemoteClass != '') - { - // assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass)); - $oFilter = new DBObjectSearch($sRemoteClass); - } - else - { - // No remote class specified use the one defined in the linkedset - $oFilter = new DBObjectSearch($this->m_sRemoteClass); - } - if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0) - { - $oFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN'); - } - $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); - $oBlock = new DisplayBlock($oFilter, 'list', false); - $oBlock->Display($oP, "ResultsToAdd_{$this->m_sAttCode}", array('menu' => false, 'cssCount'=> '#count_'.$this->m_sAttCode.$this->m_sNameSuffix , 'selection_mode' => true, 'table_id' => 'add_'.$this->m_sAttCode)); // Don't display the 'Actions' menu on the results - } - - /** - * @param WebPage $oP - * @param int $iMaxAddedId - * @param $oFullSetFilter - * @param DBObject $oCurrentObj - * - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function DoAddObjects(WebPage $oP, $iMaxAddedId, $oFullSetFilter, $oCurrentObj) - { - $aLinkedObjectIds = utils::ReadMultipleSelection($oFullSetFilter); - - $iAdditionId = $iMaxAddedId + 1; - foreach($aLinkedObjectIds as $iObjectId) - { - $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $iObjectId, false); - if (is_object($oLinkedObj)) - { - $aRow = $this->GetFormRow($oP, $oLinkedObj, $iObjectId, array(), $oCurrentObj, $iAdditionId); // Not yet created link get negative Ids - $oP->add($this->DisplayFormRow($oP, $this->m_aTableConfig, $aRow, -$iAdditionId)); - $iAdditionId++; - } - else - { - $oP->p(Dict::Format('UI:Error:Object_Class_Id_NotFound', $this->m_sLinkedClass, $iObjectId)); - } - } - } - - /** - * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context - * - * @param DBObject $oSourceObj - * @param DBSearch|DBObjectSearch $oSearch - * - * @throws \CoreException - * @throws \Exception - */ - protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch) - { - $oAppContext = new ApplicationContext(); - $sSrcClass = get_class($oSourceObj); - $sDestClass = $oSearch->GetClass(); - foreach($oAppContext->GetNames() as $key) - { - // Find the value of the object corresponding to each 'context' parameter - $aCallSpec = array($sSrcClass, 'MapContextParam'); - $sAttCode = ''; - if (is_callable($aCallSpec)) - { - $sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter - } - - if (MetaModel::IsValidAttCode($sSrcClass, $sAttCode)) - { - $defaultValue = $oSourceObj->Get($sAttCode); - - // Find the attcode for the same 'context' parameter in the destination class - // and sets its value as the default value for the search condition - $aCallSpec = array($sDestClass, 'MapContextParam'); - $sAttCode = ''; - if (is_callable($aCallSpec)) - { - $sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter - } - - if (MetaModel::IsValidAttCode($sDestClass, $sAttCode) && !empty($defaultValue)) - { - // Add Hierarchical condition if hierarchical key - $oAttDef = MetaModel::GetAttributeDef($sDestClass, $sAttCode); - if (isset($oAttDef) && ($oAttDef->IsExternalKey())) - { - try - { - /** @var AttributeExternalKey $oAttDef */ - $sTargetClass = $oAttDef->GetTargetClass(); - $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass($sTargetClass); - if ($sHierarchicalKeyCode !== false) - { - $oFilter = new DBObjectSearch($sTargetClass); - $oFilter->AddCondition('id', $defaultValue); - $oHKFilter = new DBObjectSearch($sTargetClass); - $oHKFilter->AddCondition_PointingTo($oFilter, $sHierarchicalKeyCode, TREE_OPERATOR_BELOW); - $oSearch->AddCondition_PointingTo($oHKFilter, $sAttCode); - } - } catch (Exception $e) - { - } - } - else - { - $oSearch->AddCondition($sAttCode, $defaultValue); - } - } - } - } - } -} + + + +/** + * Class UILinksWidget + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'application/webpage.class.inc.php'); +require_once(APPROOT.'application/displayblock.class.inc.php'); + +class UILinksWidget +{ + protected $m_sClass; + protected $m_sAttCode; + protected $m_sNameSuffix; + protected $m_iInputId; + protected $m_aAttributes; + protected $m_sExtKeyToRemote; + protected $m_sExtKeyToMe; + protected $m_sLinkedClass; + protected $m_sRemoteClass; + protected $m_bDuplicatesAllowed; + protected $m_aEditableFields; + protected $m_aTableConfig; + + /** + * UILinksWidget constructor. + * + * @param string $sClass + * @param string $sAttCode + * @param int $iInputId + * @param string $sNameSuffix + * @param bool $bDuplicatesAllowed + * + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \Exception + */ + public function __construct($sClass, $sAttCode, $iInputId, $sNameSuffix = '', $bDuplicatesAllowed = false) + { + $this->m_sClass = $sClass; + $this->m_sAttCode = $sAttCode; + $this->m_sNameSuffix = $sNameSuffix; + $this->m_iInputId = $iInputId; + $this->m_bDuplicatesAllowed = $bDuplicatesAllowed; + $this->m_aEditableFields = array(); + + /** @var AttributeLinkedSetIndirect $oAttDef */ + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sAttCode); + $this->m_sLinkedClass = $oAttDef->GetLinkedClass(); + $this->m_sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); + $this->m_sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + + /** @var AttributeExternalKey $oLinkingAttDef */ + $oLinkingAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $this->m_sExtKeyToRemote); + $this->m_sRemoteClass = $oLinkingAttDef->GetTargetClass(); + $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + $sStateAttCode = MetaModel::GetStateAttributeCode($this->m_sClass); + $sDefaultState = MetaModel::GetDefaultState($this->m_sClass); + + $this->m_aEditableFields = array(); + $this->m_aTableConfig = array(); + $this->m_aTableConfig['form::checkbox'] = array( 'label' => "m_sAttCode}{$this->m_sNameSuffix} .selection', this.checked); oWidget".$this->m_iInputId.".OnSelectChange();\">", 'description' => Dict::S('UI:SelectAllToggle+')); + + foreach(MetaModel::FlattenZList(MetaModel::GetZListItems($this->m_sLinkedClass, 'list')) as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sAttCode); + if ($sStateAttCode == $sAttCode) + { + // State attribute is always hidden from the UI + } + else if ($oAttDef->IsWritable() && ($sAttCode != $sExtKeyToMe) && ($sAttCode != $this->m_sExtKeyToRemote) && ($sAttCode != 'finalclass')) + { + $iFlags = MetaModel::GetAttributeFlags($this->m_sLinkedClass, $sDefaultState, $sAttCode); + if ( !($iFlags & OPT_ATT_HIDDEN) && !($iFlags & OPT_ATT_READONLY) ) + { + $this->m_aEditableFields[] = $sAttCode; + $this->m_aTableConfig[$sAttCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); + } + } + } + + $this->m_aTableConfig['static::key'] = array( 'label' => MetaModel::GetName($this->m_sRemoteClass), 'description' => MetaModel::GetClassDescription($this->m_sRemoteClass)); + foreach(MetaModel::GetZListItems($this->m_sRemoteClass, 'list') as $sFieldCode) + { + // TO DO: check the state of the attribute: hidden or visible ? + if ($sFieldCode != 'finalclass') + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sRemoteClass, $sFieldCode); + $this->m_aTableConfig['static::'.$sFieldCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription()); + } + } + } + + /** + * A one-row form for editing a link record + * + * @param WebPage $oP Web page used for the ouput + * @param DBObject $oLinkedObj Remote object + * @param mixed $linkObjOrId Either the object linked or a unique number for new link records to add + * @param array $aArgs Extra context arguments + * @param DBObject $oCurrentObj The object to which all the elements of the linked set refer to + * @param int $iUniqueId A unique identifier of new links + * @param boolean $bReadOnly Display link as editable or read-only. Default is false (editable) + * + * @return array The HTML fragment of the one-row form + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \Exception + */ + protected function GetFormRow(WebPage $oP, DBObject $oLinkedObj, $linkObjOrId, $aArgs, $oCurrentObj, $iUniqueId, $bReadOnly = false) + { + $sPrefix = "$this->m_sAttCode{$this->m_sNameSuffix}"; + $aRow = array(); + $aFieldsMap = array(); + if(is_object($linkObjOrId) && (!$linkObjOrId->IsNew())) + { + $key = $linkObjOrId->GetKey(); + $iRemoteObjKey = $linkObjOrId->Get($this->m_sExtKeyToRemote); + $sPrefix .= "[$key]["; + $sNameSuffix = "]"; // To make a tabular form + $aArgs['prefix'] = $sPrefix; + $aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}{$key}"; + $aArgs['this'] = $linkObjOrId; + + if($bReadOnly) + { + $aRow['form::checkbox'] = ""; + foreach($this->m_aEditableFields as $sFieldCode) + { + $sDisplayValue = $linkObjOrId->GetEditValue($sFieldCode); + $aRow[$sFieldCode] = $sDisplayValue; + } + } + else + { + $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"$key\">"; + foreach($this->m_aEditableFields as $sFieldCode) + { + $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.$linkObjOrId->GetKey().']'; + $sSafeId = utils::GetSafeId($sFieldId); + $sValue = $linkObjOrId->Get($sFieldCode); + $sDisplayValue = $linkObjOrId->GetEditValue($sFieldCode); + $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); + $aRow[$sFieldCode] = '
    '. + cmdbAbstractObject::GetFormElementForField($oP, $this->m_sLinkedClass, $sFieldCode, $oAttDef, $sValue, $sDisplayValue, $sSafeId, $sNameSuffix, 0, $aArgs). + '
    '; + $aFieldsMap[$sFieldCode] = $sSafeId; + } + } + + $sState = $linkObjOrId->GetState(); + } + else + { + // form for creating a new record + if (is_object($linkObjOrId)) + { + // New link existing only in memory + $oNewLinkObj = $linkObjOrId; + $iRemoteObjKey = $oNewLinkObj->Get($this->m_sExtKeyToRemote); + $oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields + } + else + { + $iRemoteObjKey = $linkObjOrId; + $oNewLinkObj = MetaModel::NewObject($this->m_sLinkedClass); + $oRemoteObj = MetaModel::GetObject($this->m_sRemoteClass, $iRemoteObjKey); + $oNewLinkObj->Set($this->m_sExtKeyToRemote, $oRemoteObj); // Setting the extkey with the object alsoo fills the related external fields + $oNewLinkObj->Set($this->m_sExtKeyToMe, $oCurrentObj); // Setting the extkey with the object also fills the related external fields + } + $sPrefix .= "[-$iUniqueId]["; + $sNameSuffix = "]"; // To make a tabular form + $aArgs['prefix'] = $sPrefix; + $aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}_".($iUniqueId < 0 ? -$iUniqueId : $iUniqueId); + $aArgs['this'] = $oNewLinkObj; + $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"-$iUniqueId\">"; + foreach($this->m_aEditableFields as $sFieldCode) + { + $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.-$iUniqueId.']'; + $sSafeId = utils::GetSafeId($sFieldId); + $sValue = $oNewLinkObj->Get($sFieldCode); + $sDisplayValue = $oNewLinkObj->GetEditValue($sFieldCode); + $oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode); + $aRow[$sFieldCode] = '
    '. + cmdbAbstractObject::GetFormElementForField($oP, $this->m_sLinkedClass, $sFieldCode, $oAttDef, $sValue, $sDisplayValue, $sSafeId /* id */, $sNameSuffix, 0, $aArgs). + '
    '; + $aFieldsMap[$sFieldCode] = $sSafeId; + } + $sState = ''; + + // Rows created with ajax call need OnLinkAdded call. + // Rows added before loading the form cannot call OnLinkAdded. + if ($iUniqueId > 0) + { + $oP->add_script( + <<m_iInputId}.OnLinkAdded($iUniqueId, $iRemoteObjKey); +EOF + ); + } + } + + if(!$bReadOnly) + { + $sExtKeyToMeId = utils::GetSafeId($sPrefix.$this->m_sExtKeyToMe); + $aFieldsMap[$this->m_sExtKeyToMe] = $sExtKeyToMeId; + $aRow['form::checkbox'] .= "GetKey()."\">"; + + $sExtKeyToRemoteId = utils::GetSafeId($sPrefix.$this->m_sExtKeyToRemote); + $aFieldsMap[$this->m_sExtKeyToRemote] = $sExtKeyToRemoteId; + $aRow['form::checkbox'] .= ""; + } + + $iFieldsCount = count($aFieldsMap); + $sJsonFieldsMap = json_encode($aFieldsMap); + + $oP->add_script( +<<m_sLinkedClass}', '', '$sState'); +{$aArgs['wizHelper']}.SetFieldsMap($sJsonFieldsMap); +{$aArgs['wizHelper']}.SetFieldsCount($iFieldsCount); +EOF + ); + $aRow['static::key'] = $oLinkedObj->GetHyperLink(); + foreach(MetaModel::GetZListItems($this->m_sRemoteClass, 'list') as $sFieldCode) + { + $aRow['static::'.$sFieldCode] = $oLinkedObj->GetAsHTML($sFieldCode); + } + return $aRow; + } + + /** + * Display one row of the whole form + * @param WebPage $oP + * @param array $aConfig + * @param array $aRow + * @param int $iRowId + * @return string + */ + protected function DisplayFormRow(WebPage $oP, $aConfig, $aRow, $iRowId) + { + $sHtml = ''; + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}_row_$iRowId\">\n"; + foreach($aConfig as $sName=>$void) + { + $sHtml .= "".$aRow[$sName]."\n"; + } + $sHtml .= "\n"; + + return $sHtml; + } + + /** + * Display the table with the form for editing all the links at once + * @param WebPage $oP The web page used for the output + * @param array $aConfig The table's header configuration + * @param array $aData The tabular data to be displayed + * @return string Html fragment representing the form table + */ + protected function DisplayFormTable(WebPage $oP, $aConfig, $aData) + { + $sHtml = "m_sAttCode}{$this->m_sNameSuffix}\" value=\"\">"; + $sHtml .= "\n"; + // Header + $sHtml .= "\n"; + $sHtml .= "\n"; + foreach($aConfig as $sName=>$aDef) + { + $sHtml .= "\n"; + } + $sHtml .= "\n"; + $sHtml .= "\n"; + + // Content + $sHtml .= "\n"; + $sEmptyRowStyle = ''; + if (count($aData) != 0) + { + $sEmptyRowStyle = 'style="display:none;"'; + } + + foreach($aData as $iRowId => $aRow) + { + $sHtml .= $this->DisplayFormRow($oP, $aConfig, $aRow, $iRowId); + } + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}_empty_row\">"; + $sHtml .= "\n"; + + // Footer + $sHtml .= "
    ".$aDef['label']."
    ".Dict::S('UI:Message:EmptyList:UseAdd')."
    \n"; + + return $sHtml; + } + + + /** + * Get the HTML fragment corresponding to the linkset editing widget + * + * @param WebPage $oPage + * @param DBObject|ormLinkSet $oValue + * @param array $aArgs Extra context arguments + * @param string $sFormPrefix prefix of the fields in the current form + * @param DBObject $oCurrentObj the current object to which the linkset is related + * + * @return string The HTML fragment to be inserted into the page + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function Display(WebPage $oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj) + { + $sHtmlValue = ''; + $sHtmlValue .= "
    m_sAttCode}{$this->m_sNameSuffix}\">\n"; + $sHtmlValue .= "m_iInputId}\">\n"; + $oValue->Rewind(); + $aForm = array(); + $iAddedId = 1; // Unique id for new links + while($oCurrentLink = $oValue->Fetch()) + { + // We try to retrieve the remote object as usual + $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote), false /* Must not be found */); + // If successful, it means that we can edit its link + if($oLinkedObj !== null) + { + $bReadOnly = false; + } + // Else we retrieve it without restrictions (silos) and will display its link as readonly + else + { + $bReadOnly = true; + $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $oCurrentLink->Get($this->m_sExtKeyToRemote), false /* Must not be found */, true); + } + + if ($oCurrentLink->IsNew()) + { + $key = -($iAddedId++); + } + else + { + $key = $oCurrentLink->GetKey(); + } + $aForm[$key] = $this->GetFormRow($oPage, $oLinkedObj, $oCurrentLink, $aArgs, $oCurrentObj, $key, $bReadOnly); + } + $sHtmlValue .= $this->DisplayFormTable($oPage, $this->m_aTableConfig, $aForm); + $sDuplicates = ($this->m_bDuplicatesAllowed) ? 'true' : 'false'; + // Don't automatically launch the search if the table is huge + $bDoSearch = !utils::IsHighCardinality($this->m_sRemoteClass); + $sJSDoSearch = $bDoSearch ? 'true' : 'false'; + $sWizHelper = 'oWizardHelper'.$sFormPrefix; + $oPage->add_ready_script(<<m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates, $sWizHelper, '{$this->m_sExtKeyToRemote}', $sJSDoSearch); + oWidget{$this->m_iInputId}.Init(); +EOF +); + $sHtmlValue .= "     m_sAttCode}{$this->m_sNameSuffix}_btnRemove\" type=\"button\" value=\"".Dict::S('UI:RemoveLinkedObjectsOf_Class')."\" onClick=\"oWidget{$this->m_iInputId}.RemoveSelected();\" >"; + $sHtmlValue .= "   m_sAttCode}{$this->m_sNameSuffix}_btnAdd\" type=\"button\" value=\"".Dict::Format('UI:AddLinkedObjectsOf_Class', MetaModel::GetName($this->m_sRemoteClass))."\" onClick=\"oWidget{$this->m_iInputId}.AddObjects();\">m_sAttCode}{$this->m_sNameSuffix}_indicatorAdd\">\n"; + $sHtmlValue .= "

     

    \n"; + $sHtmlValue .= "
    \n"; + $oPage->add_at_the_end("
    m_sAttCode}{$this->m_sNameSuffix}\">
    "); // To prevent adding forms inside the main form + return $sHtmlValue; + } + + /** + * @param string $sClass + * @param string $sAttCode + * + * @return string + * @throws \Exception + */ + protected static function GetTargetClass($sClass, $sAttCode) + { + /** @var AttributeLinkedSet $oAttDef */ + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $sLinkedClass = $oAttDef->GetLinkedClass(); + $sTargetClass = ''; + switch(get_class($oAttDef)) + { + case 'AttributeLinkedSetIndirect': + /** @var AttributeExternalKey $oLinkingAttDef */ + /** @var AttributeLinkedSetIndirect $oAttDef */ + $oLinkingAttDef = MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote()); + $sTargetClass = $oLinkingAttDef->GetTargetClass(); + break; + + case 'AttributeLinkedSet': + $sTargetClass = $sLinkedClass; + break; + } + + return $sTargetClass; + } + + /** + * @param WebPage $oPage + * @param DBObject $oCurrentObj + * @param $sJson + * @param array $aAlreadyLinkedIds + * + * @throws DictExceptionMissingString + * @throws Exception + */ + public function GetObjectPickerDialog($oPage, $oCurrentObj, $sJson, $aAlreadyLinkedIds = array(), $aPrefillFormParam = array()) + { + $sHtml = "
    \n"; + + $oAlreadyLinkedFilter = new DBObjectSearch($this->m_sRemoteClass); + if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0) + { + $oAlreadyLinkedFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN'); + $oAlreadyLinkedExpression = $oAlreadyLinkedFilter->GetCriteria(); + $sAlreadyLinkedExpression = $oAlreadyLinkedExpression->Render(); + } + else + { + $sAlreadyLinkedExpression = ''; + } + + $oFilter = new DBObjectSearch($this->m_sRemoteClass); + + if(!empty($oCurrentObj)) + { + $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); + $aPrefillFormParam['filter'] = $oFilter; + $aPrefillFormParam['dest_class'] = $this->m_sRemoteClass; + $oCurrentObj->PrefillForm('search', $aPrefillFormParam); + } + $oBlock = new DisplayBlock($oFilter, 'search', false); + $sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", + array( + 'menu' => false, + 'result_list_outer_selector' => "SearchResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", + 'table_id' => 'add_'.$this->m_sAttCode, + 'table_inner_id' => "ResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", + 'selection_mode' => true, + 'json' => $sJson, + 'cssCount' => '#count_'.$this->m_sAttCode.$this->m_sNameSuffix, + 'query_params' => $oFilter->GetInternalParams(), + 'hidden_criteria' => $sAlreadyLinkedExpression, + )); + $sHtml .= "
    m_sAttCode}{$this->m_sNameSuffix}\">\n"; + $sHtml .= "
    m_sAttCode}{$this->m_sNameSuffix}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n"; + $sHtml .= "

    ".Dict::S('UI:Message:EmptyList:UseSearchForm')."

    \n"; + $sHtml .= "
    \n"; + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}\" value=\"0\"/>"; + $sHtml .= "m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">  m_sAttCode}{$this->m_sNameSuffix}\" disabled=\"disabled\" type=\"button\" onclick=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\" value=\"".Dict::S('UI:Button:Add')."\">"; + $sHtml .= "
    \n"; + $sHtml .= "\n"; + $oPage->add($sHtml); + $oPage->add_ready_script("$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, resizeStop: oWidget{$this->m_iInputId}.UpdateSizes });"); + $oPage->add_ready_script("$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog('option', {title:'".addslashes(Dict::Format('UI:AddObjectsOf_Class_LinkedWith_Class', MetaModel::GetName($this->m_sLinkedClass), MetaModel::GetName($this->m_sClass)))."'});"); + $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix} form').bind('submit.uilinksWizard', oWidget{$this->m_iInputId}.SearchObjectsToAdd);"); + $oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}').resize(oWidget{$this->m_iInputId}.UpdateSizes);"); + } + + /** + * Search for objects to be linked to the current object (i.e "remote" objects) + * + * @param WebPage $oP The page used for the output (usually an AjaxWebPage) + * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of + * m_sRemoteClass + * @param array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of + * the search + * + * @throws \CoreException + * @throws \Exception + */ + public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array(), $oCurrentObj = null) + { + if ($sRemoteClass != '') + { + // assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass)); + $oFilter = new DBObjectSearch($sRemoteClass); + } + else + { + // No remote class specified use the one defined in the linkedset + $oFilter = new DBObjectSearch($this->m_sRemoteClass); + } + if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0) + { + $oFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN'); + } + $this->SetSearchDefaultFromContext($oCurrentObj, $oFilter); + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, "ResultsToAdd_{$this->m_sAttCode}", array('menu' => false, 'cssCount'=> '#count_'.$this->m_sAttCode.$this->m_sNameSuffix , 'selection_mode' => true, 'table_id' => 'add_'.$this->m_sAttCode)); // Don't display the 'Actions' menu on the results + } + + /** + * @param WebPage $oP + * @param int $iMaxAddedId + * @param $oFullSetFilter + * @param DBObject $oCurrentObj + * + * @throws \ArchivedObjectException + * @throws \CoreException + */ + public function DoAddObjects(WebPage $oP, $iMaxAddedId, $oFullSetFilter, $oCurrentObj) + { + $aLinkedObjectIds = utils::ReadMultipleSelection($oFullSetFilter); + + $iAdditionId = $iMaxAddedId + 1; + foreach($aLinkedObjectIds as $iObjectId) + { + $oLinkedObj = MetaModel::GetObject($this->m_sRemoteClass, $iObjectId, false); + if (is_object($oLinkedObj)) + { + $aRow = $this->GetFormRow($oP, $oLinkedObj, $iObjectId, array(), $oCurrentObj, $iAdditionId); // Not yet created link get negative Ids + $oP->add($this->DisplayFormRow($oP, $this->m_aTableConfig, $aRow, -$iAdditionId)); + $iAdditionId++; + } + else + { + $oP->p(Dict::Format('UI:Error:Object_Class_Id_NotFound', $this->m_sLinkedClass, $iObjectId)); + } + } + } + + /** + * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context + * + * @param DBObject $oSourceObj + * @param DBSearch|DBObjectSearch $oSearch + * + * @throws \CoreException + * @throws \Exception + */ + protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch) + { + $oAppContext = new ApplicationContext(); + $sSrcClass = get_class($oSourceObj); + $sDestClass = $oSearch->GetClass(); + foreach($oAppContext->GetNames() as $key) + { + // Find the value of the object corresponding to each 'context' parameter + $aCallSpec = array($sSrcClass, 'MapContextParam'); + $sAttCode = ''; + if (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter + } + + if (MetaModel::IsValidAttCode($sSrcClass, $sAttCode)) + { + $defaultValue = $oSourceObj->Get($sAttCode); + + // Find the attcode for the same 'context' parameter in the destination class + // and sets its value as the default value for the search condition + $aCallSpec = array($sDestClass, 'MapContextParam'); + $sAttCode = ''; + if (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter + } + + if (MetaModel::IsValidAttCode($sDestClass, $sAttCode) && !empty($defaultValue)) + { + // Add Hierarchical condition if hierarchical key + $oAttDef = MetaModel::GetAttributeDef($sDestClass, $sAttCode); + if (isset($oAttDef) && ($oAttDef->IsExternalKey())) + { + try + { + /** @var AttributeExternalKey $oAttDef */ + $sTargetClass = $oAttDef->GetTargetClass(); + $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass($sTargetClass); + if ($sHierarchicalKeyCode !== false) + { + $oFilter = new DBObjectSearch($sTargetClass); + $oFilter->AddCondition('id', $defaultValue); + $oHKFilter = new DBObjectSearch($sTargetClass); + $oHKFilter->AddCondition_PointingTo($oFilter, $sHierarchicalKeyCode, TREE_OPERATOR_BELOW); + $oSearch->AddCondition_PointingTo($oHKFilter, $sAttCode); + } + } catch (Exception $e) + { + } + } + else + { + $oSearch->AddCondition($sAttCode, $defaultValue); + } + } + } + } + } +} diff --git a/application/uiwizard.class.inc.php b/application/uiwizard.class.inc.php index b57948449..14bc90e06 100644 --- a/application/uiwizard.class.inc.php +++ b/application/uiwizard.class.inc.php @@ -1,331 +1,331 @@ - - - -/** - * Class UIWizard - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class UIWizard -{ - protected $m_oPage; - protected $m_sClass; - protected $m_sTargetState; - protected $m_aWizardSteps; - - public function __construct($oPage, $sClass, $sTargetState = '') - { - $this->m_oPage = $oPage; - $this->m_sClass = $sClass; - if (empty($sTargetState)) - { - $sTargetState = MetaModel::GetDefaultState($sClass); - } - $this->m_sTargetState = $sTargetState; - $this->m_aWizardSteps = $this->ComputeWizardStructure(); - } - - public function GetObjectClass() { return $this->m_sClass; } - public function GetTargetState() { return $this->m_sTargetState; } - public function GetWizardStructure() { return $this->m_aWizardSteps; } - - /** - * Displays one step of the wizard - */ - public function DisplayWizardStep($aStep, $iStepIndex, &$iMaxInputId, &$aFieldsMap, $bFinishEnabled = false, $aArgs = array()) - { - if ($iStepIndex == 1) // one big form that contains everything, to make sure that the uploaded files are posted too - { - $this->m_oPage->add("
    \n"); - } - $this->m_oPage->add("
    \n"); - $this->m_oPage->add("\n"); - $aStates = MetaModel::EnumStates($this->m_sClass); - $aDetails = array(); - $sJSHandlerCode = ''; // Javascript code to be executed each time this step of the wizard is entered - foreach($aStep as $sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef->IsWritable()) - { - $sAttLabel = $oAttDef->GetLabel(); - $iOptions = isset($aStates[$this->m_sTargetState]['attribute_list'][$sAttCode]) ? $aStates[$this->m_sTargetState]['attribute_list'][$sAttCode] : 0; - - $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); - if ($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) - { - $aFields[$sAttCode] = array(); - foreach($aPrerequisites as $sCode) - { - $aFields[$sAttCode][$sCode] = ''; - } - } - if (count($aPrerequisites) > 0) - { - $aOptions[] = 'Prerequisites: '.implode(', ', $aPrerequisites); - } - - $sFieldFlag = (($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE)) || (!$oAttDef->IsNullAllowed()) )? ' *' : ''; - $oDefaultValuesSet = $oAttDef->GetDefaultValue(/* $oObject->ToArgs() */); // @@@ TO DO: get the object's current value if the object exists - $sHTMLValue = cmdbAbstractObject::GetFormElementForField($this->m_oPage, $this->m_sClass, $sAttCode, $oAttDef, $oDefaultValuesSet, '', "att_$iMaxInputId", '', $iOptions, $aArgs); - $aFieldsMap["att_$iMaxInputId"] = $sAttCode; - $aDetails[] = array('label' => ''.$oAttDef->GetLabel().$sFieldFlag.'', 'value' => "$sHTMLValue"); - if ($oAttDef->GetValuesDef() != null) - { - $sJSHandlerCode .= "\toWizardHelper.RequestAllowedValues('$sAttCode');\n"; - } - if ($oAttDef->GetDefaultValue() != null) - { - $sJSHandlerCode .= "\toWizardHelper.RequestDefaultValue('$sAttCode');\n"; - } - if ($oAttDef->IsLinkSet()) - { - $sJSHandlerCode .= "\toLinkWidgetatt_$iMaxInputId.Init();"; - } - $iMaxInputId++; - } - } - //$aDetails[] = array('label' => '', 'value' => ''); - $this->m_oPage->details($aDetails); - $sBackButtonDisabled = ($iStepIndex <= 1) ? 'disabled' : ''; - $sDisabled = $bFinishEnabled ? '' : 'disabled'; - $nbSteps = count($this->m_aWizardSteps['mandatory']) + count($this->m_aWizardSteps['optional']); - $this->m_oPage->add("
    - - - -
    \n"); - $this->m_oPage->add_script(" -function OnEnterStep{$iStepIndex}() -{ - oWizardHelper.ResetQuery(); - oWizardHelper.UpdateWizard(); - -$sJSHandlerCode - - oWizardHelper.AjaxQueryServer(); -} -"); - $this->m_oPage->add("
    \n\n"); - } - - /** - * Display the final step of the wizard: a confirmation screen - */ - public function DisplayFinalStep($iStepIndex, $aFieldsMap) - { - $oAppContext = new ApplicationContext(); - $this->m_oPage->add("\n"); - $this->m_oPage->add("\n"); - } - /** - * Compute the order of the fields & pages in the wizard - * @param $oPage iTopWebPage The current page (used to display error messages) - * @param $sClass string Name of the class - * @param $sStateCode string Code of the target state of the object - * @return hash Two dimensional array: each element represents the list of fields for a given page - */ - protected function ComputeWizardStructure() - { - $aWizardSteps = array( 'mandatory' => array(), 'optional' => array()); - $aFieldsDone = array(); // Store all the fields that are already covered by a previous step of the wizard - - $aStates = MetaModel::EnumStates($this->m_sClass); - $sStateAttCode = MetaModel::GetStateAttributeCode($this->m_sClass); - - $aMandatoryAttributes = array(); - // Some attributes are always mandatory independently of the state machine (if any) - foreach(MetaModel::GetAttributesList($this->m_sClass) as $sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if (!$oAttDef->IsExternalField() && !$oAttDef->IsNullAllowed() && - $oAttDef->IsWritable() && ($sAttCode != $sStateAttCode) ) - { - $aMandatoryAttributes[$sAttCode] = OPT_ATT_MANDATORY; - } - } - - // Now check the attributes that are mandatory in the specified state - if ( (!empty($this->m_sTargetState)) && (count($aStates[$this->m_sTargetState]['attribute_list']) > 0) ) - { - // Check all the fields that *must* be included in the wizard for this - // particular target state - $aFields = array(); - foreach($aStates[$this->m_sTargetState]['attribute_list'] as $sAttCode => $iOptions) - { - if ( (isset($aMandatoryAttributes[$sAttCode])) && - ($aMandatoryAttributes[$sAttCode] & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) ) - { - $aMandatoryAttributes[$sAttCode] |= $iOptions; - } - else - { - $aMandatoryAttributes[$sAttCode] = $iOptions; - } - } - } - - // Check all the fields that *must* be included in the wizard - // i.e. all mandatory, must-change or must-prompt fields that are - // not also read-only or hidden. - // Some fields may be required (null not allowed) from the database - // perspective, but hidden or read-only from the user interface perspective - $aFields = array(); - foreach($aMandatoryAttributes as $sAttCode => $iOptions) - { - if ( ($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) && - !($iOptions & (OPT_ATT_READONLY | OPT_ATT_HIDDEN)) ) - { - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); - $aFields[$sAttCode] = array(); - foreach($aPrerequisites as $sCode) - { - $aFields[$sAttCode][$sCode] = ''; - } - } - } - - // Now use the dependencies between the fields to order them - // Start from the order of the 'details' - $aList = MetaModel::FlattenZlist(MetaModel::GetZListItems($this->m_sClass, 'details')); - $index = 0; - $aOrder = array(); - foreach($aFields as $sAttCode => $void) - { - $aOrder[$sAttCode] = 999; // At the end of the list... - } - foreach($aList as $sAttCode) - { - if (array_key_exists($sAttCode, $aFields)) - { - $aOrder[$sAttCode] = $index; - } - $index++; - } - foreach($aFields as $sAttCode => $aDependencies) - { - // All fields with no remaining dependencies can be entered at this - // step of the wizard - if (count($aDependencies) > 0) - { - $iMaxPos = 0; - // Remove this field from the dependencies of the other fields - foreach($aDependencies as $sDependentAttCode => $void) - { - // position the current field after the ones it depends on - $iMaxPos = max($iMaxPos, 1+$aOrder[$sDependentAttCode]); - } - } - } - asort($aOrder); - $aCurrentStep = array(); - foreach($aOrder as $sAttCode => $rank) - { - $aCurrentStep[] = $sAttCode; - $aFieldsDone[$sAttCode] = ''; - } - $aWizardSteps['mandatory'][] = $aCurrentStep; - - - // Now computes the steps to fill the optional fields - $aFields = array(); // reset - foreach(MetaModel::ListAttributeDefs($this->m_sClass) as $sAttCode=>$oAttDef) - { - $iOptions = (isset($aStates[$this->m_sTargetState]['attribute_list'][$sAttCode])) ? $aStates[$this->m_sTargetState]['attribute_list'][$sAttCode] : 0; - if ( ($sStateAttCode != $sAttCode) && - (!$oAttDef->IsExternalField()) && - (($iOptions & (OPT_ATT_HIDDEN | OPT_ATT_READONLY)) == 0) && - (!isset($aFieldsDone[$sAttCode])) ) - - { - // 'State', external fields, read-only and hidden fields - // and fields that are already listed in the wizard - // are removed from the 'optional' part of the wizard - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); - $aFields[$sAttCode] = array(); - foreach($aPrerequisites as $sCode) - { - if (!isset($aFieldsDone[$sCode])) - { - // retain only the dependencies that were not covered - // in the 'mandatory' part of the wizard - $aFields[$sAttCode][$sCode] = ''; - } - } - } - } - // Now use the dependencies between the fields to order them - while(count($aFields) > 0) - { - $aCurrentStep = array(); - foreach($aFields as $sAttCode => $aDependencies) - { - // All fields with no remaining dependencies can be entered at this - // step of the wizard - if (count($aDependencies) == 0) - { - $aCurrentStep[] = $sAttCode; - $aFieldsDone[$sAttCode] = ''; - unset($aFields[$sAttCode]); - // Remove this field from the dependencies of the other fields - foreach($aFields as $sUpdatedCode => $aDummy) - { - // remove the dependency - unset($aFields[$sUpdatedCode][$sAttCode]); - } - } - } - if (count($aCurrentStep) == 0) - { - // This step of the wizard would contain NO field ! - $this->m_oPage->add(Dict::S('UI:Error:WizardCircularReferenceInDependencies')); - print_r($aFields); - break; - } - $aWizardSteps['optional'][] = $aCurrentStep; - } - return $aWizardSteps; - - } -} -?> + + + +/** + * Class UIWizard + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class UIWizard +{ + protected $m_oPage; + protected $m_sClass; + protected $m_sTargetState; + protected $m_aWizardSteps; + + public function __construct($oPage, $sClass, $sTargetState = '') + { + $this->m_oPage = $oPage; + $this->m_sClass = $sClass; + if (empty($sTargetState)) + { + $sTargetState = MetaModel::GetDefaultState($sClass); + } + $this->m_sTargetState = $sTargetState; + $this->m_aWizardSteps = $this->ComputeWizardStructure(); + } + + public function GetObjectClass() { return $this->m_sClass; } + public function GetTargetState() { return $this->m_sTargetState; } + public function GetWizardStructure() { return $this->m_aWizardSteps; } + + /** + * Displays one step of the wizard + */ + public function DisplayWizardStep($aStep, $iStepIndex, &$iMaxInputId, &$aFieldsMap, $bFinishEnabled = false, $aArgs = array()) + { + if ($iStepIndex == 1) // one big form that contains everything, to make sure that the uploaded files are posted too + { + $this->m_oPage->add("
    \n"); + } + $this->m_oPage->add("
    \n"); + $this->m_oPage->add("\n"); + $aStates = MetaModel::EnumStates($this->m_sClass); + $aDetails = array(); + $sJSHandlerCode = ''; // Javascript code to be executed each time this step of the wizard is entered + foreach($aStep as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oAttDef->IsWritable()) + { + $sAttLabel = $oAttDef->GetLabel(); + $iOptions = isset($aStates[$this->m_sTargetState]['attribute_list'][$sAttCode]) ? $aStates[$this->m_sTargetState]['attribute_list'][$sAttCode] : 0; + + $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); + if ($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) + { + $aFields[$sAttCode] = array(); + foreach($aPrerequisites as $sCode) + { + $aFields[$sAttCode][$sCode] = ''; + } + } + if (count($aPrerequisites) > 0) + { + $aOptions[] = 'Prerequisites: '.implode(', ', $aPrerequisites); + } + + $sFieldFlag = (($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE)) || (!$oAttDef->IsNullAllowed()) )? ' *' : ''; + $oDefaultValuesSet = $oAttDef->GetDefaultValue(/* $oObject->ToArgs() */); // @@@ TO DO: get the object's current value if the object exists + $sHTMLValue = cmdbAbstractObject::GetFormElementForField($this->m_oPage, $this->m_sClass, $sAttCode, $oAttDef, $oDefaultValuesSet, '', "att_$iMaxInputId", '', $iOptions, $aArgs); + $aFieldsMap["att_$iMaxInputId"] = $sAttCode; + $aDetails[] = array('label' => ''.$oAttDef->GetLabel().$sFieldFlag.'', 'value' => "$sHTMLValue"); + if ($oAttDef->GetValuesDef() != null) + { + $sJSHandlerCode .= "\toWizardHelper.RequestAllowedValues('$sAttCode');\n"; + } + if ($oAttDef->GetDefaultValue() != null) + { + $sJSHandlerCode .= "\toWizardHelper.RequestDefaultValue('$sAttCode');\n"; + } + if ($oAttDef->IsLinkSet()) + { + $sJSHandlerCode .= "\toLinkWidgetatt_$iMaxInputId.Init();"; + } + $iMaxInputId++; + } + } + //$aDetails[] = array('label' => '', 'value' => ''); + $this->m_oPage->details($aDetails); + $sBackButtonDisabled = ($iStepIndex <= 1) ? 'disabled' : ''; + $sDisabled = $bFinishEnabled ? '' : 'disabled'; + $nbSteps = count($this->m_aWizardSteps['mandatory']) + count($this->m_aWizardSteps['optional']); + $this->m_oPage->add("
    + + + +
    \n"); + $this->m_oPage->add_script(" +function OnEnterStep{$iStepIndex}() +{ + oWizardHelper.ResetQuery(); + oWizardHelper.UpdateWizard(); + +$sJSHandlerCode + + oWizardHelper.AjaxQueryServer(); +} +"); + $this->m_oPage->add("
    \n\n"); + } + + /** + * Display the final step of the wizard: a confirmation screen + */ + public function DisplayFinalStep($iStepIndex, $aFieldsMap) + { + $oAppContext = new ApplicationContext(); + $this->m_oPage->add("\n"); + $this->m_oPage->add("\n"); + } + /** + * Compute the order of the fields & pages in the wizard + * @param $oPage iTopWebPage The current page (used to display error messages) + * @param $sClass string Name of the class + * @param $sStateCode string Code of the target state of the object + * @return hash Two dimensional array: each element represents the list of fields for a given page + */ + protected function ComputeWizardStructure() + { + $aWizardSteps = array( 'mandatory' => array(), 'optional' => array()); + $aFieldsDone = array(); // Store all the fields that are already covered by a previous step of the wizard + + $aStates = MetaModel::EnumStates($this->m_sClass); + $sStateAttCode = MetaModel::GetStateAttributeCode($this->m_sClass); + + $aMandatoryAttributes = array(); + // Some attributes are always mandatory independently of the state machine (if any) + foreach(MetaModel::GetAttributesList($this->m_sClass) as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if (!$oAttDef->IsExternalField() && !$oAttDef->IsNullAllowed() && + $oAttDef->IsWritable() && ($sAttCode != $sStateAttCode) ) + { + $aMandatoryAttributes[$sAttCode] = OPT_ATT_MANDATORY; + } + } + + // Now check the attributes that are mandatory in the specified state + if ( (!empty($this->m_sTargetState)) && (count($aStates[$this->m_sTargetState]['attribute_list']) > 0) ) + { + // Check all the fields that *must* be included in the wizard for this + // particular target state + $aFields = array(); + foreach($aStates[$this->m_sTargetState]['attribute_list'] as $sAttCode => $iOptions) + { + if ( (isset($aMandatoryAttributes[$sAttCode])) && + ($aMandatoryAttributes[$sAttCode] & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) ) + { + $aMandatoryAttributes[$sAttCode] |= $iOptions; + } + else + { + $aMandatoryAttributes[$sAttCode] = $iOptions; + } + } + } + + // Check all the fields that *must* be included in the wizard + // i.e. all mandatory, must-change or must-prompt fields that are + // not also read-only or hidden. + // Some fields may be required (null not allowed) from the database + // perspective, but hidden or read-only from the user interface perspective + $aFields = array(); + foreach($aMandatoryAttributes as $sAttCode => $iOptions) + { + if ( ($iOptions & (OPT_ATT_MANDATORY | OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) && + !($iOptions & (OPT_ATT_READONLY | OPT_ATT_HIDDEN)) ) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); + $aFields[$sAttCode] = array(); + foreach($aPrerequisites as $sCode) + { + $aFields[$sAttCode][$sCode] = ''; + } + } + } + + // Now use the dependencies between the fields to order them + // Start from the order of the 'details' + $aList = MetaModel::FlattenZlist(MetaModel::GetZListItems($this->m_sClass, 'details')); + $index = 0; + $aOrder = array(); + foreach($aFields as $sAttCode => $void) + { + $aOrder[$sAttCode] = 999; // At the end of the list... + } + foreach($aList as $sAttCode) + { + if (array_key_exists($sAttCode, $aFields)) + { + $aOrder[$sAttCode] = $index; + } + $index++; + } + foreach($aFields as $sAttCode => $aDependencies) + { + // All fields with no remaining dependencies can be entered at this + // step of the wizard + if (count($aDependencies) > 0) + { + $iMaxPos = 0; + // Remove this field from the dependencies of the other fields + foreach($aDependencies as $sDependentAttCode => $void) + { + // position the current field after the ones it depends on + $iMaxPos = max($iMaxPos, 1+$aOrder[$sDependentAttCode]); + } + } + } + asort($aOrder); + $aCurrentStep = array(); + foreach($aOrder as $sAttCode => $rank) + { + $aCurrentStep[] = $sAttCode; + $aFieldsDone[$sAttCode] = ''; + } + $aWizardSteps['mandatory'][] = $aCurrentStep; + + + // Now computes the steps to fill the optional fields + $aFields = array(); // reset + foreach(MetaModel::ListAttributeDefs($this->m_sClass) as $sAttCode=>$oAttDef) + { + $iOptions = (isset($aStates[$this->m_sTargetState]['attribute_list'][$sAttCode])) ? $aStates[$this->m_sTargetState]['attribute_list'][$sAttCode] : 0; + if ( ($sStateAttCode != $sAttCode) && + (!$oAttDef->IsExternalField()) && + (($iOptions & (OPT_ATT_HIDDEN | OPT_ATT_READONLY)) == 0) && + (!isset($aFieldsDone[$sAttCode])) ) + + { + // 'State', external fields, read-only and hidden fields + // and fields that are already listed in the wizard + // are removed from the 'optional' part of the wizard + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $aPrerequisites = $oAttDef->GetPrerequisiteAttributes(); + $aFields[$sAttCode] = array(); + foreach($aPrerequisites as $sCode) + { + if (!isset($aFieldsDone[$sCode])) + { + // retain only the dependencies that were not covered + // in the 'mandatory' part of the wizard + $aFields[$sAttCode][$sCode] = ''; + } + } + } + } + // Now use the dependencies between the fields to order them + while(count($aFields) > 0) + { + $aCurrentStep = array(); + foreach($aFields as $sAttCode => $aDependencies) + { + // All fields with no remaining dependencies can be entered at this + // step of the wizard + if (count($aDependencies) == 0) + { + $aCurrentStep[] = $sAttCode; + $aFieldsDone[$sAttCode] = ''; + unset($aFields[$sAttCode]); + // Remove this field from the dependencies of the other fields + foreach($aFields as $sUpdatedCode => $aDummy) + { + // remove the dependency + unset($aFields[$sUpdatedCode][$sAttCode]); + } + } + } + if (count($aCurrentStep) == 0) + { + // This step of the wizard would contain NO field ! + $this->m_oPage->add(Dict::S('UI:Error:WizardCircularReferenceInDependencies')); + print_r($aFields); + break; + } + $aWizardSteps['optional'][] = $aCurrentStep; + } + return $aWizardSteps; + + } +} +?> diff --git a/application/utils.inc.php b/application/utils.inc.php index 1ff5d3b6f..1d520771c 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1,1955 +1,1955 @@ - - - -/** - * Static class utils - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/core/config.class.inc.php'); -require_once(APPROOT.'/application/transaction.class.inc.php'); -require_once(APPROOT.'application/Html2Text.php'); -require_once(APPROOT.'application/Html2TextException.php'); - -define('ITOP_CONFIG_FILE', 'config-itop.php'); -define('ITOP_DEFAULT_CONFIG_FILE', APPCONF.ITOP_DEFAULT_ENV.'/'.ITOP_CONFIG_FILE); - -define('SERVER_NAME_PLACEHOLDER', '$SERVER_NAME$'); - -class FileUploadException extends Exception -{ -} - - -/** - * Helper functions to interact with forms: read parameters, upload files... - * @package iTop - */ -class utils -{ - private static $oConfig = null; - private static $m_bCASClient = false; - - // Parameters loaded from a file, parameters of the page/command line still have precedence - private static $m_aParamsFromFile = null; - private static $m_aParamSource = array(); - - protected static function LoadParamFile($sParamFile) - { - if (!file_exists($sParamFile)) - { - throw new Exception("Could not find the parameter file: '$sParamFile'"); - } - if (!is_readable($sParamFile)) - { - throw new Exception("Could not load parameter file: '$sParamFile'"); - } - $sParams = file_get_contents($sParamFile); - - if (is_null(self::$m_aParamsFromFile)) - { - self::$m_aParamsFromFile = array(); - } - - $aParamLines = explode("\n", $sParams); - foreach ($aParamLines as $sLine) - { - $sLine = trim($sLine); - - // Ignore the line after a '#' - if (($iCommentPos = strpos($sLine, '#')) !== false) - { - $sLine = substr($sLine, 0, $iCommentPos); - $sLine = trim($sLine); - } - - // Note: the line is supposed to be already trimmed - if (preg_match('/^(\S*)\s*=(.*)$/', $sLine, $aMatches)) - { - $sParam = $aMatches[1]; - $value = trim($aMatches[2]); - self::$m_aParamsFromFile[$sParam] = $value; - self::$m_aParamSource[$sParam] = $sParamFile; - } - } - } - - public static function UseParamFile($sParamFileArgName = 'param_file', $bAllowCLI = true) - { - $sFileSpec = self::ReadParam($sParamFileArgName, '', $bAllowCLI, 'raw_data'); - foreach(explode(',', $sFileSpec) as $sFile) - { - $sFile = trim($sFile); - if (!empty($sFile)) - { - self::LoadParamFile($sFile); - } - } - } - - /** - * Return the source file from which the parameter has been found, - * usefull when it comes to pass user credential to a process executed - * in the background - * @param $sName Parameter name - * @return The file name if any, or null - */ - public static function GetParamSourceFile($sName) - { - if (array_key_exists($sName, self::$m_aParamSource)) - { - return self::$m_aParamSource[$sName]; - } - else - { - return null; - } - } - - public static function IsModeCLI() - { - $sSAPIName = php_sapi_name(); - $sCleanName = strtolower(trim($sSAPIName)); - if ($sCleanName == 'cli') - { - return true; - } - else - { - return false; - } - } - - protected static $bPageMode = null; - /** - * @var boolean[] - */ - protected static $aModes = array(); - - public static function InitArchiveMode() - { - if (isset($_SESSION['archive_mode'])) - { - $iDefault = $_SESSION['archive_mode']; - } - else - { - $iDefault = 0; - } - // Read and record the value for switching the archive mode - $iCurrent = self::ReadParam('with-archive', $iDefault); - if (isset($_SESSION)) - { - $_SESSION['archive_mode'] = $iCurrent; - } - // Read and use the value for the current page (web services) - $iCurrent = self::ReadParam('with_archive', $iCurrent, true); - self::$bPageMode = ($iCurrent == 1); - } - - /** - * @param boolean $bMode if true then activate archive mode (archived objects are visible), otherwise archived objects are - * hidden (archive = "soft deletion") - */ - public static function PushArchiveMode($bMode) - { - array_push(self::$aModes, $bMode); - } - - public static function PopArchiveMode() - { - array_pop(self::$aModes); - } - - /** - * @return boolean true if archive mode is enabled - */ - public static function IsArchiveMode() - { - if (count(self::$aModes) > 0) - { - $bRet = end(self::$aModes); - } - else - { - if (self::$bPageMode === null) - { - self::InitArchiveMode(); - } - $bRet = self::$bPageMode; - } - return $bRet; - } - - /** - * Helper to be called by the GUI and define if the user will see obsolete data (otherwise, the user will have to dig further) - * @return bool - */ - public static function ShowObsoleteData() - { - $bDefault = MetaModel::GetConfig()->Get('obsolescence.show_obsolete_data'); // default is false - $bShow = appUserPreferences::GetPref('show_obsolete_data', $bDefault); - if (static::IsArchiveMode()) - { - $bShow = true; - } - return $bShow; - } - - public static function ReadParam($sName, $defaultValue = "", $bAllowCLI = false, $sSanitizationFilter = 'parameter') - { - global $argv; - $retValue = $defaultValue; - - if (!is_null(self::$m_aParamsFromFile)) - { - if (isset(self::$m_aParamsFromFile[$sName])) - { - $retValue = self::$m_aParamsFromFile[$sName]; - } - } - - if (isset($_REQUEST[$sName])) - { - $retValue = $_REQUEST[$sName]; - } - elseif ($bAllowCLI && isset($argv)) - { - foreach($argv as $iArg => $sArg) - { - if (preg_match('/^--'.$sName.'=(.*)$/', $sArg, $aMatches)) - { - $retValue = $aMatches[1]; - } - } - } - return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); - } - - public static function ReadPostedParam($sName, $defaultValue = '', $sSanitizationFilter = 'parameter') - { - $retValue = isset($_POST[$sName]) ? $_POST[$sName] : $defaultValue; - return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); - } - - public static function Sanitize($value, $defaultValue, $sSanitizationFilter) - { - if ($value === $defaultValue) - { - // Preserve the real default value (can be used to detect missing mandatory parameters) - $retValue = $value; - } - else - { - $retValue = self::Sanitize_Internal($value, $sSanitizationFilter); - if ($retValue === false) - { - $retValue = $defaultValue; - } - } - return $retValue; - } - - protected static function Sanitize_Internal($value, $sSanitizationFilter) - { - switch($sSanitizationFilter) - { - case 'integer': - $retValue = filter_var($value, FILTER_SANITIZE_NUMBER_INT); - break; - - case 'class': - $retValue = $value; - if (!MetaModel::IsValidClass($value)) - { - $retValue = false; - } - break; - - case 'string': - $retValue = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS); - break; - - case 'context_param': - case 'parameter': - case 'field_name': - if (is_array($value)) - { - $retValue = array(); - foreach($value as $key => $val) - { - $retValue[$key] = self::Sanitize_Internal($val, $sSanitizationFilter); // recursively check arrays - if ($retValue[$key] === false) - { - $retValue = false; - break; - } - } - } - else - { - switch($sSanitizationFilter) - { - case 'parameter': - $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^([ A-Za-z0-9_=-]|%3D|%2B|%2F)*$/'))); // the '=', '%3D, '%2B', '%2F' characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC) - break; - - case 'field_name': - $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name - break; - - case 'context_param': - $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[ A-Za-z0-9_=%:+-]*$/'))); - break; - - } - } - break; - - default: - case 'raw_data': - $retValue = $value; - // Do nothing - } - return $retValue; - } - - /** - * Reads an uploaded file and turns it into an ormDocument object - Triggers an exception in case of error - * @param string $sName Name of the input used from uploading the file - * @param string $sIndex If Name is an array of posted files, then the index must be used to point out the file - * @return ormDocument The uploaded file (can be 'empty' if nothing was uploaded) - */ - public static function ReadPostedDocument($sName, $sIndex = null) - { - $oDocument = new ormDocument(); // an empty document - if(isset($_FILES[$sName])) - { - $aFileInfo = $_FILES[$sName]; - - $sError = is_null($sIndex) ? $aFileInfo['error'] : $aFileInfo['error'][$sIndex]; - switch($sError) - { - case UPLOAD_ERR_OK: - $sTmpName = is_null($sIndex) ? $aFileInfo['tmp_name'] : $aFileInfo['tmp_name'][$sIndex]; - $sMimeType = is_null($sIndex) ? $aFileInfo['type'] : $aFileInfo['type'][$sIndex]; - $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; - - $doc_content = file_get_contents($sTmpName); - if (function_exists('finfo_file')) - { - // as of PHP 5.3 the fileinfo extension is bundled within PHP - // in which case we don't trust the mime type provided by the browser - $rInfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension - if ($rInfo !== false) - { - $sType = @finfo_file($rInfo, $sTmpName); - if ( ($sType !== false) - && is_string($sType) - && (strlen($sType)>0)) - { - $sMimeType = $sType; - } - } - @finfo_close($rInfo); - } - $oDocument = new ormDocument($doc_content, $sMimeType, $sName); - break; - - case UPLOAD_ERR_NO_FILE: - // no file to load, it's a normal case, just return an empty document - break; - - case UPLOAD_ERR_FORM_SIZE: - case UPLOAD_ERR_INI_SIZE: - throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize'))); - break; - - case UPLOAD_ERR_PARTIAL: - throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.')); - break; - - case UPLOAD_ERR_NO_TMP_DIR: - throw new FileUploadException(Dict::S('UI:Error:NoTmpDir')); - break; - - case UPLOAD_ERR_CANT_WRITE: - throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir'))); - break; - - case UPLOAD_ERR_EXTENSION: - $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; - throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName)); - break; - - default: - throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError)); - break; - - } - } - return $oDocument; - } - - /** - * Interprets the results posted by a normal or paginated list (in multiple selection mode) - * - * @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected - * - * @return Array An array of object IDs corresponding to the objects selected in the set - */ - public static function ReadMultipleSelection($oFullSetFilter) - { - $aSelectedObj = utils::ReadParam('selectObject', array()); - $sSelectionMode = utils::ReadParam('selectionMode', ''); - if ($sSelectionMode != '') - { - // Paginated selection - $aExceptions = utils::ReadParam('storedSelection', array()); - if ($sSelectionMode == 'positive') - { - // Only the explicitely listed items are selected - $aSelectedObj = $aExceptions; - } - else - { - // All items of the set are selected, except the one explicitely listed - $aSelectedObj = array(); - $oFullSet = new DBObjectSet($oFullSetFilter); - $sClassAlias = $oFullSetFilter->GetClassAlias(); - $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field - while($oObj = $oFullSet->Fetch()) - { - if (!in_array($oObj->GetKey(), $aExceptions)) - { - $aSelectedObj[] = $oObj->GetKey(); - } - } - } - } - return $aSelectedObj; - } - - /** - * Interprets the results posted by a normal or paginated list (in multiple selection mode) - * - * @param DBSearch $oFullSetFilter The criteria defining the whole sets of objects being selected - * - * @return Array An array of object IDs:friendlyname corresponding to the objects selected in the set - * @throws \CoreException - */ - public static function ReadMultipleSelectionWithFriendlyname($oFullSetFilter) - { - $sSelectionMode = utils::ReadParam('selectionMode', ''); - - if ($sSelectionMode === '') - { - throw new CoreException('selectionMode is mandatory'); - } - - // Paginated selection - $aSelectedIds = utils::ReadParam('storedSelection', array()); - if (count($aSelectedIds) > 0 ) - { - if ($sSelectionMode == 'positive') - { - // Only the explicitly listed items are selected - $oFullSetFilter->AddCondition('id', $aSelectedIds, 'IN'); - } - else - { - // All items of the set are selected, except the one explicitly listed - $oFullSetFilter->AddCondition('id', $aSelectedIds, 'NOTIN'); - } - } - - $aSelectedObj = array(); - $oFullSet = new DBObjectSet($oFullSetFilter); - $sClassAlias = $oFullSetFilter->GetClassAlias(); - $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field - while ($oObj = $oFullSet->Fetch()) - { - $aSelectedObj[$oObj->GetKey()] = $oObj->Get('friendlyname'); - } - - return $aSelectedObj; - } - - public static function GetNewTransactionId() - { - return privUITransaction::GetNewTransactionId(); - } - - public static function IsTransactionValid($sId, $bRemoveTransaction = true) - { - return privUITransaction::IsTransactionValid($sId, $bRemoveTransaction); - } - - public static function RemoveTransaction($sId) - { - return privUITransaction::RemoveTransaction($sId); - } - - /** - * Returns a unique tmp id for the current upload based on the transaction system (db). - * - * Build as session_id() . '_' . static::GetNewTransactionId() - * - * @return string - */ - public static function GetUploadTempId($sTransactionId = null) - { - if ($sTransactionId === null) - { - $sTransactionId = static::GetNewTransactionId(); - } - return session_id() . '_' . $sTransactionId; - } - - public static function ReadFromFile($sFileName) - { - if (!file_exists($sFileName)) return false; - return file_get_contents($sFileName); - } - - /** - * Helper function to convert a value expressed in a 'user friendly format' - * as in php.ini, e.g. 256k, 2M, 1G etc. Into a number of bytes - * @param mixed $value The value as read from php.ini - * @return number - */ - public static function ConvertToBytes( $value ) - { - $iReturn = $value; - if ( !is_numeric( $value ) ) - { - $iLength = strlen( $value ); - $iReturn = substr( $value, 0, $iLength - 1 ); - $sUnit = strtoupper( substr( $value, $iLength - 1 ) ); - switch ( $sUnit ) - { - case 'G': - $iReturn *= 1024; - case 'M': - $iReturn *= 1024; - case 'K': - $iReturn *= 1024; - } - } - return $iReturn; - } - - /** - * Format a value into a more friendly format (KB, MB, GB, TB) instead a juste a Bytes amount. - * - * @param type $value - * @return string - */ - public static function BytesToFriendlyFormat($value) - { - $sReturn = ''; - // Kilobytes - if ($value >= 1024) - { - $sReturn = 'K'; - $value = $value / 1024; - } - // Megabytes - if ($value >= 1024) - { - $sReturn = 'M'; - $value = $value / 1024; - } - // Gigabytes - if ($value >= 1024) - { - $sReturn = 'G'; - $value = $value / 1024; - } - // Terabytes - if ($value >= 1024) - { - $sReturn = 'T'; - $value = $value / 1024; - } - - $value = round($value, 1); - - return $value . '' . $sReturn . 'B'; - } - - /** - * Helper function to convert a string to a date, given a format specification. It replaces strtotime which does not allow for specifying a date in a french format (for instance) - * Example: StringToTime('01/05/11 12:03:45', '%d/%m/%y %H:%i:%s') - * @param string $sDate - * @param string $sFormat - * @return timestamp or false if the input format is not correct - */ - public static function StringToTime($sDate, $sFormat) - { - // Source: http://php.net/manual/fr/function.strftime.php - // (alternative: http://www.php.net/manual/fr/datetime.formats.date.php) - static $aDateTokens = null; - static $aDateRegexps = null; - if (is_null($aDateTokens)) - { - $aSpec = array( - '%d' =>'(?[0-9]{2})', - '%m' => '(?[0-9]{2})', - '%y' => '(?[0-9]{2})', - '%Y' => '(?[0-9]{4})', - '%H' => '(?[0-2][0-9])', - '%i' => '(?[0-5][0-9])', - '%s' => '(?[0-5][0-9])', - ); - $aDateTokens = array_keys($aSpec); - $aDateRegexps = array_values($aSpec); - } - - $sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat); - - if (preg_match('!^(?)'.$sDateRegexp.'(?)$!', $sDate, $aMatches)) - { - $sYear = isset($aMatches['year']) ? $aMatches['year'] : 0; - $sMonth = isset($aMatches['month']) ? $aMatches['month'] : 1; - $sDay = isset($aMatches['day']) ? $aMatches['day'] : 1; - $sHour = isset($aMatches['hour']) ? $aMatches['hour'] : 0; - $sMinute = isset($aMatches['minute']) ? $aMatches['minute'] : 0; - $sSecond = isset($aMatches['second']) ? $aMatches['second'] : 0; - return strtotime("$sYear-$sMonth-$sDay $sHour:$sMinute:$sSecond"); - } - else - { - return false; - } - // http://www.spaweditor.com/scripts/regex/index.php - } - - /** - * Convert an old date/time format specifciation (using % placeholders) - * to a format compatible with DateTime::createFromFormat - * @param string $sOldDateTimeFormat - * @return string - */ - static public function DateTimeFormatToPHP($sOldDateTimeFormat) - { - $aSearch = array('%d', '%m', '%y', '%Y', '%H', '%i', '%s'); - $aReplacement = array('d', 'm', 'y', 'Y', 'H', 'i', 's'); - return str_replace($aSearch, $aReplacement, $sOldDateTimeFormat); - } - - /** - * @return \Config from the current environement, or if not existing from the production env, else new Config made from scratch - * @uses \MetaModel::GetConfig() don't forget to add the needed require_once(APPROOT.'core/metamodel.class.php'); - */ - static public function GetConfig() - { - if (self::$oConfig == null) - { - self::$oConfig = MetaModel::GetConfig(); - - if (self::$oConfig == null) - { - $sConfigFile = self::GetConfigFilePath(); - if (!file_exists($sConfigFile)) - { - $sConfigFile = self::GetConfigFilePath('production'); - if (!file_exists($sConfigFile)) - { - $sConfigFile = null; - } - } - - self::$oConfig = new Config($sConfigFile); - } - } - return self::$oConfig; - } - - public static function InitTimeZone() { - $oConfig = self::GetConfig(); - $sItopTimeZone = $oConfig->Get('timezone'); - - if (!empty($sItopTimeZone)) - { - date_default_timezone_set($sItopTimeZone); - } - else - { - // Leave as is... up to the admin to set a value somewhere... - // see http://php.net/manual/en/datetime.configuration.php#ini.date.timezone - } - } - - /** - * Returns the absolute URL to the application root path - * - * @return string The absolute URL to the application root, without the first slash - * - * @throws \Exception - */ - static public function GetAbsoluteUrlAppRoot() - { - static $sUrl = null; - if ($sUrl === null) - { - $sUrl = self::GetConfig()->Get('app_root_url'); - if ($sUrl == '') - { - $sUrl = self::GetDefaultUrlAppRoot(); - } - elseif (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1) - { - if (isset($_SERVER['SERVER_NAME'])) - { - $sServerName = $_SERVER['SERVER_NAME']; - } - else - { - // CLI mode ? - $sServerName = php_uname('n'); - } - $sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl); - } - } - return $sUrl; - } - - /** - * Builds an root url from the server's variables. - * For most usages, when an root url is needed, use utils::GetAbsoluteUrlAppRoot() instead as uses this only as a fallback when the app_root_url conf parameter is not defined. - * - * @return string - * - * @throws \Exception - */ - static public function GetDefaultUrlAppRoot() - { - // Build an absolute URL to this page on this server/port - $sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; - $sProtocol = self::IsConnectionSecure() ? 'https' : 'http'; - $iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; - if ($sProtocol == 'http') - { - $sPort = ($iPort == 80) ? '' : ':'.$iPort; - } - else - { - $sPort = ($iPort == 443) ? '' : ':'.$iPort; - } - // $_SERVER['REQUEST_URI'] is empty when running on IIS - // Let's use Ivan Tcholakov's fix (found on www.dokeos.com) - if (!empty($_SERVER['REQUEST_URI'])) - { - $sPath = $_SERVER['REQUEST_URI']; - } - else - { - $sPath = $_SERVER['SCRIPT_NAME']; - if (!empty($_SERVER['QUERY_STRING'])) - { - $sPath .= '?'.$_SERVER['QUERY_STRING']; - } - $_SERVER['REQUEST_URI'] = $sPath; - } - $sPath = $_SERVER['REQUEST_URI']; - - // remove all the parameters from the query string - $iQuestionMarkPos = strpos($sPath, '?'); - if ($iQuestionMarkPos !== false) - { - $sPath = substr($sPath, 0, $iQuestionMarkPos); - } - $sAbsoluteUrl = "$sProtocol://{$sServerName}{$sPort}{$sPath}"; - - $sCurrentScript = realpath($_SERVER['SCRIPT_FILENAME']); - $sCurrentScript = str_replace('\\', '/', $sCurrentScript); // canonical path - $sAppRoot = str_replace('\\', '/', APPROOT); // canonical path - $sCurrentRelativePath = str_replace($sAppRoot, '', $sCurrentScript); - - $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); - if ($sAppRootPos !== false) - { - $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path - } - else - { - // Second attempt without index.php at the end... - $sCurrentRelativePath = str_replace('index.php', '', $sCurrentRelativePath); - $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); - if ($sAppRootPos !== false) - { - $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path - } - else - { - // No luck... - throw new Exception("Failed to determine application root path $sAbsoluteUrl ($sCurrentRelativePath) APPROOT:'$sAppRoot'"); - } - } - return $sAppRootUrl; - } - - /** - * Helper to handle the variety of HTTP servers - * See #286 (fixed in [896]), and #634 (this fix) - * - * Though the official specs says 'a non empty string', some servers like IIS do set it to 'off' ! - * nginx set it to an empty string - * Others might leave it unset (no array entry) - */ - static public function IsConnectionSecure() - { - $bSecured = false; - - if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off')) - { - $bSecured = true; - } - return $bSecured; - } - - /** - * Tells whether or not log off operation is supported. - * Actually in only one case: - * 1) iTop is using an internal authentication - * 2) the user did not log-in using the "basic" mode (i.e basic authentication) or by passing credentials in the URL - * @return boolean True if logoff is supported, false otherwise - */ - static function CanLogOff() - { - $bResult = false; - if(isset($_SESSION['login_mode'])) - { - $sLoginMode = $_SESSION['login_mode']; - switch($sLoginMode) - { - case 'external': - $bResult = false; - break; - - case 'form': - case 'basic': - case 'url': - case 'cas': - default: - $bResult = true; - - } - } - return $bResult; - } - - /** - * Initializes the CAS client - */ - static function InitCASClient() - { - $sCASIncludePath = self::GetConfig()->Get('cas_include_path'); - include_once($sCASIncludePath.'/CAS.php'); - - $bCASDebug = self::GetConfig()->Get('cas_debug'); - if ($bCASDebug) - { - phpCAS::setDebug(APPROOT.'log/error.log'); - } - - if (!self::$m_bCASClient) - { - // Initialize phpCAS - $sCASVersion = self::GetConfig()->Get('cas_version'); - $sCASHost = self::GetConfig()->Get('cas_host'); - $iCASPort = self::GetConfig()->Get('cas_port'); - $sCASContext = self::GetConfig()->Get('cas_context'); - phpCAS::client($sCASVersion, $sCASHost, $iCASPort, $sCASContext, false /* session already started */); - self::$m_bCASClient = true; - $sCASCACertPath = self::GetConfig()->Get('cas_server_ca_cert_path'); - if (empty($sCASCACertPath)) - { - // If no certificate authority is provided, do not attempt to validate - // the server's certificate - // THIS SETTING IS NOT RECOMMENDED FOR PRODUCTION. - // VALIDATING THE CAS SERVER IS CRUCIAL TO THE SECURITY OF THE CAS PROTOCOL! - phpCAS::setNoCasServerValidation(); - } - else - { - phpCAS::setCasServerCACert($sCASCACertPath); - } - } - } - - static function DebugBacktrace($iLimit = 5) - { - $aFullTrace = debug_backtrace(); - $aLightTrace = array(); - for($i=1; ($i<=$iLimit && $i < count($aFullTrace)); $i++) // Skip the last function call... which is the call to this function ! - { - $aLightTrace[$i] = $aFullTrace[$i]['function'].'(), called from line '.$aFullTrace[$i]['line'].' in '.$aFullTrace[$i]['file']; - } - echo "

    ".print_r($aLightTrace, true)."

    \n"; - } - - /** - * Execute the given iTop PHP script, passing it the current credentials - * Only CLI mode is supported, because of the need to hand the credentials over to the next process - * Throws an exception if the execution fails or could not be attempted (config issue) - * @param string $sScript Name and relative path to the file (relative to the iTop root dir) - * @param hash $aArguments Associative array of 'arg' => 'value' - * @return array(iCode, array(output lines)) - */ - /** - */ - static function ExecITopScript($sScriptName, $aArguments) - { - $aDisabled = explode(', ', ini_get('disable_functions')); - if (in_array('exec', $aDisabled)) - { - throw new Exception("The PHP exec() function has been disabled on this server"); - } - - $sPHPExec = trim(self::GetConfig()->Get('php_path')); - if (strlen($sPHPExec) == 0) - { - throw new Exception("The path to php must not be empty. Please set a value for 'php_path' in your configuration file."); - } - - $sAuthUser = self::ReadParam('auth_user', '', 'raw_data'); - $sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data'); - $sParamFile = self::GetParamSourceFile('auth_user'); - if (is_null($sParamFile)) - { - $aArguments['auth_user'] = $sAuthUser; - $aArguments['auth_pwd'] = $sAuthPwd; - } - else - { - $aArguments['param_file'] = $sParamFile; - } - - $aArgs = array(); - foreach($aArguments as $sName => $value) - { - // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation - // It suggests to rely on pctnl_* function instead of using escapeshellargs - $aArgs[] = "--$sName=".escapeshellarg($value); - } - $sArgs = implode(' ', $aArgs); - - $sScript = realpath(APPROOT.$sScriptName); - if (!file_exists($sScript)) - { - throw new Exception("Could not find the script file '$sScriptName' from the directory '".APPROOT."'"); - } - - $sCommand = '"'.$sPHPExec.'" '.escapeshellarg($sScript).' -- '.$sArgs; - - if (version_compare(phpversion(), '5.3.0', '<')) - { - if (substr(PHP_OS,0,3) == 'WIN') - { - // Under Windows, and for PHP 5.2.x, the whole command has to be quoted - // Cf PHP doc: http://php.net/manual/fr/function.exec.php, comment from the 27-Dec-2010 - $sCommand = '"'.$sCommand.'"'; - } - } - - $sLastLine = exec($sCommand, $aOutput, $iRes); - if ($iRes == 1) - { - throw new Exception(Dict::S('Core:ExecProcess:Code1')." - ".$sCommand); - } - elseif ($iRes == 255) - { - $sErrors = implode("\n", $aOutput); - throw new Exception(Dict::S('Core:ExecProcess:Code255')." - ".$sCommand.":\n".$sErrors); - } - - //$aOutput[] = $sCommand; - return array($iRes, $aOutput); - } - - /** - * Get the current environment - */ - public static function GetCurrentEnvironment() - { - if (isset($_SESSION['itop_env'])) - { - return $_SESSION['itop_env']; - } - else - { - return ITOP_DEFAULT_ENV; - } - } - - /** - * Returns a path to a folder into which any module can store cache data - * The corresponding folder is created or cleaned upon code compilation - * @return string - */ - public static function GetCachePath() - { - return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/'; - } - /** - * Merge standard menu items with plugin provided menus items - */ - public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions, $sTableId = null, $sDataTableId = null) - { - // 1st - add standard built-in menu items - // - switch($iMenuId) - { - case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT: - // $param is a DBObjectSet - $oAppContext = new ApplicationContext(); - $sContext = $oAppContext->GetForLink(); - $sDataTableId = is_null($sDataTableId) ? '' : $sDataTableId; - $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($param->GetFilter()->GetClass()); - $sOQL = addslashes($param->GetFilter()->ToOQL(true)); - $sFilter = urlencode($param->GetFilter()->serialize()); - $sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}"; - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - - $aResult = array( - new SeparatorPopupMenuItem(), - // Static menus: Email this page, CSV Export & Add to Dashboard - new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook - ); - - if (UserRights::IsActionAllowed($param->GetFilter()->GetClass(), UR_ACTION_BULK_READ, $param) != UR_ALLOWED_NO) - { - // Bulk export actions - $aResult[] = new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '$sDataTableId', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"); - $aResult[] = new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '$sDataTableId', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"); - if (extension_loaded('gd')) - { - // PDF export requires GD - $aResult[] = new JSPopupMenuItem('UI:Menu:ExportPDF', Dict::S('UI:Menu:ExportPDF'), "ExportListDlg('$sOQL', '$sDataTableId', 'pdf', ".json_encode(Dict::S('UI:Menu:ExportPDF')).")"); - } - } - $aResult[] = new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL', '$sContext')"); - $aResult[] = new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"); - - break; - - case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS: - // $param is a DBObject - $oObj = $param; - $sOQL = "SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey(); - $oFilter = DBObjectSearch::FromOQL($sOQL); - $sFilter = $oFilter->serialize(); - $sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey()); - $sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj)); - $oAppContext = new ApplicationContext(); - $sContext = $oAppContext->GetForLink(); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - - $aResult = array( - new SeparatorPopupMenuItem(), - // Static menus: Email this page & CSV Export - new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook - new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"), - new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"), - new SeparatorPopupMenuItem(), - new URLPopupMenuItem('UI:Menu:PrintableVersion', Dict::S('UI:Menu:PrintableVersion'), $sUrl.'&printable=1', '_blank'), - ); - break; - - case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS: - // $param is a Dashboard - $oAppContext = new ApplicationContext(); - $aParams = $oAppContext->GetAsHash(); - $sMenuId = ApplicationMenu::GetActiveNodeId(); - $sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle')); - $sDlgText = addslashes(Dict::S('UI:ImportDashboardText')); - $sCloseBtn = addslashes(Dict::S('UI:Button:Cancel')); - $aResult = array( - new SeparatorPopupMenuItem(), - new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId), - new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"), - ); - break; - - default: - // Unknown type of menu, do nothing - $aResult = array(); - } - foreach($aResult as $oMenuItem) - { - $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); - } - - // Invoke the plugins - // - foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) - { - if (is_object($param) && !($param instanceof DBObject)) - { - $tmpParam = clone $param; // In case the parameter is an DBObjectSet, clone it to prevent alterations - } - else - { - $tmpParam = $param; - } - foreach($oExtensionInstance->EnumItems($iMenuId, $tmpParam) as $oMenuItem) - { - if (is_object($oMenuItem)) - { - $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); - - foreach($oMenuItem->GetLinkedScripts() as $sLinkedScript) - { - $oPage->add_linked_script($sLinkedScript); - } - } - } - } - } - /** - * Get target configuration file name (including full path) - */ - public static function GetConfigFilePath($sEnvironment = null) - { - if (is_null($sEnvironment)) - { - $sEnvironment = self::GetCurrentEnvironment(); - } - return APPCONF.$sEnvironment.'/'.ITOP_CONFIG_FILE; - } - - /** - * @return string the absolute URL to the modules root path - */ - static public function GetAbsoluteUrlModulesRoot() - { - $sUrl = self::GetAbsoluteUrlAppRoot().'env-'.self::GetCurrentEnvironment().'/'; - return $sUrl; - } - - /** - * To be compatible with this mechanism, the called page must include approot with an absolute path OR not include - * it at all (losing the direct access to the page) : - * - * ```php - * if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); - * require_once(__DIR__.'/../../approot.inc.php'); - * ``` - * - * @param string $sModule - * @param string $sPage - * @param string[] $aArguments - * @param string $sEnvironment - * - * @return string the URL to a page that will execute the requested module page, with query string values url encoded - * - * @see GetExecPageArguments can be used to submit using the GET method (see bug in N.1108) - * @see GetAbsoluteUrlExecPage - */ - static public function GetAbsoluteUrlModulePage($sModule, $sPage, $aArguments = array(), $sEnvironment = null) - { - $aArgs = self::GetExecPageArguments($sModule, $sPage, $aArguments, $sEnvironment); - $sArgs = http_build_query($aArgs); - - return self::GetAbsoluteUrlExecPage()."?".$sArgs; - } - - /** - * @param string $sModule - * @param string $sPage - * @param string[] $aArguments - * @param string $sEnvironment - * - * @return string[] key/value pair for the exec page query string. Warning : values are not url encoded ! - * @throws \Exception if one of the argument has a reserved name - */ - static public function GetExecPageArguments($sModule, $sPage, $aArguments = array(), $sEnvironment = null) - { - $sEnvironment = is_null($sEnvironment) ? self::GetCurrentEnvironment() : $sEnvironment; - $aArgs = array(); - $aArgs['exec_module'] = $sModule; - $aArgs['exec_page'] = $sPage; - $aArgs['exec_env'] = $sEnvironment; - foreach($aArguments as $sName => $sValue) - { - if (($sName == 'exec_module') || ($sName == 'exec_page') || ($sName == 'exec_env')) - { - throw new Exception("Module page: $sName is a reserved page argument name"); - } - $aArgs[$sName] = $sValue; - } - - return $aArgs; - } - - /** - * @return string - */ - static public function GetAbsoluteUrlExecPage() - { - return self::GetAbsoluteUrlAppRoot().'pages/exec.php'; - } - - /** - * Returns a name unique amongst the given list - * @param string $sProposed The default value - * @param array $aExisting An array of existing values (strings) - */ - static public function MakeUniqueName($sProposed, $aExisting) - { - if (in_array($sProposed, $aExisting)) - { - $i = 1; - while (in_array($sProposed.$i, $aExisting) && ($i < 50)) - { - $i++; - } - return $sProposed.$i; - } - else - { - return $sProposed; - } - } - - /** - * Some characters cause troubles with jQuery when used inside DOM IDs, so let's replace them by the safe _ (underscore) - * @param string $sId The ID to sanitize - * @return string The sanitized ID - */ - static public function GetSafeId($sId) - { - return str_replace(array(':', '[', ']', '+', '-'), '_', $sId); - } - - /** - * Helper to execute an HTTP POST request - * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl - * originaly named after do_post_request - * Does not require cUrl but requires openssl for performing https POSTs. - * - * @param string $sUrl The URL to POST the data to - * @param hash $aData The data to POST as an array('param_name' => value) - * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers - * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php - * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones. Example: CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3 - * @return string The result of the POST request - * @throws Exception - */ - static public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = array()) - { - // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request. - - if (function_exists('curl_init')) - { - // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options - // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 - // by setting the SSLVERSION to 3 as done below. - $aHeaders = explode("\n", $sOptionnalHeaders); - $aHTTPHeaders = array(); - foreach($aHeaders as $sHeaderString) - { - if(preg_match('/^([^:]): (.+)$/', $sHeaderString, $aMatches)) - { - $aHTTPHeaders[$aMatches[1]] = $aMatches[2]; - } - } - // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions - $aOptions = array( - CURLOPT_RETURNTRANSFER => true, // return the content of the request - CURLOPT_HEADER => false, // don't return the headers in the output - CURLOPT_FOLLOWLOCATION => true, // follow redirects - CURLOPT_ENCODING => "", // handle all encodings - CURLOPT_USERAGENT => "spider", // who am i - CURLOPT_AUTOREFERER => true, // set referer on redirect - CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect - CURLOPT_TIMEOUT => 120, // timeout on response - CURLOPT_MAXREDIRS => 10, // stop after 10 redirects - CURLOPT_SSL_VERIFYPEER => false, // Disabled SSL Cert checks - // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why - // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 - // CURLOPT_SSLVERSION => 3, - CURLOPT_POST => count($aData), - CURLOPT_POSTFIELDS => http_build_query($aData), - CURLOPT_HTTPHEADER => $aHTTPHeaders, - ); - - $aAllOptions = $aCurlOptions + $aOptions; - $ch = curl_init($sUrl); - curl_setopt_array($ch, $aAllOptions); - $response = curl_exec($ch); - $iErr = curl_errno($ch); - $sErrMsg = curl_error( $ch ); - $aHeaders = curl_getinfo( $ch ); - if ($iErr !== 0) - { - throw new Exception("Problem opening URL: $sUrl, $sErrMsg"); - } - if (is_array($aResponseHeaders)) - { - $aHeaders = curl_getinfo($ch); - foreach($aHeaders as $sCode => $sValue) - { - $sName = str_replace(' ' , '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type" - $aResponseHeaders[$sName] = $sValue; - } - } - curl_close( $ch ); - } - else - { - // cURL is not available let's try with streams and fopen... - - $sData = http_build_query($aData); - $aParams = array('http' => array( - 'method' => 'POST', - 'content' => $sData, - 'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n", - )); - if ($sOptionnalHeaders !== null) - { - $aParams['http']['header'] .= $sOptionnalHeaders; - } - $ctx = stream_context_create($aParams); - - $fp = @fopen($sUrl, 'rb', false, $ctx); - if (!$fp) - { - global $php_errormsg; - if (isset($php_errormsg)) - { - throw new Exception("Wrong URL: $sUrl, $php_errormsg"); - } - elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) - { - throw new Exception("Cannot connect to $sUrl: missing module 'openssl'"); - } - else - { - throw new Exception("Wrong URL: $sUrl"); - } - } - $response = @stream_get_contents($fp); - if ($response === false) - { - throw new Exception("Problem reading data from $sUrl, $php_errormsg"); - } - if (is_array($aResponseHeaders)) - { - $aMeta = stream_get_meta_data($fp); - $aHeaders = $aMeta['wrapper_data']; - foreach($aHeaders as $sHeaderString) - { - if(preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) - { - $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]); - } - } - } - } - return $response; - } - - /** - * Get a standard list of character sets - * - * @param array $aAdditionalEncodings Additional values - * @return array of iconv code => english label, sorted by label - */ - public static function GetPossibleEncodings($aAdditionalEncodings = array()) - { - // Encodings supported: - // ICONV_CODE => Display Name - // Each iconv installation supports different encodings - // Some reasonably common and useful encodings are listed here - $aPossibleEncodings = array( - 'UTF-8' => 'Unicode (UTF-8)', - 'ISO-8859-1' => 'Western (ISO-8859-1)', - 'WINDOWS-1251' => 'Cyrilic (Windows 1251)', - 'WINDOWS-1252' => 'Western (Windows 1252)', - 'ISO-8859-15' => 'Western (ISO-8859-15)', - ); - $aPossibleEncodings = array_merge($aPossibleEncodings, $aAdditionalEncodings); - asort($aPossibleEncodings); - return $aPossibleEncodings; - } - - /** - * Convert a string containing some (valid) HTML markup to plain text - * @param string $sHtml - * @return string - */ - public static function HtmlToText($sHtml) - { - try - { - //return ''.$sHtml; - return \Html2Text\Html2Text::convert(''.$sHtml); - } - catch(Exception $e) - { - return $e->getMessage(); - } - } - - /** - * Convert (?) plain text to some HTML markup by replacing newlines by
    tags - * and escaping HTML entities - * @param string $sText - * @return string - */ - public static function TextToHtml($sText) - { - $sText = str_replace("\r\n", "\n", $sText); - $sText = str_replace("\r", "\n", $sText); - return str_replace("\n", '
    ', htmlentities($sText, ENT_QUOTES, 'UTF-8')); - } - - /** - * Eventually compiles the SASS (.scss) file into the CSS (.css) file - * - * @param string $sSassRelPath Relative path to the SCSS file (must have the extension .scss) - * @param array $aImportPaths Array of absolute paths to load imports from - * @return string Relative path to the CSS file (.css) - */ - static public function GetCSSFromSASS($sSassRelPath, $aImportPaths = null) - { - // Avoiding compilation if file is already a css file. - if (preg_match('/\.css$/', $sSassRelPath)) - { - return $sSassRelPath; - } - - // Setting import paths - if ($aImportPaths === null) - { - $aImportPaths = array(); - } - $aImportPaths[] = APPROOT . '/css'; - - $sSassPath = APPROOT.$sSassRelPath; - $sCssRelPath = preg_replace('/\.scss$/', '.css', $sSassRelPath); - $sCssPath = APPROOT.$sCssRelPath; - clearstatcache(); - if (!file_exists($sCssPath) || (is_writable($sCssPath) && (filemtime($sCssPath) < filemtime($sSassPath)))) - { - require_once(APPROOT.'lib/scssphp/scss.inc.php'); - $oScss = new Compiler(); - $oScss->setImportPaths($aImportPaths); - $oScss->setFormatter('Leafo\\ScssPhp\\Formatter\\Expanded'); - // Temporary disabling max exec time while compiling - $iCurrentMaxExecTime = (int) ini_get('max_execution_time'); - set_time_limit(0); - $sCss = $oScss->compile(file_get_contents($sSassPath)); - set_time_limit($iCurrentMaxExecTime); - file_put_contents($sCssPath, $sCss); - } - return $sCssRelPath; - } - - static public function GetImageSize($sImageData) - { - if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher - { - $aRet = @getimagesizefromstring($sImageData); - } - else if(ini_get('allow_url_fopen')) - { - // work around to avoid creating a tmp file - $sUri = 'data://application/octet-stream;base64,'.base64_encode($sImageData); - $aRet = @getimagesize($sUri); - } - else - { - // Damned, need to create a tmp file - $sTempFile = tempnam(SetupUtils::GetTmpDir(), 'img-'); - @file_put_contents($sTempFile, $sImageData); - $aRet = @getimagesize($sTempFile); - @unlink($sTempFile); - } - return $aRet; - } - - /** - * Resize an image attachment so that it fits in the given dimensions - * @param ormDocument $oImage The original image stored as an ormDocument - * @param int $iWidth Image's original width - * @param int $iHeight Image's original height - * @param int $iMaxImageWidth Maximum width for the resized image - * @param int $iMaxImageHeight Maximum height for the resized image - * @return ormDocument The resampled image - */ - public static function ResizeImageToFit(ormDocument $oImage, $iWidth, $iHeight, $iMaxImageWidth, $iMaxImageHeight) - { - // If image size smaller than maximums, we do nothing - if (($iWidth <= $iMaxImageWidth) && ($iHeight <= $iMaxImageHeight)) - { - return $oImage; - } - - - // If gd extension is not loaded, we put a warning in the log and return the image as is - if (extension_loaded('gd') === false) - { - IssueLog::Warning('Image could not be resized as the "gd" extension does not seem to be loaded. It will remain as ' . $iWidth . 'x' . $iHeight . ' instead of ' . $iMaxImageWidth . 'x' . $iMaxImageHeight); - return $oImage; - } - - - switch($oImage->GetMimeType()) - { - case 'image/gif': - case 'image/jpeg': - case 'image/png': - $img = @imagecreatefromstring($oImage->GetData()); - break; - - default: - // Unsupported image type, return the image as-is - //throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); - return $oImage; - } - if ($img === false) - { - //throw new Exception("Warning: corrupted image: '".$oImage->GetFileName()." / ".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); - return $oImage; - } - else - { - // Let's scale the image, preserving the transparency for GIFs and PNGs - - $fScale = min($iMaxImageWidth / $iWidth, $iMaxImageHeight / $iHeight); - - $iNewWidth = $iWidth * $fScale; - $iNewHeight = $iHeight * $fScale; - - $new = imagecreatetruecolor($iNewWidth, $iNewHeight); - - // Preserve transparency - if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png")) - { - imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127)); - imagealphablending($new, false); - imagesavealpha($new, true); - } - - imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight); - - ob_start(); - switch ($oImage->GetMimeType()) - { - case 'image/gif': - imagegif($new); // send image to output buffer - break; - - case 'image/jpeg': - imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality - break; - - case 'image/png': - imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression - break; - } - $oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName()); - @ob_end_clean(); - - imagedestroy($img); - imagedestroy($new); - - return $oResampledImage; - } - - } - - /** - * Create a 128 bit UUID in the format: {########-####-####-####-############} - * - * Note: this method can be run from the command line as well as from the web server. - * Note2: this method is not cryptographically secure! If you need a cryptographically secure value - * consider using open_ssl or PHP 7 methods. - * @param string $sPrefix - * @return string - */ - static public function CreateUUID($sPrefix = '') - { - $uid = uniqid("", true); - $data = $sPrefix; - $data .= __FILE__; - $data .= mt_rand(); - $hash = strtoupper(hash('ripemd128', $uid . md5($data))); - $sUUID = '{' . - substr($hash, 0, 8) . - '-' . - substr($hash, 8, 4) . - '-' . - substr($hash, 12, 4) . - '-' . - substr($hash, 16, 4) . - '-' . - substr($hash, 20, 12) . - '}'; - return $sUUID; - } - - /** - * Returns the name of the module containing the file where the call to this function is made - * or an empty string if no such module is found (or not called within a module file) - * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module - * @return string - */ - static public function GetCurrentModuleName($iCallDepth = 0) - { - $sCurrentModuleName = ''; - $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); - - foreach(GetModulesInfo() as $sModuleName => $aInfo) - { - if ($aInfo['root_dir'] !== '') - { - $sRootDir = realpath(APPROOT.$aInfo['root_dir']); - - if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) - { - $sCurrentModuleName = $sModuleName; - break; - } - } - } - return $sCurrentModuleName; - } - - /** - * Returns the relative (to APPROOT) path of the root directory of the module containing the file where the call to this function is made - * or an empty string if no such module is found (or not called within a module file) - * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module - * @return string - */ - static public function GetCurrentModuleDir($iCallDepth) - { - $sCurrentModuleDir = ''; - $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); - - foreach(GetModulesInfo() as $sModuleName => $aInfo) - { - if ($aInfo['root_dir'] !== '') - { - $sRootDir = realpath(APPROOT.$aInfo['root_dir']); - - if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) - { - $sCurrentModuleDir = basename($sRootDir); - break; - } - } - } - return $sCurrentModuleDir; - } - - /** - * Returns the base URL for all files in the current module from which this method is called - * or an empty string if no such module is found (or not called within a module file) - * @return string - */ - static public function GetCurrentModuleUrl() - { - $sDir = static::GetCurrentModuleDir(1); - if ( $sDir !== '') - { - return static::GetAbsoluteUrlModulesRoot().'/'.$sDir; - } - return ''; - } - - /** - * Get the value of a given setting for the current module - * @param string $sProperty The name of the property to retrieve - * @param mixed $defaultvalue - * @return mixed - */ - static public function GetCurrentModuleSetting($sProperty, $defaultvalue = null) - { - $sModuleName = static::GetCurrentModuleName(1); - return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue); - } - - /** - * Get the compiled version of a given module, as it was seen by the compiler - * @param string $sModuleName - * @return string|NULL - */ - static public function GetCompiledModuleVersion($sModuleName) - { - $aModulesInfo = GetModulesInfo(); - if (array_key_exists($sModuleName, $aModulesInfo)) - { - return $aModulesInfo[$sModuleName]['version']; - } - return null; - } - - /** - * Check if the given path/url is an http(s) URL - * @param string $sPath - * @return boolean - */ - public static function IsURL($sPath) - { - $bRet = false; - if ((substr($sPath, 0, 7) == 'http://') || (substr($sPath, 0, 8) == 'https://') || (substr($sPath, 0, 8) == 'ftp://')) - { - $bRet = true; - } - return $bRet; - } - - /** - * Check if the given URL is a link to download a document/image on the CURRENT iTop - * In such a case we can read the content of the file directly in the database (if the users rights allow) and return the ormDocument - * @param string $sPath - * @return false|ormDocument - * @throws Exception - */ - public static function IsSelfURL($sPath) - { - $result = false; - $sPageUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php'; - if (substr($sPath, 0, strlen($sPageUrl)) == $sPageUrl) - { - // If the URL is an URL pointing to this instance of iTop, then - // extract the "query" part of the URL and analyze it - $sQuery = parse_url($sPath, PHP_URL_QUERY); - if ($sQuery !== null) - { - $aParams = array(); - foreach(explode('&', $sQuery) as $sChunk) - { - $aParts = explode('=', $sChunk); - if (count($aParts) != 2) continue; - $aParams[$aParts[0]] = urldecode($aParts[1]); - } - $result = array_key_exists('operation', $aParams) && array_key_exists('class', $aParams) && array_key_exists('id', $aParams) && array_key_exists('field', $aParams) && ($aParams['operation'] == 'download_document'); - if ($result) - { - // This is a 'download_document' operation, let's retrieve the document directly from the database - $sClass = $aParams['class']; - $iKey = $aParams['id']; - $sAttCode = $aParams['field']; - - $oObj = MetaModel::GetObject($sClass, $iKey, false /* must exist */); // Users rights apply here !! - if ($oObj) - { - /** - * @var ormDocument $result - */ - $result = clone $oObj->Get($sAttCode); - return $result; - } - } - } - throw new Exception('Invalid URL. This iTop URL is not pointing to a valid Document/Image.'); - } - return $result; - } - - /** - * Read the content of a file (and retrieve its MIME type) from either: - * - an URL pointing to a blob (image/document) on the current iTop server - * - an http(s) URL - * - the local file system (but only if you are an administrator) - * @param string $sPath - * @return ormDocument|null - * @throws Exception - */ - public static function FileGetContentsAndMIMEType($sPath) - { - $oUploadedDoc = null; - $aKnownExtensions = array( - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', - 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', - 'pdf' => 'application/pdf', - 'doc' => 'application/msword', - 'dot' => 'application/msword', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', - 'vsd' => 'application/x-visio', - 'vdx' => 'application/visio.drawing', - 'odt' => 'application/vnd.oasis.opendocument.text', - 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', - 'odp' => 'application/vnd.oasis.opendocument.presentation', - 'zip' => 'application/zip', - 'txt' => 'text/plain', - 'htm' => 'text/html', - 'html' => 'text/html', - 'exe' => 'application/octet-stream' - ); - - $sData = null; - $sMimeType = 'text/plain'; // Default MIME Type: treat the file as a bunch a characters... - $sFileName = 'uploaded-file'; // Default name for downloaded-files - $sExtension = '.txt'; // Default file extension in case we don't know the MIME Type - - if(empty($sPath)) - { - // Empty path (NULL or '') means that there is no input, making an empty document. - $oUploadedDoc = new ormDocument('', '', ''); - } - elseif (static::IsURL($sPath)) - { - if ($oUploadedDoc = static::IsSelfURL($sPath)) - { - // Nothing more to do, we've got it !! - } - else - { - // Remote file, let's use the HTTP headers to find the MIME Type - $sData = @file_get_contents($sPath); - if ($sData === false) - { - throw new Exception("Failed to load the file from the URL '$sPath'."); - } - else - { - if (isset($http_response_header)) - { - $aHeaders = static::ParseHeaders($http_response_header); - $sMimeType = array_key_exists('Content-Type', $aHeaders) ? strtolower($aHeaders['Content-Type']) : 'application/x-octet-stream'; - // Compute the file extension from the MIME Type - foreach($aKnownExtensions as $sExtValue => $sMime) - { - if ($sMime === $sMimeType) - { - $sExtension = '.'.$sExtValue; - break; - } - } - } - $sFileName .= $sExtension; - } - $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); - } - } - else if (UserRights::IsAdministrator()) - { - // Only administrators are allowed to read local files - $sData = @file_get_contents($sPath); - if ($sData === false) - { - throw new Exception("Failed to load the file '$sPath'. The file does not exist or the current process is not allowed to access it."); - } - $sExtension = strtolower(pathinfo($sPath, PATHINFO_EXTENSION)); - $sFileName = basename($sPath); - - if (array_key_exists($sExtension, $aKnownExtensions)) - { - $sMimeType = $aKnownExtensions[$sExtension]; - } - else if (extension_loaded('fileinfo')) - { - $finfo = new finfo(FILEINFO_MIME); - $sMimeType = $finfo->file($sPath); - } - $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); - } - return $oUploadedDoc; - } - - protected static function ParseHeaders($aHeaders) - { - $aCleanHeaders = array(); - foreach( $aHeaders as $sKey => $sValue ) - { - $aTokens = explode(':', $sValue, 2); - if(isset($aTokens[1])) - { - $aCleanHeaders[trim($aTokens[0])] = trim($aTokens[1]); - } - else - { - // The header is not in the form Header-Code: Value - $aCleanHeaders[] = $sValue; // Store the value as-is - $aMatches = array(); - // Check if it's not the HTTP response code - if( preg_match("|HTTP/[0-9\.]+\s+([0-9]+)|", $sValue, $aMatches) ) - { - $aCleanHeaders['reponse_code'] = intval($aMatches[1]); - } - } - } - return $aCleanHeaders; - } - - /** - * Return a string based on compilation time or (if not available because the datamodel has not been loaded) - * the version of iTop. This string is useful to prevent browser side caching of content that may vary at each - * (re)installation of iTop (especially during development). - * @return string - */ - public static function GetCacheBusterTimestamp() - { - if(!defined('COMPILATION_TIMESTAMP')) - { - return ITOP_VERSION; - } - return COMPILATION_TIMESTAMP; - } - - /** - * Check if the given class if configured as a high cardinality class. - * - * @param $sClass - * - * @return bool - */ - public static function IsHighCardinality($sClass) - { - if (utils::GetConfig()->Get('search_manual_submit')) - { - return true; - } - $aHugeClasses = MetaModel::GetConfig()->Get('high_cardinality_classes'); - return in_array($sClass, $aHugeClasses); - } -} + + + +/** + * Static class utils + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/core/config.class.inc.php'); +require_once(APPROOT.'/application/transaction.class.inc.php'); +require_once(APPROOT.'application/Html2Text.php'); +require_once(APPROOT.'application/Html2TextException.php'); + +define('ITOP_CONFIG_FILE', 'config-itop.php'); +define('ITOP_DEFAULT_CONFIG_FILE', APPCONF.ITOP_DEFAULT_ENV.'/'.ITOP_CONFIG_FILE); + +define('SERVER_NAME_PLACEHOLDER', '$SERVER_NAME$'); + +class FileUploadException extends Exception +{ +} + + +/** + * Helper functions to interact with forms: read parameters, upload files... + * @package iTop + */ +class utils +{ + private static $oConfig = null; + private static $m_bCASClient = false; + + // Parameters loaded from a file, parameters of the page/command line still have precedence + private static $m_aParamsFromFile = null; + private static $m_aParamSource = array(); + + protected static function LoadParamFile($sParamFile) + { + if (!file_exists($sParamFile)) + { + throw new Exception("Could not find the parameter file: '$sParamFile'"); + } + if (!is_readable($sParamFile)) + { + throw new Exception("Could not load parameter file: '$sParamFile'"); + } + $sParams = file_get_contents($sParamFile); + + if (is_null(self::$m_aParamsFromFile)) + { + self::$m_aParamsFromFile = array(); + } + + $aParamLines = explode("\n", $sParams); + foreach ($aParamLines as $sLine) + { + $sLine = trim($sLine); + + // Ignore the line after a '#' + if (($iCommentPos = strpos($sLine, '#')) !== false) + { + $sLine = substr($sLine, 0, $iCommentPos); + $sLine = trim($sLine); + } + + // Note: the line is supposed to be already trimmed + if (preg_match('/^(\S*)\s*=(.*)$/', $sLine, $aMatches)) + { + $sParam = $aMatches[1]; + $value = trim($aMatches[2]); + self::$m_aParamsFromFile[$sParam] = $value; + self::$m_aParamSource[$sParam] = $sParamFile; + } + } + } + + public static function UseParamFile($sParamFileArgName = 'param_file', $bAllowCLI = true) + { + $sFileSpec = self::ReadParam($sParamFileArgName, '', $bAllowCLI, 'raw_data'); + foreach(explode(',', $sFileSpec) as $sFile) + { + $sFile = trim($sFile); + if (!empty($sFile)) + { + self::LoadParamFile($sFile); + } + } + } + + /** + * Return the source file from which the parameter has been found, + * usefull when it comes to pass user credential to a process executed + * in the background + * @param $sName Parameter name + * @return The file name if any, or null + */ + public static function GetParamSourceFile($sName) + { + if (array_key_exists($sName, self::$m_aParamSource)) + { + return self::$m_aParamSource[$sName]; + } + else + { + return null; + } + } + + public static function IsModeCLI() + { + $sSAPIName = php_sapi_name(); + $sCleanName = strtolower(trim($sSAPIName)); + if ($sCleanName == 'cli') + { + return true; + } + else + { + return false; + } + } + + protected static $bPageMode = null; + /** + * @var boolean[] + */ + protected static $aModes = array(); + + public static function InitArchiveMode() + { + if (isset($_SESSION['archive_mode'])) + { + $iDefault = $_SESSION['archive_mode']; + } + else + { + $iDefault = 0; + } + // Read and record the value for switching the archive mode + $iCurrent = self::ReadParam('with-archive', $iDefault); + if (isset($_SESSION)) + { + $_SESSION['archive_mode'] = $iCurrent; + } + // Read and use the value for the current page (web services) + $iCurrent = self::ReadParam('with_archive', $iCurrent, true); + self::$bPageMode = ($iCurrent == 1); + } + + /** + * @param boolean $bMode if true then activate archive mode (archived objects are visible), otherwise archived objects are + * hidden (archive = "soft deletion") + */ + public static function PushArchiveMode($bMode) + { + array_push(self::$aModes, $bMode); + } + + public static function PopArchiveMode() + { + array_pop(self::$aModes); + } + + /** + * @return boolean true if archive mode is enabled + */ + public static function IsArchiveMode() + { + if (count(self::$aModes) > 0) + { + $bRet = end(self::$aModes); + } + else + { + if (self::$bPageMode === null) + { + self::InitArchiveMode(); + } + $bRet = self::$bPageMode; + } + return $bRet; + } + + /** + * Helper to be called by the GUI and define if the user will see obsolete data (otherwise, the user will have to dig further) + * @return bool + */ + public static function ShowObsoleteData() + { + $bDefault = MetaModel::GetConfig()->Get('obsolescence.show_obsolete_data'); // default is false + $bShow = appUserPreferences::GetPref('show_obsolete_data', $bDefault); + if (static::IsArchiveMode()) + { + $bShow = true; + } + return $bShow; + } + + public static function ReadParam($sName, $defaultValue = "", $bAllowCLI = false, $sSanitizationFilter = 'parameter') + { + global $argv; + $retValue = $defaultValue; + + if (!is_null(self::$m_aParamsFromFile)) + { + if (isset(self::$m_aParamsFromFile[$sName])) + { + $retValue = self::$m_aParamsFromFile[$sName]; + } + } + + if (isset($_REQUEST[$sName])) + { + $retValue = $_REQUEST[$sName]; + } + elseif ($bAllowCLI && isset($argv)) + { + foreach($argv as $iArg => $sArg) + { + if (preg_match('/^--'.$sName.'=(.*)$/', $sArg, $aMatches)) + { + $retValue = $aMatches[1]; + } + } + } + return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); + } + + public static function ReadPostedParam($sName, $defaultValue = '', $sSanitizationFilter = 'parameter') + { + $retValue = isset($_POST[$sName]) ? $_POST[$sName] : $defaultValue; + return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); + } + + public static function Sanitize($value, $defaultValue, $sSanitizationFilter) + { + if ($value === $defaultValue) + { + // Preserve the real default value (can be used to detect missing mandatory parameters) + $retValue = $value; + } + else + { + $retValue = self::Sanitize_Internal($value, $sSanitizationFilter); + if ($retValue === false) + { + $retValue = $defaultValue; + } + } + return $retValue; + } + + protected static function Sanitize_Internal($value, $sSanitizationFilter) + { + switch($sSanitizationFilter) + { + case 'integer': + $retValue = filter_var($value, FILTER_SANITIZE_NUMBER_INT); + break; + + case 'class': + $retValue = $value; + if (!MetaModel::IsValidClass($value)) + { + $retValue = false; + } + break; + + case 'string': + $retValue = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS); + break; + + case 'context_param': + case 'parameter': + case 'field_name': + if (is_array($value)) + { + $retValue = array(); + foreach($value as $key => $val) + { + $retValue[$key] = self::Sanitize_Internal($val, $sSanitizationFilter); // recursively check arrays + if ($retValue[$key] === false) + { + $retValue = false; + break; + } + } + } + else + { + switch($sSanitizationFilter) + { + case 'parameter': + $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^([ A-Za-z0-9_=-]|%3D|%2B|%2F)*$/'))); // the '=', '%3D, '%2B', '%2F' characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC) + break; + + case 'field_name': + $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name + break; + + case 'context_param': + $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[ A-Za-z0-9_=%:+-]*$/'))); + break; + + } + } + break; + + default: + case 'raw_data': + $retValue = $value; + // Do nothing + } + return $retValue; + } + + /** + * Reads an uploaded file and turns it into an ormDocument object - Triggers an exception in case of error + * @param string $sName Name of the input used from uploading the file + * @param string $sIndex If Name is an array of posted files, then the index must be used to point out the file + * @return ormDocument The uploaded file (can be 'empty' if nothing was uploaded) + */ + public static function ReadPostedDocument($sName, $sIndex = null) + { + $oDocument = new ormDocument(); // an empty document + if(isset($_FILES[$sName])) + { + $aFileInfo = $_FILES[$sName]; + + $sError = is_null($sIndex) ? $aFileInfo['error'] : $aFileInfo['error'][$sIndex]; + switch($sError) + { + case UPLOAD_ERR_OK: + $sTmpName = is_null($sIndex) ? $aFileInfo['tmp_name'] : $aFileInfo['tmp_name'][$sIndex]; + $sMimeType = is_null($sIndex) ? $aFileInfo['type'] : $aFileInfo['type'][$sIndex]; + $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; + + $doc_content = file_get_contents($sTmpName); + if (function_exists('finfo_file')) + { + // as of PHP 5.3 the fileinfo extension is bundled within PHP + // in which case we don't trust the mime type provided by the browser + $rInfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension + if ($rInfo !== false) + { + $sType = @finfo_file($rInfo, $sTmpName); + if ( ($sType !== false) + && is_string($sType) + && (strlen($sType)>0)) + { + $sMimeType = $sType; + } + } + @finfo_close($rInfo); + } + $oDocument = new ormDocument($doc_content, $sMimeType, $sName); + break; + + case UPLOAD_ERR_NO_FILE: + // no file to load, it's a normal case, just return an empty document + break; + + case UPLOAD_ERR_FORM_SIZE: + case UPLOAD_ERR_INI_SIZE: + throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize'))); + break; + + case UPLOAD_ERR_PARTIAL: + throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.')); + break; + + case UPLOAD_ERR_NO_TMP_DIR: + throw new FileUploadException(Dict::S('UI:Error:NoTmpDir')); + break; + + case UPLOAD_ERR_CANT_WRITE: + throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir'))); + break; + + case UPLOAD_ERR_EXTENSION: + $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; + throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName)); + break; + + default: + throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError)); + break; + + } + } + return $oDocument; + } + + /** + * Interprets the results posted by a normal or paginated list (in multiple selection mode) + * + * @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected + * + * @return Array An array of object IDs corresponding to the objects selected in the set + */ + public static function ReadMultipleSelection($oFullSetFilter) + { + $aSelectedObj = utils::ReadParam('selectObject', array()); + $sSelectionMode = utils::ReadParam('selectionMode', ''); + if ($sSelectionMode != '') + { + // Paginated selection + $aExceptions = utils::ReadParam('storedSelection', array()); + if ($sSelectionMode == 'positive') + { + // Only the explicitely listed items are selected + $aSelectedObj = $aExceptions; + } + else + { + // All items of the set are selected, except the one explicitely listed + $aSelectedObj = array(); + $oFullSet = new DBObjectSet($oFullSetFilter); + $sClassAlias = $oFullSetFilter->GetClassAlias(); + $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field + while($oObj = $oFullSet->Fetch()) + { + if (!in_array($oObj->GetKey(), $aExceptions)) + { + $aSelectedObj[] = $oObj->GetKey(); + } + } + } + } + return $aSelectedObj; + } + + /** + * Interprets the results posted by a normal or paginated list (in multiple selection mode) + * + * @param DBSearch $oFullSetFilter The criteria defining the whole sets of objects being selected + * + * @return Array An array of object IDs:friendlyname corresponding to the objects selected in the set + * @throws \CoreException + */ + public static function ReadMultipleSelectionWithFriendlyname($oFullSetFilter) + { + $sSelectionMode = utils::ReadParam('selectionMode', ''); + + if ($sSelectionMode === '') + { + throw new CoreException('selectionMode is mandatory'); + } + + // Paginated selection + $aSelectedIds = utils::ReadParam('storedSelection', array()); + if (count($aSelectedIds) > 0 ) + { + if ($sSelectionMode == 'positive') + { + // Only the explicitly listed items are selected + $oFullSetFilter->AddCondition('id', $aSelectedIds, 'IN'); + } + else + { + // All items of the set are selected, except the one explicitly listed + $oFullSetFilter->AddCondition('id', $aSelectedIds, 'NOTIN'); + } + } + + $aSelectedObj = array(); + $oFullSet = new DBObjectSet($oFullSetFilter); + $sClassAlias = $oFullSetFilter->GetClassAlias(); + $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field + while ($oObj = $oFullSet->Fetch()) + { + $aSelectedObj[$oObj->GetKey()] = $oObj->Get('friendlyname'); + } + + return $aSelectedObj; + } + + public static function GetNewTransactionId() + { + return privUITransaction::GetNewTransactionId(); + } + + public static function IsTransactionValid($sId, $bRemoveTransaction = true) + { + return privUITransaction::IsTransactionValid($sId, $bRemoveTransaction); + } + + public static function RemoveTransaction($sId) + { + return privUITransaction::RemoveTransaction($sId); + } + + /** + * Returns a unique tmp id for the current upload based on the transaction system (db). + * + * Build as session_id() . '_' . static::GetNewTransactionId() + * + * @return string + */ + public static function GetUploadTempId($sTransactionId = null) + { + if ($sTransactionId === null) + { + $sTransactionId = static::GetNewTransactionId(); + } + return session_id() . '_' . $sTransactionId; + } + + public static function ReadFromFile($sFileName) + { + if (!file_exists($sFileName)) return false; + return file_get_contents($sFileName); + } + + /** + * Helper function to convert a value expressed in a 'user friendly format' + * as in php.ini, e.g. 256k, 2M, 1G etc. Into a number of bytes + * @param mixed $value The value as read from php.ini + * @return number + */ + public static function ConvertToBytes( $value ) + { + $iReturn = $value; + if ( !is_numeric( $value ) ) + { + $iLength = strlen( $value ); + $iReturn = substr( $value, 0, $iLength - 1 ); + $sUnit = strtoupper( substr( $value, $iLength - 1 ) ); + switch ( $sUnit ) + { + case 'G': + $iReturn *= 1024; + case 'M': + $iReturn *= 1024; + case 'K': + $iReturn *= 1024; + } + } + return $iReturn; + } + + /** + * Format a value into a more friendly format (KB, MB, GB, TB) instead a juste a Bytes amount. + * + * @param type $value + * @return string + */ + public static function BytesToFriendlyFormat($value) + { + $sReturn = ''; + // Kilobytes + if ($value >= 1024) + { + $sReturn = 'K'; + $value = $value / 1024; + } + // Megabytes + if ($value >= 1024) + { + $sReturn = 'M'; + $value = $value / 1024; + } + // Gigabytes + if ($value >= 1024) + { + $sReturn = 'G'; + $value = $value / 1024; + } + // Terabytes + if ($value >= 1024) + { + $sReturn = 'T'; + $value = $value / 1024; + } + + $value = round($value, 1); + + return $value . '' . $sReturn . 'B'; + } + + /** + * Helper function to convert a string to a date, given a format specification. It replaces strtotime which does not allow for specifying a date in a french format (for instance) + * Example: StringToTime('01/05/11 12:03:45', '%d/%m/%y %H:%i:%s') + * @param string $sDate + * @param string $sFormat + * @return timestamp or false if the input format is not correct + */ + public static function StringToTime($sDate, $sFormat) + { + // Source: http://php.net/manual/fr/function.strftime.php + // (alternative: http://www.php.net/manual/fr/datetime.formats.date.php) + static $aDateTokens = null; + static $aDateRegexps = null; + if (is_null($aDateTokens)) + { + $aSpec = array( + '%d' =>'(?[0-9]{2})', + '%m' => '(?[0-9]{2})', + '%y' => '(?[0-9]{2})', + '%Y' => '(?[0-9]{4})', + '%H' => '(?[0-2][0-9])', + '%i' => '(?[0-5][0-9])', + '%s' => '(?[0-5][0-9])', + ); + $aDateTokens = array_keys($aSpec); + $aDateRegexps = array_values($aSpec); + } + + $sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat); + + if (preg_match('!^(?)'.$sDateRegexp.'(?)$!', $sDate, $aMatches)) + { + $sYear = isset($aMatches['year']) ? $aMatches['year'] : 0; + $sMonth = isset($aMatches['month']) ? $aMatches['month'] : 1; + $sDay = isset($aMatches['day']) ? $aMatches['day'] : 1; + $sHour = isset($aMatches['hour']) ? $aMatches['hour'] : 0; + $sMinute = isset($aMatches['minute']) ? $aMatches['minute'] : 0; + $sSecond = isset($aMatches['second']) ? $aMatches['second'] : 0; + return strtotime("$sYear-$sMonth-$sDay $sHour:$sMinute:$sSecond"); + } + else + { + return false; + } + // http://www.spaweditor.com/scripts/regex/index.php + } + + /** + * Convert an old date/time format specifciation (using % placeholders) + * to a format compatible with DateTime::createFromFormat + * @param string $sOldDateTimeFormat + * @return string + */ + static public function DateTimeFormatToPHP($sOldDateTimeFormat) + { + $aSearch = array('%d', '%m', '%y', '%Y', '%H', '%i', '%s'); + $aReplacement = array('d', 'm', 'y', 'Y', 'H', 'i', 's'); + return str_replace($aSearch, $aReplacement, $sOldDateTimeFormat); + } + + /** + * @return \Config from the current environement, or if not existing from the production env, else new Config made from scratch + * @uses \MetaModel::GetConfig() don't forget to add the needed require_once(APPROOT.'core/metamodel.class.php'); + */ + static public function GetConfig() + { + if (self::$oConfig == null) + { + self::$oConfig = MetaModel::GetConfig(); + + if (self::$oConfig == null) + { + $sConfigFile = self::GetConfigFilePath(); + if (!file_exists($sConfigFile)) + { + $sConfigFile = self::GetConfigFilePath('production'); + if (!file_exists($sConfigFile)) + { + $sConfigFile = null; + } + } + + self::$oConfig = new Config($sConfigFile); + } + } + return self::$oConfig; + } + + public static function InitTimeZone() { + $oConfig = self::GetConfig(); + $sItopTimeZone = $oConfig->Get('timezone'); + + if (!empty($sItopTimeZone)) + { + date_default_timezone_set($sItopTimeZone); + } + else + { + // Leave as is... up to the admin to set a value somewhere... + // see http://php.net/manual/en/datetime.configuration.php#ini.date.timezone + } + } + + /** + * Returns the absolute URL to the application root path + * + * @return string The absolute URL to the application root, without the first slash + * + * @throws \Exception + */ + static public function GetAbsoluteUrlAppRoot() + { + static $sUrl = null; + if ($sUrl === null) + { + $sUrl = self::GetConfig()->Get('app_root_url'); + if ($sUrl == '') + { + $sUrl = self::GetDefaultUrlAppRoot(); + } + elseif (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1) + { + if (isset($_SERVER['SERVER_NAME'])) + { + $sServerName = $_SERVER['SERVER_NAME']; + } + else + { + // CLI mode ? + $sServerName = php_uname('n'); + } + $sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl); + } + } + return $sUrl; + } + + /** + * Builds an root url from the server's variables. + * For most usages, when an root url is needed, use utils::GetAbsoluteUrlAppRoot() instead as uses this only as a fallback when the app_root_url conf parameter is not defined. + * + * @return string + * + * @throws \Exception + */ + static public function GetDefaultUrlAppRoot() + { + // Build an absolute URL to this page on this server/port + $sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; + $sProtocol = self::IsConnectionSecure() ? 'https' : 'http'; + $iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; + if ($sProtocol == 'http') + { + $sPort = ($iPort == 80) ? '' : ':'.$iPort; + } + else + { + $sPort = ($iPort == 443) ? '' : ':'.$iPort; + } + // $_SERVER['REQUEST_URI'] is empty when running on IIS + // Let's use Ivan Tcholakov's fix (found on www.dokeos.com) + if (!empty($_SERVER['REQUEST_URI'])) + { + $sPath = $_SERVER['REQUEST_URI']; + } + else + { + $sPath = $_SERVER['SCRIPT_NAME']; + if (!empty($_SERVER['QUERY_STRING'])) + { + $sPath .= '?'.$_SERVER['QUERY_STRING']; + } + $_SERVER['REQUEST_URI'] = $sPath; + } + $sPath = $_SERVER['REQUEST_URI']; + + // remove all the parameters from the query string + $iQuestionMarkPos = strpos($sPath, '?'); + if ($iQuestionMarkPos !== false) + { + $sPath = substr($sPath, 0, $iQuestionMarkPos); + } + $sAbsoluteUrl = "$sProtocol://{$sServerName}{$sPort}{$sPath}"; + + $sCurrentScript = realpath($_SERVER['SCRIPT_FILENAME']); + $sCurrentScript = str_replace('\\', '/', $sCurrentScript); // canonical path + $sAppRoot = str_replace('\\', '/', APPROOT); // canonical path + $sCurrentRelativePath = str_replace($sAppRoot, '', $sCurrentScript); + + $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); + if ($sAppRootPos !== false) + { + $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path + } + else + { + // Second attempt without index.php at the end... + $sCurrentRelativePath = str_replace('index.php', '', $sCurrentRelativePath); + $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); + if ($sAppRootPos !== false) + { + $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path + } + else + { + // No luck... + throw new Exception("Failed to determine application root path $sAbsoluteUrl ($sCurrentRelativePath) APPROOT:'$sAppRoot'"); + } + } + return $sAppRootUrl; + } + + /** + * Helper to handle the variety of HTTP servers + * See #286 (fixed in [896]), and #634 (this fix) + * + * Though the official specs says 'a non empty string', some servers like IIS do set it to 'off' ! + * nginx set it to an empty string + * Others might leave it unset (no array entry) + */ + static public function IsConnectionSecure() + { + $bSecured = false; + + if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off')) + { + $bSecured = true; + } + return $bSecured; + } + + /** + * Tells whether or not log off operation is supported. + * Actually in only one case: + * 1) iTop is using an internal authentication + * 2) the user did not log-in using the "basic" mode (i.e basic authentication) or by passing credentials in the URL + * @return boolean True if logoff is supported, false otherwise + */ + static function CanLogOff() + { + $bResult = false; + if(isset($_SESSION['login_mode'])) + { + $sLoginMode = $_SESSION['login_mode']; + switch($sLoginMode) + { + case 'external': + $bResult = false; + break; + + case 'form': + case 'basic': + case 'url': + case 'cas': + default: + $bResult = true; + + } + } + return $bResult; + } + + /** + * Initializes the CAS client + */ + static function InitCASClient() + { + $sCASIncludePath = self::GetConfig()->Get('cas_include_path'); + include_once($sCASIncludePath.'/CAS.php'); + + $bCASDebug = self::GetConfig()->Get('cas_debug'); + if ($bCASDebug) + { + phpCAS::setDebug(APPROOT.'log/error.log'); + } + + if (!self::$m_bCASClient) + { + // Initialize phpCAS + $sCASVersion = self::GetConfig()->Get('cas_version'); + $sCASHost = self::GetConfig()->Get('cas_host'); + $iCASPort = self::GetConfig()->Get('cas_port'); + $sCASContext = self::GetConfig()->Get('cas_context'); + phpCAS::client($sCASVersion, $sCASHost, $iCASPort, $sCASContext, false /* session already started */); + self::$m_bCASClient = true; + $sCASCACertPath = self::GetConfig()->Get('cas_server_ca_cert_path'); + if (empty($sCASCACertPath)) + { + // If no certificate authority is provided, do not attempt to validate + // the server's certificate + // THIS SETTING IS NOT RECOMMENDED FOR PRODUCTION. + // VALIDATING THE CAS SERVER IS CRUCIAL TO THE SECURITY OF THE CAS PROTOCOL! + phpCAS::setNoCasServerValidation(); + } + else + { + phpCAS::setCasServerCACert($sCASCACertPath); + } + } + } + + static function DebugBacktrace($iLimit = 5) + { + $aFullTrace = debug_backtrace(); + $aLightTrace = array(); + for($i=1; ($i<=$iLimit && $i < count($aFullTrace)); $i++) // Skip the last function call... which is the call to this function ! + { + $aLightTrace[$i] = $aFullTrace[$i]['function'].'(), called from line '.$aFullTrace[$i]['line'].' in '.$aFullTrace[$i]['file']; + } + echo "

    ".print_r($aLightTrace, true)."

    \n"; + } + + /** + * Execute the given iTop PHP script, passing it the current credentials + * Only CLI mode is supported, because of the need to hand the credentials over to the next process + * Throws an exception if the execution fails or could not be attempted (config issue) + * @param string $sScript Name and relative path to the file (relative to the iTop root dir) + * @param hash $aArguments Associative array of 'arg' => 'value' + * @return array(iCode, array(output lines)) + */ + /** + */ + static function ExecITopScript($sScriptName, $aArguments) + { + $aDisabled = explode(', ', ini_get('disable_functions')); + if (in_array('exec', $aDisabled)) + { + throw new Exception("The PHP exec() function has been disabled on this server"); + } + + $sPHPExec = trim(self::GetConfig()->Get('php_path')); + if (strlen($sPHPExec) == 0) + { + throw new Exception("The path to php must not be empty. Please set a value for 'php_path' in your configuration file."); + } + + $sAuthUser = self::ReadParam('auth_user', '', 'raw_data'); + $sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data'); + $sParamFile = self::GetParamSourceFile('auth_user'); + if (is_null($sParamFile)) + { + $aArguments['auth_user'] = $sAuthUser; + $aArguments['auth_pwd'] = $sAuthPwd; + } + else + { + $aArguments['param_file'] = $sParamFile; + } + + $aArgs = array(); + foreach($aArguments as $sName => $value) + { + // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation + // It suggests to rely on pctnl_* function instead of using escapeshellargs + $aArgs[] = "--$sName=".escapeshellarg($value); + } + $sArgs = implode(' ', $aArgs); + + $sScript = realpath(APPROOT.$sScriptName); + if (!file_exists($sScript)) + { + throw new Exception("Could not find the script file '$sScriptName' from the directory '".APPROOT."'"); + } + + $sCommand = '"'.$sPHPExec.'" '.escapeshellarg($sScript).' -- '.$sArgs; + + if (version_compare(phpversion(), '5.3.0', '<')) + { + if (substr(PHP_OS,0,3) == 'WIN') + { + // Under Windows, and for PHP 5.2.x, the whole command has to be quoted + // Cf PHP doc: http://php.net/manual/fr/function.exec.php, comment from the 27-Dec-2010 + $sCommand = '"'.$sCommand.'"'; + } + } + + $sLastLine = exec($sCommand, $aOutput, $iRes); + if ($iRes == 1) + { + throw new Exception(Dict::S('Core:ExecProcess:Code1')." - ".$sCommand); + } + elseif ($iRes == 255) + { + $sErrors = implode("\n", $aOutput); + throw new Exception(Dict::S('Core:ExecProcess:Code255')." - ".$sCommand.":\n".$sErrors); + } + + //$aOutput[] = $sCommand; + return array($iRes, $aOutput); + } + + /** + * Get the current environment + */ + public static function GetCurrentEnvironment() + { + if (isset($_SESSION['itop_env'])) + { + return $_SESSION['itop_env']; + } + else + { + return ITOP_DEFAULT_ENV; + } + } + + /** + * Returns a path to a folder into which any module can store cache data + * The corresponding folder is created or cleaned upon code compilation + * @return string + */ + public static function GetCachePath() + { + return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/'; + } + /** + * Merge standard menu items with plugin provided menus items + */ + public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions, $sTableId = null, $sDataTableId = null) + { + // 1st - add standard built-in menu items + // + switch($iMenuId) + { + case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT: + // $param is a DBObjectSet + $oAppContext = new ApplicationContext(); + $sContext = $oAppContext->GetForLink(); + $sDataTableId = is_null($sDataTableId) ? '' : $sDataTableId; + $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($param->GetFilter()->GetClass()); + $sOQL = addslashes($param->GetFilter()->ToOQL(true)); + $sFilter = urlencode($param->GetFilter()->serialize()); + $sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}"; + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + + $aResult = array( + new SeparatorPopupMenuItem(), + // Static menus: Email this page, CSV Export & Add to Dashboard + new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook + ); + + if (UserRights::IsActionAllowed($param->GetFilter()->GetClass(), UR_ACTION_BULK_READ, $param) != UR_ALLOWED_NO) + { + // Bulk export actions + $aResult[] = new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '$sDataTableId', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"); + $aResult[] = new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '$sDataTableId', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"); + if (extension_loaded('gd')) + { + // PDF export requires GD + $aResult[] = new JSPopupMenuItem('UI:Menu:ExportPDF', Dict::S('UI:Menu:ExportPDF'), "ExportListDlg('$sOQL', '$sDataTableId', 'pdf', ".json_encode(Dict::S('UI:Menu:ExportPDF')).")"); + } + } + $aResult[] = new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL', '$sContext')"); + $aResult[] = new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"); + + break; + + case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS: + // $param is a DBObject + $oObj = $param; + $sOQL = "SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey(); + $oFilter = DBObjectSearch::FromOQL($sOQL); + $sFilter = $oFilter->serialize(); + $sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey()); + $sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj)); + $oAppContext = new ApplicationContext(); + $sContext = $oAppContext->GetForLink(); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + + $aResult = array( + new SeparatorPopupMenuItem(), + // Static menus: Email this page & CSV Export + new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook + new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"), + new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"), + new SeparatorPopupMenuItem(), + new URLPopupMenuItem('UI:Menu:PrintableVersion', Dict::S('UI:Menu:PrintableVersion'), $sUrl.'&printable=1', '_blank'), + ); + break; + + case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS: + // $param is a Dashboard + $oAppContext = new ApplicationContext(); + $aParams = $oAppContext->GetAsHash(); + $sMenuId = ApplicationMenu::GetActiveNodeId(); + $sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle')); + $sDlgText = addslashes(Dict::S('UI:ImportDashboardText')); + $sCloseBtn = addslashes(Dict::S('UI:Button:Cancel')); + $aResult = array( + new SeparatorPopupMenuItem(), + new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId), + new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"), + ); + break; + + default: + // Unknown type of menu, do nothing + $aResult = array(); + } + foreach($aResult as $oMenuItem) + { + $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); + } + + // Invoke the plugins + // + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) + { + if (is_object($param) && !($param instanceof DBObject)) + { + $tmpParam = clone $param; // In case the parameter is an DBObjectSet, clone it to prevent alterations + } + else + { + $tmpParam = $param; + } + foreach($oExtensionInstance->EnumItems($iMenuId, $tmpParam) as $oMenuItem) + { + if (is_object($oMenuItem)) + { + $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); + + foreach($oMenuItem->GetLinkedScripts() as $sLinkedScript) + { + $oPage->add_linked_script($sLinkedScript); + } + } + } + } + } + /** + * Get target configuration file name (including full path) + */ + public static function GetConfigFilePath($sEnvironment = null) + { + if (is_null($sEnvironment)) + { + $sEnvironment = self::GetCurrentEnvironment(); + } + return APPCONF.$sEnvironment.'/'.ITOP_CONFIG_FILE; + } + + /** + * @return string the absolute URL to the modules root path + */ + static public function GetAbsoluteUrlModulesRoot() + { + $sUrl = self::GetAbsoluteUrlAppRoot().'env-'.self::GetCurrentEnvironment().'/'; + return $sUrl; + } + + /** + * To be compatible with this mechanism, the called page must include approot with an absolute path OR not include + * it at all (losing the direct access to the page) : + * + * ```php + * if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); + * require_once(__DIR__.'/../../approot.inc.php'); + * ``` + * + * @param string $sModule + * @param string $sPage + * @param string[] $aArguments + * @param string $sEnvironment + * + * @return string the URL to a page that will execute the requested module page, with query string values url encoded + * + * @see GetExecPageArguments can be used to submit using the GET method (see bug in N.1108) + * @see GetAbsoluteUrlExecPage + */ + static public function GetAbsoluteUrlModulePage($sModule, $sPage, $aArguments = array(), $sEnvironment = null) + { + $aArgs = self::GetExecPageArguments($sModule, $sPage, $aArguments, $sEnvironment); + $sArgs = http_build_query($aArgs); + + return self::GetAbsoluteUrlExecPage()."?".$sArgs; + } + + /** + * @param string $sModule + * @param string $sPage + * @param string[] $aArguments + * @param string $sEnvironment + * + * @return string[] key/value pair for the exec page query string. Warning : values are not url encoded ! + * @throws \Exception if one of the argument has a reserved name + */ + static public function GetExecPageArguments($sModule, $sPage, $aArguments = array(), $sEnvironment = null) + { + $sEnvironment = is_null($sEnvironment) ? self::GetCurrentEnvironment() : $sEnvironment; + $aArgs = array(); + $aArgs['exec_module'] = $sModule; + $aArgs['exec_page'] = $sPage; + $aArgs['exec_env'] = $sEnvironment; + foreach($aArguments as $sName => $sValue) + { + if (($sName == 'exec_module') || ($sName == 'exec_page') || ($sName == 'exec_env')) + { + throw new Exception("Module page: $sName is a reserved page argument name"); + } + $aArgs[$sName] = $sValue; + } + + return $aArgs; + } + + /** + * @return string + */ + static public function GetAbsoluteUrlExecPage() + { + return self::GetAbsoluteUrlAppRoot().'pages/exec.php'; + } + + /** + * Returns a name unique amongst the given list + * @param string $sProposed The default value + * @param array $aExisting An array of existing values (strings) + */ + static public function MakeUniqueName($sProposed, $aExisting) + { + if (in_array($sProposed, $aExisting)) + { + $i = 1; + while (in_array($sProposed.$i, $aExisting) && ($i < 50)) + { + $i++; + } + return $sProposed.$i; + } + else + { + return $sProposed; + } + } + + /** + * Some characters cause troubles with jQuery when used inside DOM IDs, so let's replace them by the safe _ (underscore) + * @param string $sId The ID to sanitize + * @return string The sanitized ID + */ + static public function GetSafeId($sId) + { + return str_replace(array(':', '[', ']', '+', '-'), '_', $sId); + } + + /** + * Helper to execute an HTTP POST request + * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl + * originaly named after do_post_request + * Does not require cUrl but requires openssl for performing https POSTs. + * + * @param string $sUrl The URL to POST the data to + * @param hash $aData The data to POST as an array('param_name' => value) + * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers + * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php + * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones. Example: CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3 + * @return string The result of the POST request + * @throws Exception + */ + static public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = array()) + { + // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request. + + if (function_exists('curl_init')) + { + // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options + // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 + // by setting the SSLVERSION to 3 as done below. + $aHeaders = explode("\n", $sOptionnalHeaders); + $aHTTPHeaders = array(); + foreach($aHeaders as $sHeaderString) + { + if(preg_match('/^([^:]): (.+)$/', $sHeaderString, $aMatches)) + { + $aHTTPHeaders[$aMatches[1]] = $aMatches[2]; + } + } + // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions + $aOptions = array( + CURLOPT_RETURNTRANSFER => true, // return the content of the request + CURLOPT_HEADER => false, // don't return the headers in the output + CURLOPT_FOLLOWLOCATION => true, // follow redirects + CURLOPT_ENCODING => "", // handle all encodings + CURLOPT_USERAGENT => "spider", // who am i + CURLOPT_AUTOREFERER => true, // set referer on redirect + CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect + CURLOPT_TIMEOUT => 120, // timeout on response + CURLOPT_MAXREDIRS => 10, // stop after 10 redirects + CURLOPT_SSL_VERIFYPEER => false, // Disabled SSL Cert checks + // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why + // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 + // CURLOPT_SSLVERSION => 3, + CURLOPT_POST => count($aData), + CURLOPT_POSTFIELDS => http_build_query($aData), + CURLOPT_HTTPHEADER => $aHTTPHeaders, + ); + + $aAllOptions = $aCurlOptions + $aOptions; + $ch = curl_init($sUrl); + curl_setopt_array($ch, $aAllOptions); + $response = curl_exec($ch); + $iErr = curl_errno($ch); + $sErrMsg = curl_error( $ch ); + $aHeaders = curl_getinfo( $ch ); + if ($iErr !== 0) + { + throw new Exception("Problem opening URL: $sUrl, $sErrMsg"); + } + if (is_array($aResponseHeaders)) + { + $aHeaders = curl_getinfo($ch); + foreach($aHeaders as $sCode => $sValue) + { + $sName = str_replace(' ' , '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type" + $aResponseHeaders[$sName] = $sValue; + } + } + curl_close( $ch ); + } + else + { + // cURL is not available let's try with streams and fopen... + + $sData = http_build_query($aData); + $aParams = array('http' => array( + 'method' => 'POST', + 'content' => $sData, + 'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n", + )); + if ($sOptionnalHeaders !== null) + { + $aParams['http']['header'] .= $sOptionnalHeaders; + } + $ctx = stream_context_create($aParams); + + $fp = @fopen($sUrl, 'rb', false, $ctx); + if (!$fp) + { + global $php_errormsg; + if (isset($php_errormsg)) + { + throw new Exception("Wrong URL: $sUrl, $php_errormsg"); + } + elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) + { + throw new Exception("Cannot connect to $sUrl: missing module 'openssl'"); + } + else + { + throw new Exception("Wrong URL: $sUrl"); + } + } + $response = @stream_get_contents($fp); + if ($response === false) + { + throw new Exception("Problem reading data from $sUrl, $php_errormsg"); + } + if (is_array($aResponseHeaders)) + { + $aMeta = stream_get_meta_data($fp); + $aHeaders = $aMeta['wrapper_data']; + foreach($aHeaders as $sHeaderString) + { + if(preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) + { + $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]); + } + } + } + } + return $response; + } + + /** + * Get a standard list of character sets + * + * @param array $aAdditionalEncodings Additional values + * @return array of iconv code => english label, sorted by label + */ + public static function GetPossibleEncodings($aAdditionalEncodings = array()) + { + // Encodings supported: + // ICONV_CODE => Display Name + // Each iconv installation supports different encodings + // Some reasonably common and useful encodings are listed here + $aPossibleEncodings = array( + 'UTF-8' => 'Unicode (UTF-8)', + 'ISO-8859-1' => 'Western (ISO-8859-1)', + 'WINDOWS-1251' => 'Cyrilic (Windows 1251)', + 'WINDOWS-1252' => 'Western (Windows 1252)', + 'ISO-8859-15' => 'Western (ISO-8859-15)', + ); + $aPossibleEncodings = array_merge($aPossibleEncodings, $aAdditionalEncodings); + asort($aPossibleEncodings); + return $aPossibleEncodings; + } + + /** + * Convert a string containing some (valid) HTML markup to plain text + * @param string $sHtml + * @return string + */ + public static function HtmlToText($sHtml) + { + try + { + //return ''.$sHtml; + return \Html2Text\Html2Text::convert(''.$sHtml); + } + catch(Exception $e) + { + return $e->getMessage(); + } + } + + /** + * Convert (?) plain text to some HTML markup by replacing newlines by
    tags + * and escaping HTML entities + * @param string $sText + * @return string + */ + public static function TextToHtml($sText) + { + $sText = str_replace("\r\n", "\n", $sText); + $sText = str_replace("\r", "\n", $sText); + return str_replace("\n", '
    ', htmlentities($sText, ENT_QUOTES, 'UTF-8')); + } + + /** + * Eventually compiles the SASS (.scss) file into the CSS (.css) file + * + * @param string $sSassRelPath Relative path to the SCSS file (must have the extension .scss) + * @param array $aImportPaths Array of absolute paths to load imports from + * @return string Relative path to the CSS file (.css) + */ + static public function GetCSSFromSASS($sSassRelPath, $aImportPaths = null) + { + // Avoiding compilation if file is already a css file. + if (preg_match('/\.css$/', $sSassRelPath)) + { + return $sSassRelPath; + } + + // Setting import paths + if ($aImportPaths === null) + { + $aImportPaths = array(); + } + $aImportPaths[] = APPROOT . '/css'; + + $sSassPath = APPROOT.$sSassRelPath; + $sCssRelPath = preg_replace('/\.scss$/', '.css', $sSassRelPath); + $sCssPath = APPROOT.$sCssRelPath; + clearstatcache(); + if (!file_exists($sCssPath) || (is_writable($sCssPath) && (filemtime($sCssPath) < filemtime($sSassPath)))) + { + require_once(APPROOT.'lib/scssphp/scss.inc.php'); + $oScss = new Compiler(); + $oScss->setImportPaths($aImportPaths); + $oScss->setFormatter('Leafo\\ScssPhp\\Formatter\\Expanded'); + // Temporary disabling max exec time while compiling + $iCurrentMaxExecTime = (int) ini_get('max_execution_time'); + set_time_limit(0); + $sCss = $oScss->compile(file_get_contents($sSassPath)); + set_time_limit($iCurrentMaxExecTime); + file_put_contents($sCssPath, $sCss); + } + return $sCssRelPath; + } + + static public function GetImageSize($sImageData) + { + if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher + { + $aRet = @getimagesizefromstring($sImageData); + } + else if(ini_get('allow_url_fopen')) + { + // work around to avoid creating a tmp file + $sUri = 'data://application/octet-stream;base64,'.base64_encode($sImageData); + $aRet = @getimagesize($sUri); + } + else + { + // Damned, need to create a tmp file + $sTempFile = tempnam(SetupUtils::GetTmpDir(), 'img-'); + @file_put_contents($sTempFile, $sImageData); + $aRet = @getimagesize($sTempFile); + @unlink($sTempFile); + } + return $aRet; + } + + /** + * Resize an image attachment so that it fits in the given dimensions + * @param ormDocument $oImage The original image stored as an ormDocument + * @param int $iWidth Image's original width + * @param int $iHeight Image's original height + * @param int $iMaxImageWidth Maximum width for the resized image + * @param int $iMaxImageHeight Maximum height for the resized image + * @return ormDocument The resampled image + */ + public static function ResizeImageToFit(ormDocument $oImage, $iWidth, $iHeight, $iMaxImageWidth, $iMaxImageHeight) + { + // If image size smaller than maximums, we do nothing + if (($iWidth <= $iMaxImageWidth) && ($iHeight <= $iMaxImageHeight)) + { + return $oImage; + } + + + // If gd extension is not loaded, we put a warning in the log and return the image as is + if (extension_loaded('gd') === false) + { + IssueLog::Warning('Image could not be resized as the "gd" extension does not seem to be loaded. It will remain as ' . $iWidth . 'x' . $iHeight . ' instead of ' . $iMaxImageWidth . 'x' . $iMaxImageHeight); + return $oImage; + } + + + switch($oImage->GetMimeType()) + { + case 'image/gif': + case 'image/jpeg': + case 'image/png': + $img = @imagecreatefromstring($oImage->GetData()); + break; + + default: + // Unsupported image type, return the image as-is + //throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); + return $oImage; + } + if ($img === false) + { + //throw new Exception("Warning: corrupted image: '".$oImage->GetFileName()." / ".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); + return $oImage; + } + else + { + // Let's scale the image, preserving the transparency for GIFs and PNGs + + $fScale = min($iMaxImageWidth / $iWidth, $iMaxImageHeight / $iHeight); + + $iNewWidth = $iWidth * $fScale; + $iNewHeight = $iHeight * $fScale; + + $new = imagecreatetruecolor($iNewWidth, $iNewHeight); + + // Preserve transparency + if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png")) + { + imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127)); + imagealphablending($new, false); + imagesavealpha($new, true); + } + + imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight); + + ob_start(); + switch ($oImage->GetMimeType()) + { + case 'image/gif': + imagegif($new); // send image to output buffer + break; + + case 'image/jpeg': + imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality + break; + + case 'image/png': + imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression + break; + } + $oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName()); + @ob_end_clean(); + + imagedestroy($img); + imagedestroy($new); + + return $oResampledImage; + } + + } + + /** + * Create a 128 bit UUID in the format: {########-####-####-####-############} + * + * Note: this method can be run from the command line as well as from the web server. + * Note2: this method is not cryptographically secure! If you need a cryptographically secure value + * consider using open_ssl or PHP 7 methods. + * @param string $sPrefix + * @return string + */ + static public function CreateUUID($sPrefix = '') + { + $uid = uniqid("", true); + $data = $sPrefix; + $data .= __FILE__; + $data .= mt_rand(); + $hash = strtoupper(hash('ripemd128', $uid . md5($data))); + $sUUID = '{' . + substr($hash, 0, 8) . + '-' . + substr($hash, 8, 4) . + '-' . + substr($hash, 12, 4) . + '-' . + substr($hash, 16, 4) . + '-' . + substr($hash, 20, 12) . + '}'; + return $sUUID; + } + + /** + * Returns the name of the module containing the file where the call to this function is made + * or an empty string if no such module is found (or not called within a module file) + * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module + * @return string + */ + static public function GetCurrentModuleName($iCallDepth = 0) + { + $sCurrentModuleName = ''; + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); + + foreach(GetModulesInfo() as $sModuleName => $aInfo) + { + if ($aInfo['root_dir'] !== '') + { + $sRootDir = realpath(APPROOT.$aInfo['root_dir']); + + if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) + { + $sCurrentModuleName = $sModuleName; + break; + } + } + } + return $sCurrentModuleName; + } + + /** + * Returns the relative (to APPROOT) path of the root directory of the module containing the file where the call to this function is made + * or an empty string if no such module is found (or not called within a module file) + * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module + * @return string + */ + static public function GetCurrentModuleDir($iCallDepth) + { + $sCurrentModuleDir = ''; + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); + + foreach(GetModulesInfo() as $sModuleName => $aInfo) + { + if ($aInfo['root_dir'] !== '') + { + $sRootDir = realpath(APPROOT.$aInfo['root_dir']); + + if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) + { + $sCurrentModuleDir = basename($sRootDir); + break; + } + } + } + return $sCurrentModuleDir; + } + + /** + * Returns the base URL for all files in the current module from which this method is called + * or an empty string if no such module is found (or not called within a module file) + * @return string + */ + static public function GetCurrentModuleUrl() + { + $sDir = static::GetCurrentModuleDir(1); + if ( $sDir !== '') + { + return static::GetAbsoluteUrlModulesRoot().'/'.$sDir; + } + return ''; + } + + /** + * Get the value of a given setting for the current module + * @param string $sProperty The name of the property to retrieve + * @param mixed $defaultvalue + * @return mixed + */ + static public function GetCurrentModuleSetting($sProperty, $defaultvalue = null) + { + $sModuleName = static::GetCurrentModuleName(1); + return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue); + } + + /** + * Get the compiled version of a given module, as it was seen by the compiler + * @param string $sModuleName + * @return string|NULL + */ + static public function GetCompiledModuleVersion($sModuleName) + { + $aModulesInfo = GetModulesInfo(); + if (array_key_exists($sModuleName, $aModulesInfo)) + { + return $aModulesInfo[$sModuleName]['version']; + } + return null; + } + + /** + * Check if the given path/url is an http(s) URL + * @param string $sPath + * @return boolean + */ + public static function IsURL($sPath) + { + $bRet = false; + if ((substr($sPath, 0, 7) == 'http://') || (substr($sPath, 0, 8) == 'https://') || (substr($sPath, 0, 8) == 'ftp://')) + { + $bRet = true; + } + return $bRet; + } + + /** + * Check if the given URL is a link to download a document/image on the CURRENT iTop + * In such a case we can read the content of the file directly in the database (if the users rights allow) and return the ormDocument + * @param string $sPath + * @return false|ormDocument + * @throws Exception + */ + public static function IsSelfURL($sPath) + { + $result = false; + $sPageUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php'; + if (substr($sPath, 0, strlen($sPageUrl)) == $sPageUrl) + { + // If the URL is an URL pointing to this instance of iTop, then + // extract the "query" part of the URL and analyze it + $sQuery = parse_url($sPath, PHP_URL_QUERY); + if ($sQuery !== null) + { + $aParams = array(); + foreach(explode('&', $sQuery) as $sChunk) + { + $aParts = explode('=', $sChunk); + if (count($aParts) != 2) continue; + $aParams[$aParts[0]] = urldecode($aParts[1]); + } + $result = array_key_exists('operation', $aParams) && array_key_exists('class', $aParams) && array_key_exists('id', $aParams) && array_key_exists('field', $aParams) && ($aParams['operation'] == 'download_document'); + if ($result) + { + // This is a 'download_document' operation, let's retrieve the document directly from the database + $sClass = $aParams['class']; + $iKey = $aParams['id']; + $sAttCode = $aParams['field']; + + $oObj = MetaModel::GetObject($sClass, $iKey, false /* must exist */); // Users rights apply here !! + if ($oObj) + { + /** + * @var ormDocument $result + */ + $result = clone $oObj->Get($sAttCode); + return $result; + } + } + } + throw new Exception('Invalid URL. This iTop URL is not pointing to a valid Document/Image.'); + } + return $result; + } + + /** + * Read the content of a file (and retrieve its MIME type) from either: + * - an URL pointing to a blob (image/document) on the current iTop server + * - an http(s) URL + * - the local file system (but only if you are an administrator) + * @param string $sPath + * @return ormDocument|null + * @throws Exception + */ + public static function FileGetContentsAndMIMEType($sPath) + { + $oUploadedDoc = null; + $aKnownExtensions = array( + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'pdf' => 'application/pdf', + 'doc' => 'application/msword', + 'dot' => 'application/msword', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'vsd' => 'application/x-visio', + 'vdx' => 'application/visio.drawing', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'zip' => 'application/zip', + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'exe' => 'application/octet-stream' + ); + + $sData = null; + $sMimeType = 'text/plain'; // Default MIME Type: treat the file as a bunch a characters... + $sFileName = 'uploaded-file'; // Default name for downloaded-files + $sExtension = '.txt'; // Default file extension in case we don't know the MIME Type + + if(empty($sPath)) + { + // Empty path (NULL or '') means that there is no input, making an empty document. + $oUploadedDoc = new ormDocument('', '', ''); + } + elseif (static::IsURL($sPath)) + { + if ($oUploadedDoc = static::IsSelfURL($sPath)) + { + // Nothing more to do, we've got it !! + } + else + { + // Remote file, let's use the HTTP headers to find the MIME Type + $sData = @file_get_contents($sPath); + if ($sData === false) + { + throw new Exception("Failed to load the file from the URL '$sPath'."); + } + else + { + if (isset($http_response_header)) + { + $aHeaders = static::ParseHeaders($http_response_header); + $sMimeType = array_key_exists('Content-Type', $aHeaders) ? strtolower($aHeaders['Content-Type']) : 'application/x-octet-stream'; + // Compute the file extension from the MIME Type + foreach($aKnownExtensions as $sExtValue => $sMime) + { + if ($sMime === $sMimeType) + { + $sExtension = '.'.$sExtValue; + break; + } + } + } + $sFileName .= $sExtension; + } + $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); + } + } + else if (UserRights::IsAdministrator()) + { + // Only administrators are allowed to read local files + $sData = @file_get_contents($sPath); + if ($sData === false) + { + throw new Exception("Failed to load the file '$sPath'. The file does not exist or the current process is not allowed to access it."); + } + $sExtension = strtolower(pathinfo($sPath, PATHINFO_EXTENSION)); + $sFileName = basename($sPath); + + if (array_key_exists($sExtension, $aKnownExtensions)) + { + $sMimeType = $aKnownExtensions[$sExtension]; + } + else if (extension_loaded('fileinfo')) + { + $finfo = new finfo(FILEINFO_MIME); + $sMimeType = $finfo->file($sPath); + } + $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); + } + return $oUploadedDoc; + } + + protected static function ParseHeaders($aHeaders) + { + $aCleanHeaders = array(); + foreach( $aHeaders as $sKey => $sValue ) + { + $aTokens = explode(':', $sValue, 2); + if(isset($aTokens[1])) + { + $aCleanHeaders[trim($aTokens[0])] = trim($aTokens[1]); + } + else + { + // The header is not in the form Header-Code: Value + $aCleanHeaders[] = $sValue; // Store the value as-is + $aMatches = array(); + // Check if it's not the HTTP response code + if( preg_match("|HTTP/[0-9\.]+\s+([0-9]+)|", $sValue, $aMatches) ) + { + $aCleanHeaders['reponse_code'] = intval($aMatches[1]); + } + } + } + return $aCleanHeaders; + } + + /** + * Return a string based on compilation time or (if not available because the datamodel has not been loaded) + * the version of iTop. This string is useful to prevent browser side caching of content that may vary at each + * (re)installation of iTop (especially during development). + * @return string + */ + public static function GetCacheBusterTimestamp() + { + if(!defined('COMPILATION_TIMESTAMP')) + { + return ITOP_VERSION; + } + return COMPILATION_TIMESTAMP; + } + + /** + * Check if the given class if configured as a high cardinality class. + * + * @param $sClass + * + * @return bool + */ + public static function IsHighCardinality($sClass) + { + if (utils::GetConfig()->Get('search_manual_submit')) + { + return true; + } + $aHugeClasses = MetaModel::GetConfig()->Get('high_cardinality_classes'); + return in_array($sClass, $aHugeClasses); + } +} diff --git a/application/webpage.class.inc.php b/application/webpage.class.inc.php index 8a7cd06e7..b7224ca4b 100644 --- a/application/webpage.class.inc.php +++ b/application/webpage.class.inc.php @@ -1,1243 +1,1243 @@ - - - -/** - * Class WebPage - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * Generic interface common to CLI and Web pages - */ -Interface Page -{ - public function output(); - - public function add($sText); - - public function p($sText); - - public function pre($sText); - - public function add_comment($sText); - - public function table($aConfig, $aData, $aParams = array()); -} - - -/** - *

    Simple helper class to ease the production of HTML pages - * - *

    This class provide methods to add content, scripts, includes... to a web page - * and renders the full web page by putting the elements in the proper place & order - * when the output() method is called. - * - *

    Usage: - * ```php - * $oPage = new WebPage("Title of my page"); - * $oPage->p("Hello World !"); - * $oPage->output(); - * ``` - */ -class WebPage implements Page -{ - protected $s_title; - protected $s_content; - protected $s_deferred_content; - protected $a_scripts; - protected $a_dict_entries; - protected $a_dict_entries_prefixes; - protected $a_styles; - protected $a_linked_scripts; - protected $a_linked_stylesheets; - protected $a_headers; - protected $a_base; - protected $iNextId; - protected $iTransactionId; - protected $sContentType; - protected $sContentDisposition; - protected $sContentFileName; - protected $bTrashUnexpectedOutput; - protected $s_sOutputFormat; - protected $a_OutputOptions; - protected $bPrintable; - - public function __construct($s_title, $bPrintable = false) - { - $this->s_title = $s_title; - $this->s_content = ""; - $this->s_deferred_content = ''; - $this->a_scripts = array(); - $this->a_dict_entries = array(); - $this->a_dict_entries_prefixes = array(); - $this->a_styles = array(); - $this->a_linked_scripts = array(); - $this->a_linked_stylesheets = array(); - $this->a_headers = array(); - $this->a_base = array('href' => '', 'target' => ''); - $this->iNextId = 0; - $this->iTransactionId = 0; - $this->sContentType = ''; - $this->sContentDisposition = ''; - $this->sContentFileName = ''; - $this->bTrashUnexpectedOutput = false; - $this->s_OutputFormat = utils::ReadParam('output_format', 'html'); - $this->a_OutputOptions = array(); - $this->bPrintable = $bPrintable; - ob_start(); // Start capturing the output - } - - /** - * Change the title of the page after its creation - */ - public function set_title($s_title) - { - $this->s_title = $s_title; - } - - /** - * Specify a default URL and a default target for all links on a page - */ - public function set_base($s_href = '', $s_target = '') - { - $this->a_base['href'] = $s_href; - $this->a_base['target'] = $s_target; - } - - /** - * Add any text or HTML fragment to the body of the page - */ - public function add($s_html) - { - $this->s_content .= $s_html; - } - - /** - * Add any text or HTML fragment (identified by an ID) at the end of the body of the page - * This is useful to add hidden content, DIVs or FORMs that should not - * be embedded into each other. - */ - public function add_at_the_end($s_html, $sId = '') - { - $this->s_deferred_content .= $s_html; - } - - /** - * Add a paragraph to the body of the page - */ - public function p($s_html) - { - $this->add($this->GetP($s_html)); - } - - /** - * Add a pre-formatted text to the body of the page - */ - public function pre($s_html) - { - $this->add('

    '.$s_html.'
    '); - } - - /** - * Add a comment - */ - public function add_comment($sText) - { - $this->add(''); - } - - /** - * Add a paragraph to the body of the page - */ - public function GetP($s_html) - { - return "

    $s_html

    \n"; - } - - /** - * Adds a tabular content to the web page - * - * @param string[] $aConfig Configuration of the table: hash array of 'column_id' => 'Column Label' - * @param string[] $aData Hash array. Data to display in the table: each row is made of 'column_id' => Data. A - * column 'pkey' is expected for each row - * @param array $aParams Hash array. Extra parameters for the table. - * - * @return void - */ - public function table($aConfig, $aData, $aParams = array()) - { - $this->add($this->GetTable($aConfig, $aData, $aParams)); - } - - public function GetTable($aConfig, $aData, $aParams = array()) - { - $oAppContext = new ApplicationContext(); - - static $iNbTables = 0; - $iNbTables++; - $sHtml = ""; - $sHtml .= "\n"; - $sHtml .= "\n"; - $sHtml .= "\n"; - foreach ($aConfig as $sName => $aDef) - { - $sHtml .= "\n"; - } - $sHtml .= "\n"; - $sHtml .= "\n"; - $sHtml .= "\n"; - foreach ($aData as $aRow) - { - $sHtml .= $this->GetTableRow($aRow, $aConfig); - } - $sHtml .= "\n"; - $sHtml .= "
    ".$aDef['label']."
    \n"; - - return $sHtml; - } - - public function GetTableRow($aRow, $aConfig) - { - $sHtml = ''; - if (isset($aRow['@class'])) // Row specific class, for hilighting certain rows - { - $sHtml .= ""; - } - else - { - $sHtml .= ""; - } - foreach ($aConfig as $sName => $aAttribs) - { - $sClass = isset($aAttribs['class']) ? 'class="'.$aAttribs['class'].'"' : ''; - $sValue = ($aRow[$sName] === '') ? ' ' : $aRow[$sName]; - $sHtml .= "$sValue"; - } - $sHtml .= ""; - - return $sHtml; - } - - /** - * Add some Javascript to the header of the page - */ - public function add_script($s_script) - { - $this->a_scripts[] = $s_script; - } - - /** - * Add some Javascript to the header of the page - */ - public function add_ready_script($s_script) - { - // Do nothing silently... this is not supported by this type of page... - } - - /** - * Allow a dictionnary entry to be used client side with Dict.S() - * - * @param string $s_entryId a translation label key - * - * @see \WebPage::add_dict_entries() - * @see utils.js - */ - public function add_dict_entry($s_entryId) - { - $this->a_dict_entries[] = $s_entryId; - } - - /** - * Add a set of dictionary entries (based on the given prefix) for the Javascript side - * - * @param string $s_entriesPrefix translation label prefix (eg 'UI:Button:' to add all keys beginning with this) - * - * @see \WebPage::add_dict_entry() - * @see utils.js - */ - public function add_dict_entries($s_entriesPrefix) - { - $this->a_dict_entries_prefixes[] = $s_entriesPrefix; - } - - protected function get_dict_signature() - { - return str_replace('_', '', Dict::GetUserLanguage()).'-'.md5(implode(',', - $this->a_dict_entries).'|'.implode(',', $this->a_dict_entries_prefixes)); - } - - protected function get_dict_file_content() - { - $aEntries = array(); - foreach ($this->a_dict_entries as $sCode) - { - $aEntries[$sCode] = Dict::S($sCode); - } - foreach ($this->a_dict_entries_prefixes as $sPrefix) - { - $aEntries = array_merge($aEntries, Dict::ExportEntries($sPrefix)); - } - $sJSFile = 'var aDictEntries = '.json_encode($aEntries); - - return $sJSFile; - } - - - /** - * Add some CSS definitions to the header of the page - */ - public function add_style($s_style) - { - $this->a_styles[] = $s_style; - } - - /** - * Add a script (as an include, i.e. link) to the header of the page - */ - public function add_linked_script($s_linked_script) - { - $this->a_linked_scripts[$s_linked_script] = $s_linked_script; - } - - /** - * Add a CSS stylesheet (as an include, i.e. link) to the header of the page - */ - public function add_linked_stylesheet($s_linked_stylesheet, $s_condition = "") - { - $this->a_linked_stylesheets[] = array('link' => $s_linked_stylesheet, 'condition' => $s_condition); - } - - public function add_saas($sSaasRelPath) - { - $sCssRelPath = utils::GetCSSFromSASS($sSaasRelPath); - $sRootUrl = utils::GetAbsoluteUrlAppRoot(); - if ($sRootUrl === '') - { - // We're running the setup of the first install... - $sRootUrl = '../'; - } - $sCSSUrl = $sRootUrl.$sCssRelPath; - $this->add_linked_stylesheet($sCSSUrl); - } - - /** - * Add some custom header to the page - */ - public function add_header($s_header) - { - $this->a_headers[] = $s_header; - } - - /** - * Add needed headers to the page so that it will no be cached - */ - public function no_cache() - { - $this->add_header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 - $this->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past - } - - /** - * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data - */ - public function details($aFields) - { - - $this->add($this->GetDetails($aFields)); - } - - /** - * Whether or not the page is a PDF page - * - * @return boolean - */ - public function is_pdf() - { - return false; - } - - /** - * Records the current state of the 'html' part of the page output - * - * @return mixed The current state of the 'html' output - */ - public function start_capture() - { - return strlen($this->s_content); - } - - /** - * Returns the part of the html output that occurred since the call to start_capture - * and removes this part from the current html output - * - * @param $offset mixed The value returned by start_capture - * - * @return string The part of the html output that was added since the call to start_capture - */ - public function end_capture($offset) - { - $sCaptured = substr($this->s_content, $offset); - $this->s_content = substr($this->s_content, 0, $offset); - - return $sCaptured; - } - - /** - * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data - */ - public function GetDetails($aFields) - { - $sHtml = "
    \n"; - foreach ($aFields as $aAttrib) - { - $sDataAttCode = isset($aAttrib['attcode']) ? "data-attcode=\"{$aAttrib['attcode']}\"" : ''; - $sLayout = isset($aAttrib['layout']) ? $aAttrib['layout'] : 'small'; - $sHtml .= "
    \n"; - $sHtml .= "
    {$aAttrib['label']}
    \n"; - - $sHtml .= "
    \n"; - // By Rom, for csv import, proposed to show several values for column selection - if (is_array($aAttrib['value'])) - { - $sHtml .= "
    ".implode("
    ", $aAttrib['value'])."
    \n"; - } - else - { - $sHtml .= "
    ".$aAttrib['value']."
    \n"; - } - // Checking if we should add comments & infos - $sComment = (isset($aAttrib['comments'])) ? $aAttrib['comments'] : ''; - $sInfo = (isset($aAttrib['infos'])) ? $aAttrib['infos'] : ''; - if ($sComment !== '') - { - $sHtml .= "
    $sComment
    \n"; - } - if ($sInfo !== '') - { - $sHtml .= "
    $sInfo
    \n"; - } - $sHtml .= "
    \n"; - - $sHtml .= "
    \n"; - } - $sHtml .= "
    \n"; - - return $sHtml; - } - - /** - * Build a set of radio buttons suitable for editing a field/attribute of an object (including its validation) - * - * @param $aAllowedValues hash Array of value => display_value - * @param $value mixed Current value for the field/attribute - * @param $iId mixed Unique Id for the input control in the page - * @param $sFieldName string The name of the field, attr_<$sFieldName> will hold the value for the field - * @param $bMandatory bool Whether or not the field is mandatory - * @param $bVertical bool Disposition of the radio buttons vertical or horizontal - * @param $sValidationField string HTML fragment holding the validation field (exclamation icon...) - * - * @return string The HTML fragment corresponding to the radio buttons - */ - public function GetRadioButtons( - $aAllowedValues, $value, $iId, $sFieldName, $bMandatory, $bVertical, $sValidationField - ) { - $idx = 0; - $sHTMLValue = ''; - foreach ($aAllowedValues as $key => $display_value) - { - if ((count($aAllowedValues) == 1) && ($bMandatory == 'true')) - { - // When there is only once choice, select it by default - $sSelected = ' checked'; - } - else - { - $sSelected = ($value == $key) ? ' checked' : ''; - } - $sHTMLValue .= " "; - if ($bVertical) - { - if ($idx == 0) - { - // Validation icon at the end of the first line - $sHTMLValue .= " {$sValidationField}\n"; - } - $sHTMLValue .= "
    \n"; - } - $idx++; - } - $sHTMLValue .= ""; - if (!$bVertical) - { - // Validation icon at the end of the line - $sHTMLValue .= " {$sValidationField}\n"; - } - - return $sHTMLValue; - } - - /** - * Discard unexpected output data (such as PHP warnings) - * This is a MUST when the Page output is DATA (download of a document, download CSV export, download ...) - */ - public function TrashUnexpectedOutput() - { - $this->bTrashUnexpectedOutput = true; - } - - /** - * Read the output buffer and deal with its contents: - * - trash unexpected output if the flag has been set - * - report unexpected behaviors such as the output buffering being stopped - * - * Possible improvement: I've noticed that several output buffers are stacked, - * if they are not empty, the output will be corrupted. The solution would - * consist in unstacking all of them (and concatenate the contents). - */ - protected function ob_get_clean_safe() - { - $sOutput = ob_get_contents(); - if ($sOutput === false) - { - $sMsg = "Design/integration issue: No output buffer. Some piece of code has called ob_get_clean() or ob_end_clean() without calling ob_start()"; - if ($this->bTrashUnexpectedOutput) - { - IssueLog::Error($sMsg); - $sOutput = ''; - } - else - { - $sOutput = $sMsg; - } - } - else - { - ob_end_clean(); // on some versions of PHP doing so when the output buffering is stopped can cause a notice - if ($this->bTrashUnexpectedOutput) - { - if (trim($sOutput) != '') - { - if (Utils::GetConfig() && Utils::GetConfig()->Get('debug_report_spurious_chars')) - { - IssueLog::Error("Trashing unexpected output:'$sOutput'\n"); - } - } - $sOutput = ''; - } - } - - return $sOutput; - } - - /** - * Outputs (via some echo) the complete HTML page by assembling all its elements - */ - public function output() - { - foreach ($this->a_headers as $s_header) - { - header($s_header); - } - - $s_captured_output = $this->ob_get_clean_safe(); - echo "\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo ""; - echo "".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."\n"; - echo $this->get_base_tag(); - - $this->output_dict_entries(); - - foreach ($this->a_linked_scripts as $s_script) - { - // Make sure that the URL to the script contains the application's version number - // so that the new script do NOT get reloaded from the cache when the application is upgraded - if (strpos($s_script, '?') === false) - { - $s_script .= "?t=".utils::GetCacheBusterTimestamp(); - } - else - { - $s_script .= "&t=".utils::GetCacheBusterTimestamp(); - } - echo "\n"; - } - if (count($this->a_scripts) > 0) - { - echo "\n"; - } - foreach ($this->a_linked_stylesheets as $a_stylesheet) - { - if (strpos($a_stylesheet['link'], '?') === false) - { - $s_stylesheet = $a_stylesheet['link']."?t=".utils::GetCacheBusterTimestamp(); - } - else - { - $s_stylesheet = $a_stylesheet['link']."&t=".utils::GetCacheBusterTimestamp(); - } - if ($a_stylesheet['condition'] != "") - { - echo "\n"; - } - } - - if (count($this->a_styles) > 0) - { - echo "\n"; - } - if (class_exists('MetaModel') && MetaModel::GetConfig()) - { - echo "\n"; - } - echo "\n"; - echo "\n"; - echo self::FilterXSS($this->s_content); - if (trim($s_captured_output) != "") - { - echo "
    ".self::FilterXSS($s_captured_output)."
    \n"; - } - echo '
    '.self::FilterXSS($this->s_deferred_content).'
    '; - echo "\n"; - echo "\n"; - - if (class_exists('DBSearch')) - { - DBSearch::RecordQueryTrace(); - } - if (class_exists('ExecutionKPI')) - { - ExecutionKPI::ReportStats(); - } - } - - /** - * Build a series of hidden field[s] from an array - */ - public function add_input_hidden($sLabel, $aData) - { - foreach ($aData as $sKey => $sValue) - { - // Note: protection added to protect against the Notice 'array to string conversion' that appeared with PHP 5.4 - // (this function seems unused though!) - if (is_scalar($sValue)) - { - $this->add(""); - } - } - } - - protected function get_base_tag() - { - $sTag = ''; - if (($this->a_base['href'] != '') || ($this->a_base['target'] != '')) - { - $sTag = 'a_base['href'] != '')) - { - $sTag .= "href =\"{$this->a_base['href']}\" "; - } - if (($this->a_base['target'] != '')) - { - $sTag .= "target =\"{$this->a_base['target']}\" "; - } - $sTag .= " />\n"; - } - - return $sTag; - } - - /** - * Get an ID (for any kind of HTML tag) that is guaranteed unique in this page - * - * @return int The unique ID (in this page) - */ - public function GetUniqueId() - { - return $this->iNextId++; - } - - /** - * Set the content-type (mime type) for the page's content - * - * @param $sContentType string - * - * @return void - */ - public function SetContentType($sContentType) - { - $this->sContentType = $sContentType; - } - - /** - * Set the content-disposition (mime type) for the page's content - * - * @param $sDisposition string The disposition: 'inline' or 'attachment' - * @param $sFileName string The original name of the file - * - * @return void - */ - public function SetContentDisposition($sDisposition, $sFileName) - { - $this->sContentDisposition = $sDisposition; - $this->sContentFileName = $sFileName; - } - - /** - * Set the transactionId of the current form - * - * @param $iTransactionId integer - * - * @return void - */ - public function SetTransactionId($iTransactionId) - { - $this->iTransactionId = $iTransactionId; - } - - /** - * Returns the transactionId of the current form - * - * @return integer The current transactionID - */ - public function GetTransactionId() - { - return $this->iTransactionId; - } - - public static function FilterXSS($sHTML) - { - return str_ireplace('s_OutputFormat; - } - - /** - * Check whether the desired output format is possible or not - * - * @param string $sOutputFormat The desired output format: html, pdf... - * - * @return bool True if the format is Ok, false otherwise - */ - function IsOutputFormatAvailable($sOutputFormat) - { - $bResult = false; - switch ($sOutputFormat) - { - case 'html': - $bResult = true; // Always supported - break; - - case 'pdf': - $bResult = @is_readable(APPROOT.'lib/MPDF/mpdf.php'); - break; - } - - return $bResult; - } - - /** - * Check whether the output must be printable (using print.css, for sure!) - * - * @return bool ... - */ - public function IsPrintableVersion() - { - return $this->bPrintable; - } - - /** - * Retrieves the value of a named output option for the given format - * - * @param string $sFormat The format: html or pdf - * @param string $sOptionName The name of the option - * - * @return mixed false if the option was never set or the options's value - */ - public function GetOutputOption($sFormat, $sOptionName) - { - if (isset($this->a_OutputOptions[$sFormat][$sOptionName])) - { - return $this->a_OutputOptions[$sFormat][$sOptionName]; - } - - return false; - } - - /** - * Sets a named output option for the given format - * - * @param string $sFormat The format for which to set the option: html or pdf - * @param string $sOptionName the name of the option - * @param mixed $sValue The value of the option - */ - public function SetOutputOption($sFormat, $sOptionName, $sValue) - { - if (!isset($this->a_OutputOptions[$sFormat])) - { - $this->a_OutputOptions[$sFormat] = array($sOptionName => $sValue); - } - else - { - $this->a_OutputOptions[$sFormat][$sOptionName] = $sValue; - } - } - - public function RenderPopupMenuItems($aActions, $aFavoriteActions = array()) - { - $sPrevUrl = ''; - $sHtml = ''; - if (!$this->IsPrintableVersion()) - { - foreach ($aActions as $aAction) - { - $sClass = isset($aAction['css_classes']) ? ' class="'.implode(' ', $aAction['css_classes']).'"' : ''; - $sOnClick = isset($aAction['onclick']) ? ' onclick="'.htmlspecialchars($aAction['onclick'], ENT_QUOTES, - "UTF-8").'"' : ''; - $sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : ""; - if (empty($aAction['url'])) - { - if ($sPrevUrl != '') // Don't output consecutively two separators... - { - $sHtml .= "
  • {$aAction['label']}
  • "; - } - $sPrevUrl = ''; - } - else - { - $sHtml .= "
  • {$aAction['label']}
  • "; - $sPrevUrl = $aAction['url']; - } - } - $sHtml .= "
    "; - foreach (array_reverse($aFavoriteActions) as $aAction) - { - $sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : ""; - $sHtml .= ""; - } - } - - return $sHtml; - } - - protected function output_dict_entries($bReturnOutput = false) - { - if ((count($this->a_dict_entries) > 0) || (count($this->a_dict_entries_prefixes) > 0)) - { - if (class_exists('Dict')) - { - // The dictionary may not be available for example during the setup... - // Create a specific dictionary file and load it as a JS script - $sSignature = $this->get_dict_signature(); - $sJSFileName = utils::GetCachePath().$sSignature.'.js'; - if (!file_exists($sJSFileName) && is_writable(utils::GetCachePath())) - { - file_put_contents($sJSFileName, $this->get_dict_file_content()); - } - // Load the dictionary as the first javascript file, so that other JS file benefit from the translations - array_unshift($this->a_linked_scripts, - utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=dict&s='.$sSignature); - } - } - } -} - - -interface iTabbedPage -{ - public function AddTabContainer($sTabContainer, $sPrefix = ''); - - public function AddToTab($sTabContainer, $sTabLabel, $sHtml); - - public function SetCurrentTabContainer($sTabContainer = ''); - - public function SetCurrentTab($sTabLabel = ''); - - /** - * Add a tab which content will be loaded asynchronously via the supplied URL - * - * Limitations: - * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to - * pull content from another server. Static content cannot be added inside such tabs. - * - * @param string $sTabLabel The (localised) label of the tab - * @param string $sUrl The URL to load (on the same server) - * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause - * the tab to be reloaded upon each activation. - * - * @since 2.0.3 - */ - public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true); - - public function GetCurrentTab(); - - public function RemoveTab($sTabLabel, $sTabContainer = null); - - /** - * Finds the tab whose title matches a given pattern - * - * @return mixed The name of the tab as a string or false if not found - */ - public function FindTab($sPattern, $sTabContainer = null); -} - -/** - * Helper class to implement JQueryUI tabs inside a page - */ -class TabManager -{ - protected $m_aTabs; - protected $m_sCurrentTabContainer; - protected $m_sCurrentTab; - - public function __construct() - { - $this->m_aTabs = array(); - $this->m_sCurrentTabContainer = ''; - $this->m_sCurrentTab = ''; - } - - public function AddTabContainer($sTabContainer, $sPrefix = '') - { - $this->m_aTabs[$sTabContainer] = array('prefix' => $sPrefix, 'tabs' => array()); - - return "\$Tabs:$sTabContainer\$"; - } - - public function AddToCurrentTab($sHtml) - { - $this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml); - } - - public function GetCurrentTabLength($sHtml) - { - $iLength = isset($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) : 0; - - return $iLength; - } - - /** - * Truncates the given tab to the specifed length and returns the truncated part - * - * @param string $sTabContainer The tab container in which to truncate the tab - * @param string $sTab The name/identifier of the tab to truncate - * @param integer $iLength The length/offset at which to truncate the tab - * - * @return string The truncated part - */ - public function TruncateTab($sTabContainer, $sTab, $iLength) - { - $sResult = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], - $iLength); - $this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'] = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], - 0, $iLength); - - return $sResult; - } - - public function TabExists($sTabContainer, $sTab) - { - return isset($this->m_aTabs[$sTabContainer]['tabs'][$sTab]); - } - - public function TabsContainerCount() - { - return count($this->m_aTabs); - } - - public function AddToTab($sTabContainer, $sTabLabel, $sHtml) - { - if (!isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel])) - { - // Set the content of the tab - $this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel] = array( - 'type' => 'html', - 'html' => $sHtml, - ); - } - else - { - if ($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type'] != 'html') - { - throw new Exception("Cannot add HTML content to the tab '$sTabLabel' of type '{$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type']}'"); - } - // Append to the content of the tab - $this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['html'] .= $sHtml; - } - - return ''; // Nothing to add to the page for now - } - - public function SetCurrentTabContainer($sTabContainer = '') - { - $sPreviousTabContainer = $this->m_sCurrentTabContainer; - $this->m_sCurrentTabContainer = $sTabContainer; - - return $sPreviousTabContainer; - } - - public function SetCurrentTab($sTabLabel = '') - { - $sPreviousTab = $this->m_sCurrentTab; - $this->m_sCurrentTab = $sTabLabel; - - return $sPreviousTab; - } - - /** - * Add a tab which content will be loaded asynchronously via the supplied URL - * - * Limitations: - * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to - * pull content from another server. Static content cannot be added inside such tabs. - * - * @param string $sTabLabel The (localised) label of the tab - * @param string $sUrl The URL to load (on the same server) - * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause - * the tab to be reloaded upon each activation. - * - * @since 2.0.3 - */ - public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) - { - // Set the content of the tab - $this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$sTabLabel] = array( - 'type' => 'ajax', - 'url' => $sUrl, - 'cache' => $bCache, - ); - - return ''; // Nothing to add to the page for now - } - - - public function GetCurrentTabContainer() - { - return $this->m_sCurrentTabContainer; - } - - public function GetCurrentTab() - { - return $this->m_sCurrentTab; - } - - public function RemoveTab($sTabLabel, $sTabContainer = null) - { - if ($sTabContainer == null) - { - $sTabContainer = $this->m_sCurrentTabContainer; - } - if (isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel])) - { - // Delete the content of the tab - unset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]); - - // If we just removed the active tab, let's reset the active tab - if (($this->m_sCurrentTabContainer == $sTabContainer) && ($this->m_sCurrentTab == $sTabLabel)) - { - $this->m_sCurrentTab = ''; - } - } - } - - /** - * Finds the tab whose title matches a given pattern - * - * @return mixed The actual name of the tab (as a string) or false if not found - */ - public function FindTab($sPattern, $sTabContainer = null) - { - $result = false; - if ($sTabContainer == null) - { - $sTabContainer = $this->m_sCurrentTabContainer; - } - foreach ($this->m_aTabs[$sTabContainer]['tabs'] as $sTabLabel => $void) - { - if (preg_match($sPattern, $sTabLabel)) - { - $result = $sTabLabel; - break; - } - } - - return $result; - } - - /** - * Make the given tab the active one, as if it were clicked - * DOES NOT WORK: apparently in the *old* version of jquery - * that we are using this is not supported... TO DO upgrade - * the whole jquery bundle... - */ - public function SelectTab($sTabContainer, $sTabLabel) - { - $container_index = 0; - $tab_index = 0; - foreach ($this->m_aTabs as $sCurrentTabContainerName => $aTabs) - { - if ($sTabContainer == $sCurrentTabContainerName) - { - foreach ($aTabs['tabs'] as $sCurrentTabLabel => $void) - { - if ($sCurrentTabLabel == $sTabLabel) - { - break; - } - $tab_index++; - } - break; - } - $container_index++; - } - $sSelector = '#tabbedContent_'.$container_index.' > ul'; - - return "window.setTimeout(\"$('$sSelector').tabs('select', $tab_index);\", 100);"; // Let the time to the tabs widget to initialize - } - - public function RenderIntoContent($sContent, WebPage $oPage) - { - // Render the tabs in the page (if any) - foreach ($this->m_aTabs as $sTabContainerName => $aTabs) - { - $sTabs = ''; - $sPrefix = $aTabs['prefix']; - $container_index = 0; - if (count($aTabs['tabs']) > 0) - { - if ($oPage->IsPrintableVersion()) - { - $oPage->add_ready_script( - <<< EOF -oHiddeableChapters = {}; -EOF - ); - $sTabs = "\n
    \n"; - $i = 0; - foreach ($aTabs['tabs'] as $sTabName => $aTabData) - { - $sTabNameEsc = addslashes($sTabName); - $sTabId = "tab_{$sPrefix}{$container_index}$i"; - switch ($aTabData['type']) - { - case 'ajax': - $sTabHtml = ''; - $sUrl = $aTabData['url']; - $oPage->add_ready_script( - <<< EOF -$.post('$sUrl', {printable: '1'}, function(data){ - $('#$sTabId > .printable-tab-content').append(data); -}); -EOF - ); - break; - - case 'html': - default: - $sTabHtml = $aTabData['html']; - } - $sTabs .= "

    ".htmlentities($sTabName, - ENT_QUOTES, - 'UTF-8')."

    ".$sTabHtml."
    \n"; - $oPage->add_ready_script( - <<< EOF -oHiddeableChapters['$sTabId'] = '$sTabNameEsc'; -EOF - ); - $i++; - } - $sTabs .= "
    \n\n"; - } - else - { - $sTabs = "\n
    \n"; - $sTabs .= "\n"; - // Now add the content of the tabs themselves - $i = 0; - foreach ($aTabs['tabs'] as $sTabName => $aTabData) - { - switch ($aTabData['type']) - { - case 'ajax': - // Nothing to add - break; - - case 'html': - default: - $sTabs .= "
    ".$aTabData['html']."
    \n"; - } - $i++; - } - $sTabs .= "
    \n\n"; - } - } - $sContent = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $sContent); - $container_index++; - } - - return $sContent; - } + + + +/** + * Class WebPage + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * Generic interface common to CLI and Web pages + */ +Interface Page +{ + public function output(); + + public function add($sText); + + public function p($sText); + + public function pre($sText); + + public function add_comment($sText); + + public function table($aConfig, $aData, $aParams = array()); +} + + +/** + *

    Simple helper class to ease the production of HTML pages + * + *

    This class provide methods to add content, scripts, includes... to a web page + * and renders the full web page by putting the elements in the proper place & order + * when the output() method is called. + * + *

    Usage: + * ```php + * $oPage = new WebPage("Title of my page"); + * $oPage->p("Hello World !"); + * $oPage->output(); + * ``` + */ +class WebPage implements Page +{ + protected $s_title; + protected $s_content; + protected $s_deferred_content; + protected $a_scripts; + protected $a_dict_entries; + protected $a_dict_entries_prefixes; + protected $a_styles; + protected $a_linked_scripts; + protected $a_linked_stylesheets; + protected $a_headers; + protected $a_base; + protected $iNextId; + protected $iTransactionId; + protected $sContentType; + protected $sContentDisposition; + protected $sContentFileName; + protected $bTrashUnexpectedOutput; + protected $s_sOutputFormat; + protected $a_OutputOptions; + protected $bPrintable; + + public function __construct($s_title, $bPrintable = false) + { + $this->s_title = $s_title; + $this->s_content = ""; + $this->s_deferred_content = ''; + $this->a_scripts = array(); + $this->a_dict_entries = array(); + $this->a_dict_entries_prefixes = array(); + $this->a_styles = array(); + $this->a_linked_scripts = array(); + $this->a_linked_stylesheets = array(); + $this->a_headers = array(); + $this->a_base = array('href' => '', 'target' => ''); + $this->iNextId = 0; + $this->iTransactionId = 0; + $this->sContentType = ''; + $this->sContentDisposition = ''; + $this->sContentFileName = ''; + $this->bTrashUnexpectedOutput = false; + $this->s_OutputFormat = utils::ReadParam('output_format', 'html'); + $this->a_OutputOptions = array(); + $this->bPrintable = $bPrintable; + ob_start(); // Start capturing the output + } + + /** + * Change the title of the page after its creation + */ + public function set_title($s_title) + { + $this->s_title = $s_title; + } + + /** + * Specify a default URL and a default target for all links on a page + */ + public function set_base($s_href = '', $s_target = '') + { + $this->a_base['href'] = $s_href; + $this->a_base['target'] = $s_target; + } + + /** + * Add any text or HTML fragment to the body of the page + */ + public function add($s_html) + { + $this->s_content .= $s_html; + } + + /** + * Add any text or HTML fragment (identified by an ID) at the end of the body of the page + * This is useful to add hidden content, DIVs or FORMs that should not + * be embedded into each other. + */ + public function add_at_the_end($s_html, $sId = '') + { + $this->s_deferred_content .= $s_html; + } + + /** + * Add a paragraph to the body of the page + */ + public function p($s_html) + { + $this->add($this->GetP($s_html)); + } + + /** + * Add a pre-formatted text to the body of the page + */ + public function pre($s_html) + { + $this->add('

    '.$s_html.'
    '); + } + + /** + * Add a comment + */ + public function add_comment($sText) + { + $this->add(''); + } + + /** + * Add a paragraph to the body of the page + */ + public function GetP($s_html) + { + return "

    $s_html

    \n"; + } + + /** + * Adds a tabular content to the web page + * + * @param string[] $aConfig Configuration of the table: hash array of 'column_id' => 'Column Label' + * @param string[] $aData Hash array. Data to display in the table: each row is made of 'column_id' => Data. A + * column 'pkey' is expected for each row + * @param array $aParams Hash array. Extra parameters for the table. + * + * @return void + */ + public function table($aConfig, $aData, $aParams = array()) + { + $this->add($this->GetTable($aConfig, $aData, $aParams)); + } + + public function GetTable($aConfig, $aData, $aParams = array()) + { + $oAppContext = new ApplicationContext(); + + static $iNbTables = 0; + $iNbTables++; + $sHtml = ""; + $sHtml .= "\n"; + $sHtml .= "\n"; + $sHtml .= "\n"; + foreach ($aConfig as $sName => $aDef) + { + $sHtml .= "\n"; + } + $sHtml .= "\n"; + $sHtml .= "\n"; + $sHtml .= "\n"; + foreach ($aData as $aRow) + { + $sHtml .= $this->GetTableRow($aRow, $aConfig); + } + $sHtml .= "\n"; + $sHtml .= "
    ".$aDef['label']."
    \n"; + + return $sHtml; + } + + public function GetTableRow($aRow, $aConfig) + { + $sHtml = ''; + if (isset($aRow['@class'])) // Row specific class, for hilighting certain rows + { + $sHtml .= ""; + } + else + { + $sHtml .= ""; + } + foreach ($aConfig as $sName => $aAttribs) + { + $sClass = isset($aAttribs['class']) ? 'class="'.$aAttribs['class'].'"' : ''; + $sValue = ($aRow[$sName] === '') ? ' ' : $aRow[$sName]; + $sHtml .= "$sValue"; + } + $sHtml .= ""; + + return $sHtml; + } + + /** + * Add some Javascript to the header of the page + */ + public function add_script($s_script) + { + $this->a_scripts[] = $s_script; + } + + /** + * Add some Javascript to the header of the page + */ + public function add_ready_script($s_script) + { + // Do nothing silently... this is not supported by this type of page... + } + + /** + * Allow a dictionnary entry to be used client side with Dict.S() + * + * @param string $s_entryId a translation label key + * + * @see \WebPage::add_dict_entries() + * @see utils.js + */ + public function add_dict_entry($s_entryId) + { + $this->a_dict_entries[] = $s_entryId; + } + + /** + * Add a set of dictionary entries (based on the given prefix) for the Javascript side + * + * @param string $s_entriesPrefix translation label prefix (eg 'UI:Button:' to add all keys beginning with this) + * + * @see \WebPage::add_dict_entry() + * @see utils.js + */ + public function add_dict_entries($s_entriesPrefix) + { + $this->a_dict_entries_prefixes[] = $s_entriesPrefix; + } + + protected function get_dict_signature() + { + return str_replace('_', '', Dict::GetUserLanguage()).'-'.md5(implode(',', + $this->a_dict_entries).'|'.implode(',', $this->a_dict_entries_prefixes)); + } + + protected function get_dict_file_content() + { + $aEntries = array(); + foreach ($this->a_dict_entries as $sCode) + { + $aEntries[$sCode] = Dict::S($sCode); + } + foreach ($this->a_dict_entries_prefixes as $sPrefix) + { + $aEntries = array_merge($aEntries, Dict::ExportEntries($sPrefix)); + } + $sJSFile = 'var aDictEntries = '.json_encode($aEntries); + + return $sJSFile; + } + + + /** + * Add some CSS definitions to the header of the page + */ + public function add_style($s_style) + { + $this->a_styles[] = $s_style; + } + + /** + * Add a script (as an include, i.e. link) to the header of the page + */ + public function add_linked_script($s_linked_script) + { + $this->a_linked_scripts[$s_linked_script] = $s_linked_script; + } + + /** + * Add a CSS stylesheet (as an include, i.e. link) to the header of the page + */ + public function add_linked_stylesheet($s_linked_stylesheet, $s_condition = "") + { + $this->a_linked_stylesheets[] = array('link' => $s_linked_stylesheet, 'condition' => $s_condition); + } + + public function add_saas($sSaasRelPath) + { + $sCssRelPath = utils::GetCSSFromSASS($sSaasRelPath); + $sRootUrl = utils::GetAbsoluteUrlAppRoot(); + if ($sRootUrl === '') + { + // We're running the setup of the first install... + $sRootUrl = '../'; + } + $sCSSUrl = $sRootUrl.$sCssRelPath; + $this->add_linked_stylesheet($sCSSUrl); + } + + /** + * Add some custom header to the page + */ + public function add_header($s_header) + { + $this->a_headers[] = $s_header; + } + + /** + * Add needed headers to the page so that it will no be cached + */ + public function no_cache() + { + $this->add_header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 + $this->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past + } + + /** + * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data + */ + public function details($aFields) + { + + $this->add($this->GetDetails($aFields)); + } + + /** + * Whether or not the page is a PDF page + * + * @return boolean + */ + public function is_pdf() + { + return false; + } + + /** + * Records the current state of the 'html' part of the page output + * + * @return mixed The current state of the 'html' output + */ + public function start_capture() + { + return strlen($this->s_content); + } + + /** + * Returns the part of the html output that occurred since the call to start_capture + * and removes this part from the current html output + * + * @param $offset mixed The value returned by start_capture + * + * @return string The part of the html output that was added since the call to start_capture + */ + public function end_capture($offset) + { + $sCaptured = substr($this->s_content, $offset); + $this->s_content = substr($this->s_content, 0, $offset); + + return $sCaptured; + } + + /** + * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data + */ + public function GetDetails($aFields) + { + $sHtml = "
    \n"; + foreach ($aFields as $aAttrib) + { + $sDataAttCode = isset($aAttrib['attcode']) ? "data-attcode=\"{$aAttrib['attcode']}\"" : ''; + $sLayout = isset($aAttrib['layout']) ? $aAttrib['layout'] : 'small'; + $sHtml .= "
    \n"; + $sHtml .= "
    {$aAttrib['label']}
    \n"; + + $sHtml .= "
    \n"; + // By Rom, for csv import, proposed to show several values for column selection + if (is_array($aAttrib['value'])) + { + $sHtml .= "
    ".implode("
    ", $aAttrib['value'])."
    \n"; + } + else + { + $sHtml .= "
    ".$aAttrib['value']."
    \n"; + } + // Checking if we should add comments & infos + $sComment = (isset($aAttrib['comments'])) ? $aAttrib['comments'] : ''; + $sInfo = (isset($aAttrib['infos'])) ? $aAttrib['infos'] : ''; + if ($sComment !== '') + { + $sHtml .= "
    $sComment
    \n"; + } + if ($sInfo !== '') + { + $sHtml .= "
    $sInfo
    \n"; + } + $sHtml .= "
    \n"; + + $sHtml .= "
    \n"; + } + $sHtml .= "
    \n"; + + return $sHtml; + } + + /** + * Build a set of radio buttons suitable for editing a field/attribute of an object (including its validation) + * + * @param $aAllowedValues hash Array of value => display_value + * @param $value mixed Current value for the field/attribute + * @param $iId mixed Unique Id for the input control in the page + * @param $sFieldName string The name of the field, attr_<$sFieldName> will hold the value for the field + * @param $bMandatory bool Whether or not the field is mandatory + * @param $bVertical bool Disposition of the radio buttons vertical or horizontal + * @param $sValidationField string HTML fragment holding the validation field (exclamation icon...) + * + * @return string The HTML fragment corresponding to the radio buttons + */ + public function GetRadioButtons( + $aAllowedValues, $value, $iId, $sFieldName, $bMandatory, $bVertical, $sValidationField + ) { + $idx = 0; + $sHTMLValue = ''; + foreach ($aAllowedValues as $key => $display_value) + { + if ((count($aAllowedValues) == 1) && ($bMandatory == 'true')) + { + // When there is only once choice, select it by default + $sSelected = ' checked'; + } + else + { + $sSelected = ($value == $key) ? ' checked' : ''; + } + $sHTMLValue .= " "; + if ($bVertical) + { + if ($idx == 0) + { + // Validation icon at the end of the first line + $sHTMLValue .= " {$sValidationField}\n"; + } + $sHTMLValue .= "
    \n"; + } + $idx++; + } + $sHTMLValue .= ""; + if (!$bVertical) + { + // Validation icon at the end of the line + $sHTMLValue .= " {$sValidationField}\n"; + } + + return $sHTMLValue; + } + + /** + * Discard unexpected output data (such as PHP warnings) + * This is a MUST when the Page output is DATA (download of a document, download CSV export, download ...) + */ + public function TrashUnexpectedOutput() + { + $this->bTrashUnexpectedOutput = true; + } + + /** + * Read the output buffer and deal with its contents: + * - trash unexpected output if the flag has been set + * - report unexpected behaviors such as the output buffering being stopped + * + * Possible improvement: I've noticed that several output buffers are stacked, + * if they are not empty, the output will be corrupted. The solution would + * consist in unstacking all of them (and concatenate the contents). + */ + protected function ob_get_clean_safe() + { + $sOutput = ob_get_contents(); + if ($sOutput === false) + { + $sMsg = "Design/integration issue: No output buffer. Some piece of code has called ob_get_clean() or ob_end_clean() without calling ob_start()"; + if ($this->bTrashUnexpectedOutput) + { + IssueLog::Error($sMsg); + $sOutput = ''; + } + else + { + $sOutput = $sMsg; + } + } + else + { + ob_end_clean(); // on some versions of PHP doing so when the output buffering is stopped can cause a notice + if ($this->bTrashUnexpectedOutput) + { + if (trim($sOutput) != '') + { + if (Utils::GetConfig() && Utils::GetConfig()->Get('debug_report_spurious_chars')) + { + IssueLog::Error("Trashing unexpected output:'$sOutput'\n"); + } + } + $sOutput = ''; + } + } + + return $sOutput; + } + + /** + * Outputs (via some echo) the complete HTML page by assembling all its elements + */ + public function output() + { + foreach ($this->a_headers as $s_header) + { + header($s_header); + } + + $s_captured_output = $this->ob_get_clean_safe(); + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo ""; + echo "".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."\n"; + echo $this->get_base_tag(); + + $this->output_dict_entries(); + + foreach ($this->a_linked_scripts as $s_script) + { + // Make sure that the URL to the script contains the application's version number + // so that the new script do NOT get reloaded from the cache when the application is upgraded + if (strpos($s_script, '?') === false) + { + $s_script .= "?t=".utils::GetCacheBusterTimestamp(); + } + else + { + $s_script .= "&t=".utils::GetCacheBusterTimestamp(); + } + echo "\n"; + } + if (count($this->a_scripts) > 0) + { + echo "\n"; + } + foreach ($this->a_linked_stylesheets as $a_stylesheet) + { + if (strpos($a_stylesheet['link'], '?') === false) + { + $s_stylesheet = $a_stylesheet['link']."?t=".utils::GetCacheBusterTimestamp(); + } + else + { + $s_stylesheet = $a_stylesheet['link']."&t=".utils::GetCacheBusterTimestamp(); + } + if ($a_stylesheet['condition'] != "") + { + echo "\n"; + } + } + + if (count($this->a_styles) > 0) + { + echo "\n"; + } + if (class_exists('MetaModel') && MetaModel::GetConfig()) + { + echo "\n"; + } + echo "\n"; + echo "\n"; + echo self::FilterXSS($this->s_content); + if (trim($s_captured_output) != "") + { + echo "
    ".self::FilterXSS($s_captured_output)."
    \n"; + } + echo '
    '.self::FilterXSS($this->s_deferred_content).'
    '; + echo "\n"; + echo "\n"; + + if (class_exists('DBSearch')) + { + DBSearch::RecordQueryTrace(); + } + if (class_exists('ExecutionKPI')) + { + ExecutionKPI::ReportStats(); + } + } + + /** + * Build a series of hidden field[s] from an array + */ + public function add_input_hidden($sLabel, $aData) + { + foreach ($aData as $sKey => $sValue) + { + // Note: protection added to protect against the Notice 'array to string conversion' that appeared with PHP 5.4 + // (this function seems unused though!) + if (is_scalar($sValue)) + { + $this->add(""); + } + } + } + + protected function get_base_tag() + { + $sTag = ''; + if (($this->a_base['href'] != '') || ($this->a_base['target'] != '')) + { + $sTag = 'a_base['href'] != '')) + { + $sTag .= "href =\"{$this->a_base['href']}\" "; + } + if (($this->a_base['target'] != '')) + { + $sTag .= "target =\"{$this->a_base['target']}\" "; + } + $sTag .= " />\n"; + } + + return $sTag; + } + + /** + * Get an ID (for any kind of HTML tag) that is guaranteed unique in this page + * + * @return int The unique ID (in this page) + */ + public function GetUniqueId() + { + return $this->iNextId++; + } + + /** + * Set the content-type (mime type) for the page's content + * + * @param $sContentType string + * + * @return void + */ + public function SetContentType($sContentType) + { + $this->sContentType = $sContentType; + } + + /** + * Set the content-disposition (mime type) for the page's content + * + * @param $sDisposition string The disposition: 'inline' or 'attachment' + * @param $sFileName string The original name of the file + * + * @return void + */ + public function SetContentDisposition($sDisposition, $sFileName) + { + $this->sContentDisposition = $sDisposition; + $this->sContentFileName = $sFileName; + } + + /** + * Set the transactionId of the current form + * + * @param $iTransactionId integer + * + * @return void + */ + public function SetTransactionId($iTransactionId) + { + $this->iTransactionId = $iTransactionId; + } + + /** + * Returns the transactionId of the current form + * + * @return integer The current transactionID + */ + public function GetTransactionId() + { + return $this->iTransactionId; + } + + public static function FilterXSS($sHTML) + { + return str_ireplace('s_OutputFormat; + } + + /** + * Check whether the desired output format is possible or not + * + * @param string $sOutputFormat The desired output format: html, pdf... + * + * @return bool True if the format is Ok, false otherwise + */ + function IsOutputFormatAvailable($sOutputFormat) + { + $bResult = false; + switch ($sOutputFormat) + { + case 'html': + $bResult = true; // Always supported + break; + + case 'pdf': + $bResult = @is_readable(APPROOT.'lib/MPDF/mpdf.php'); + break; + } + + return $bResult; + } + + /** + * Check whether the output must be printable (using print.css, for sure!) + * + * @return bool ... + */ + public function IsPrintableVersion() + { + return $this->bPrintable; + } + + /** + * Retrieves the value of a named output option for the given format + * + * @param string $sFormat The format: html or pdf + * @param string $sOptionName The name of the option + * + * @return mixed false if the option was never set or the options's value + */ + public function GetOutputOption($sFormat, $sOptionName) + { + if (isset($this->a_OutputOptions[$sFormat][$sOptionName])) + { + return $this->a_OutputOptions[$sFormat][$sOptionName]; + } + + return false; + } + + /** + * Sets a named output option for the given format + * + * @param string $sFormat The format for which to set the option: html or pdf + * @param string $sOptionName the name of the option + * @param mixed $sValue The value of the option + */ + public function SetOutputOption($sFormat, $sOptionName, $sValue) + { + if (!isset($this->a_OutputOptions[$sFormat])) + { + $this->a_OutputOptions[$sFormat] = array($sOptionName => $sValue); + } + else + { + $this->a_OutputOptions[$sFormat][$sOptionName] = $sValue; + } + } + + public function RenderPopupMenuItems($aActions, $aFavoriteActions = array()) + { + $sPrevUrl = ''; + $sHtml = ''; + if (!$this->IsPrintableVersion()) + { + foreach ($aActions as $aAction) + { + $sClass = isset($aAction['css_classes']) ? ' class="'.implode(' ', $aAction['css_classes']).'"' : ''; + $sOnClick = isset($aAction['onclick']) ? ' onclick="'.htmlspecialchars($aAction['onclick'], ENT_QUOTES, + "UTF-8").'"' : ''; + $sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : ""; + if (empty($aAction['url'])) + { + if ($sPrevUrl != '') // Don't output consecutively two separators... + { + $sHtml .= "
  • {$aAction['label']}
  • "; + } + $sPrevUrl = ''; + } + else + { + $sHtml .= "
  • {$aAction['label']}
  • "; + $sPrevUrl = $aAction['url']; + } + } + $sHtml .= "
    "; + foreach (array_reverse($aFavoriteActions) as $aAction) + { + $sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : ""; + $sHtml .= ""; + } + } + + return $sHtml; + } + + protected function output_dict_entries($bReturnOutput = false) + { + if ((count($this->a_dict_entries) > 0) || (count($this->a_dict_entries_prefixes) > 0)) + { + if (class_exists('Dict')) + { + // The dictionary may not be available for example during the setup... + // Create a specific dictionary file and load it as a JS script + $sSignature = $this->get_dict_signature(); + $sJSFileName = utils::GetCachePath().$sSignature.'.js'; + if (!file_exists($sJSFileName) && is_writable(utils::GetCachePath())) + { + file_put_contents($sJSFileName, $this->get_dict_file_content()); + } + // Load the dictionary as the first javascript file, so that other JS file benefit from the translations + array_unshift($this->a_linked_scripts, + utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=dict&s='.$sSignature); + } + } + } +} + + +interface iTabbedPage +{ + public function AddTabContainer($sTabContainer, $sPrefix = ''); + + public function AddToTab($sTabContainer, $sTabLabel, $sHtml); + + public function SetCurrentTabContainer($sTabContainer = ''); + + public function SetCurrentTab($sTabLabel = ''); + + /** + * Add a tab which content will be loaded asynchronously via the supplied URL + * + * Limitations: + * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to + * pull content from another server. Static content cannot be added inside such tabs. + * + * @param string $sTabLabel The (localised) label of the tab + * @param string $sUrl The URL to load (on the same server) + * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause + * the tab to be reloaded upon each activation. + * + * @since 2.0.3 + */ + public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true); + + public function GetCurrentTab(); + + public function RemoveTab($sTabLabel, $sTabContainer = null); + + /** + * Finds the tab whose title matches a given pattern + * + * @return mixed The name of the tab as a string or false if not found + */ + public function FindTab($sPattern, $sTabContainer = null); +} + +/** + * Helper class to implement JQueryUI tabs inside a page + */ +class TabManager +{ + protected $m_aTabs; + protected $m_sCurrentTabContainer; + protected $m_sCurrentTab; + + public function __construct() + { + $this->m_aTabs = array(); + $this->m_sCurrentTabContainer = ''; + $this->m_sCurrentTab = ''; + } + + public function AddTabContainer($sTabContainer, $sPrefix = '') + { + $this->m_aTabs[$sTabContainer] = array('prefix' => $sPrefix, 'tabs' => array()); + + return "\$Tabs:$sTabContainer\$"; + } + + public function AddToCurrentTab($sHtml) + { + $this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml); + } + + public function GetCurrentTabLength($sHtml) + { + $iLength = isset($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) : 0; + + return $iLength; + } + + /** + * Truncates the given tab to the specifed length and returns the truncated part + * + * @param string $sTabContainer The tab container in which to truncate the tab + * @param string $sTab The name/identifier of the tab to truncate + * @param integer $iLength The length/offset at which to truncate the tab + * + * @return string The truncated part + */ + public function TruncateTab($sTabContainer, $sTab, $iLength) + { + $sResult = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], + $iLength); + $this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'] = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'], + 0, $iLength); + + return $sResult; + } + + public function TabExists($sTabContainer, $sTab) + { + return isset($this->m_aTabs[$sTabContainer]['tabs'][$sTab]); + } + + public function TabsContainerCount() + { + return count($this->m_aTabs); + } + + public function AddToTab($sTabContainer, $sTabLabel, $sHtml) + { + if (!isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel])) + { + // Set the content of the tab + $this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel] = array( + 'type' => 'html', + 'html' => $sHtml, + ); + } + else + { + if ($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type'] != 'html') + { + throw new Exception("Cannot add HTML content to the tab '$sTabLabel' of type '{$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type']}'"); + } + // Append to the content of the tab + $this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['html'] .= $sHtml; + } + + return ''; // Nothing to add to the page for now + } + + public function SetCurrentTabContainer($sTabContainer = '') + { + $sPreviousTabContainer = $this->m_sCurrentTabContainer; + $this->m_sCurrentTabContainer = $sTabContainer; + + return $sPreviousTabContainer; + } + + public function SetCurrentTab($sTabLabel = '') + { + $sPreviousTab = $this->m_sCurrentTab; + $this->m_sCurrentTab = $sTabLabel; + + return $sPreviousTab; + } + + /** + * Add a tab which content will be loaded asynchronously via the supplied URL + * + * Limitations: + * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to + * pull content from another server. Static content cannot be added inside such tabs. + * + * @param string $sTabLabel The (localised) label of the tab + * @param string $sUrl The URL to load (on the same server) + * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause + * the tab to be reloaded upon each activation. + * + * @since 2.0.3 + */ + public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true) + { + // Set the content of the tab + $this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$sTabLabel] = array( + 'type' => 'ajax', + 'url' => $sUrl, + 'cache' => $bCache, + ); + + return ''; // Nothing to add to the page for now + } + + + public function GetCurrentTabContainer() + { + return $this->m_sCurrentTabContainer; + } + + public function GetCurrentTab() + { + return $this->m_sCurrentTab; + } + + public function RemoveTab($sTabLabel, $sTabContainer = null) + { + if ($sTabContainer == null) + { + $sTabContainer = $this->m_sCurrentTabContainer; + } + if (isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel])) + { + // Delete the content of the tab + unset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]); + + // If we just removed the active tab, let's reset the active tab + if (($this->m_sCurrentTabContainer == $sTabContainer) && ($this->m_sCurrentTab == $sTabLabel)) + { + $this->m_sCurrentTab = ''; + } + } + } + + /** + * Finds the tab whose title matches a given pattern + * + * @return mixed The actual name of the tab (as a string) or false if not found + */ + public function FindTab($sPattern, $sTabContainer = null) + { + $result = false; + if ($sTabContainer == null) + { + $sTabContainer = $this->m_sCurrentTabContainer; + } + foreach ($this->m_aTabs[$sTabContainer]['tabs'] as $sTabLabel => $void) + { + if (preg_match($sPattern, $sTabLabel)) + { + $result = $sTabLabel; + break; + } + } + + return $result; + } + + /** + * Make the given tab the active one, as if it were clicked + * DOES NOT WORK: apparently in the *old* version of jquery + * that we are using this is not supported... TO DO upgrade + * the whole jquery bundle... + */ + public function SelectTab($sTabContainer, $sTabLabel) + { + $container_index = 0; + $tab_index = 0; + foreach ($this->m_aTabs as $sCurrentTabContainerName => $aTabs) + { + if ($sTabContainer == $sCurrentTabContainerName) + { + foreach ($aTabs['tabs'] as $sCurrentTabLabel => $void) + { + if ($sCurrentTabLabel == $sTabLabel) + { + break; + } + $tab_index++; + } + break; + } + $container_index++; + } + $sSelector = '#tabbedContent_'.$container_index.' > ul'; + + return "window.setTimeout(\"$('$sSelector').tabs('select', $tab_index);\", 100);"; // Let the time to the tabs widget to initialize + } + + public function RenderIntoContent($sContent, WebPage $oPage) + { + // Render the tabs in the page (if any) + foreach ($this->m_aTabs as $sTabContainerName => $aTabs) + { + $sTabs = ''; + $sPrefix = $aTabs['prefix']; + $container_index = 0; + if (count($aTabs['tabs']) > 0) + { + if ($oPage->IsPrintableVersion()) + { + $oPage->add_ready_script( + <<< EOF +oHiddeableChapters = {}; +EOF + ); + $sTabs = "\n
    \n"; + $i = 0; + foreach ($aTabs['tabs'] as $sTabName => $aTabData) + { + $sTabNameEsc = addslashes($sTabName); + $sTabId = "tab_{$sPrefix}{$container_index}$i"; + switch ($aTabData['type']) + { + case 'ajax': + $sTabHtml = ''; + $sUrl = $aTabData['url']; + $oPage->add_ready_script( + <<< EOF +$.post('$sUrl', {printable: '1'}, function(data){ + $('#$sTabId > .printable-tab-content').append(data); +}); +EOF + ); + break; + + case 'html': + default: + $sTabHtml = $aTabData['html']; + } + $sTabs .= "

    ".htmlentities($sTabName, + ENT_QUOTES, + 'UTF-8')."

    ".$sTabHtml."
    \n"; + $oPage->add_ready_script( + <<< EOF +oHiddeableChapters['$sTabId'] = '$sTabNameEsc'; +EOF + ); + $i++; + } + $sTabs .= "
    \n\n"; + } + else + { + $sTabs = "\n
    \n"; + $sTabs .= "\n"; + // Now add the content of the tabs themselves + $i = 0; + foreach ($aTabs['tabs'] as $sTabName => $aTabData) + { + switch ($aTabData['type']) + { + case 'ajax': + // Nothing to add + break; + + case 'html': + default: + $sTabs .= "
    ".$aTabData['html']."
    \n"; + } + $i++; + } + $sTabs .= "
    \n\n"; + } + } + $sContent = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $sContent); + $container_index++; + } + + return $sContent; + } } \ No newline at end of file diff --git a/application/wizardhelper.class.inc.php b/application/wizardhelper.class.inc.php index f9c078361..379cd58a2 100644 --- a/application/wizardhelper.class.inc.php +++ b/application/wizardhelper.class.inc.php @@ -1,346 +1,346 @@ - - - -/** - * Class WizardHelper - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/application/uiwizard.class.inc.php'); - -class WizardHelper -{ - protected $m_aData; - - public function __construct() - { - } - /** - * Constructs the PHP target object from the parameters sent to the web page by the wizard - * @param boolean $bReadUploadedFiles True to also read any uploaded file (for blob/document fields) - * @return object - */ - public function GetTargetObject($bReadUploadedFiles = false) - { - if (isset($this->m_aData['m_oCurrentValues']['id'])) - { - $oObj = MetaModel::GetObject($this->m_aData['m_sClass'], $this->m_aData['m_oCurrentValues']['id']); - } - else - { - $oObj = MetaModel::NewObject($this->m_aData['m_sClass']); - } - foreach($this->m_aData['m_oCurrentValues'] as $sAttCode => $value) - { - // Because this is stored in a Javascript array, unused indexes - // are filled with null values and unused keys (stored as strings) contain $$NULL$$ - if ( ($sAttCode !='id') && ($value !== '$$NULL$$')) - { - $oAttDef = MetaModel::GetAttributeDef($this->m_aData['m_sClass'], $sAttCode); - if (($oAttDef->IsLinkSet()) && ($value != '') ) - { - // special handling for lists - // assumes this is handled as an array of objects - // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] - $aData = json_decode($value, true); // true means decode as a hash array (not an object) - // Check what are the meaningful attributes - $aFields = $this->GetLinkedWizardStructure($oAttDef); - $sLinkedClass = $oAttDef->GetLinkedClass(); - $aLinkedObjectsArray = array(); - if (!is_array($aData)) - { - echo ("aData: '$aData' (value: '$value')\n"); - } - foreach($aData as $aLinkedObject) - { - $oLinkedObj = MetaModel::NewObject($sLinkedClass); - foreach($aFields as $sLinkedAttCode) - { - if ( isset($aLinkedObject[$sLinkedAttCode]) && ($aLinkedObject[$sLinkedAttCode] !== null) ) - { - $sLinkedAttDef = MetaModel::GetAttributeDef($sLinkedClass, $sLinkedAttCode); - if (($sLinkedAttDef->IsExternalKey()) && ($aLinkedObject[$sLinkedAttCode] != '') && ($aLinkedObject[$sLinkedAttCode] > 0) ) - { - // For external keys: load the target object so that external fields - // get filled too - $oTargetObj = MetaModel::GetObject($sLinkedAttDef->GetTargetClass(), $aLinkedObject[$sLinkedAttCode]); - $oLinkedObj->Set($sLinkedAttCode, $oTargetObj); - } - elseif($sLinkedAttDef instanceof AttributeDateTime) - { - $sDateClass = get_class($sLinkedAttDef); - $sDate = $aLinkedObject[$sLinkedAttCode]; - if($sDate !== null && $sDate !== '') - { - $oDateTimeFormat = $sDateClass::GetFormat(); - $oDate = $oDateTimeFormat->Parse($sDate); - if ($sDateClass == "AttributeDate") - { - $sDate = $oDate->format('Y-m-d'); - } - else - { - $sDate = $oDate->format('Y-m-d H:i:s'); - } - } - - $oLinkedObj->Set($sLinkedAttCode, $sDate); - } - else - { - $oLinkedObj->Set($sLinkedAttCode, $aLinkedObject[$sLinkedAttCode]); - } - } - } - $aLinkedObjectsArray[] = $oLinkedObj; - } - $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); - $oObj->Set($sAttCode, $oSet); - } - else if ( $oAttDef->GetEditClass() == 'Document' ) - { - if ($bReadUploadedFiles) - { - $oDocument = utils::ReadPostedDocument('attr_'.$sAttCode, 'fcontents'); - $oObj->Set($sAttCode, $oDocument); - } - else - { - // Create a new empty document, just for displaying the file name - $oDocument = new ormDocument(null, '', $value); - $oObj->Set($sAttCode, $oDocument); - } - } - else if ( $oAttDef->GetEditClass() == 'Image' ) - { - if ($bReadUploadedFiles) - { - $oDocument = utils::ReadPostedDocument('attr_'.$sAttCode, 'fcontents'); - $oObj->Set($sAttCode, $oDocument); - } - else - { - // Create a new empty document, just for displaying the file name - $oDocument = new ormDocument(null, '', $value); - $oObj->Set($sAttCode, $oDocument); - } - } - else if (($oAttDef->IsExternalKey()) && (!empty($value)) && ($value > 0) ) - { - // For external keys: load the target object so that external fields - // get filled too - $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value, false); - if ($oTargetObj) - { - $oObj->Set($sAttCode, $oTargetObj); - } - else - { - // May happen for security reasons (portal, see ticket #1074) - $oObj->Set($sAttCode, $value); - } - } - else if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime - { - if ($value != null) - { - $oDate = $oAttDef->GetFormat()->Parse($value); - if ($oDate instanceof DateTime) - { - $value = $oDate->format($oAttDef->GetInternalFormat()); - } - else - { - $value = null; - } - } - $oObj->Set($sAttCode, $value); - } - else - { - $oObj->Set($sAttCode, $value); - } - } - } - if (isset($this->m_aData['m_sState']) && !empty($this->m_aData['m_sState'])) - { - $oObj->Set(MetaModel::GetStateAttributeCode($this->m_aData['m_sClass']), $this->m_aData['m_sState']); - } - $oObj->DoComputeValues(); - return $oObj; - } - - public function GetFieldsForDefaultValue() - { - return $this->m_aData['m_aDefaultValueRequested']; - } - - public function SetDefaultValue($sAttCode, $value) - { - // Protect against a request for a non existing field - if (isset($this->m_aData['m_oFieldsMap'][$sAttCode])) - { - $oAttDef = MetaModel::GetAttributeDef($this->m_aData['m_sClass'], $sAttCode); - if ($oAttDef->GetEditClass() == 'List') - { - // special handling for lists - // this as to be handled as an array of objects - // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] - // NOT YET IMPLEMENTED !! - $sLinkedClass = $oAttDef->GetLinkedClass(); - $oSet = $value; - $aData = array(); - $aFields = $this->GetLinkedWizardStructure($oAttDef); - while($oSet->fetch()) - { - foreach($aFields as $sLinkedAttCode) - { - $aRow[$sAttCode] = $oLinkedObj->Get($sLinkedAttCode); - } - $aData[] = $aRow; - } - $this->m_aData['m_oDefaultValue'][$sAttCode] = json_encode($aData); - - } - else - { - // Normal handling for all other scalar attributes - $this->m_aData['m_oDefaultValue'][$sAttCode] = $value; - } - } - } - - public function GetFieldsForAllowedValues() - { - return $this->m_aData['m_aAllowedValuesRequested']; - } - - public function SetAllowedValuesHtml($sAttCode, $sHtml) - { - // Protect against a request for a non existing field - if (isset($this->m_aData['m_oFieldsMap'][$sAttCode])) - { - $this->m_aData['m_oAllowedValues'][$sAttCode] = $sHtml; - } - } - - public function ToJSON() - { - return json_encode($this->m_aData); - } - - static public function FromJSON($sJSON) - { - $oWizHelper = new WizardHelper(); - if (get_magic_quotes_gpc()) - { - $sJSON = stripslashes($sJSON); - } - $aData = json_decode($sJSON, true); // true means hash array instead of object - $oWizHelper->m_aData = $aData; - return $oWizHelper; - } - - protected function GetLinkedWizardStructure($oAttDef) - { - $oWizard = new UIWizard(null, $oAttDef->GetLinkedClass()); - $aWizardSteps = $oWizard->GetWizardStructure(); - $aFields = array(); - $sExtKeyToMeCode = $oAttDef->GetExtKeyToMe(); - // Retrieve as a flat list, all the attributes that are needed to create - // an object of the linked class and put them into a flat array, except - // the attribute 'ext_key_to_me' which is a constant in our case - foreach($aWizardSteps as $sDummy => $aMainSteps) - { - // 2 entries: 'mandatory' and 'optional' - foreach($aMainSteps as $aSteps) - { - // One entry for each step of the wizard - foreach($aSteps as $sAttCode) - { - if ($sAttCode != $sExtKeyToMeCode) - { - $aFields[] = $sAttCode; - } - } - } - } - return $aFields; - } - - public function GetTargetClass() - { - return $this->m_aData['m_sClass']; - } - - public function GetFormPrefix() - { - return $this->m_aData['m_sFormPrefix']; - } - - public function GetInitialState() - { - return isset($this->m_aData['m_sInitialState']) ? $this->m_aData['m_sInitialState'] : null; - } - - public function GetStimulus() - { - return isset($this->m_aData['m_sStimulus']) ? $this->m_aData['m_sStimulus'] : null; - } - - public function GetIdForField($sFieldName) - { - $sResult = ''; - // It may happen that the field we'd like to update does not - // exist in the form. For example, if the field should be hidden/read-only - // in the current state of the object - if (isset($this->m_aData['m_oFieldsMap'][$sFieldName])) - { - $sResult = $this->m_aData['m_oFieldsMap'][$sFieldName]; - } - return $sResult; - } - - static function ParseJsonSet($oMe, $sLinkClass, $sExtKeyToMe, $sJsonSet) - { - $aSet = json_decode($sJsonSet, true); // true means hash array instead of object - $oSet = CMDBObjectSet::FromScratch($sLinkClass); - foreach($aSet as $aLinkObj) - { - $oLink = MetaModel::NewObject($sLinkClass); - foreach($aLinkObj as $sAttCode => $value) - { - $oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode); - if (($oAttDef->IsExternalKey()) && ($value != '') && ($value > 0)) - { - // For external keys: load the target object so that external fields - // get filled too - $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value); - $oLink->Set($sAttCode, $oTargetObj); - } - $oLink->Set($sAttCode, $value); - } - $oLink->Set($sExtKeyToMe, $oMe->GetKey()); - $oSet->AddObject($oLink); - } - return $oSet; - } -} + + + +/** + * Class WizardHelper + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/application/uiwizard.class.inc.php'); + +class WizardHelper +{ + protected $m_aData; + + public function __construct() + { + } + /** + * Constructs the PHP target object from the parameters sent to the web page by the wizard + * @param boolean $bReadUploadedFiles True to also read any uploaded file (for blob/document fields) + * @return object + */ + public function GetTargetObject($bReadUploadedFiles = false) + { + if (isset($this->m_aData['m_oCurrentValues']['id'])) + { + $oObj = MetaModel::GetObject($this->m_aData['m_sClass'], $this->m_aData['m_oCurrentValues']['id']); + } + else + { + $oObj = MetaModel::NewObject($this->m_aData['m_sClass']); + } + foreach($this->m_aData['m_oCurrentValues'] as $sAttCode => $value) + { + // Because this is stored in a Javascript array, unused indexes + // are filled with null values and unused keys (stored as strings) contain $$NULL$$ + if ( ($sAttCode !='id') && ($value !== '$$NULL$$')) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_aData['m_sClass'], $sAttCode); + if (($oAttDef->IsLinkSet()) && ($value != '') ) + { + // special handling for lists + // assumes this is handled as an array of objects + // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] + $aData = json_decode($value, true); // true means decode as a hash array (not an object) + // Check what are the meaningful attributes + $aFields = $this->GetLinkedWizardStructure($oAttDef); + $sLinkedClass = $oAttDef->GetLinkedClass(); + $aLinkedObjectsArray = array(); + if (!is_array($aData)) + { + echo ("aData: '$aData' (value: '$value')\n"); + } + foreach($aData as $aLinkedObject) + { + $oLinkedObj = MetaModel::NewObject($sLinkedClass); + foreach($aFields as $sLinkedAttCode) + { + if ( isset($aLinkedObject[$sLinkedAttCode]) && ($aLinkedObject[$sLinkedAttCode] !== null) ) + { + $sLinkedAttDef = MetaModel::GetAttributeDef($sLinkedClass, $sLinkedAttCode); + if (($sLinkedAttDef->IsExternalKey()) && ($aLinkedObject[$sLinkedAttCode] != '') && ($aLinkedObject[$sLinkedAttCode] > 0) ) + { + // For external keys: load the target object so that external fields + // get filled too + $oTargetObj = MetaModel::GetObject($sLinkedAttDef->GetTargetClass(), $aLinkedObject[$sLinkedAttCode]); + $oLinkedObj->Set($sLinkedAttCode, $oTargetObj); + } + elseif($sLinkedAttDef instanceof AttributeDateTime) + { + $sDateClass = get_class($sLinkedAttDef); + $sDate = $aLinkedObject[$sLinkedAttCode]; + if($sDate !== null && $sDate !== '') + { + $oDateTimeFormat = $sDateClass::GetFormat(); + $oDate = $oDateTimeFormat->Parse($sDate); + if ($sDateClass == "AttributeDate") + { + $sDate = $oDate->format('Y-m-d'); + } + else + { + $sDate = $oDate->format('Y-m-d H:i:s'); + } + } + + $oLinkedObj->Set($sLinkedAttCode, $sDate); + } + else + { + $oLinkedObj->Set($sLinkedAttCode, $aLinkedObject[$sLinkedAttCode]); + } + } + } + $aLinkedObjectsArray[] = $oLinkedObj; + } + $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); + $oObj->Set($sAttCode, $oSet); + } + else if ( $oAttDef->GetEditClass() == 'Document' ) + { + if ($bReadUploadedFiles) + { + $oDocument = utils::ReadPostedDocument('attr_'.$sAttCode, 'fcontents'); + $oObj->Set($sAttCode, $oDocument); + } + else + { + // Create a new empty document, just for displaying the file name + $oDocument = new ormDocument(null, '', $value); + $oObj->Set($sAttCode, $oDocument); + } + } + else if ( $oAttDef->GetEditClass() == 'Image' ) + { + if ($bReadUploadedFiles) + { + $oDocument = utils::ReadPostedDocument('attr_'.$sAttCode, 'fcontents'); + $oObj->Set($sAttCode, $oDocument); + } + else + { + // Create a new empty document, just for displaying the file name + $oDocument = new ormDocument(null, '', $value); + $oObj->Set($sAttCode, $oDocument); + } + } + else if (($oAttDef->IsExternalKey()) && (!empty($value)) && ($value > 0) ) + { + // For external keys: load the target object so that external fields + // get filled too + $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value, false); + if ($oTargetObj) + { + $oObj->Set($sAttCode, $oTargetObj); + } + else + { + // May happen for security reasons (portal, see ticket #1074) + $oObj->Set($sAttCode, $value); + } + } + else if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime + { + if ($value != null) + { + $oDate = $oAttDef->GetFormat()->Parse($value); + if ($oDate instanceof DateTime) + { + $value = $oDate->format($oAttDef->GetInternalFormat()); + } + else + { + $value = null; + } + } + $oObj->Set($sAttCode, $value); + } + else + { + $oObj->Set($sAttCode, $value); + } + } + } + if (isset($this->m_aData['m_sState']) && !empty($this->m_aData['m_sState'])) + { + $oObj->Set(MetaModel::GetStateAttributeCode($this->m_aData['m_sClass']), $this->m_aData['m_sState']); + } + $oObj->DoComputeValues(); + return $oObj; + } + + public function GetFieldsForDefaultValue() + { + return $this->m_aData['m_aDefaultValueRequested']; + } + + public function SetDefaultValue($sAttCode, $value) + { + // Protect against a request for a non existing field + if (isset($this->m_aData['m_oFieldsMap'][$sAttCode])) + { + $oAttDef = MetaModel::GetAttributeDef($this->m_aData['m_sClass'], $sAttCode); + if ($oAttDef->GetEditClass() == 'List') + { + // special handling for lists + // this as to be handled as an array of objects + // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] + // NOT YET IMPLEMENTED !! + $sLinkedClass = $oAttDef->GetLinkedClass(); + $oSet = $value; + $aData = array(); + $aFields = $this->GetLinkedWizardStructure($oAttDef); + while($oSet->fetch()) + { + foreach($aFields as $sLinkedAttCode) + { + $aRow[$sAttCode] = $oLinkedObj->Get($sLinkedAttCode); + } + $aData[] = $aRow; + } + $this->m_aData['m_oDefaultValue'][$sAttCode] = json_encode($aData); + + } + else + { + // Normal handling for all other scalar attributes + $this->m_aData['m_oDefaultValue'][$sAttCode] = $value; + } + } + } + + public function GetFieldsForAllowedValues() + { + return $this->m_aData['m_aAllowedValuesRequested']; + } + + public function SetAllowedValuesHtml($sAttCode, $sHtml) + { + // Protect against a request for a non existing field + if (isset($this->m_aData['m_oFieldsMap'][$sAttCode])) + { + $this->m_aData['m_oAllowedValues'][$sAttCode] = $sHtml; + } + } + + public function ToJSON() + { + return json_encode($this->m_aData); + } + + static public function FromJSON($sJSON) + { + $oWizHelper = new WizardHelper(); + if (get_magic_quotes_gpc()) + { + $sJSON = stripslashes($sJSON); + } + $aData = json_decode($sJSON, true); // true means hash array instead of object + $oWizHelper->m_aData = $aData; + return $oWizHelper; + } + + protected function GetLinkedWizardStructure($oAttDef) + { + $oWizard = new UIWizard(null, $oAttDef->GetLinkedClass()); + $aWizardSteps = $oWizard->GetWizardStructure(); + $aFields = array(); + $sExtKeyToMeCode = $oAttDef->GetExtKeyToMe(); + // Retrieve as a flat list, all the attributes that are needed to create + // an object of the linked class and put them into a flat array, except + // the attribute 'ext_key_to_me' which is a constant in our case + foreach($aWizardSteps as $sDummy => $aMainSteps) + { + // 2 entries: 'mandatory' and 'optional' + foreach($aMainSteps as $aSteps) + { + // One entry for each step of the wizard + foreach($aSteps as $sAttCode) + { + if ($sAttCode != $sExtKeyToMeCode) + { + $aFields[] = $sAttCode; + } + } + } + } + return $aFields; + } + + public function GetTargetClass() + { + return $this->m_aData['m_sClass']; + } + + public function GetFormPrefix() + { + return $this->m_aData['m_sFormPrefix']; + } + + public function GetInitialState() + { + return isset($this->m_aData['m_sInitialState']) ? $this->m_aData['m_sInitialState'] : null; + } + + public function GetStimulus() + { + return isset($this->m_aData['m_sStimulus']) ? $this->m_aData['m_sStimulus'] : null; + } + + public function GetIdForField($sFieldName) + { + $sResult = ''; + // It may happen that the field we'd like to update does not + // exist in the form. For example, if the field should be hidden/read-only + // in the current state of the object + if (isset($this->m_aData['m_oFieldsMap'][$sFieldName])) + { + $sResult = $this->m_aData['m_oFieldsMap'][$sFieldName]; + } + return $sResult; + } + + static function ParseJsonSet($oMe, $sLinkClass, $sExtKeyToMe, $sJsonSet) + { + $aSet = json_decode($sJsonSet, true); // true means hash array instead of object + $oSet = CMDBObjectSet::FromScratch($sLinkClass); + foreach($aSet as $aLinkObj) + { + $oLink = MetaModel::NewObject($sLinkClass); + foreach($aLinkObj as $sAttCode => $value) + { + $oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode); + if (($oAttDef->IsExternalKey()) && ($value != '') && ($value > 0)) + { + // For external keys: load the target object so that external fields + // get filled too + $oTargetObj = MetaModel::GetObject($oAttDef->GetTargetClass(), $value); + $oLink->Set($sAttCode, $oTargetObj); + } + $oLink->Set($sAttCode, $value); + } + $oLink->Set($sExtKeyToMe, $oMe->GetKey()); + $oSet->AddObject($oLink); + } + return $oSet; + } +} diff --git a/application/xmlpage.class.inc.php b/application/xmlpage.class.inc.php index 0c6f749b5..bb2341248 100644 --- a/application/xmlpage.class.inc.php +++ b/application/xmlpage.class.inc.php @@ -1,106 +1,106 @@ - - - -/** - * Class XMLPage - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT."/application/webpage.class.inc.php"); -/** - * Simple web page with no includes or fancy formatting, useful to generateXML documents - * The page adds the content-type text/XML and the encoding into the headers - */ -class XMLPage extends WebPage -{ - /** - * For big XML files, it's better NOT to store everything in memory and output the XML piece by piece - */ - var $m_bPassThrough; - var $m_bHeaderSent; - - function __construct($s_title, $bPassThrough = false) - { - parent::__construct($s_title); - $this->m_bPassThrough = $bPassThrough; - $this->m_bHeaderSent = false; - $this->add_header("Content-type: text/xml; charset=utf-8"); - $this->add_header("Cache-control: no-cache"); - $this->add_header("Content-location: export.xml"); - } - - public function output() - { - if (!$this->m_bPassThrough) - { - // Get the unexpected output but do nothing with it - $sTrash = $this->ob_get_clean_safe(); - - $this->s_content = "\n".trim($this->s_content); - $this->add_header("Content-Length: ".strlen($this->s_content)); - foreach($this->a_headers as $s_header) - { - header($s_header); - } - echo $this->s_content; - } - if (class_exists('DBSearch')) - { - DBSearch::RecordQueryTrace(); - } - } - - public function add($sText) - { - if (!$this->m_bPassThrough) - { - parent::add($sText); - } - else - { - if ($this->m_bHeaderSent) - { - echo $sText; - } - else - { - $s_captured_output = $this->ob_get_clean_safe(); - foreach($this->a_headers as $s_header) - { - header($s_header); - } - echo "\n"; - echo trim($s_captured_output); - echo trim($this->s_content); - echo $sText; - $this->m_bHeaderSent = true; - } - } - } - - public function small_p($sText) - { - } - - public function table($aConfig, $aData, $aParams = array()) - { - } -} + + + +/** + * Class XMLPage + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT."/application/webpage.class.inc.php"); +/** + * Simple web page with no includes or fancy formatting, useful to generateXML documents + * The page adds the content-type text/XML and the encoding into the headers + */ +class XMLPage extends WebPage +{ + /** + * For big XML files, it's better NOT to store everything in memory and output the XML piece by piece + */ + var $m_bPassThrough; + var $m_bHeaderSent; + + function __construct($s_title, $bPassThrough = false) + { + parent::__construct($s_title); + $this->m_bPassThrough = $bPassThrough; + $this->m_bHeaderSent = false; + $this->add_header("Content-type: text/xml; charset=utf-8"); + $this->add_header("Cache-control: no-cache"); + $this->add_header("Content-location: export.xml"); + } + + public function output() + { + if (!$this->m_bPassThrough) + { + // Get the unexpected output but do nothing with it + $sTrash = $this->ob_get_clean_safe(); + + $this->s_content = "\n".trim($this->s_content); + $this->add_header("Content-Length: ".strlen($this->s_content)); + foreach($this->a_headers as $s_header) + { + header($s_header); + } + echo $this->s_content; + } + if (class_exists('DBSearch')) + { + DBSearch::RecordQueryTrace(); + } + } + + public function add($sText) + { + if (!$this->m_bPassThrough) + { + parent::add($sText); + } + else + { + if ($this->m_bHeaderSent) + { + echo $sText; + } + else + { + $s_captured_output = $this->ob_get_clean_safe(); + foreach($this->a_headers as $s_header) + { + header($s_header); + } + echo "\n"; + echo trim($s_captured_output); + echo trim($this->s_content); + echo $sText; + $this->m_bHeaderSent = true; + } + } + } + + public function small_p($sText) + { + } + + public function table($aConfig, $aData, $aParams = array()) + { + } +} diff --git a/core/MyHelpers.class.inc.php b/core/MyHelpers.class.inc.php index 43a2d2cd2..a1cdeb832 100644 --- a/core/MyHelpers.class.inc.php +++ b/core/MyHelpers.class.inc.php @@ -1,528 +1,528 @@ - - - -/** - * Various dev/debug helpers - * TODO: cleanup or at least re-organize - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * MyHelpers - * - * @package iTopORM - */ -class MyHelpers -{ - public static function CheckValueInArray($sDescription, $value, $aData) - { - if (!in_array($value, $aData)) - { - self::HandleWrongValue($sDescription, $value, $aData); - } - } - - public static function CheckKeyInArray($sDescription, $key, $aData) - { - if (!array_key_exists($key, $aData)) - { - self::HandleWrongValue($sDescription, $key, array_keys($aData)); - } - } - - public static function HandleWrongValue($sDescription, $value, $aData) - { - if (count($aData) == 0) - { - $sArrayDesc = "{}"; - } - else - { - $sArrayDesc = "{".implode(", ", $aData)."}"; - } - // exit! - throw new CoreException("Wrong value for $sDescription, found '$value' while expecting a value in $sArrayDesc"); - } - - // getmicrotime() - // format sss.mmmuuupppnnn - public static function getmicrotime() - { - list($usec, $sec) = explode(" ",microtime()); - return ((float)$usec + (float)$sec); - } - - /* - * MakeSQLComment - * converts hash into text comment which we can use in a (mySQL) query - */ - public static function MakeSQLComment ($aHash) - { - if (empty($aHash)) return ""; - $sComment = ""; - { - foreach($aHash as $sKey=>$sValue) - { - $sComment .= "\n-- ". $sKey ."=>" . $sValue; - } - } - return $sComment; - } - - public static function var_dump_html($aWords, $bFullDisplay = false) - { - echo "
    \n";
    -		if ($bFullDisplay)
    -		{
    -			print_r($aWords); // full dump!
    -		}
    -		else
    -		{
    -			var_dump($aWords); // truncate things when they are too big
    -		}
    -		echo "\n
    \n"; - } - - public static function arg_dump_html() - { - echo "
    \n";
    -		echo "GET:\n";
    -		var_dump($_GET);
    -		echo "POST:\n";
    -		var_dump($_POST);
    -		echo "\n
    \n"; - } - - public static function var_dump_string($var) - { - ob_start(); - print_r($var); - $sRet = ob_get_clean(); - return $sRet; - } - - protected static function first_diff_line($s1, $s2) - { - $aLines1 = explode("\n", $s1); - $aLines2 = explode("\n", $s2); - for ($i = 0 ; $i < min(count($aLines1), count($aLines2)) ; $i++) - { - if ($aLines1[$i] != $aLines2[$i]) return $i; - } - return false; - } - - protected static function highlight_line($sMultiline, $iLine, $sHighlightStart = '', $sHightlightEnd = '') - { - $aLines = explode("\n", $sMultiline); - $aLines[$iLine] = $sHighlightStart.$aLines[$iLine].$sHightlightEnd; - return implode("\n", $aLines); - } - - protected static function first_diff($s1, $s2) - { - // do not work fine with multiline strings - $iLen1 = strlen($s1); - $iLen2 = strlen($s2); - for ($i = 0 ; $i < min($iLen1, $iLen2) ; $i++) - { - if ($s1[$i] !== $s2[$i]) return $i; - } - return false; - } - - protected static function last_diff($s1, $s2) - { - // do not work fine with multiline strings - $iLen1 = strlen($s1); - $iLen2 = strlen($s2); - for ($i = 0 ; $i < min(strlen($s1), strlen($s2)) ; $i++) - { - if ($s1[$iLen1 - $i - 1] !== $s2[$iLen2 - $i - 1]) return array($iLen1 - $i, $iLen2 - $i); - } - return false; - } - - protected static function text_cmp_html($sText1, $sText2, $sHighlight) - { - $iDiffPos = self::first_diff_line($sText1, $sText2); - $sDisp1 = self::highlight_line($sText1, $iDiffPos, '
    ', '
    '); - $sDisp2 = self::highlight_line($sText2, $iDiffPos, '
    ', '
    '); - echo "\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo "
    $sDisp1
    $sDisp2
    \n"; - } - - protected static function string_cmp_html($s1, $s2, $sHighlight) - { - $iDiffPos = self::first_diff($s1, $s2); - if ($iDiffPos === false) - { - echo "strings are identical"; - return; - } - $sStart = substr($s1, 0, $iDiffPos); - - $aLastDiff = self::last_diff($s1, $s2); - $sEnd = substr($s1, $aLastDiff[0]); - - $sMiddle1 = substr($s1, $iDiffPos, $aLastDiff[0] - $iDiffPos); - $sMiddle2 = substr($s2, $iDiffPos, $aLastDiff[1] - $iDiffPos); - - echo "

    $sStart$sMiddle1$sEnd

    \n"; - echo "

    $sStart$sMiddle2$sEnd

    \n"; - } - - protected static function object_cmp_html($oObj1, $oObj2, $sHighlight) - { - $sObj1 = self::var_dump_string($oObj1); - $sObj2 = self::var_dump_string($oObj2); - return self::text_cmp_html($sObj1, $sObj2, $sHighlight); - } - - public static function var_cmp_html($var1, $var2, $sHighlight = 'color:red; font-weight:bold;') - { - if (is_object($var1)) - { - return self::object_cmp_html($var1, $var2, $sHighlight); - } - else if (count(explode("\n", $var1)) > 1) - { - // multiline string - return self::text_cmp_html($var1, $var2, $sHighlight); - } - else - { - return self::string_cmp_html($var1, $var2, $sHighlight); - } - } - - public static function get_callstack($iLevelsToIgnore = 0, $aCallStack = null) - { - if ($aCallStack == null) $aCallStack = debug_backtrace(); - - $aCallStack = array_slice($aCallStack, $iLevelsToIgnore); - - $aDigestCallStack = array(); - $bFirstLine = true; - foreach ($aCallStack as $aCallInfo) - { - $sLine = empty($aCallInfo['line']) ? "" : $aCallInfo['line']; - $sFile = empty($aCallInfo['file']) ? "" : $aCallInfo['file']; - if ($sFile != '') - { - $sFile = str_replace('\\', '/', $sFile); - $sAppRoot = str_replace('\\', '/', APPROOT); - $iPos = strpos($sFile, $sAppRoot); - if ($iPos !== false) - { - $sFile = substr($sFile, strlen($sAppRoot)); - } - } - $sClass = empty($aCallInfo['class']) ? "" : $aCallInfo['class']; - $sType = empty($aCallInfo['type']) ? "" : $aCallInfo['type']; - $sFunction = empty($aCallInfo['function']) ? "" : $aCallInfo['function']; - - if ($bFirstLine) - { - $bFirstLine = false; - // For this line do not display the "function name" because - // that will be the name of our error handler for sure ! - $sFunctionInfo = "N/A"; - } - else - { - $args = ''; - if (empty($aCallInfo['args'])) $aCallInfo['args'] = array(); - foreach ($aCallInfo['args'] as $a) - { - if (!empty($args)) - { - $args .= ', '; - } - switch (gettype($a)) - { - case 'integer': - case 'double': - $args .= $a; - break; - case 'string': - $a = Str::pure2html(self::beautifulstr($a, 64, true, false)); - $args .= "\"$a\""; - break; - case 'array': - $args .= 'array('.count($a).')'; - break; - case 'object': - $args .= 'Object('.get_class($a).')'; - break; - case 'resource': - $args .= 'Resource('.strstr($a, '#').')'; - break; - case 'boolean': - $args .= $a ? 'true' : 'false'; - break; - case 'NULL': - $args .= 'null'; - break; - default: - $args .= 'Unknown'; - } - } - $sFunctionInfo = "$sClass$sType$sFunction($args)"; - } - $aDigestCallStack[] = array('File'=>$sFile, 'Line'=>$sLine, 'Function'=>$sFunctionInfo); - } - return $aDigestCallStack; - } - - public static function get_callstack_html($iLevelsToIgnore = 0, $aCallStack = null) - { - $aDigestCallStack = self::get_callstack($iLevelsToIgnore, $aCallStack); - return self::make_table_from_assoc_array($aDigestCallStack); - } - - public static function dump_callstack($iLevelsToIgnore = 0, $aCallStack = null) - { - return self::get_callstack_html($iLevelsToIgnore, $aCallStack); - } - - public static function get_callstack_text($iLevelsToIgnore = 0, $aCallStack = null) - { - $aDigestCallStack = self::get_callstack($iLevelsToIgnore, $aCallStack); - $aRes = array(); - foreach ($aDigestCallStack as $aCall) - { - $aRes[] = $aCall['File'].' at '.$aCall['Line'].', '.$aCall['Function']; - } - return implode("\n", $aRes); - } - - /////////////////////////////////////////////////////////////////////////////// - // Source: New - // Last modif: 2004/12/20 RQU - /////////////////////////////////////////////////////////////////////////////// - public static function make_table_from_assoc_array(&$aData) - { - if (!is_array($aData)) throw new CoreException("make_table_from_assoc_array: Error - the passed argument is not an array"); - $aFirstRow = reset($aData); - if (count($aData) == 0) return ''; - if (!is_array($aFirstRow)) throw new CoreException("make_table_from_assoc_array: Error - the passed argument is not a bi-dimensional array"); - $sOutput = ""; - $sOutput .= "\n"; - - // Table header - // - $sOutput .= " \n"; - foreach ($aFirstRow as $fieldname=>$trash) { - $sOutput .= " \n"; - } - $sOutput .= " \n"; - - // Table contents - // - $iCount = 0; - foreach ($aData as $aRow) { - $sStyle = ($iCount++ % 2 ? "STYLE=\"background-color : #eeeeee\"" : ""); - $sOutput .= " \n"; - foreach ($aRow as $data) { - if (strlen($data) == 0) { - $data = " "; - } - $sOutput .= " \n"; - } - $sOutput .= " \n"; - } - - $sOutput .= "
    ".$fieldname."
    ".$data."
    \n"; - return $sOutput; - } - - public static function debug_breakpoint($arg) - { - echo "

    Debug breakpoint

    \n"; - MyHelpers::var_dump_html($arg); - MyHelpers::dump_callstack(); - exit; - } - public static function debug_breakpoint_notempty($arg) - { - if (empty($arg)) return; - echo "

    Debug breakpoint (triggered on non-empty value)

    \n"; - MyHelpers::var_dump_html($arg); - MyHelpers::dump_callstack(); - exit; - } - - /** - * xmlentities() - * ... same as htmlentities, but designed for xml ! - */ - public static function xmlentities($string) - { - return str_replace( array( '&', '"', "'", '<', '>' ), array ( '&' , '"', ''' , '<' , '>' ), $string ); - } - - /** - * xmlencode() - * Encodes a string so that for sure it can be output as an xml data string - */ - public static function xmlencode($string) - { - return xmlentities(iconv("UTF-8", "UTF-8//IGNORE",$string)); - } - - /////////////////////////////////////////////////////////////////////////////// - // Source: New - format strings for output - // Last modif: 2005/01/18 RQU - /////////////////////////////////////////////////////////////////////////////// - public static function beautifulstr($sLongString, $iMaxLen, $bShowLen=false, $bShowTooltip=true) - { - if (!is_string($sLongString)) throw new CoreException("beautifulstr: expect a string as 1st argument"); - - // Nothing to do if the string is short - if (strlen($sLongString) <= $iMaxLen) return $sLongString; - - // Truncate the string - $sSuffix = "..."; - if ($bShowLen) { - $sSuffix .= "(".strlen($sLongString)." chars)..."; - } - $sOutput = substr($sLongString, 0, $iMaxLen - strlen($sSuffix)).$sSuffix; - $sOutput = htmlspecialchars($sOutput); - - // Add tooltip if required - //if ($bShowTooltip) { - // $oTooltip = new gui_tooltip($sLongString); - // $sOutput = "get_mouseOver_code().">".$sOutput.""; - //} - return $sOutput; - } -} - -/** -Utility class: static methods for cleaning & escaping untrusted (i.e. -user-supplied) strings. -Any string can (usually) be thought of as being in one of these 'modes': -pure = what the user actually typed / what you want to see on the page / - what is actually stored in the DB -gpc = incoming GET, POST or COOKIE data -sql = escaped for passing safely to RDBMS via SQL (also, data from DB - queries and file reads if you have magic_quotes_runtime on--which - is rare) -html = safe for html display (htmlentities applied) -Always knowing what mode your string is in--using these methods to -convert between modes--will prevent SQL injection and cross-site scripting. -This class refers to its own namespace (so it can work in PHP 4--there is no -self keyword until PHP 5). Do not change the name of the class w/o changing -all the internal references. -Example usage: a POST value that you want to query with: -$username = Str::gpc2sql($_POST['username']); -*/ -//This sets SQL escaping to use slashes; for Sybase(/MSSQL)-style escaping -// ( ' --> '' ), set to true. -define('STR_SYBASE', false); -class Str -{ - public static function gpc2sql($gpc, $maxLength = false) - { - return self::pure2sql(self::gpc2pure($gpc), $maxLength); - } - public static function gpc2html($gpc, $maxLength = false) - { - return self::pure2html(self::gpc2pure($gpc), $maxLength); - } - public static function gpc2pure($gpc) - { - if (ini_get('magic_quotes_sybase')) $pure = str_replace("''", "'", $gpc); - else $pure = get_magic_quotes_gpc() ? stripslashes($gpc) : $gpc; - return $pure; - } - public static function html2pure($html) - { - return html_entity_decode($html); - } - public static function html2sql($html, $maxLength = false) - { - return self::pure2sql(self::html2pure($html), $maxLength); - } - public static function pure2html($pure, $maxLength = false) - { - // Check for HTML entities, but be careful the DB is in UTF-8 - return $maxLength - ? htmlentities(substr($pure, 0, $maxLength), ENT_QUOTES, 'UTF-8') - : htmlentities($pure, ENT_QUOTES, 'UTF-8'); - } - public static function pure2sql($pure, $maxLength = false) - { - if ($maxLength) $pure = substr($pure, 0, $maxLength); - return (STR_SYBASE) - ? str_replace("'", "''", $pure) - : addslashes($pure); - } - public static function sql2html($sql, $maxLength = false) - { - $pure = self::sql2pure($sql); - if ($maxLength) $pure = substr($pure, 0, $maxLength); - return self::pure2html($pure); - } - public static function sql2pure($sql) - { - return (STR_SYBASE) - ? str_replace("''", "'", $sql) - : stripslashes($sql); - } - - public static function xml2pure($xml) - { - // #@# - not implemented - return $xml; - } - public static function pure2xml($pure) - { - return self::xmlencode($pure); - } - - protected static function xmlentities($string) - { - return str_replace( array( '&', '"', "'", '<', '>' ), array ( '&' , '"', ''' , '<' , '>' ), $string ); - } - - /** - * xmlencode() - * Encodes a string so that for sure it can be output as an xml data string - */ - protected static function xmlencode($string) - { - return self::xmlentities(iconv("UTF-8", "UTF-8//IGNORE",$string)); - } - - public static function islowcase($sString) - { - return (strtolower($sString) == $sString); - } -} - -?> + + + +/** + * Various dev/debug helpers + * TODO: cleanup or at least re-organize + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * MyHelpers + * + * @package iTopORM + */ +class MyHelpers +{ + public static function CheckValueInArray($sDescription, $value, $aData) + { + if (!in_array($value, $aData)) + { + self::HandleWrongValue($sDescription, $value, $aData); + } + } + + public static function CheckKeyInArray($sDescription, $key, $aData) + { + if (!array_key_exists($key, $aData)) + { + self::HandleWrongValue($sDescription, $key, array_keys($aData)); + } + } + + public static function HandleWrongValue($sDescription, $value, $aData) + { + if (count($aData) == 0) + { + $sArrayDesc = "{}"; + } + else + { + $sArrayDesc = "{".implode(", ", $aData)."}"; + } + // exit! + throw new CoreException("Wrong value for $sDescription, found '$value' while expecting a value in $sArrayDesc"); + } + + // getmicrotime() + // format sss.mmmuuupppnnn + public static function getmicrotime() + { + list($usec, $sec) = explode(" ",microtime()); + return ((float)$usec + (float)$sec); + } + + /* + * MakeSQLComment + * converts hash into text comment which we can use in a (mySQL) query + */ + public static function MakeSQLComment ($aHash) + { + if (empty($aHash)) return ""; + $sComment = ""; + { + foreach($aHash as $sKey=>$sValue) + { + $sComment .= "\n-- ". $sKey ."=>" . $sValue; + } + } + return $sComment; + } + + public static function var_dump_html($aWords, $bFullDisplay = false) + { + echo "
    \n";
    +		if ($bFullDisplay)
    +		{
    +			print_r($aWords); // full dump!
    +		}
    +		else
    +		{
    +			var_dump($aWords); // truncate things when they are too big
    +		}
    +		echo "\n
    \n"; + } + + public static function arg_dump_html() + { + echo "
    \n";
    +		echo "GET:\n";
    +		var_dump($_GET);
    +		echo "POST:\n";
    +		var_dump($_POST);
    +		echo "\n
    \n"; + } + + public static function var_dump_string($var) + { + ob_start(); + print_r($var); + $sRet = ob_get_clean(); + return $sRet; + } + + protected static function first_diff_line($s1, $s2) + { + $aLines1 = explode("\n", $s1); + $aLines2 = explode("\n", $s2); + for ($i = 0 ; $i < min(count($aLines1), count($aLines2)) ; $i++) + { + if ($aLines1[$i] != $aLines2[$i]) return $i; + } + return false; + } + + protected static function highlight_line($sMultiline, $iLine, $sHighlightStart = '', $sHightlightEnd = '') + { + $aLines = explode("\n", $sMultiline); + $aLines[$iLine] = $sHighlightStart.$aLines[$iLine].$sHightlightEnd; + return implode("\n", $aLines); + } + + protected static function first_diff($s1, $s2) + { + // do not work fine with multiline strings + $iLen1 = strlen($s1); + $iLen2 = strlen($s2); + for ($i = 0 ; $i < min($iLen1, $iLen2) ; $i++) + { + if ($s1[$i] !== $s2[$i]) return $i; + } + return false; + } + + protected static function last_diff($s1, $s2) + { + // do not work fine with multiline strings + $iLen1 = strlen($s1); + $iLen2 = strlen($s2); + for ($i = 0 ; $i < min(strlen($s1), strlen($s2)) ; $i++) + { + if ($s1[$iLen1 - $i - 1] !== $s2[$iLen2 - $i - 1]) return array($iLen1 - $i, $iLen2 - $i); + } + return false; + } + + protected static function text_cmp_html($sText1, $sText2, $sHighlight) + { + $iDiffPos = self::first_diff_line($sText1, $sText2); + $sDisp1 = self::highlight_line($sText1, $iDiffPos, '
    ', '
    '); + $sDisp2 = self::highlight_line($sText2, $iDiffPos, '
    ', '
    '); + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
    $sDisp1
    $sDisp2
    \n"; + } + + protected static function string_cmp_html($s1, $s2, $sHighlight) + { + $iDiffPos = self::first_diff($s1, $s2); + if ($iDiffPos === false) + { + echo "strings are identical"; + return; + } + $sStart = substr($s1, 0, $iDiffPos); + + $aLastDiff = self::last_diff($s1, $s2); + $sEnd = substr($s1, $aLastDiff[0]); + + $sMiddle1 = substr($s1, $iDiffPos, $aLastDiff[0] - $iDiffPos); + $sMiddle2 = substr($s2, $iDiffPos, $aLastDiff[1] - $iDiffPos); + + echo "

    $sStart$sMiddle1$sEnd

    \n"; + echo "

    $sStart$sMiddle2$sEnd

    \n"; + } + + protected static function object_cmp_html($oObj1, $oObj2, $sHighlight) + { + $sObj1 = self::var_dump_string($oObj1); + $sObj2 = self::var_dump_string($oObj2); + return self::text_cmp_html($sObj1, $sObj2, $sHighlight); + } + + public static function var_cmp_html($var1, $var2, $sHighlight = 'color:red; font-weight:bold;') + { + if (is_object($var1)) + { + return self::object_cmp_html($var1, $var2, $sHighlight); + } + else if (count(explode("\n", $var1)) > 1) + { + // multiline string + return self::text_cmp_html($var1, $var2, $sHighlight); + } + else + { + return self::string_cmp_html($var1, $var2, $sHighlight); + } + } + + public static function get_callstack($iLevelsToIgnore = 0, $aCallStack = null) + { + if ($aCallStack == null) $aCallStack = debug_backtrace(); + + $aCallStack = array_slice($aCallStack, $iLevelsToIgnore); + + $aDigestCallStack = array(); + $bFirstLine = true; + foreach ($aCallStack as $aCallInfo) + { + $sLine = empty($aCallInfo['line']) ? "" : $aCallInfo['line']; + $sFile = empty($aCallInfo['file']) ? "" : $aCallInfo['file']; + if ($sFile != '') + { + $sFile = str_replace('\\', '/', $sFile); + $sAppRoot = str_replace('\\', '/', APPROOT); + $iPos = strpos($sFile, $sAppRoot); + if ($iPos !== false) + { + $sFile = substr($sFile, strlen($sAppRoot)); + } + } + $sClass = empty($aCallInfo['class']) ? "" : $aCallInfo['class']; + $sType = empty($aCallInfo['type']) ? "" : $aCallInfo['type']; + $sFunction = empty($aCallInfo['function']) ? "" : $aCallInfo['function']; + + if ($bFirstLine) + { + $bFirstLine = false; + // For this line do not display the "function name" because + // that will be the name of our error handler for sure ! + $sFunctionInfo = "N/A"; + } + else + { + $args = ''; + if (empty($aCallInfo['args'])) $aCallInfo['args'] = array(); + foreach ($aCallInfo['args'] as $a) + { + if (!empty($args)) + { + $args .= ', '; + } + switch (gettype($a)) + { + case 'integer': + case 'double': + $args .= $a; + break; + case 'string': + $a = Str::pure2html(self::beautifulstr($a, 64, true, false)); + $args .= "\"$a\""; + break; + case 'array': + $args .= 'array('.count($a).')'; + break; + case 'object': + $args .= 'Object('.get_class($a).')'; + break; + case 'resource': + $args .= 'Resource('.strstr($a, '#').')'; + break; + case 'boolean': + $args .= $a ? 'true' : 'false'; + break; + case 'NULL': + $args .= 'null'; + break; + default: + $args .= 'Unknown'; + } + } + $sFunctionInfo = "$sClass$sType$sFunction($args)"; + } + $aDigestCallStack[] = array('File'=>$sFile, 'Line'=>$sLine, 'Function'=>$sFunctionInfo); + } + return $aDigestCallStack; + } + + public static function get_callstack_html($iLevelsToIgnore = 0, $aCallStack = null) + { + $aDigestCallStack = self::get_callstack($iLevelsToIgnore, $aCallStack); + return self::make_table_from_assoc_array($aDigestCallStack); + } + + public static function dump_callstack($iLevelsToIgnore = 0, $aCallStack = null) + { + return self::get_callstack_html($iLevelsToIgnore, $aCallStack); + } + + public static function get_callstack_text($iLevelsToIgnore = 0, $aCallStack = null) + { + $aDigestCallStack = self::get_callstack($iLevelsToIgnore, $aCallStack); + $aRes = array(); + foreach ($aDigestCallStack as $aCall) + { + $aRes[] = $aCall['File'].' at '.$aCall['Line'].', '.$aCall['Function']; + } + return implode("\n", $aRes); + } + + /////////////////////////////////////////////////////////////////////////////// + // Source: New + // Last modif: 2004/12/20 RQU + /////////////////////////////////////////////////////////////////////////////// + public static function make_table_from_assoc_array(&$aData) + { + if (!is_array($aData)) throw new CoreException("make_table_from_assoc_array: Error - the passed argument is not an array"); + $aFirstRow = reset($aData); + if (count($aData) == 0) return ''; + if (!is_array($aFirstRow)) throw new CoreException("make_table_from_assoc_array: Error - the passed argument is not a bi-dimensional array"); + $sOutput = ""; + $sOutput .= "\n"; + + // Table header + // + $sOutput .= " \n"; + foreach ($aFirstRow as $fieldname=>$trash) { + $sOutput .= " \n"; + } + $sOutput .= " \n"; + + // Table contents + // + $iCount = 0; + foreach ($aData as $aRow) { + $sStyle = ($iCount++ % 2 ? "STYLE=\"background-color : #eeeeee\"" : ""); + $sOutput .= " \n"; + foreach ($aRow as $data) { + if (strlen($data) == 0) { + $data = " "; + } + $sOutput .= " \n"; + } + $sOutput .= " \n"; + } + + $sOutput .= "
    ".$fieldname."
    ".$data."
    \n"; + return $sOutput; + } + + public static function debug_breakpoint($arg) + { + echo "

    Debug breakpoint

    \n"; + MyHelpers::var_dump_html($arg); + MyHelpers::dump_callstack(); + exit; + } + public static function debug_breakpoint_notempty($arg) + { + if (empty($arg)) return; + echo "

    Debug breakpoint (triggered on non-empty value)

    \n"; + MyHelpers::var_dump_html($arg); + MyHelpers::dump_callstack(); + exit; + } + + /** + * xmlentities() + * ... same as htmlentities, but designed for xml ! + */ + public static function xmlentities($string) + { + return str_replace( array( '&', '"', "'", '<', '>' ), array ( '&' , '"', ''' , '<' , '>' ), $string ); + } + + /** + * xmlencode() + * Encodes a string so that for sure it can be output as an xml data string + */ + public static function xmlencode($string) + { + return xmlentities(iconv("UTF-8", "UTF-8//IGNORE",$string)); + } + + /////////////////////////////////////////////////////////////////////////////// + // Source: New - format strings for output + // Last modif: 2005/01/18 RQU + /////////////////////////////////////////////////////////////////////////////// + public static function beautifulstr($sLongString, $iMaxLen, $bShowLen=false, $bShowTooltip=true) + { + if (!is_string($sLongString)) throw new CoreException("beautifulstr: expect a string as 1st argument"); + + // Nothing to do if the string is short + if (strlen($sLongString) <= $iMaxLen) return $sLongString; + + // Truncate the string + $sSuffix = "..."; + if ($bShowLen) { + $sSuffix .= "(".strlen($sLongString)." chars)..."; + } + $sOutput = substr($sLongString, 0, $iMaxLen - strlen($sSuffix)).$sSuffix; + $sOutput = htmlspecialchars($sOutput); + + // Add tooltip if required + //if ($bShowTooltip) { + // $oTooltip = new gui_tooltip($sLongString); + // $sOutput = "get_mouseOver_code().">".$sOutput.""; + //} + return $sOutput; + } +} + +/** +Utility class: static methods for cleaning & escaping untrusted (i.e. +user-supplied) strings. +Any string can (usually) be thought of as being in one of these 'modes': +pure = what the user actually typed / what you want to see on the page / + what is actually stored in the DB +gpc = incoming GET, POST or COOKIE data +sql = escaped for passing safely to RDBMS via SQL (also, data from DB + queries and file reads if you have magic_quotes_runtime on--which + is rare) +html = safe for html display (htmlentities applied) +Always knowing what mode your string is in--using these methods to +convert between modes--will prevent SQL injection and cross-site scripting. +This class refers to its own namespace (so it can work in PHP 4--there is no +self keyword until PHP 5). Do not change the name of the class w/o changing +all the internal references. +Example usage: a POST value that you want to query with: +$username = Str::gpc2sql($_POST['username']); +*/ +//This sets SQL escaping to use slashes; for Sybase(/MSSQL)-style escaping +// ( ' --> '' ), set to true. +define('STR_SYBASE', false); +class Str +{ + public static function gpc2sql($gpc, $maxLength = false) + { + return self::pure2sql(self::gpc2pure($gpc), $maxLength); + } + public static function gpc2html($gpc, $maxLength = false) + { + return self::pure2html(self::gpc2pure($gpc), $maxLength); + } + public static function gpc2pure($gpc) + { + if (ini_get('magic_quotes_sybase')) $pure = str_replace("''", "'", $gpc); + else $pure = get_magic_quotes_gpc() ? stripslashes($gpc) : $gpc; + return $pure; + } + public static function html2pure($html) + { + return html_entity_decode($html); + } + public static function html2sql($html, $maxLength = false) + { + return self::pure2sql(self::html2pure($html), $maxLength); + } + public static function pure2html($pure, $maxLength = false) + { + // Check for HTML entities, but be careful the DB is in UTF-8 + return $maxLength + ? htmlentities(substr($pure, 0, $maxLength), ENT_QUOTES, 'UTF-8') + : htmlentities($pure, ENT_QUOTES, 'UTF-8'); + } + public static function pure2sql($pure, $maxLength = false) + { + if ($maxLength) $pure = substr($pure, 0, $maxLength); + return (STR_SYBASE) + ? str_replace("'", "''", $pure) + : addslashes($pure); + } + public static function sql2html($sql, $maxLength = false) + { + $pure = self::sql2pure($sql); + if ($maxLength) $pure = substr($pure, 0, $maxLength); + return self::pure2html($pure); + } + public static function sql2pure($sql) + { + return (STR_SYBASE) + ? str_replace("''", "'", $sql) + : stripslashes($sql); + } + + public static function xml2pure($xml) + { + // #@# - not implemented + return $xml; + } + public static function pure2xml($pure) + { + return self::xmlencode($pure); + } + + protected static function xmlentities($string) + { + return str_replace( array( '&', '"', "'", '<', '>' ), array ( '&' , '"', ''' , '<' , '>' ), $string ); + } + + /** + * xmlencode() + * Encodes a string so that for sure it can be output as an xml data string + */ + protected static function xmlencode($string) + { + return self::xmlentities(iconv("UTF-8", "UTF-8//IGNORE",$string)); + } + + public static function islowcase($sString) + { + return (strtolower($sString) == $sString); + } +} + +?> diff --git a/core/action.class.inc.php b/core/action.class.inc.php index 55a96fcda..277995f3d 100644 --- a/core/action.class.inc.php +++ b/core/action.class.inc.php @@ -1,427 +1,427 @@ - - - -/** - * Persistent classes (internal): user defined actions - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -require_once(APPROOT.'/core/asynctask.class.inc.php'); -require_once(APPROOT.'/core/email.class.inc.php'); - -/** - * A user defined action, to customize the application - * - * @package iTopORM - */ -abstract class Action extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array('name'), - "db_table" => "priv_action", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum(array('test'=>'Being tested' ,'enabled'=>'In production', 'disabled'=>'Inactive')), "sql"=>"status", "default_value"=>"test", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("trigger_list", array("linked_class"=>"lnkTriggerAction", "ext_key_to_me"=>"action_id", "ext_key_to_remote"=>"trigger_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - abstract public function DoExecute($oTrigger, $aContextArgs); - - public function IsActive() - { - switch($this->Get('status')) - { - case 'enabled': - case 'test': - return true; - - default: - return false; - } - } - - public function IsBeingTested() - { - switch($this->Get('status')) - { - case 'test': - return true; - - default: - return false; - } - } -} - -/** - * A notification - * - * @package iTopORM - */ -abstract class ActionNotification extends Action -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array('name'), - "db_table" => "priv_action_notification", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -/** - * An email notification - * - * @package iTopORM - */ -class ActionEmail extends ActionNotification -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array('name'), - "db_table" => "priv_action_email", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeEmailAddress("test_recipient", array("allowed_values"=>null, "sql"=>"test_recipient", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeString("from", array("allowed_values"=>null, "sql"=>"from", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("reply_to", array("allowed_values"=>null, "sql"=>"reply_to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("cc", array("allowed_values"=>null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeTemplateHTML("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values"=>new ValueSetEnum('low,normal,high'), "sql"=>"importance", "default_value"=>'normal', "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('name','description', 'status', 'subject')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - // count the recipients found - protected $m_iRecipients; - - // Errors management : not that simple because we need that function to be - // executed in the background, while making sure that any issue would be reported clearly - protected $m_aMailErrors; //array of strings explaining the issue - - // returns a the list of emails as a string, or a detailed error description - protected function FindRecipients($sRecipAttCode, $aArgs) - { - $sOQL = $this->Get($sRecipAttCode); - if (strlen($sOQL) == '') return ''; - - try - { - $oSearch = DBObjectSearch::FromOQL($sOQL); - $oSearch->AllowAllData(); - } - catch (OQLException $e) - { - $this->m_aMailErrors[] = "query syntax error for recipient '$sRecipAttCode'"; - return $e->getMessage(); - } - - $sClass = $oSearch->GetClass(); - // Determine the email attribute (the first one will be our choice) - foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeEmailAddress) - { - $sEmailAttCode = $sAttCode; - // we've got one, exit the loop - break; - } - } - if (!isset($sEmailAttCode)) - { - $this->m_aMailErrors[] = "wrong target for recipient '$sRecipAttCode'"; - return "The objects of the class '$sClass' do not have any email attribute"; - } - - $oSet = new DBObjectSet($oSearch, array() /* order */, $aArgs); - $aRecipients = array(); - while ($oObj = $oSet->Fetch()) - { - $sAddress = trim($oObj->Get($sEmailAttCode)); - if (strlen($sAddress) > 0) - { - $aRecipients[] = $sAddress; - $this->m_iRecipients++; - } - } - return implode(', ', $aRecipients); - } - - - public function DoExecute($oTrigger, $aContextArgs) - { - if (MetaModel::IsLogEnabledNotification()) - { - $oLog = new EventNotificationEmail(); - if ($this->IsBeingTested()) - { - $oLog->Set('message', 'TEST - Notification sent ('.$this->Get('test_recipient').')'); - } - else - { - $oLog->Set('message', 'Notification pending'); - } - $oLog->Set('userinfo', UserRights::GetUser()); - $oLog->Set('trigger_id', $oTrigger->GetKey()); - $oLog->Set('action_id', $this->GetKey()); - $oLog->Set('object_id', $aContextArgs['this->object()']->GetKey()); - // Must be inserted now so that it gets a valid id that will make the link - // between an eventual asynchronous task (queued) and the log - $oLog->DBInsertNoReload(); - } - else - { - $oLog = null; - } - - try - { - $sRes = $this->_DoExecute($oTrigger, $aContextArgs, $oLog); - - if ($this->IsBeingTested()) - { - $sPrefix = 'TEST ('.$this->Get('test_recipient').') - '; - } - else - { - $sPrefix = ''; - } - - if ($oLog) - { - $oLog->Set('message', $sPrefix . $sRes); - $oLog->DBUpdate(); - } - - } - catch (Exception $e) - { - if ($oLog) - { - $oLog->Set('message', 'Error: '.$e->getMessage()); - - try - { - $oLog->DBUpdate(); - } - catch (Exception $eSecondTryUpdate) - { - IssueLog::Error("Failed to process email ".$oLog->GetKey()." - reason: ".$e->getMessage()."\nTrace:\n".$e->getTraceAsString()); - - $oLog->Set('message', 'Error: more details in the log for email "'.$oLog->GetKey().'"'); - $oLog->DBUpdate(); - } - } - } - - } - - protected function _DoExecute($oTrigger, $aContextArgs, &$oLog) - { - $sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass(); - try - { - $this->m_iRecipients = 0; - $this->m_aMailErrors = array(); - $bRes = false; // until we do succeed in sending the email - - // Determine recicipients - // - $sTo = $this->FindRecipients('to', $aContextArgs); - $sCC = $this->FindRecipients('cc', $aContextArgs); - $sBCC = $this->FindRecipients('bcc', $aContextArgs); - - $sFrom = MetaModel::ApplyParams($this->Get('from'), $aContextArgs); - $sReplyTo = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs); - - $sSubject = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs); - $sBody = MetaModel::ApplyParams($this->Get('body'), $aContextArgs); - - $oObj = $aContextArgs['this->object()']; - $sMessageId = sprintf('iTop_%s_%d_%f@%s.openitop.org', get_class($oObj), $oObj->GetKey(), microtime(true /* get as float*/), MetaModel::GetEnvironmentId()); - $sReference = '<'.$sMessageId.'>'; - } - catch(Exception $e) - { - ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker); - throw $e; - } - ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker); - - if (!is_null($oLog)) - { - // Note: we have to secure this because those values are calculated - // inside the try statement, and we would like to keep track of as - // many data as we could while some variables may still be undefined - if (isset($sTo)) $oLog->Set('to', $sTo); - if (isset($sCC)) $oLog->Set('cc', $sCC); - if (isset($sBCC)) $oLog->Set('bcc', $sBCC); - if (isset($sFrom)) $oLog->Set('from', $sFrom); - if (isset($sSubject)) $oLog->Set('subject', $sSubject); - if (isset($sBody)) $oLog->Set('body', $sBody); - } - $sStyles = file_get_contents(APPROOT.'css/email.css'); - $sStyles .= MetaModel::GetConfig()->Get('email_css'); - - $oEmail = new EMail(); - - if ($this->IsBeingTested()) - { - $oEmail->SetSubject('TEST['.$sSubject.']'); - $sTestBody = $sBody; - $sTestBody .= "
    \n"; - $sTestBody .= "

    Testing email notification ".$this->GetHyperlink()."

    \n"; - $sTestBody .= "

    The email should be sent with the following properties\n"; - $sTestBody .= "

      \n"; - $sTestBody .= "
    • TO: $sTo
    • \n"; - $sTestBody .= "
    • CC: $sCC
    • \n"; - $sTestBody .= "
    • BCC: $sBCC
    • \n"; - $sTestBody .= "
    • From: $sFrom
    • \n"; - $sTestBody .= "
    • Reply-To: $sReplyTo
    • \n"; - $sTestBody .= "
    • References: $sReference
    • \n"; - $sTestBody .= "
    \n"; - $sTestBody .= "

    \n"; - $sTestBody .= "
    \n"; - $oEmail->SetBody($sTestBody, 'text/html', $sStyles); - $oEmail->SetRecipientTO($this->Get('test_recipient')); - $oEmail->SetRecipientFrom($sFrom); - $oEmail->SetReferences($sReference); - $oEmail->SetMessageId($sMessageId); - } - else - { - $oEmail->SetSubject($sSubject); - $oEmail->SetBody($sBody, 'text/html', $sStyles); - $oEmail->SetRecipientTO($sTo); - $oEmail->SetRecipientCC($sCC); - $oEmail->SetRecipientBCC($sBCC); - $oEmail->SetRecipientFrom($sFrom); - $oEmail->SetRecipientReplyTo($sReplyTo); - $oEmail->SetReferences($sReference); - $oEmail->SetMessageId($sMessageId); - } - - if (isset($aContextArgs['attachments'])) - { - $aAttachmentReport = array(); - foreach($aContextArgs['attachments'] as $oDocument) - { - $oEmail->AddAttachment($oDocument->GetData(), $oDocument->GetFileName(), $oDocument->GetMimeType()); - $aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData())); - } - $oLog->Set('attachments', $aAttachmentReport); - } - - if (empty($this->m_aMailErrors)) - { - if ($this->m_iRecipients == 0) - { - return 'No recipient'; - } - else - { - $iRes = $oEmail->Send($aErrors, false, $oLog); // allow asynchronous mode - switch ($iRes) - { - case EMAIL_SEND_OK: - return "Sent"; - - case EMAIL_SEND_PENDING: - return "Pending"; - - case EMAIL_SEND_ERROR: - return "Errors: ".implode(', ', $aErrors); - } - } - } - else - { - if (is_array($this->m_aMailErrors) && count($this->m_aMailErrors) > 0) - { - $sError = implode(', ', $this->m_aMailErrors); - } - else - { - $sError = 'Unknown reason'; - } - return 'Notification was not sent: '.$sError; - } - } -} -?> + + + +/** + * Persistent classes (internal): user defined actions + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +require_once(APPROOT.'/core/asynctask.class.inc.php'); +require_once(APPROOT.'/core/email.class.inc.php'); + +/** + * A user defined action, to customize the application + * + * @package iTopORM + */ +abstract class Action extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array('name'), + "db_table" => "priv_action", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum(array('test'=>'Being tested' ,'enabled'=>'In production', 'disabled'=>'Inactive')), "sql"=>"status", "default_value"=>"test", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("trigger_list", array("linked_class"=>"lnkTriggerAction", "ext_key_to_me"=>"action_id", "ext_key_to_remote"=>"trigger_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + abstract public function DoExecute($oTrigger, $aContextArgs); + + public function IsActive() + { + switch($this->Get('status')) + { + case 'enabled': + case 'test': + return true; + + default: + return false; + } + } + + public function IsBeingTested() + { + switch($this->Get('status')) + { + case 'test': + return true; + + default: + return false; + } + } +} + +/** + * A notification + * + * @package iTopORM + */ +abstract class ActionNotification extends Action +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array('name'), + "db_table" => "priv_action_notification", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +/** + * An email notification + * + * @package iTopORM + */ +class ActionEmail extends ActionNotification +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array('name'), + "db_table" => "priv_action_email", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeEmailAddress("test_recipient", array("allowed_values"=>null, "sql"=>"test_recipient", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeString("from", array("allowed_values"=>null, "sql"=>"from", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("reply_to", array("allowed_values"=>null, "sql"=>"reply_to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("cc", array("allowed_values"=>null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTemplateHTML("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values"=>new ValueSetEnum('low,normal,high'), "sql"=>"importance", "default_value"=>'normal', "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('name','description', 'status', 'subject')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + // count the recipients found + protected $m_iRecipients; + + // Errors management : not that simple because we need that function to be + // executed in the background, while making sure that any issue would be reported clearly + protected $m_aMailErrors; //array of strings explaining the issue + + // returns a the list of emails as a string, or a detailed error description + protected function FindRecipients($sRecipAttCode, $aArgs) + { + $sOQL = $this->Get($sRecipAttCode); + if (strlen($sOQL) == '') return ''; + + try + { + $oSearch = DBObjectSearch::FromOQL($sOQL); + $oSearch->AllowAllData(); + } + catch (OQLException $e) + { + $this->m_aMailErrors[] = "query syntax error for recipient '$sRecipAttCode'"; + return $e->getMessage(); + } + + $sClass = $oSearch->GetClass(); + // Determine the email attribute (the first one will be our choice) + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeEmailAddress) + { + $sEmailAttCode = $sAttCode; + // we've got one, exit the loop + break; + } + } + if (!isset($sEmailAttCode)) + { + $this->m_aMailErrors[] = "wrong target for recipient '$sRecipAttCode'"; + return "The objects of the class '$sClass' do not have any email attribute"; + } + + $oSet = new DBObjectSet($oSearch, array() /* order */, $aArgs); + $aRecipients = array(); + while ($oObj = $oSet->Fetch()) + { + $sAddress = trim($oObj->Get($sEmailAttCode)); + if (strlen($sAddress) > 0) + { + $aRecipients[] = $sAddress; + $this->m_iRecipients++; + } + } + return implode(', ', $aRecipients); + } + + + public function DoExecute($oTrigger, $aContextArgs) + { + if (MetaModel::IsLogEnabledNotification()) + { + $oLog = new EventNotificationEmail(); + if ($this->IsBeingTested()) + { + $oLog->Set('message', 'TEST - Notification sent ('.$this->Get('test_recipient').')'); + } + else + { + $oLog->Set('message', 'Notification pending'); + } + $oLog->Set('userinfo', UserRights::GetUser()); + $oLog->Set('trigger_id', $oTrigger->GetKey()); + $oLog->Set('action_id', $this->GetKey()); + $oLog->Set('object_id', $aContextArgs['this->object()']->GetKey()); + // Must be inserted now so that it gets a valid id that will make the link + // between an eventual asynchronous task (queued) and the log + $oLog->DBInsertNoReload(); + } + else + { + $oLog = null; + } + + try + { + $sRes = $this->_DoExecute($oTrigger, $aContextArgs, $oLog); + + if ($this->IsBeingTested()) + { + $sPrefix = 'TEST ('.$this->Get('test_recipient').') - '; + } + else + { + $sPrefix = ''; + } + + if ($oLog) + { + $oLog->Set('message', $sPrefix . $sRes); + $oLog->DBUpdate(); + } + + } + catch (Exception $e) + { + if ($oLog) + { + $oLog->Set('message', 'Error: '.$e->getMessage()); + + try + { + $oLog->DBUpdate(); + } + catch (Exception $eSecondTryUpdate) + { + IssueLog::Error("Failed to process email ".$oLog->GetKey()." - reason: ".$e->getMessage()."\nTrace:\n".$e->getTraceAsString()); + + $oLog->Set('message', 'Error: more details in the log for email "'.$oLog->GetKey().'"'); + $oLog->DBUpdate(); + } + } + } + + } + + protected function _DoExecute($oTrigger, $aContextArgs, &$oLog) + { + $sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass(); + try + { + $this->m_iRecipients = 0; + $this->m_aMailErrors = array(); + $bRes = false; // until we do succeed in sending the email + + // Determine recicipients + // + $sTo = $this->FindRecipients('to', $aContextArgs); + $sCC = $this->FindRecipients('cc', $aContextArgs); + $sBCC = $this->FindRecipients('bcc', $aContextArgs); + + $sFrom = MetaModel::ApplyParams($this->Get('from'), $aContextArgs); + $sReplyTo = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs); + + $sSubject = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs); + $sBody = MetaModel::ApplyParams($this->Get('body'), $aContextArgs); + + $oObj = $aContextArgs['this->object()']; + $sMessageId = sprintf('iTop_%s_%d_%f@%s.openitop.org', get_class($oObj), $oObj->GetKey(), microtime(true /* get as float*/), MetaModel::GetEnvironmentId()); + $sReference = '<'.$sMessageId.'>'; + } + catch(Exception $e) + { + ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker); + throw $e; + } + ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker); + + if (!is_null($oLog)) + { + // Note: we have to secure this because those values are calculated + // inside the try statement, and we would like to keep track of as + // many data as we could while some variables may still be undefined + if (isset($sTo)) $oLog->Set('to', $sTo); + if (isset($sCC)) $oLog->Set('cc', $sCC); + if (isset($sBCC)) $oLog->Set('bcc', $sBCC); + if (isset($sFrom)) $oLog->Set('from', $sFrom); + if (isset($sSubject)) $oLog->Set('subject', $sSubject); + if (isset($sBody)) $oLog->Set('body', $sBody); + } + $sStyles = file_get_contents(APPROOT.'css/email.css'); + $sStyles .= MetaModel::GetConfig()->Get('email_css'); + + $oEmail = new EMail(); + + if ($this->IsBeingTested()) + { + $oEmail->SetSubject('TEST['.$sSubject.']'); + $sTestBody = $sBody; + $sTestBody .= "
    \n"; + $sTestBody .= "

    Testing email notification ".$this->GetHyperlink()."

    \n"; + $sTestBody .= "

    The email should be sent with the following properties\n"; + $sTestBody .= "

      \n"; + $sTestBody .= "
    • TO: $sTo
    • \n"; + $sTestBody .= "
    • CC: $sCC
    • \n"; + $sTestBody .= "
    • BCC: $sBCC
    • \n"; + $sTestBody .= "
    • From: $sFrom
    • \n"; + $sTestBody .= "
    • Reply-To: $sReplyTo
    • \n"; + $sTestBody .= "
    • References: $sReference
    • \n"; + $sTestBody .= "
    \n"; + $sTestBody .= "

    \n"; + $sTestBody .= "
    \n"; + $oEmail->SetBody($sTestBody, 'text/html', $sStyles); + $oEmail->SetRecipientTO($this->Get('test_recipient')); + $oEmail->SetRecipientFrom($sFrom); + $oEmail->SetReferences($sReference); + $oEmail->SetMessageId($sMessageId); + } + else + { + $oEmail->SetSubject($sSubject); + $oEmail->SetBody($sBody, 'text/html', $sStyles); + $oEmail->SetRecipientTO($sTo); + $oEmail->SetRecipientCC($sCC); + $oEmail->SetRecipientBCC($sBCC); + $oEmail->SetRecipientFrom($sFrom); + $oEmail->SetRecipientReplyTo($sReplyTo); + $oEmail->SetReferences($sReference); + $oEmail->SetMessageId($sMessageId); + } + + if (isset($aContextArgs['attachments'])) + { + $aAttachmentReport = array(); + foreach($aContextArgs['attachments'] as $oDocument) + { + $oEmail->AddAttachment($oDocument->GetData(), $oDocument->GetFileName(), $oDocument->GetMimeType()); + $aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData())); + } + $oLog->Set('attachments', $aAttachmentReport); + } + + if (empty($this->m_aMailErrors)) + { + if ($this->m_iRecipients == 0) + { + return 'No recipient'; + } + else + { + $iRes = $oEmail->Send($aErrors, false, $oLog); // allow asynchronous mode + switch ($iRes) + { + case EMAIL_SEND_OK: + return "Sent"; + + case EMAIL_SEND_PENDING: + return "Pending"; + + case EMAIL_SEND_ERROR: + return "Errors: ".implode(', ', $aErrors); + } + } + } + else + { + if (is_array($this->m_aMailErrors) && count($this->m_aMailErrors) > 0) + { + $sError = implode(', ', $this->m_aMailErrors); + } + else + { + $sError = 'Unknown reason'; + } + return 'Notification was not sent: '.$sError; + } + } +} +?> diff --git a/core/apc-emulation.php b/core/apc-emulation.php index 44163cc49..aff084307 100644 --- a/core/apc-emulation.php +++ b/core/apc-emulation.php @@ -1,333 +1,333 @@ - -// - -/** - * Date: 27/09/2017 - */ - -/** - * @param string $cache_type - * @param bool $limited - * @return array|bool - */ -function apc_cache_info($cache_type = '', $limited = false) -{ - $aInfo = array(); - $sRootCacheDir = apcFile::GetCacheFileName(); - $aInfo['cache_list'] = apcFile::GetCacheEntries($sRootCacheDir); - return $aInfo; -} - -/** - * @param array|string $key - * @param $var - * @param int $ttl - * @return array|bool - */ -function apc_store($key, $var = NULL, $ttl = 0) -{ - if (is_array($key)) - { - $aResult = array(); - foreach($key as $sKey => $value) - { - $aResult[] = apcFile::StoreOneFile($sKey, $value, $ttl); - } - return $aResult; - } - return apcFile::StoreOneFile($key, $var, $ttl); -} - -/** - * @param $key string|array - * @return mixed - */ -function apc_fetch($key) -{ - if (is_array($key)) - { - $aResult = array(); - foreach($key as $sKey) - { - $aResult[$sKey] = apcFile::FetchOneFile($sKey); - } - return $aResult; - } - return apcFile::FetchOneFile($key); -} - -/** - * @param string $cache_type - * @return bool - */ -function apc_clear_cache($cache_type = '') -{ - apcFile::DeleteEntry(utils::GetCachePath()); - return true; -} - -/** - * @param $key - * @return bool|string[] - */ -function apc_delete($key) -{ - if (empty($key)) - { - return false; - } - $bRet1 = apcFile::DeleteEntry(apcFile::GetCacheFileName($key)); - $bRet2 = apcFile::DeleteEntry(apcFile::GetCacheFileName('-'.$key)); - return $bRet1 || $bRet2; -} - -class apcFile -{ - // Check only once per request - static public $aFilesByTime = null; - static public $iFileCount = 0; - - /** Get the file name corresponding to the cache entry. - * If an empty key is provided, the root of the cache is returned. - * @param $sKey - * @return string - */ - static public function GetCacheFileName($sKey = '') - { - $sPath = str_replace(array(' ', '/', '\\', '.'), '-', $sKey); - return utils::GetCachePath().'apc-emul/'.$sPath; - } - - /** Get the list of entries from a starting folder. - * @param $sEntry string starting folder. - * @return array list of entries stored into array of key 'info' - */ - static public function GetCacheEntries($sEntry) - { - $aResult = array(); - if (is_dir($sEntry)) - { - $aFiles = array_diff(scandir($sEntry), array('.', '..')); - foreach($aFiles as $sFile) - { - $sSubFile = $sEntry.'/'.$sFile; - $aResult = array_merge($aResult, self::GetCacheEntries($sSubFile)); - } - } - else - { - $sKey = basename($sEntry); - if (strpos($sKey, '-') === 0) - { - $sKey = substr($sKey, 1); - } - $aResult[] = array('info' => $sKey); - } - return $aResult; - } - - /** Delete one cache entry. - * @param $sCache - * @return bool true if the entry was deleted false if error occurs (like entry did not exist). - */ - static public function DeleteEntry($sCache) - { - if (is_dir($sCache)) - { - $aFiles = array_diff(scandir($sCache), array('.', '..')); - foreach($aFiles as $sFile) - { - $sSubFile = $sCache.'/'.$sFile; - if (!self::DeleteEntry($sSubFile)) - { - return false; - } - } - if (!@rmdir($sCache)) - { - return false; - } - } - else - { - if (!@unlink($sCache)) - { - return false; - } - } - - self::ResetFileCount(); - return true; - } - - /** Get one cache entry content. - * @param $sKey - * @return bool|mixed - */ - static public function FetchOneFile($sKey) - { - // Try the 'TTLed' version - $sValue = self::ReadCacheLocked(self::GetCacheFileName('-'.$sKey)); - if ($sValue === false) - { - $sValue = self::ReadCacheLocked(self::GetCacheFileName($sKey)); - if ($sValue === false) - { - return false; - } - } - $oRes = @unserialize($sValue); - return $oRes; - } - - /** Add one cache entry. - * @param string $sKey - * @param $value - * @param int $iTTL time to live - * @return bool - */ - static public function StoreOneFile($sKey, $value, $iTTL) - { - if (empty($sKey)) - { - return false; - } - - @unlink(self::GetCacheFileName($sKey)); - @unlink(self::GetCacheFileName('-'.$sKey)); - if ($iTTL > 0) - { - // hint for ttl management - $sKey = '-'.$sKey; - } - - $sFilename = self::GetCacheFileName($sKey); - // try to create the folder - $sDirname = dirname($sFilename); - if (!file_exists($sDirname)) - { - if (!@mkdir($sDirname, 0755, true)) - { - return false; - } - } - $bRes = !(@file_put_contents($sFilename, serialize($value), LOCK_EX) === false); - self::AddFile($sFilename); - return $bRes; - } - - /** Manage the cache files when adding a new cache entry: - * remove older files if the mamximum is reached. - * @param $sNewFilename - */ - static protected function AddFile($sNewFilename) - { - if (strpos(basename($sNewFilename), '-') !== 0) - { - return; - } - - $iMaxFiles = MetaModel::GetConfig()->Get('apc_cache_emulation.max_entries'); - if ($iMaxFiles == 0) - { - return; - } - if (!self::$aFilesByTime) - { - self::ListFilesByTime(); - self::$iFileCount = count(self::$aFilesByTime); - if ($iMaxFiles !== 0) - { - asort(self::$aFilesByTime); - } - } - else - { - self::$aFilesByTime[$sNewFilename] = time(); - self::$iFileCount++; - } - if (self::$iFileCount > $iMaxFiles) - { - $iFileNbToRemove = self::$iFileCount - $iMaxFiles; - foreach(self::$aFilesByTime as $sFileToRemove => $iTime) - { - @unlink($sFileToRemove); - if (--$iFileNbToRemove === 0) - { - break; - } - } - self::$aFilesByTime = array_slice(self::$aFilesByTime, self::$iFileCount - $iMaxFiles, null, true); - self::$iFileCount = $iMaxFiles; - } - } - - /** Get the list of files with their associated access time - * @param string $sCheck Directory to scan - */ - static protected function ListFilesByTime($sCheck = null) - { - if (empty($sCheck)) - { - $sCheck = self::GetCacheFileName(); - } - // Garbage collection - $aFiles = array_diff(@scandir($sCheck), array('.', '..')); - foreach($aFiles as $sFile) - { - $sSubFile = $sCheck.'/'.$sFile; - if (is_dir($sSubFile)) - { - self::ListFilesByTime($sSubFile); - } - else - { - if (strpos(basename($sSubFile), '-') === 0) - { - self::$aFilesByTime[$sSubFile] = @fileatime($sSubFile); - } - } - } - } - - /** Read the content of one cache file under lock protection - * @param $sFilename - * @return bool|string the content of the cache entry or false if error - */ - static protected function ReadCacheLocked($sFilename) - { - $file = @fopen($sFilename, 'r'); - if ($file === false) - { - return false; - } - flock($file, LOCK_SH); - $sContent = @fread($file, @filesize($sFilename)); - flock($file, LOCK_UN); - fclose($file); - return $sContent; - } - - static protected function ResetFileCount() - { - self::$aFilesByTime = null; - self::$iFileCount = 0; - } - -} + +// + +/** + * Date: 27/09/2017 + */ + +/** + * @param string $cache_type + * @param bool $limited + * @return array|bool + */ +function apc_cache_info($cache_type = '', $limited = false) +{ + $aInfo = array(); + $sRootCacheDir = apcFile::GetCacheFileName(); + $aInfo['cache_list'] = apcFile::GetCacheEntries($sRootCacheDir); + return $aInfo; +} + +/** + * @param array|string $key + * @param $var + * @param int $ttl + * @return array|bool + */ +function apc_store($key, $var = NULL, $ttl = 0) +{ + if (is_array($key)) + { + $aResult = array(); + foreach($key as $sKey => $value) + { + $aResult[] = apcFile::StoreOneFile($sKey, $value, $ttl); + } + return $aResult; + } + return apcFile::StoreOneFile($key, $var, $ttl); +} + +/** + * @param $key string|array + * @return mixed + */ +function apc_fetch($key) +{ + if (is_array($key)) + { + $aResult = array(); + foreach($key as $sKey) + { + $aResult[$sKey] = apcFile::FetchOneFile($sKey); + } + return $aResult; + } + return apcFile::FetchOneFile($key); +} + +/** + * @param string $cache_type + * @return bool + */ +function apc_clear_cache($cache_type = '') +{ + apcFile::DeleteEntry(utils::GetCachePath()); + return true; +} + +/** + * @param $key + * @return bool|string[] + */ +function apc_delete($key) +{ + if (empty($key)) + { + return false; + } + $bRet1 = apcFile::DeleteEntry(apcFile::GetCacheFileName($key)); + $bRet2 = apcFile::DeleteEntry(apcFile::GetCacheFileName('-'.$key)); + return $bRet1 || $bRet2; +} + +class apcFile +{ + // Check only once per request + static public $aFilesByTime = null; + static public $iFileCount = 0; + + /** Get the file name corresponding to the cache entry. + * If an empty key is provided, the root of the cache is returned. + * @param $sKey + * @return string + */ + static public function GetCacheFileName($sKey = '') + { + $sPath = str_replace(array(' ', '/', '\\', '.'), '-', $sKey); + return utils::GetCachePath().'apc-emul/'.$sPath; + } + + /** Get the list of entries from a starting folder. + * @param $sEntry string starting folder. + * @return array list of entries stored into array of key 'info' + */ + static public function GetCacheEntries($sEntry) + { + $aResult = array(); + if (is_dir($sEntry)) + { + $aFiles = array_diff(scandir($sEntry), array('.', '..')); + foreach($aFiles as $sFile) + { + $sSubFile = $sEntry.'/'.$sFile; + $aResult = array_merge($aResult, self::GetCacheEntries($sSubFile)); + } + } + else + { + $sKey = basename($sEntry); + if (strpos($sKey, '-') === 0) + { + $sKey = substr($sKey, 1); + } + $aResult[] = array('info' => $sKey); + } + return $aResult; + } + + /** Delete one cache entry. + * @param $sCache + * @return bool true if the entry was deleted false if error occurs (like entry did not exist). + */ + static public function DeleteEntry($sCache) + { + if (is_dir($sCache)) + { + $aFiles = array_diff(scandir($sCache), array('.', '..')); + foreach($aFiles as $sFile) + { + $sSubFile = $sCache.'/'.$sFile; + if (!self::DeleteEntry($sSubFile)) + { + return false; + } + } + if (!@rmdir($sCache)) + { + return false; + } + } + else + { + if (!@unlink($sCache)) + { + return false; + } + } + + self::ResetFileCount(); + return true; + } + + /** Get one cache entry content. + * @param $sKey + * @return bool|mixed + */ + static public function FetchOneFile($sKey) + { + // Try the 'TTLed' version + $sValue = self::ReadCacheLocked(self::GetCacheFileName('-'.$sKey)); + if ($sValue === false) + { + $sValue = self::ReadCacheLocked(self::GetCacheFileName($sKey)); + if ($sValue === false) + { + return false; + } + } + $oRes = @unserialize($sValue); + return $oRes; + } + + /** Add one cache entry. + * @param string $sKey + * @param $value + * @param int $iTTL time to live + * @return bool + */ + static public function StoreOneFile($sKey, $value, $iTTL) + { + if (empty($sKey)) + { + return false; + } + + @unlink(self::GetCacheFileName($sKey)); + @unlink(self::GetCacheFileName('-'.$sKey)); + if ($iTTL > 0) + { + // hint for ttl management + $sKey = '-'.$sKey; + } + + $sFilename = self::GetCacheFileName($sKey); + // try to create the folder + $sDirname = dirname($sFilename); + if (!file_exists($sDirname)) + { + if (!@mkdir($sDirname, 0755, true)) + { + return false; + } + } + $bRes = !(@file_put_contents($sFilename, serialize($value), LOCK_EX) === false); + self::AddFile($sFilename); + return $bRes; + } + + /** Manage the cache files when adding a new cache entry: + * remove older files if the mamximum is reached. + * @param $sNewFilename + */ + static protected function AddFile($sNewFilename) + { + if (strpos(basename($sNewFilename), '-') !== 0) + { + return; + } + + $iMaxFiles = MetaModel::GetConfig()->Get('apc_cache_emulation.max_entries'); + if ($iMaxFiles == 0) + { + return; + } + if (!self::$aFilesByTime) + { + self::ListFilesByTime(); + self::$iFileCount = count(self::$aFilesByTime); + if ($iMaxFiles !== 0) + { + asort(self::$aFilesByTime); + } + } + else + { + self::$aFilesByTime[$sNewFilename] = time(); + self::$iFileCount++; + } + if (self::$iFileCount > $iMaxFiles) + { + $iFileNbToRemove = self::$iFileCount - $iMaxFiles; + foreach(self::$aFilesByTime as $sFileToRemove => $iTime) + { + @unlink($sFileToRemove); + if (--$iFileNbToRemove === 0) + { + break; + } + } + self::$aFilesByTime = array_slice(self::$aFilesByTime, self::$iFileCount - $iMaxFiles, null, true); + self::$iFileCount = $iMaxFiles; + } + } + + /** Get the list of files with their associated access time + * @param string $sCheck Directory to scan + */ + static protected function ListFilesByTime($sCheck = null) + { + if (empty($sCheck)) + { + $sCheck = self::GetCacheFileName(); + } + // Garbage collection + $aFiles = array_diff(@scandir($sCheck), array('.', '..')); + foreach($aFiles as $sFile) + { + $sSubFile = $sCheck.'/'.$sFile; + if (is_dir($sSubFile)) + { + self::ListFilesByTime($sSubFile); + } + else + { + if (strpos(basename($sSubFile), '-') === 0) + { + self::$aFilesByTime[$sSubFile] = @fileatime($sSubFile); + } + } + } + } + + /** Read the content of one cache file under lock protection + * @param $sFilename + * @return bool|string the content of the cache entry or false if error + */ + static protected function ReadCacheLocked($sFilename) + { + $file = @fopen($sFilename, 'r'); + if ($file === false) + { + return false; + } + flock($file, LOCK_SH); + $sContent = @fread($file, @filesize($sFilename)); + flock($file, LOCK_UN); + fclose($file); + return $sContent; + } + + static protected function ResetFileCount() + { + self::$aFilesByTime = null; + self::$iFileCount = 0; + } + +} diff --git a/core/archive.class.inc.php b/core/archive.class.inc.php index 82c34a554..94cb891e5 100644 --- a/core/archive.class.inc.php +++ b/core/archive.class.inc.php @@ -1,335 +1,335 @@ - - - -/** - * Utility to import/export the DB from/to a ZIP file - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * iTopArchive a class to manipulate (read/write) iTop archives with their catalog - * Each iTop archive is a zip file that contains (at the root of the archive) - * a file called catalog.xml holding the description of the archive - */ -class iTopArchive -{ - const read = 0; - const create = ZipArchive::CREATE; - - protected $m_sZipPath; - protected $m_oZip; - protected $m_sVersion; - protected $m_sTitle; - protected $m_sDescription; - protected $m_aPackages; - protected $m_aErrorMessages; - - /** - * Construct an iTopArchive object - * @param $sArchivePath string The full path the archive file - * @param $iMode integrer Either iTopArchive::read for reading an existing archive or iTopArchive::create for creating a new one. Updating is not supported (yet) - */ - public function __construct($sArchivePath, $iMode = iTopArchive::read) - { - $this->m_sZipPath = $sArchivePath; - $this->m_oZip = new ZipArchive(); - $this->m_oZip->open($this->m_sZipPath, $iMode); - $this->m_aErrorMessages = array(); - $this->m_sVersion = '1.0'; - $this->m_sTitle = ''; - $this->m_sDescription = ''; - $this->m_aPackages = array(); - } - - public function SetTitle($sTitle) - { - $this->m_sTitle = $sTitle; - } - - public function SetDescription($sDescription) - { - $this->m_sDescription = $sDescription; - } - - public function GetTitle() - { - return $this->m_sTitle; - } - - public function GetDescription() - { - return $this->m_sDescription; - } - - public function GetPackages() - { - return $this->m_aPackages; - } - - public function __destruct() - { - $this->m_oZip->close(); - } - - /** - * Get the error message explaining the latest error encountered - * @return array All the error messages encountered during the validation - */ - public function GetErrors() - { - return $this->m_aErrorMessages; - } - - /** - * Read the catalog from the archive (zip) file - * @param sPath string Path the the zip file - * @return boolean True in case of success, false otherwise - */ - public function ReadCatalog() - { - if ($this->IsValid()) - { - $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); - $oParser = xml_parser_create(); - xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); - xml_parser_free($oParser); - - $iIndex = $aIndexes['ARCHIVE'][0]; - $this->m_sVersion = $aValues[$iIndex]['attributes']['VERSION']; - $iIndex = $aIndexes['TITLE'][0]; - $this->m_sTitle = $aValues[$iIndex]['value']; - $iIndex = $aIndexes['DESCRIPTION'][0]; - if (array_key_exists('value', $aValues[$iIndex])) - { - // #@# implement a get_array_value(array, key, default) ? - $this->m_sDescription = $aValues[$iIndex]['value']; - } - - foreach($aIndexes['PACKAGE'] as $iIndex) - { - $this->m_aPackages[$aValues[$iIndex]['attributes']['HREF']] = array( 'type' => $aValues[$iIndex]['attributes']['TYPE'], 'title'=> $aValues[$iIndex]['attributes']['TITLE'], 'description' => $aValues[$iIndex]['value']); - } - - //echo "Archive path: {$this->m_sZipPath}
    \n"; - //echo "Archive format version: {$this->m_sVersion}
    \n"; - //echo "Title: {$this->m_sTitle}
    \n"; - //echo "Description: {$this->m_sDescription}
    \n"; - //foreach($this->m_aPackages as $aFile) - //{ - // echo "{$aFile['title']} ({$aFile['type']}): {$aFile['description']}
    \n"; - //} - } - return true; - } - - public function WriteCatalog() - { - $sXml = "\n"; // split the XML closing tag that disturbs PSPad's syntax coloring - $sXml .= "\n"; - $sXml .= "{$this->m_sTitle}\n"; - $sXml .= "{$this->m_sDescription}\n"; - foreach($this->m_aPackages as $sFileName => $aFile) - { - $sXml .= "{$aFile['description']}\n"; - } - $sXml .= ""; - $this->m_oZip->addFromString('catalog.xml', $sXml); - } - - /** - * Add a package to the archive - * @param string $sExternalFilePath The path to the file to be added to the archive as a package (directories are not yet implemented) - * @param string $sFilePath The name of the file inside the archive - * @param string $sTitle A short title for this package - * @param string $sType Type of the package. SQL scripts must be of type 'text/sql' - * @param string $sDescription A longer description of the purpose of this package - * @return none - */ - public function AddPackage($sExternalFilePath, $sFilePath, $sTitle, $sType, $sDescription) - { - $this->m_aPackages[$sFilePath] = array('title' => $sTitle, 'type' => $sType, 'description' => $sDescription); - $this->m_oZip->addFile($sExternalFilePath, $sFilePath); - } - - /** - * Reads the contents of the given file from the archive - * @param string $sFileName The path to the file inside the archive - * @return string The content of the file read from the archive - */ - public function GetFileContents($sFileName) - { - return $this->m_oZip->getFromName($sFileName); - } - - /** - * Extracts the contents of the given file from the archive - * @param string $sFileName The path to the file inside the archive - * @param string $sDestinationFileName The path of the file to write - * @return none - */ - public function ExtractToFile($sFileName, $sDestinationFileName) - { - $iBufferSize = 64 * 1024; // Read 64K at a time - $oZipStream = $this->m_oZip->getStream($sFileName); - $oDestinationStream = fopen($sDestinationFileName, 'wb'); - while (!feof($oZipStream)) { - $sContents = fread($oZipStream, $iBufferSize); - fwrite($oDestinationStream, $sContents); - } - fclose($oZipStream); - fclose($oDestinationStream); - } - - /** - * Apply a SQL script taken from the archive. The package must be listed in the catalog and of type text/sql - * @param string $sFileName The path to the SQL package inside the archive - * @return boolean false in case of error, true otherwise - */ - public function ImportSql($sFileName, $sDatabase = 'itop') - { - if ( ($this->m_oZip->locateName($sFileName) == false) || (!isset($this->m_aPackages[$sFileName])) || ($this->m_aPackages[$sFileName]['type'] != 'text/sql')) - { - // invalid type or not listed in the catalog - return false; - } - $sTempName = tempnam("../tmp/", "sql"); - //echo "Extracting to: '$sTempName'
    \n"; - $this->ExtractToFile($sFileName, $sTempName); - // Note: the command line below works on Windows with the right path to mysql !!! - $sCommandLine = 'type "'.$sTempName.'" | "/iTop/MySQL Server 5.0/bin/mysql.exe" -u root '.$sDatabase; - //echo "Executing: '$sCommandLine'
    \n"; - exec($sCommandLine, $aOutput, $iRet); - //echo "Return code: $iRet
    \n"; - //echo "Output:
    \n";
    -		//print_r($aOutput);
    -		//echo "

    \n"; - unlink($sTempName); - return ($iRet == 0); - } - - /** - * Dumps some part of the specified MySQL database into the archive as a text/sql package - * @param $sTitle string A short title for this SQL script - * @param $sDescription string A longer description of the purpose of this SQL script - * @param $sFileName string The name of the package inside the archive - * @param $sDatabase string name of the database - * @param $aTables array array or table names. If empty, all tables are dumped - * @param $bStructureOnly boolean Whether or not to dump the data or just the schema - * @return boolean False in case of error, true otherwise - */ - public function AddDatabaseDump($sTitle, $sDescription, $sFileName, $sDatabase = 'itop', $aTables = array(), $bStructureOnly = true) - { - $sTempName = tempnam("../tmp/", "sql"); - $sNoData = $bStructureOnly ? "--no-data" : ""; - $sCommandLine = "\"/iTop/MySQL Server 5.0/bin/mysqldump.exe\" --user=root --opt $sNoData --result-file=$sTempName $sDatabase ".implode(" ", $aTables); - //echo "Executing command: '$sCommandLine'
    \n"; - exec($sCommandLine, $aOutput, $iRet); - //echo "Return code: $iRet
    \n"; - //echo "Output:
    \n";
    -		//print_r($aOutput);
    -		//echo "

    \n"; - if ($iRet == 0) - { - $this->AddPackage($sTempName, $sFileName, $sTitle, 'text/sql', $sDescription); - } - //unlink($sTempName); - return ($iRet == 0); - } - - /** - * Check the consistency of the archive - * @return boolean True if the archive file is consistent - */ - public function IsValid() - { - // TO DO: use a DTD to validate the XML instead of this hand-made validation - $bResult = true; - $aMandatoryTags = array('ARCHIVE' => array('VERSION'), - 'TITLE' => array(), - 'DESCRIPTION' => array(), - 'PACKAGE' => array('TYPE', 'HREF', 'TITLE')); - - $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); - $oParser = xml_parser_create(); - xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); - xml_parser_free($oParser); - - foreach($aMandatoryTags as $sTag => $aAttributes) - { - // Check that all the required tags are present - if (!isset($aIndexes[$sTag])) - { - $this->m_aErrorMessages[] = "The XML catalog does not contain the mandatory tag $sTag."; - $bResult = false; - } - else - { - foreach($aIndexes[$sTag] as $iIndex) - { - switch($aValues[$iIndex]['type']) - { - case 'complete': - case 'open': - // Check that all the required attributes are present - foreach($aAttributes as $sAttribute) - { - if (!isset($aValues[$iIndex]['attributes'][$sAttribute])) - { - $this->m_aErrorMessages[] = "The tag $sTag ($iIndex) does not contain the required attribute $sAttribute."; - } - } - break; - - default: - // ignore other type of tags: close or cdata - } - } - } - } - return $bResult; - } -} -/* -// Unit test - reading an archive -$sArchivePath = '../tmp/archive.zip'; -$oArchive = new iTopArchive($sArchivePath, iTopArchive::read); -$oArchive->ReadCatalog(); -$oArchive->ImportSql('full_backup.sql'); - -// Writing an archive -- - -$sArchivePath = '../tmp/archive2.zip'; -$oArchive = new iTopArchive($sArchivePath, iTopArchive::create); -$oArchive->SetTitle('First Archive !'); -$oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); -$oArchive->AddPackage('../tmp/schema.sql', 'test.sql', 'this is just a test', 'text/sql', 'My first attempt at creating an archive from PHP...'); -$oArchive->WriteCatalog(); - - -$sArchivePath = '../tmp/archive2.zip'; -$oArchive = new iTopArchive($sArchivePath, iTopArchive::create); -$oArchive->SetTitle('First Archive !'); -$oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); -$oArchive->AddDatabaseDump('Test', 'This is my first automatic dump', 'schema.sql', 'itop', array('objects')); -$oArchive->WriteCatalog(); -*/ -?> + + + +/** + * Utility to import/export the DB from/to a ZIP file + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * iTopArchive a class to manipulate (read/write) iTop archives with their catalog + * Each iTop archive is a zip file that contains (at the root of the archive) + * a file called catalog.xml holding the description of the archive + */ +class iTopArchive +{ + const read = 0; + const create = ZipArchive::CREATE; + + protected $m_sZipPath; + protected $m_oZip; + protected $m_sVersion; + protected $m_sTitle; + protected $m_sDescription; + protected $m_aPackages; + protected $m_aErrorMessages; + + /** + * Construct an iTopArchive object + * @param $sArchivePath string The full path the archive file + * @param $iMode integrer Either iTopArchive::read for reading an existing archive or iTopArchive::create for creating a new one. Updating is not supported (yet) + */ + public function __construct($sArchivePath, $iMode = iTopArchive::read) + { + $this->m_sZipPath = $sArchivePath; + $this->m_oZip = new ZipArchive(); + $this->m_oZip->open($this->m_sZipPath, $iMode); + $this->m_aErrorMessages = array(); + $this->m_sVersion = '1.0'; + $this->m_sTitle = ''; + $this->m_sDescription = ''; + $this->m_aPackages = array(); + } + + public function SetTitle($sTitle) + { + $this->m_sTitle = $sTitle; + } + + public function SetDescription($sDescription) + { + $this->m_sDescription = $sDescription; + } + + public function GetTitle() + { + return $this->m_sTitle; + } + + public function GetDescription() + { + return $this->m_sDescription; + } + + public function GetPackages() + { + return $this->m_aPackages; + } + + public function __destruct() + { + $this->m_oZip->close(); + } + + /** + * Get the error message explaining the latest error encountered + * @return array All the error messages encountered during the validation + */ + public function GetErrors() + { + return $this->m_aErrorMessages; + } + + /** + * Read the catalog from the archive (zip) file + * @param sPath string Path the the zip file + * @return boolean True in case of success, false otherwise + */ + public function ReadCatalog() + { + if ($this->IsValid()) + { + $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); + $oParser = xml_parser_create(); + xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); + xml_parser_free($oParser); + + $iIndex = $aIndexes['ARCHIVE'][0]; + $this->m_sVersion = $aValues[$iIndex]['attributes']['VERSION']; + $iIndex = $aIndexes['TITLE'][0]; + $this->m_sTitle = $aValues[$iIndex]['value']; + $iIndex = $aIndexes['DESCRIPTION'][0]; + if (array_key_exists('value', $aValues[$iIndex])) + { + // #@# implement a get_array_value(array, key, default) ? + $this->m_sDescription = $aValues[$iIndex]['value']; + } + + foreach($aIndexes['PACKAGE'] as $iIndex) + { + $this->m_aPackages[$aValues[$iIndex]['attributes']['HREF']] = array( 'type' => $aValues[$iIndex]['attributes']['TYPE'], 'title'=> $aValues[$iIndex]['attributes']['TITLE'], 'description' => $aValues[$iIndex]['value']); + } + + //echo "Archive path: {$this->m_sZipPath}
    \n"; + //echo "Archive format version: {$this->m_sVersion}
    \n"; + //echo "Title: {$this->m_sTitle}
    \n"; + //echo "Description: {$this->m_sDescription}
    \n"; + //foreach($this->m_aPackages as $aFile) + //{ + // echo "{$aFile['title']} ({$aFile['type']}): {$aFile['description']}
    \n"; + //} + } + return true; + } + + public function WriteCatalog() + { + $sXml = "\n"; // split the XML closing tag that disturbs PSPad's syntax coloring + $sXml .= "\n"; + $sXml .= "{$this->m_sTitle}\n"; + $sXml .= "{$this->m_sDescription}\n"; + foreach($this->m_aPackages as $sFileName => $aFile) + { + $sXml .= "{$aFile['description']}\n"; + } + $sXml .= ""; + $this->m_oZip->addFromString('catalog.xml', $sXml); + } + + /** + * Add a package to the archive + * @param string $sExternalFilePath The path to the file to be added to the archive as a package (directories are not yet implemented) + * @param string $sFilePath The name of the file inside the archive + * @param string $sTitle A short title for this package + * @param string $sType Type of the package. SQL scripts must be of type 'text/sql' + * @param string $sDescription A longer description of the purpose of this package + * @return none + */ + public function AddPackage($sExternalFilePath, $sFilePath, $sTitle, $sType, $sDescription) + { + $this->m_aPackages[$sFilePath] = array('title' => $sTitle, 'type' => $sType, 'description' => $sDescription); + $this->m_oZip->addFile($sExternalFilePath, $sFilePath); + } + + /** + * Reads the contents of the given file from the archive + * @param string $sFileName The path to the file inside the archive + * @return string The content of the file read from the archive + */ + public function GetFileContents($sFileName) + { + return $this->m_oZip->getFromName($sFileName); + } + + /** + * Extracts the contents of the given file from the archive + * @param string $sFileName The path to the file inside the archive + * @param string $sDestinationFileName The path of the file to write + * @return none + */ + public function ExtractToFile($sFileName, $sDestinationFileName) + { + $iBufferSize = 64 * 1024; // Read 64K at a time + $oZipStream = $this->m_oZip->getStream($sFileName); + $oDestinationStream = fopen($sDestinationFileName, 'wb'); + while (!feof($oZipStream)) { + $sContents = fread($oZipStream, $iBufferSize); + fwrite($oDestinationStream, $sContents); + } + fclose($oZipStream); + fclose($oDestinationStream); + } + + /** + * Apply a SQL script taken from the archive. The package must be listed in the catalog and of type text/sql + * @param string $sFileName The path to the SQL package inside the archive + * @return boolean false in case of error, true otherwise + */ + public function ImportSql($sFileName, $sDatabase = 'itop') + { + if ( ($this->m_oZip->locateName($sFileName) == false) || (!isset($this->m_aPackages[$sFileName])) || ($this->m_aPackages[$sFileName]['type'] != 'text/sql')) + { + // invalid type or not listed in the catalog + return false; + } + $sTempName = tempnam("../tmp/", "sql"); + //echo "Extracting to: '$sTempName'
    \n"; + $this->ExtractToFile($sFileName, $sTempName); + // Note: the command line below works on Windows with the right path to mysql !!! + $sCommandLine = 'type "'.$sTempName.'" | "/iTop/MySQL Server 5.0/bin/mysql.exe" -u root '.$sDatabase; + //echo "Executing: '$sCommandLine'
    \n"; + exec($sCommandLine, $aOutput, $iRet); + //echo "Return code: $iRet
    \n"; + //echo "Output:
    \n";
    +		//print_r($aOutput);
    +		//echo "

    \n"; + unlink($sTempName); + return ($iRet == 0); + } + + /** + * Dumps some part of the specified MySQL database into the archive as a text/sql package + * @param $sTitle string A short title for this SQL script + * @param $sDescription string A longer description of the purpose of this SQL script + * @param $sFileName string The name of the package inside the archive + * @param $sDatabase string name of the database + * @param $aTables array array or table names. If empty, all tables are dumped + * @param $bStructureOnly boolean Whether or not to dump the data or just the schema + * @return boolean False in case of error, true otherwise + */ + public function AddDatabaseDump($sTitle, $sDescription, $sFileName, $sDatabase = 'itop', $aTables = array(), $bStructureOnly = true) + { + $sTempName = tempnam("../tmp/", "sql"); + $sNoData = $bStructureOnly ? "--no-data" : ""; + $sCommandLine = "\"/iTop/MySQL Server 5.0/bin/mysqldump.exe\" --user=root --opt $sNoData --result-file=$sTempName $sDatabase ".implode(" ", $aTables); + //echo "Executing command: '$sCommandLine'
    \n"; + exec($sCommandLine, $aOutput, $iRet); + //echo "Return code: $iRet
    \n"; + //echo "Output:
    \n";
    +		//print_r($aOutput);
    +		//echo "

    \n"; + if ($iRet == 0) + { + $this->AddPackage($sTempName, $sFileName, $sTitle, 'text/sql', $sDescription); + } + //unlink($sTempName); + return ($iRet == 0); + } + + /** + * Check the consistency of the archive + * @return boolean True if the archive file is consistent + */ + public function IsValid() + { + // TO DO: use a DTD to validate the XML instead of this hand-made validation + $bResult = true; + $aMandatoryTags = array('ARCHIVE' => array('VERSION'), + 'TITLE' => array(), + 'DESCRIPTION' => array(), + 'PACKAGE' => array('TYPE', 'HREF', 'TITLE')); + + $sXmlCatalog = $this->m_oZip->getFromName('catalog.xml'); + $oParser = xml_parser_create(); + xml_parse_into_struct($oParser, $sXmlCatalog, $aValues, $aIndexes); + xml_parser_free($oParser); + + foreach($aMandatoryTags as $sTag => $aAttributes) + { + // Check that all the required tags are present + if (!isset($aIndexes[$sTag])) + { + $this->m_aErrorMessages[] = "The XML catalog does not contain the mandatory tag $sTag."; + $bResult = false; + } + else + { + foreach($aIndexes[$sTag] as $iIndex) + { + switch($aValues[$iIndex]['type']) + { + case 'complete': + case 'open': + // Check that all the required attributes are present + foreach($aAttributes as $sAttribute) + { + if (!isset($aValues[$iIndex]['attributes'][$sAttribute])) + { + $this->m_aErrorMessages[] = "The tag $sTag ($iIndex) does not contain the required attribute $sAttribute."; + } + } + break; + + default: + // ignore other type of tags: close or cdata + } + } + } + } + return $bResult; + } +} +/* +// Unit test - reading an archive +$sArchivePath = '../tmp/archive.zip'; +$oArchive = new iTopArchive($sArchivePath, iTopArchive::read); +$oArchive->ReadCatalog(); +$oArchive->ImportSql('full_backup.sql'); + +// Writing an archive -- + +$sArchivePath = '../tmp/archive2.zip'; +$oArchive = new iTopArchive($sArchivePath, iTopArchive::create); +$oArchive->SetTitle('First Archive !'); +$oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); +$oArchive->AddPackage('../tmp/schema.sql', 'test.sql', 'this is just a test', 'text/sql', 'My first attempt at creating an archive from PHP...'); +$oArchive->WriteCatalog(); + + +$sArchivePath = '../tmp/archive2.zip'; +$oArchive = new iTopArchive($sArchivePath, iTopArchive::create); +$oArchive->SetTitle('First Archive !'); +$oArchive->SetDescription('This is just a test. Does not contain a lot of useful data.'); +$oArchive->AddDatabaseDump('Test', 'This is my first automatic dump', 'schema.sql', 'itop', array('objects')); +$oArchive->WriteCatalog(); +*/ +?> diff --git a/core/asynctask.class.inc.php b/core/asynctask.class.inc.php index 0ca20d8ea..5c7d8d2c6 100644 --- a/core/asynctask.class.inc.php +++ b/core/asynctask.class.inc.php @@ -1,385 +1,385 @@ - - - -/** - * Persistent classes (internal): user defined actions - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class ExecAsyncTask implements iBackgroundProcess -{ - public function GetPeriodicity() - { - return 2; // seconds - } - - public function Process($iTimeLimit) - { - $sNow = date(AttributeDateTime::GetSQLFormat()); - // Criteria: planned, and expected to occur... ASAP or in the past - $sOQL = "SELECT AsyncTask WHERE (status = 'planned') AND (ISNULL(planned) OR (planned < '$sNow'))"; - $iProcessed = 0; - while (time() < $iTimeLimit) - { - // Next one ? - $oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */); - $oTask = $oSet->Fetch(); - if (is_null($oTask)) - { - // Nothing to be done - break; - } - $iProcessed++; - if ($oTask->Process()) - { - $oTask->DBDelete(); - } - } - return "processed $iProcessed tasks"; - } -} - -/** - * A - * - * @package iTopORM - */ -abstract class AsyncTask extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => array('created'), - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_async_task", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - - // Null is allowed to ease the migration from iTop 2.0.2 and earlier, when the status did not exist, and because the default value is not taken into account in the SQL definition - // The value is set from null to planned in the setup program - MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('planned,running,idle,error'), "sql"=>"status", "default_value"=>"planned", "is_null_allowed"=>true, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("started", array("allowed_values"=>null, "sql"=>"started", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("planned", array("allowed_values"=>null, "sql"=>"planned", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("event_id", array("targetclass"=>"Event", "jointype"=> "", "allowed_values"=>null, "sql"=>"event_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeInteger("remaining_retries", array("allowed_values"=>null, "sql"=>"remaining_retries", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("last_error_code", array("allowed_values"=>null, "sql"=>"last_error_code", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("last_error", array("allowed_values"=>null, "sql"=>"last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("last_attempt", array("allowed_values"=>null, "sql"=>"last_attempt", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - } - - /** - * Every is fine - */ - const OK = 0; - /** - * The task no longer exists - */ - const DELETED = 1; - /** - * The task is already being executed - */ - const ALREADY_RUNNING = 2; - - /** - * The current process requests the ownership on the task. - * In case the task can be accessed concurrently, this function can be overloaded to add a critical section. - * The function must not block the caller if another process is already owning the task - * - * @return integer A code among OK/DELETED/ALREADY_RUNNING. - */ - public function MarkAsRunning() - { - try - { - if ($this->Get('status') == 'running') - { - return self::ALREADY_RUNNING; - } - else - { - $this->Set('status', 'running'); - $this->Set('started', time()); - $this->DBUpdate(); - return self::OK; - } - } - catch(Exception $e) - { - // Corrupted task !! (for example: "Failed to reload object") - IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage().' - fatal error, deleting the task.'); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', 'Failed, corrupted data: '.$e->getMessage()); - $oEventLog->DBUpdate(); - } - $this->DBDelete(); - return self::DELETED; - } - } - - public function GetRetryDelay($iErrorCode = null) - { - $iRetryDelay = 600; - $aRetries = MetaModel::GetConfig()->Get('async_task_retries', array()); - if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) - { - $aConfig = $aRetries[get_class($this)]; - $iRetryDelay = $aConfig['retry_delay']; - } - return $iRetryDelay; - } - - public function GetMaxRetries($iErrorCode = null) - { - $iMaxRetries = 0; - $aRetries = MetaModel::GetConfig()->Get('async_task_retries', array()); - if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) - { - $aConfig = $aRetries[get_class($this)]; - $iMaxRetries = $aConfig['max_retries']; - } - } - - /** - * Override to notify people that a task cannot be performed - */ - protected function OnDefinitiveFailure() - { - } - - protected function OnInsert() - { - $this->Set('created', time()); - } - - /** - * @return boolean True if the task record can be deleted - */ - public function Process() - { - // By default: consider that the task is not completed - $bRet = false; - - // Attempt to take the ownership - $iStatus = $this->MarkAsRunning(); - if ($iStatus == self::OK) - { - try - { - $sStatus = $this->DoProcess(); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', $sStatus); - $oEventLog->DBUpdate(); - } - $bRet = true; - } catch (Exception $e) - { - $this->HandleError($e->getMessage(), $e->getCode()); - } - } - else - { - // Already done or being handled by another process... skip... - $bRet = false; - } - - return $bRet; - } - - /** - * Overridable to extend the behavior in case of error (logging) - */ - protected function HandleError($sErrorMessage, $iErrorCode) - { - if ($this->Get('last_attempt') == '') - { - // First attempt - $this->Set('remaining_retries', $this->GetMaxRetries($iErrorCode)); - } - - $this->Set('last_error', $sErrorMessage); - $this->Set('last_error_code', $iErrorCode); // Note: can be ZERO !!! - $this->Set('last_attempt', time()); - - $iRemaining = $this->Get('remaining_retries'); - if ($iRemaining > 0) - { - $iRetryDelay = $this->GetRetryDelay($iErrorCode); - IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage.' - remaining retries: '.$iRemaining.' - next retry in '.$iRetryDelay.'s'); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task. Remaining retries: '.$iRemaining.'. Next retry in '.$iRetryDelay.'s'"); - try - { - $oEventLog->DBUpdate(); - } - catch (Exception $e) - { - $oEventLog->Set('message', "Failed to process async task. Remaining retries: '.$iRemaining.'. Next retry in '.$iRetryDelay.'s', more details in the log"); - $oEventLog->DBUpdate(); - } - } - $this->Set('remaining_retries', $iRemaining - 1); - $this->Set('status', 'planned'); - $this->Set('started', null); - $this->Set('planned', time() + $iRetryDelay); - } - else - { - IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task."); - try - { - $oEventLog->DBUpdate(); - } - catch (Exception $e) - { - $oEventLog->Set('message', 'Failed to process async task, more details in the log'); - $oEventLog->DBUpdate(); - } - } - $this->Set('status', 'error'); - $this->Set('started', null); - $this->Set('planned', null); - $this->OnDefinitiveFailure(); - } - $this->DBUpdate(); - } - - /** - * Throws an exception (message and code) - */ - abstract public function DoProcess(); - - /** - * Describes the error codes that DoProcess can return by the mean of exceptions - */ - static public function EnumErrorCodes() - { - return array(); - } -} - -/** - * An email notification - * - * @package iTopORM - */ -class AsyncSendEmail extends AsyncTask -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "created", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_async_send_email", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeInteger("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>Email::ORIGINAL_FORMAT, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeLongText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists -// MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details -// MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - static public function AddToQueue(EMail $oEMail, $oLog) - { - $oNew = MetaModel::NewObject(__class__); - if ($oLog) - { - $oNew->Set('event_id', $oLog->GetKey()); - } - $oNew->Set('to', $oEMail->GetRecipientTO(true /* string */)); - $oNew->Set('subject', $oEMail->GetSubject()); - -// $oNew->Set('version', 1); -// $sMessage = serialize($oEMail); - $oNew->Set('version', 2); - $sMessage = $oEMail->SerializeV2(); - $oNew->Set('message', $sMessage); - $oNew->DBInsert(); - } - - public function DoProcess() - { - $sMessage = $this->Get('message'); - $iVersion = (int) $this->Get('version'); - switch($iVersion) - { - case Email::FORMAT_V2: - $oEMail = Email::UnSerializeV2($sMessage); - break; - - case Email::ORIGINAL_FORMAT: - $oEMail = unserialize($sMessage); - break; - - default: - return 'Unknown version of the serialization format: '.$iVersion; - } - $iRes = $oEMail->Send($aIssues, true /* force synchro !!!!! */); - switch ($iRes) - { - case EMAIL_SEND_OK: - return "Sent"; - - case EMAIL_SEND_PENDING: - return "Bug - the email should be sent in synchronous mode"; - - case EMAIL_SEND_ERROR: - return "Failed: ".implode(', ', $aIssues); - } - } -} -?> + + + +/** + * Persistent classes (internal): user defined actions + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +class ExecAsyncTask implements iBackgroundProcess +{ + public function GetPeriodicity() + { + return 2; // seconds + } + + public function Process($iTimeLimit) + { + $sNow = date(AttributeDateTime::GetSQLFormat()); + // Criteria: planned, and expected to occur... ASAP or in the past + $sOQL = "SELECT AsyncTask WHERE (status = 'planned') AND (ISNULL(planned) OR (planned < '$sNow'))"; + $iProcessed = 0; + while (time() < $iTimeLimit) + { + // Next one ? + $oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */); + $oTask = $oSet->Fetch(); + if (is_null($oTask)) + { + // Nothing to be done + break; + } + $iProcessed++; + if ($oTask->Process()) + { + $oTask->DBDelete(); + } + } + return "processed $iProcessed tasks"; + } +} + +/** + * A + * + * @package iTopORM + */ +abstract class AsyncTask extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => array('created'), + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_async_task", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + + // Null is allowed to ease the migration from iTop 2.0.2 and earlier, when the status did not exist, and because the default value is not taken into account in the SQL definition + // The value is set from null to planned in the setup program + MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('planned,running,idle,error'), "sql"=>"status", "default_value"=>"planned", "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("started", array("allowed_values"=>null, "sql"=>"started", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("planned", array("allowed_values"=>null, "sql"=>"planned", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("event_id", array("targetclass"=>"Event", "jointype"=> "", "allowed_values"=>null, "sql"=>"event_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeInteger("remaining_retries", array("allowed_values"=>null, "sql"=>"remaining_retries", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("last_error_code", array("allowed_values"=>null, "sql"=>"last_error_code", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("last_error", array("allowed_values"=>null, "sql"=>"last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("last_attempt", array("allowed_values"=>null, "sql"=>"last_attempt", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + } + + /** + * Every is fine + */ + const OK = 0; + /** + * The task no longer exists + */ + const DELETED = 1; + /** + * The task is already being executed + */ + const ALREADY_RUNNING = 2; + + /** + * The current process requests the ownership on the task. + * In case the task can be accessed concurrently, this function can be overloaded to add a critical section. + * The function must not block the caller if another process is already owning the task + * + * @return integer A code among OK/DELETED/ALREADY_RUNNING. + */ + public function MarkAsRunning() + { + try + { + if ($this->Get('status') == 'running') + { + return self::ALREADY_RUNNING; + } + else + { + $this->Set('status', 'running'); + $this->Set('started', time()); + $this->DBUpdate(); + return self::OK; + } + } + catch(Exception $e) + { + // Corrupted task !! (for example: "Failed to reload object") + IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage().' - fatal error, deleting the task.'); + if ($this->Get('event_id') != 0) + { + $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); + $oEventLog->Set('message', 'Failed, corrupted data: '.$e->getMessage()); + $oEventLog->DBUpdate(); + } + $this->DBDelete(); + return self::DELETED; + } + } + + public function GetRetryDelay($iErrorCode = null) + { + $iRetryDelay = 600; + $aRetries = MetaModel::GetConfig()->Get('async_task_retries', array()); + if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) + { + $aConfig = $aRetries[get_class($this)]; + $iRetryDelay = $aConfig['retry_delay']; + } + return $iRetryDelay; + } + + public function GetMaxRetries($iErrorCode = null) + { + $iMaxRetries = 0; + $aRetries = MetaModel::GetConfig()->Get('async_task_retries', array()); + if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) + { + $aConfig = $aRetries[get_class($this)]; + $iMaxRetries = $aConfig['max_retries']; + } + } + + /** + * Override to notify people that a task cannot be performed + */ + protected function OnDefinitiveFailure() + { + } + + protected function OnInsert() + { + $this->Set('created', time()); + } + + /** + * @return boolean True if the task record can be deleted + */ + public function Process() + { + // By default: consider that the task is not completed + $bRet = false; + + // Attempt to take the ownership + $iStatus = $this->MarkAsRunning(); + if ($iStatus == self::OK) + { + try + { + $sStatus = $this->DoProcess(); + if ($this->Get('event_id') != 0) + { + $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); + $oEventLog->Set('message', $sStatus); + $oEventLog->DBUpdate(); + } + $bRet = true; + } catch (Exception $e) + { + $this->HandleError($e->getMessage(), $e->getCode()); + } + } + else + { + // Already done or being handled by another process... skip... + $bRet = false; + } + + return $bRet; + } + + /** + * Overridable to extend the behavior in case of error (logging) + */ + protected function HandleError($sErrorMessage, $iErrorCode) + { + if ($this->Get('last_attempt') == '') + { + // First attempt + $this->Set('remaining_retries', $this->GetMaxRetries($iErrorCode)); + } + + $this->Set('last_error', $sErrorMessage); + $this->Set('last_error_code', $iErrorCode); // Note: can be ZERO !!! + $this->Set('last_attempt', time()); + + $iRemaining = $this->Get('remaining_retries'); + if ($iRemaining > 0) + { + $iRetryDelay = $this->GetRetryDelay($iErrorCode); + IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage.' - remaining retries: '.$iRemaining.' - next retry in '.$iRetryDelay.'s'); + if ($this->Get('event_id') != 0) + { + $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); + $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task. Remaining retries: '.$iRemaining.'. Next retry in '.$iRetryDelay.'s'"); + try + { + $oEventLog->DBUpdate(); + } + catch (Exception $e) + { + $oEventLog->Set('message', "Failed to process async task. Remaining retries: '.$iRemaining.'. Next retry in '.$iRetryDelay.'s', more details in the log"); + $oEventLog->DBUpdate(); + } + } + $this->Set('remaining_retries', $iRemaining - 1); + $this->Set('status', 'planned'); + $this->Set('started', null); + $this->Set('planned', time() + $iRetryDelay); + } + else + { + IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage); + if ($this->Get('event_id') != 0) + { + $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); + $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task."); + try + { + $oEventLog->DBUpdate(); + } + catch (Exception $e) + { + $oEventLog->Set('message', 'Failed to process async task, more details in the log'); + $oEventLog->DBUpdate(); + } + } + $this->Set('status', 'error'); + $this->Set('started', null); + $this->Set('planned', null); + $this->OnDefinitiveFailure(); + } + $this->DBUpdate(); + } + + /** + * Throws an exception (message and code) + */ + abstract public function DoProcess(); + + /** + * Describes the error codes that DoProcess can return by the mean of exceptions + */ + static public function EnumErrorCodes() + { + return array(); + } +} + +/** + * An email notification + * + * @package iTopORM + */ +class AsyncSendEmail extends AsyncTask +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "created", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_async_send_email", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeInteger("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>Email::ORIGINAL_FORMAT, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeLongText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists +// MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details +// MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + static public function AddToQueue(EMail $oEMail, $oLog) + { + $oNew = MetaModel::NewObject(__class__); + if ($oLog) + { + $oNew->Set('event_id', $oLog->GetKey()); + } + $oNew->Set('to', $oEMail->GetRecipientTO(true /* string */)); + $oNew->Set('subject', $oEMail->GetSubject()); + +// $oNew->Set('version', 1); +// $sMessage = serialize($oEMail); + $oNew->Set('version', 2); + $sMessage = $oEMail->SerializeV2(); + $oNew->Set('message', $sMessage); + $oNew->DBInsert(); + } + + public function DoProcess() + { + $sMessage = $this->Get('message'); + $iVersion = (int) $this->Get('version'); + switch($iVersion) + { + case Email::FORMAT_V2: + $oEMail = Email::UnSerializeV2($sMessage); + break; + + case Email::ORIGINAL_FORMAT: + $oEMail = unserialize($sMessage); + break; + + default: + return 'Unknown version of the serialization format: '.$iVersion; + } + $iRes = $oEMail->Send($aIssues, true /* force synchro !!!!! */); + switch ($iRes) + { + case EMAIL_SEND_OK: + return "Sent"; + + case EMAIL_SEND_PENDING: + return "Bug - the email should be sent in synchronous mode"; + + case EMAIL_SEND_ERROR: + return "Failed: ".implode(', ', $aIssues); + } + } +} +?> diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 38440d17f..251ed5d27 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -1,8712 +1,8712 @@ - - - -/** - * Typology for the attributes - * - * @copyright Copyright (C) 2010-2018 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -require_once('MyHelpers.class.inc.php'); -require_once('ormdocument.class.inc.php'); -require_once('ormstopwatch.class.inc.php'); -require_once('ormpassword.class.inc.php'); -require_once('ormcaselog.class.inc.php'); -require_once('ormlinkset.class.inc.php'); -require_once('htmlsanitizer.class.inc.php'); -require_once(APPROOT.'sources/autoload.php'); -require_once('customfieldshandler.class.inc.php'); -require_once('ormcustomfieldsvalue.class.inc.php'); -require_once('datetimeformat.class.inc.php'); -// This should be changed to a use when we go full-namespace -require_once(APPROOT . 'sources/form/validator/validator.class.inc.php'); -require_once(APPROOT . 'sources/form/validator/notemptyextkeyvalidator.class.inc.php'); - -/** - * MissingColumnException - sent if an attribute is being created but the column is missing in the row - * - * @package iTopORM - */ -class MissingColumnException extends Exception -{} - -/** - * add some description here... - * - * @package iTopORM - */ -define('EXTKEY_RELATIVE', 1); - -/** - * add some description here... - * - * @package iTopORM - */ -define('EXTKEY_ABSOLUTE', 2); - -/** - * Propagation of the deletion through an external key - ask the user to delete the referencing object - * - * @package iTopORM - */ -define('DEL_MANUAL', 1); - -/** - * Propagation of the deletion through an external key - ask the user to delete the referencing object - * - * @package iTopORM - */ -define('DEL_AUTO', 2); -/** - * Fully silent delete... not yet implemented - */ -define('DEL_SILENT', 2); -/** - * For HierarchicalKeys only: move all the children up one level automatically - */ -define('DEL_MOVEUP', 3); - - -/** - * For Link sets: tracking_level - * - * @package iTopORM - */ -define('ATTRIBUTE_TRACKING_NONE', 0); // Do not track changes of the attribute -define('ATTRIBUTE_TRACKING_ALL', 3); // Do track all changes of the attribute -define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set -define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items -define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items -define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items - -define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from inside this object -define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object -define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu -define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place -define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place - - -/** - * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) - * - * @package iTopORM - */ -abstract class AttributeDefinition -{ - const SEARCH_WIDGET_TYPE_RAW = 'raw'; - const SEARCH_WIDGET_TYPE_STRING = 'string'; - const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric'; - const SEARCH_WIDGET_TYPE_ENUM = 'enum'; - const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key'; - const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key'; - const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field'; - const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time'; - const SEARCH_WIDGET_TYPE_DATE = 'date'; - - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - const INDEX_LENGTH = 95; - - public function GetType() - { - return Dict::S('Core:'.get_class($this)); - } - public function GetTypeDesc() - { - return Dict::S('Core:'.get_class($this).'+'); - } - - abstract public function GetEditClass(); - - /** - * Return the search widget type corresponding to this attribute - * - * @return string - */ - public function GetSearchType() - { - return static::SEARCH_WIDGET_TYPE; - } - - /** - * @return bool - */ - public function IsSearchable() - { - return static::SEARCH_WIDGET_TYPE != static::SEARCH_WIDGET_TYPE_RAW; - } - - protected $m_sCode; - private $m_aParams = array(); - protected $m_sHostClass = '!undefined!'; - public function Get($sParamName) {return $this->m_aParams[$sParamName];} - - public function GetIndexLength() { - $iMaxLength = $this->GetMaxSize(); - if (is_null($iMaxLength)) - { - return null; - } - if ($iMaxLength > static::INDEX_LENGTH) - { - return static::INDEX_LENGTH; - } - return $iMaxLength; - } - - public function IsParam($sParamName) {return (array_key_exists($sParamName, $this->m_aParams));} - - protected function GetOptional($sParamName, $default) - { - if (array_key_exists($sParamName, $this->m_aParams)) - { - return $this->m_aParams[$sParamName]; - } - else - { - return $default; - } - } - - /** - * AttributeDefinition constructor. - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - */ - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $this->m_aParams = $aParams; - $this->ConsistencyCheck(); - } - - public function GetParams() - { - return $this->m_aParams; - } - - public function HasParam($sParam) - { - return array_key_exists($sParam, $this->m_aParams); - } - - public function SetHostClass($sHostClass) - { - $this->m_sHostClass = $sHostClass; - } - public function GetHostClass() - { - return $this->m_sHostClass; - } - - /** - * @return array - * - * @throws \CoreException - */ - public function ListSubItems() - { - $aSubItems = array(); - foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeSubItem) - { - if ($oAttDef->Get('target_attcode') == $this->m_sCode) - { - $aSubItems[$sAttCode] = $oAttDef; - } - } - } - return $aSubItems; - } - - // Note: I could factorize this code with the parameter management made for the AttributeDef class - // to be overloaded - static public function ListExpectedParams() - { - return array(); - } - - /** - * @throws \Exception - */ - private function ConsistencyCheck() - { - // Check that any mandatory param has been specified - // - $aExpectedParams = $this->ListExpectedParams(); - foreach($aExpectedParams as $sParamName) - { - if (!array_key_exists($sParamName, $this->m_aParams)) - { - $aBacktrace = debug_backtrace(); - $sTargetClass = $aBacktrace[2]["class"]; - $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; - throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); - } - } - } - - /** - * Check the validity of the given value - * - * @param \DBObject $oHostObject - * @param $value An error if any, null otherwise - * - * @return bool - */ - public function CheckValue(DBObject $oHostObject, $value) - { - // todo: factorize here the cases implemented into DBObject - return true; - } - - // table, key field, name field - public function ListDBJoins() - { - return ""; - // e.g: return array("Site", "infrid", "name"); - } - - public function GetFinalAttDef() - { - return $this; - } - - /** - * Deprecated - use IsBasedOnDBColumns instead - * @return bool - */ - public function IsDirectField() {return static::IsBasedOnDBColumns();} - - /** - * Returns true if the attribute value is built after DB columns - * @return bool - */ - static public function IsBasedOnDBColumns() {return false;} - /** - * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via GetOQLExpression) - * @return bool - */ - static public function IsBasedOnOQLExpression() {return false;} - /** - * Returns true if the attribute value can be shown as a string - * @return bool - */ - static public function IsScalar() {return false;} - /** - * Returns true if the attribute value is a set of related objects (1-N or N-N) - * @return bool - */ - static public function IsLinkSet() {return false;} - - /** - * @param int $iType - * - * @return bool true if the attribute is an external key, either directly (RELATIVE to the host class), or indirectly (ABSOLUTELY) - */ - public function IsExternalKey($iType = EXTKEY_RELATIVE) - { - return false; - } - /** - * @return bool true if the attribute value is an external key, pointing to the host class - */ - static public function IsHierarchicalKey() {return false;} - /** - * @return bool true if the attribute value is stored on an object pointed to be an external key - */ - static public function IsExternalField() {return false;} - /** - * @return bool true if the attribute can be written (by essence : metamodel field option) - * @see \DBObject::IsAttributeReadOnlyForCurrentState() for a specific object instance (depending on its workflow) - */ - public function IsWritable() {return false;} - /** - * @return bool true if the attribute has been added automatically by the framework - */ - public function IsMagic() {return $this->GetOptional('magic', false);} - /** - * @return bool true if the attribute value is kept in the loaded object (in memory) - */ - static public function LoadInObject() {return true;} - /** - * @return bool true if the attribute value comes from the database in one way or another - */ - static public function LoadFromDB() {return true;} - /** - * @return bool true if the attribute should be loaded anytime (in addition to the column selected by the user) - */ - public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);} - - /** - * @param \DBObject $oHostObject - * - * @return mixed Must return the value if LoadInObject returns false - */ - public function GetValue($oHostObject) - { - return null; - } - - /** - * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull()) - * @return bool - */ - public function IsNullAllowed() {return true;} - /** - * Returns the attribute code (identifies the attribute in the host class) - * @return string - */ - public function GetCode() {return $this->m_sCode;} - - /** - * Find the corresponding "link" attribute on the target class, if any - * @return null | AttributeDefinition - */ - public function GetMirrorLinkAttribute() {return null;} - - /** - * Helper to browse the hierarchy of classes, searching for a label - * - * @param string $sDictEntrySuffix - * @param string $sDefault - * @param bool $bUserLanguageOnly - * - * @return string - * @throws \Exception - */ - protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly) - { - $sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly); - if (strlen($sLabel) == 0) - { - // Nothing found: go higher in the hierarchy (if possible) - // - $sLabel = $sDefault; - $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); - if ($sParentClass) - { - if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) - { - $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); - $sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly); - } - } - } - return $sLabel; - } - - /** - * @param string|null $sDefault - * - * @return string - * - * @throws \Exception - */ - public function GetLabel($sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = str_replace('_', ' ', $this->m_sCode); - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false); - } - return $sLabel; - } - - /** - * To be overloaded for localized enums - * - * @param string $sValue - * - * @return string label corresponding to the given value (in plain text) - */ - public function GetValueLabel($sValue) - { - return $sValue; - } - - /** - * Get the value from a given string (plain text, CSV import) - * - * @param string $sProposedValue - * @param bool $bLocalizedValue - * @param string $sSepItem - * @param string $sSepAttribute - * @param string $sSepValue - * @param string $sAttributeQualifier - * - * @return mixed null if no match could be found - */ - public function MakeValueFromString( - $sProposedValue, - $bLocalizedValue = false, - $sSepItem = null, - $sSepAttribute = null, - $sSepValue = null, - $sAttributeQualifier = null - ) - { - return $this->MakeRealValue($sProposedValue, null); - } - - /** - * Parses a search string coming from user input - * @param string $sSearchString - * @return string - */ - public function ParseSearchString($sSearchString) - { - return $sSearchString; - } - - /** - * @return string - * - * @throws \Exception - */ - public function GetLabel_Obsolete() - { - // Written for compatibility with a data model written prior to version 0.9.1 - if (array_key_exists('label', $this->m_aParams)) - { - return $this->m_aParams['label']; - } - else - { - return $this->GetLabel(); - } - } - - /** - * @param string|null $sDefault - * - * @return string - * - * @throws \Exception - */ - public function GetDescription($sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = ''; - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false); - } - return $sLabel; - } - - /** - * @param string|null $sDefault - * - * @return string - * - * @throws \Exception - */ - public function GetHelpOnEdition($sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = ''; - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false); - } - return $sLabel; - } - - public function GetHelpOnSmartSearch() - { - $aParents = array_merge(array(get_class($this) => get_class($this)), class_parents($this)); - foreach ($aParents as $sClass) - { - $sHelp = Dict::S("Core:$sClass?SmartSearch", '-missing-'); - if ($sHelp != '-missing-') - { - return $sHelp; - } - } - return ''; - } - - /** - * @return string - * - * @throws \Exception - */ - public function GetDescription_Obsolete() - { - // Written for compatibility with a data model written prior to version 0.9.1 - if (array_key_exists('description', $this->m_aParams)) - { - return $this->m_aParams['description']; - } - else - { - return $this->GetDescription(); - } - } - - public function GetTrackingLevel() - { - return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL); - } - - /** - * @return \ValueSetObjects - */ - public function GetValuesDef() {return null;} - - public function GetPrerequisiteAttributes($sClass = null) - { - return array(); - } - - public function GetNullValue() {return null;} - - public function IsNull($proposedValue) - { - return is_null($proposedValue); - } - - /** - * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! - * - * @param $proposedValue - * @param $oHostObj - * - * @return mixed - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - return $proposedValue; - } - - public function Equals($val1, $val2) {return ($val1 == $val2);} - - /** - * @param string $sPrefix - * - * @return array suffix/expression pairs (1 in most of the cases), for READING (Select) - */ - public function GetSQLExpressions($sPrefix = '') - { - return array(); - } - - /** - * @param array $aCols - * @param string $sPrefix - * - * @return mixed a value out of suffix/value pairs, for SELECT result interpretation - */ - public function FromSQLToValue($aCols, $sPrefix = '') - { - return null; - } - - /** - * @param bool $bFullSpec - * - * @return array column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) - * @see \CMDBSource::GetFieldSpec() - */ - public function GetSQLColumns($bFullSpec = false) - { - return array(); - } - - /** - * @param $value - * - * @return array column/value pairs (1 in most of the cases), for WRITING (Insert, Update) - */ - public function GetSQLValues($value) - { - return array(); - } - public function RequiresIndex() {return false;} - - public function RequiresFullTextIndex() - { - return false; - } - public function CopyOnAllTables() {return false;} - - public function GetOrderBySQLExpressions($sClassAlias) - { - // Note: This is the responsibility of this function to place backticks around column aliases - return array('`'.$sClassAlias.$this->GetCode().'`'); - } - - public function GetOrderByHint() - { - return ''; - } - - // Import - differs slightly from SQL input, but identical in most cases - // - public function GetImportColumns() {return $this->GetSQLColumns();} - public function FromImportToValue($aCols, $sPrefix = '') - { - $aValues = array(); - foreach ($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr) - { - // This is working, based on the assumption that importable fields - // are not computed fields => the expression is the name of a column - $aValues[$sPrefix.$sAlias] = $aCols[$sExpr]; - } - return $this->FromSQLToValue($aValues, $sPrefix); - } - - public function GetValidationPattern() - { - return ''; - } - - public function CheckFormat($value) - { - return true; - } - - public function GetMaxSize() - { - return null; - } - - public function MakeValue() - { - $sComputeFunc = $this->Get("compute_func"); - if (empty($sComputeFunc)) return null; - - return call_user_func($sComputeFunc); - } - - abstract public function GetDefaultValue(DBObject $oHostObject = null); - - // - // To be overloaded in subclasses - // - - abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description" - abstract public function GetBasicFilterLooseOperator(); // returns an "opCode" - //abstract protected GetBasicFilterHTMLInput(); - abstract public function GetBasicFilterSQLExpr($sOpCode, $value); - - public function GetFilterDefinitions() - { - return array(); - } - - public function GetEditValue($sValue, $oHostObj = null) - { - return (string)$sValue; - } - - /** - * For fields containing a potential markup, return the value without this markup - * - * @param string $sValue - * @param \DBObject $oHostObj - * - * @return string - */ - public function GetAsPlainText($sValue, $oHostObj = null) - { - return (string) $this->GetEditValue($sValue, $oHostObj); - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - * - * @param $value - * - * @return string - */ - public function GetForJSON($value) - { - // In most of the cases, that will be the expected behavior... - return $this->GetEditValue($value); - } - - /** - * Helper to form a value, given JSON decoded data - * The operation is the opposite to GetForJSON - * - * @param $json - * - * @return mixed - */ - public function FromJSONToValue($json) - { - // Passthrough in most of the cases - return $json; - } - - /** - * Override to display the value in the GUI - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - */ - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - return Str::pure2html((string)$sValue); - } - - /** - * Override to export the value in XML - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return mixed - */ - public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) - { - return Str::pure2xml((string)$sValue); - } - - /** - * Override to escape the value when read by DBObject::GetAsCSV() - * - * @param string $sValue - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return string - */ - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - return (string)$sValue; - } - - /** - * Override to differentiate a value displayed in the UI or in the history - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - */ - public function GetAsHTMLForHistory($sValue, $oHostObject = null, $bLocalize = true) - { - return $this->GetAsHTML($sValue, $oHostObject, $bLocalize); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\StringField'; - } - - /** - * Override to specify Field class - * - * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is - * passed, MakeFormField behave more like a Prepare. - * - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\Field $oFormField - * - * @return null - * @throws \CoreException - * @throws \Exception - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - // This is a fallback in case the AttributeDefinition subclass has no overloading of this function. - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - //$oFormField->SetReadOnly(true); - } - - $oFormField->SetLabel($this->GetLabel()); - - // Attributes flags - // - Retrieving flags for the current object - if ($oObject->IsNew()) - { - $iFlags = $oObject->GetInitialStateAttributeFlags($this->GetCode()); - } - else - { - $iFlags = $oObject->GetAttributeFlags($this->GetCode()); - } - - // - Comparing flags - if ($this->IsWritable() && (!$this->IsNullAllowed() || (($iFlags & OPT_ATT_MANDATORY) === OPT_ATT_MANDATORY))) - { - $oFormField->SetMandatory(true); - } - if ((!$oObject->IsNew() || !$oFormField->GetMandatory()) && (($iFlags & OPT_ATT_READONLY) === OPT_ATT_READONLY)) - { - $oFormField->SetReadOnly(true); - } - - // CurrentValue - $oFormField->SetCurrentValue($oObject->Get($this->GetCode())); - - // Validation pattern - if ($this->GetValidationPattern() !== '') - { - $oFormField->AddValidator(new \Combodo\iTop\Form\Validator\Validator($this->GetValidationPattern())); - } - - return $oFormField; - } - - /** - * List the available verbs for 'GetForTemplate' - */ - public function EnumTemplateVerbs() - { - return array( - '' => 'Plain text (unlocalized) representation', - 'html' => 'HTML representation', - 'label' => 'Localized representation', - 'text' => 'Plain text representation (without any markup)', - ); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * - * @param mixed $value The current value of the field - * @param string $sVerb The verb specifying the representation of the value - * @param \DBObject $oHostObject - * @param bool $bLocalize Whether or not to localize the value - * - * @return mixed|null|string - * - * @throws \Exception - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - if ($this->IsScalar()) - { - switch ($sVerb) - { - case '': - return $value; - - case 'html': - return $this->GetAsHtml($value, $oHostObject, $bLocalize); - - case 'label': - return $this->GetEditValue($value); - - case 'text': - return $this->GetAsPlainText($value); - break; - - default: - throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); - } - } - return null; - } - - /** - * @param array $aArgs - * @param string $sContains - * - * @return array|null - * @throws \CoreException - * @throws \OQLException - */ - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $oValSetDef = $this->GetValuesDef(); - if (!$oValSetDef) return null; - return $oValSetDef->GetValues($aArgs, $sContains); - } - - /** - * Explain the change of the attribute (history) - * - * @param string $sOldValue - * @param string $sNewValue - * @param string $sLabel - * - * @return string - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \OQLException - * @throws \Exception - */ - public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) - { - if (is_null($sLabel)) - { - $sLabel = $this->GetLabel(); - } - - $sNewValueHtml = $this->GetAsHTMLForHistory($sNewValue); - $sOldValueHtml = $this->GetAsHTMLForHistory($sOldValue); - - if($this->IsExternalKey()) - { - /** @var \AttributeExternalKey $this */ - $sTargetClass = $this->GetTargetClass(); - $sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null; - $sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null; - } - if ( (($this->GetType() == 'String') || ($this->GetType() == 'Text')) && - (strlen($sNewValue) > strlen($sOldValue)) ) - { - // Check if some text was not appended to the field - if (substr($sNewValue,0, strlen($sOldValue)) == $sOldValue) // Text added at the end - { - $sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue))); - $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); - } - else if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning - { - $sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue))); - $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); - } - else - { - if (strlen($sOldValue) == 0) - { - $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); - } - else - { - if (is_null($sNewValue)) - { - $sNewValueHtml = Dict::S('UI:UndefinedObject'); - } - $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, $sOldValueHtml); - } - } - } - else - { - if (strlen($sOldValue) == 0) - { - $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); - } - else - { - if (is_null($sNewValue)) - { - $sNewValueHtml = Dict::S('UI:UndefinedObject'); - } - $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, $sOldValueHtml); - } - } - return $sResult; - } - - - /** - * Parses a string to find some smart search patterns and build the corresponding search/OQL condition - * Each derived class is reponsible for defining and processing their own smart patterns, the base class - * does nothing special, and just calls the default (loose) operator - * - * @param string $sSearchText The search string to analyze for smart patterns - * @param \FieldExpression $oField - * @param array $aParams Values of the query parameters - * - * @return \Expression The search condition to be added (AND) to the current search - * - * @throws \CoreException - */ - public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams) - { - $sParamName = $oField->GetParent().'_'.$oField->GetName(); - $oRightExpr = new VariableExpression($sParamName); - $sOperator = $this->GetBasicFilterLooseOperator(); - switch ($sOperator) - { - case 'Contains': - $aParams[$sParamName] = "%$sSearchText%"; - $sSQLOperator = 'LIKE'; - break; - - default: - $sSQLOperator = $sOperator; - $aParams[$sParamName] = $sSearchText; - } - $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); - return $oNewCondition; - } - - /** - * Tells if an attribute is part of the unique fingerprint of the object (used for comparing two objects) - * All attributes which value is not based on a value from the object itself (like ExternalFields or LinkedSet) - * must be excluded from the object's signature - * @return boolean - */ - public function IsPartOfFingerprint() - { - return true; - } - - /** - * The part of the current attribute in the object's signature, for the supplied value - * @param mixed $value The value of this attribute for the object - * @return string The "signature" for this field/attribute - */ - public function Fingerprint($value) - { - return (string)$value; - } -} - -/** - * Set of objects directly linked to an object, and being part of its definition - * - * @package iTopORM - */ -class AttributeLinkedSet extends AttributeDefinition -{ - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max")); - } - - public function GetEditClass() {return "LinkedSet";} - - public function IsWritable() {return true;} - static public function IsLinkSet() {return true;} - public function IsIndirect() {return false;} - - public function GetValuesDef() {return $this->Get("allowed_values");} - public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");} - - /** - * @param \DBObject|null $oHostObject - * - * @return \ormLinkSet - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreWarning - */ - public function GetDefaultValue(DBObject $oHostObject = null) - { - $sLinkClass = $this->GetLinkedClass(); - $sExtKeyToMe = $this->GetExtKeyToMe(); - - // The class to target is not the current class, because if this is a derived class, - // it may differ from the target class, then things start to become confusing - /** @var \AttributeExternalKey $oRemoteExtKeyAtt */ - $oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe); - $sMyClass = $oRemoteExtKeyAtt->GetTargetClass(); - - $oMyselfSearch = new DBObjectSearch($sMyClass); - if ($oHostObject !== null) - { - $oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '='); - } - - $oLinkSearch = new DBObjectSearch($sLinkClass); - $oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe); - if ($this->IsIndirect()) - { - // Join the remote class so that the archive flag will be taken into account - /** @var \AttributeLinkedSetIndirect $this */ - $sExtKeyToRemote = $this->GetExtKeyToRemote(); - /** @var \AttributeExternalKey $oExtKeyToRemote */ - $oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote); - $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); - if (MetaModel::IsArchivable($sRemoteClass)) - { - $oRemoteSearch = new DBObjectSearch($sRemoteClass); - /** @var \AttributeLinkedSetIndirect $this */ - $oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote()); - } - } - $oLinks = new DBObjectSet($oLinkSearch); - $oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks); - return $oLinkSet; - } - - public function GetTrackingLevel() - { - return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default')); - } - - public function GetEditMode() - { - return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS); - } - - public function GetLinkedClass() {return $this->Get('linked_class');} - public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');} - - public function GetBasicFilterOperators() {return array();} - public function GetBasicFilterLooseOperator() {return '';} - public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} - - /** - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string|null - * - * @throws \CoreException - */ - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $aItems = array(); - while ($oObj = $sValue->Fetch()) - { - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) - { - if ($sAttCode == $this->GetExtKeyToMe()) continue; - if ($oAttDef->IsExternalField()) continue; - $sAttValue = $oObj->GetAsHTML($sAttCode); - if (strlen($sAttValue) > 0) - { - $aAttributes[] = $sAttValue; - } - } - $sAttributes = implode(', ', $aAttributes); - $aItems[] = $sAttributes; - } - return implode('
    ', $aItems); - } - return null; - } - - /** - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - * - * @throws \CoreException - */ - public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) - { - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $sRes = "\n"; - while ($oObj = $sValue->Fetch()) - { - $sObjClass = get_class($oObj); - $sRes .= "<$sObjClass id=\"".$oObj->GetKey()."\">\n"; - // Show only relevant information (hide the external key to the current object) - foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) - { - if ($sAttCode == 'finalclass') - { - if ($sObjClass == $this->GetLinkedClass()) - { - // Simplify the output if the exact class could be determined implicitely - continue; - } - } - if ($sAttCode == $this->GetExtKeyToMe()) continue; - if ($oAttDef->IsExternalField()) - { - /** @var \AttributeExternalField $oAttDef */ - if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) continue; - /** @var AttributeExternalField $oAttDef */ - if ($oAttDef->IsFriendlyName()) continue; - } - if ($oAttDef instanceof AttributeFriendlyName) continue; - if (!$oAttDef->IsScalar()) continue; - $sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize); - $sRes .= "<$sAttCode>$sAttValue\n"; - } - $sRes .= "\n"; - } - $sRes .= "\n"; - } - else - { - $sRes = ''; - } - return $sRes; - } - - /** - * @param $sValue - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return mixed|string - * @throws \CoreException - */ - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); - $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); - $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); - $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); - - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $aItems = array(); - while ($oObj = $sValue->Fetch()) - { - $sObjClass = get_class($oObj); - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) - { - if ($sAttCode == 'finalclass') - { - if ($sObjClass == $this->GetLinkedClass()) - { - // Simplify the output if the exact class could be determined implicitely - continue; - } - } - if ($sAttCode == $this->GetExtKeyToMe()) continue; - if ($oAttDef->IsExternalField()) continue; - if (!$oAttDef->IsBasedOnDBColumns()) continue; - if (!$oAttDef->IsScalar()) continue; - $sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize); - if (strlen($sAttValue) > 0) - { - $sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier, $sAttCode.$sSepValue.$sAttValue); - $aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier; - } - } - $sAttributes = implode($sSepAttribute, $aAttributes); - $aItems[] = $sAttributes; - } - $sRes = implode($sSepItem, $aItems); - } - else - { - $sRes = ''; - } - $sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes); - $sRes = $sTextQualifier.$sRes.$sTextQualifier; - return $sRes; - } - - /** - * List the available verbs for 'GetForTemplate' - */ - public function EnumTemplateVerbs() - { - return array( - '' => 'Plain text (unlocalized) representation', - 'html' => 'HTML representation (unordered list)', - ); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * - * @param mixed $value The current value of the field - * @param string $sVerb The verb specifying the representation of the value - * @param DBObject $oHostObject The object - * @param bool $bLocalize Whether or not to localize the value - * - * @return string - * @throws \Exception - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - $sRemoteName = $this->IsIndirect() ? - /** @var \AttributeLinkedSetIndirect $this */ - $this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname'; - - $oLinkSet = clone $value; // Workaround/Safety net for Trac #887 - $iLimit = MetaModel::GetConfig()->Get('max_linkset_output'); - $iCount = 0; - $aNames = array(); - foreach($oLinkSet as $oItem) - { - if (($iLimit > 0) && ($iCount == $iLimit)) - { - $iTotal = $oLinkSet->Count(); - $aNames[] = '... '.Dict::Format('UI:TruncatedResults', $iCount, $iTotal); - break; - } - $aNames[] = $oItem->Get($sRemoteName); - $iCount++; - } - - switch($sVerb) - { - case '': - return implode("\n", $aNames); - - case 'html': - return '
    • '.implode("
    • ", $aNames).'
    '; - - default: - throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); - } - } - - public function DuplicatesAllowed() {return false;} // No duplicates for 1:n links, never - - public function GetImportColumns() - { - $aColumns = array(); - $aColumns[$this->GetCode()] = 'TEXT'; - return $aColumns; - } - - /** - * @param string $sProposedValue - * @param bool $bLocalizedValue - * @param string $sSepItem - * @param string $sSepAttribute - * @param string $sSepValue - * @param string $sAttributeQualifier - * - * @return \DBObjectSet|mixed - * @throws \CSVParserException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * @throws \Exception - */ - public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) - { - if (is_null($sSepItem) || empty($sSepItem)) - { - $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); - } - if (is_null($sSepAttribute) || empty($sSepAttribute)) - { - $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); - } - if (is_null($sSepValue) || empty($sSepValue)) - { - $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); - } - if (is_null($sAttributeQualifier) || empty($sAttributeQualifier)) - { - $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); - } - - $sTargetClass = $this->Get('linked_class'); - - $sInput = str_replace($sSepItem, "\n", $sProposedValue); - $oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier); - - $aInput = $oCSVParser->ToArray(0 /* do not skip lines */); - - $aLinks = array(); - foreach($aInput as $aRow) - { - // 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value - $aExtKeys = array(); - $aValues = array(); - foreach($aRow as $sCell) - { - $iSepPos = strpos($sCell, $sSepValue); - if ($iSepPos === false) - { - // Houston... - throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell)); - } - - $sAttCode = trim(substr($sCell, 0, $iSepPos)); - $sValue = substr($sCell, $iSepPos + strlen($sSepValue)); - - if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches)) - { - $sKeyAttCode = $aMatches[1]; - $sRemoteAttCode = $aMatches[2]; - $aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue; - if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode)) - { - throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sKeyAttCode)); - } - /** @var \AttributeExternalKey $oKeyAttDef */ - $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); - $sRemoteClass = $oKeyAttDef->GetTargetClass(); - if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode)) - { - throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode)); - } - } - else - { - if(!MetaModel::IsValidAttCode($sTargetClass, $sAttCode)) - { - throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sAttCode)); - } - $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode); - $aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); - } - } - - // 2nd - Instanciate the object and set the value - if (isset($aValues['finalclass'])) - { - $sLinkClass = $aValues['finalclass']; - if (!is_subclass_of($sLinkClass, $sTargetClass)) - { - throw new CoreException('Wrong class for link attribute specification', array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); - } - } - elseif (MetaModel::IsAbstract($sTargetClass)) - { - throw new CoreException('Missing finalclass for link attribute specification'); - } - else - { - $sLinkClass = $sTargetClass; - } - - $oLink = MetaModel::NewObject($sLinkClass); - foreach ($aValues as $sAttCode => $sValue) - { - $oLink->Set($sAttCode, $sValue); - } - - // 3rd - Set external keys from search conditions - foreach ($aExtKeys as $sKeyAttCode => $aReconciliation) - { - $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); - $sKeyClass = $oKeyAttDef->GetTargetClass(); - $oExtKeyFilter = new DBObjectSearch($sKeyClass); - $aReconciliationDesc = array(); - foreach($aReconciliation as $sRemoteAttCode => $sValue) - { - $oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '='); - $aReconciliationDesc[] = "$sRemoteAttCode=$sValue"; - } - $oExtKeySet = new DBObjectSet($oExtKeyFilter); - switch($oExtKeySet->Count()) - { - case 0: - $sReconciliationDesc = implode(', ', $aReconciliationDesc); - throw new CoreException("Found no match", array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); - break; - case 1: - $oRemoteObj = $oExtKeySet->Fetch(); - $oLink->Set($sKeyAttCode, $oRemoteObj->GetKey()); - break; - default: - $sReconciliationDesc = implode(', ', $aReconciliationDesc); - throw new CoreException("Found several matches", array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); - // Found several matches, ambiguous - } - } - - // Check (roughly) if such a link is valid - $aErrors = array(); - foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsExternalKey()) - { - /** @var \AttributeExternalKey $oAttDef */ - if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), $oAttDef->GetTargetClass()))) - { - continue; // Don't check the key to self - } - } - - if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) - { - $aErrors[] = $sAttCode; - } - } - if (count($aErrors) > 0) - { - throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); - } - - $aLinks[] = $oLink; - } - $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); - return $oSet; - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - * - * @param \ormLinkSet $value - * - * @return array - * @throws \CoreException - */ - public function GetForJSON($value) - { - $aRet = array(); - if (is_object($value) && ($value instanceof ormLinkSet)) - { - $value->Rewind(); - while ($oObj = $value->Fetch()) - { - $sObjClass = get_class($oObj); - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) - { - if ($sAttCode == 'finalclass') - { - if ($sObjClass == $this->GetLinkedClass()) - { - // Simplify the output if the exact class could be determined implicitely - continue; - } - } - if ($sAttCode == $this->GetExtKeyToMe()) continue; - if ($oAttDef->IsExternalField()) continue; - if (!$oAttDef->IsBasedOnDBColumns()) continue; - if (!$oAttDef->IsScalar()) continue; - $attValue = $oObj->Get($sAttCode); - $aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue); - } - $aRet[] = $aAttributes; - } - } - return $aRet; - } - - /** - * Helper to form a value, given JSON decoded data - * The operation is the opposite to GetForJSON - * - * @param $json - * - * @return \DBObjectSet - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \Exception - */ - public function FromJSONToValue($json) - { - $sTargetClass = $this->Get('linked_class'); - - $aLinks = array(); - foreach($json as $aValues) - { - if (isset($aValues['finalclass'])) - { - $sLinkClass = $aValues['finalclass']; - if (!is_subclass_of($sLinkClass, $sTargetClass)) - { - throw new CoreException('Wrong class for link attribute specification', array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); - } - } - elseif (MetaModel::IsAbstract($sTargetClass)) - { - throw new CoreException('Missing finalclass for link attribute specification'); - } - else - { - $sLinkClass = $sTargetClass; - } - - $oLink = MetaModel::NewObject($sLinkClass); - foreach ($aValues as $sAttCode => $sValue) - { - $oLink->Set($sAttCode, $sValue); - } - - // Check (roughly) if such a link is valid - $aErrors = array(); - foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsExternalKey()) - { - /** @var AttributeExternalKey $oAttDef */ - if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), $oAttDef->GetTargetClass()))) - { - continue; // Don't check the key to self - } - } - - if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) - { - $aErrors[] = $sAttCode; - } - } - if (count($aErrors) > 0) - { - throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); - } - - $aLinks[] = $oLink; - } - $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); - return $oSet; - } - - /** - * @param $proposedValue - * @param $oHostObj - * - * @return mixed - * @throws \Exception - */ - public function MakeRealValue($proposedValue, $oHostObj){ - if($proposedValue === null) - { - $sLinkedClass = $this->GetLinkedClass(); - $aLinkedObjectsArray = array(); - $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); - - return new ormLinkSet( - get_class($oHostObj), - $this->GetCode(), - $oSet - ); - } - - return $proposedValue; - } - - /** - * @param ormLinkSet $val1 - * @param ormLinkSet $val2 - * @return bool - */ - public function Equals($val1, $val2) - { - if ($val1 === $val2) - { - $bAreEquivalent = true; - } - else - { - $bAreEquivalent = ($val2->HasDelta() === false); - } - return $bAreEquivalent; - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * - * @return null | AttributeDefinition - * @throws \Exception - */ - public function GetMirrorLinkAttribute() - { - $oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe()); - return $oRemoteAtt; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\LinkedSetField $oFormField - * - * @return \Combodo\iTop\Form\Field\LinkedSetField - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \Exception - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - // Setting target class - if (!$this->IsIndirect()) - { - $sTargetClass = $this->GetLinkedClass(); - } - else - { - /** @var \AttributeExternalKey $oRemoteAttDef */ - /** @var \AttributeLinkedSetIndirect $this */ - $oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); - $sTargetClass = $oRemoteAttDef->GetTargetClass(); - - /** @var \AttributeLinkedSetIndirect $this */ - $oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote()); - } - $oFormField->SetTargetClass($sTargetClass); - $oFormField->SetIndirect($this->IsIndirect()); - // Setting attcodes to display - $aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list')); - // - Adding friendlyname attribute to the list is not already in it - $sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass); - if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) - { - $aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay); - } - // - Adding attribute labels - $aAttributesToDisplay = array(); - foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) - { - $oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay); - $aAttributesToDisplay[$sAttCodeToDisplay] = $oAttDefToDisplay->GetLabel(); - } - $oFormField->SetAttributesToDisplay($aAttributesToDisplay); - - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - public function IsPartOfFingerprint() { return false; } -} - -/** - * Set of objects linked to an object (n-n), and being part of its definition - * - * @package iTopORM - */ -class AttributeLinkedSetIndirect extends AttributeLinkedSet -{ - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote")); - } - - public function IsIndirect() - { - return true; - } - - public function GetExtKeyToRemote() { return $this->Get('ext_key_to_remote'); } - public function GetEditClass() {return "LinkedSet";} - public function DuplicatesAllowed() {return $this->GetOptional("duplicates", false);} // The same object may be linked several times... or not... - - public function GetTrackingLevel() - { - return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default')); - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * @return null | AttributeDefinition - * @throws \CoreException - */ - public function GetMirrorLinkAttribute() - { - $oRet = null; - /** @var \AttributeExternalKey $oExtKeyToRemote */ - $oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); - $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); - foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) - { - if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect) continue; - if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass()) continue; - if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote()) continue; - if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe()) continue; - $oRet = $oRemoteAttDef; - break; - } - return $oRet; - } -} - -/** - * Abstract class implementing default filters for a DB column - * - * @package iTopORM - */ -class AttributeDBFieldVoid extends AttributeDefinition -{ - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql")); - } - - // To be overriden, used in GetSQLColumns - protected function GetSQLCol($bFullSpec = false) - { - return 'VARCHAR(255)' - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - protected function GetSQLColSpec() - { - $default = $this->ScalarToSQL($this->GetDefaultValue()); - if (is_null($default)) - { - $sRet = ''; - } - else - { - if (is_numeric($default)) - { - // Though it is a string in PHP, it will be considered as a numeric value in MySQL - // Then it must not be quoted here, to preserve the compatibility with the value returned by CMDBSource::GetFieldSpec - $sRet = " DEFAULT $default"; - } - else - { - $sRet = " DEFAULT ".CMDBSource::Quote($default); - } - } - return $sRet; - } - - public function GetEditClass() {return "String";} - - public function GetValuesDef() {return $this->Get("allowed_values");} - public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");} - - static public function IsBasedOnDBColumns() {return true;} - static public function IsScalar() {return true;} - public function IsWritable() {return !$this->IsMagic();} - public function GetSQLExpr() - { - return $this->Get("sql"); - } - - public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);} - public function IsNullAllowed() {return false;} - - // - protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) - - public function GetSQLExpressions($sPrefix = '') - { - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $this->Get("sql"); - return $aColumns; - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - $value = $this->MakeRealValue($aCols[$sPrefix.''], null); - return $value; - } - public function GetSQLValues($value) - { - $aValues = array(); - $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); - return $aValues; - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->Get("sql")] = $this->GetSQLCol($bFullSpec); - return $aColumns; - } - - public function GetFilterDefinitions() - { - return array($this->GetCode() => new FilterFromAttribute($this)); - } - - public function GetBasicFilterOperators() - { - return array("="=>"equals", "!="=>"differs from"); - } - public function GetBasicFilterLooseOperator() - { - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '=': - default: - return $this->GetSQLExpr()." = $sQValue"; - } - } -} - -/** - * Base class for all kind of DB attributes, with the exception of external keys - * - * @package iTopORM - */ -class AttributeDBField extends AttributeDBFieldVoid -{ - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed")); - } - public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue($this->Get("default_value"), $oHostObject);} - public function IsNullAllowed() {return $this->Get("is_null_allowed");} -} - -/** - * Map an integer column to an attribute - * - * @package iTopORM - */ -class AttributeInteger extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "String";} - protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? $this->GetSQLColSpec() : '');} - - public function GetValidationPattern() - { - return "^[0-9]+$"; - } - - public function GetBasicFilterOperators() - { - return array( - "!="=>"differs from", - "="=>"equals", - ">"=>"greater (strict) than", - ">="=>"greater than", - "<"=>"less (strict) than", - "<="=>"less than", - "in"=>"in" - ); - } - public function GetBasicFilterLooseOperator() - { - // Unless we implement an "equals approximately..." or "same order of magnitude" - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '>': - return $this->GetSQLExpr()." > $sQValue"; - break; - case '>=': - return $this->GetSQLExpr()." >= $sQValue"; - break; - case '<': - return $this->GetSQLExpr()." < $sQValue"; - break; - case '<=': - return $this->GetSQLExpr()." <= $sQValue"; - break; - case 'in': - if (!is_array($value)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); - return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; - break; - - case '=': - default: - return $this->GetSQLExpr()." = \"$value\""; - } - } - - public function GetNullValue() - { - return null; - } - public function IsNull($proposedValue) - { - return is_null($proposedValue); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return null; - if ($proposedValue === '') return null; // 0 is transformed into '' ! - return (int)$proposedValue; - } - - public function ScalarToSQL($value) - { - assert(is_numeric($value) || is_null($value)); - return $value; // supposed to be an int - } -} - -/** - * An external key for which the class is defined as the value of another attribute - * - * @package iTopORM - */ -class AttributeObjectKey extends AttributeDBFieldVoid -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed')); - } - - public function GetEditClass() {return "String";} - protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");} - - public function GetDefaultValue(DBObject $oHostObject = null) {return 0;} - public function IsNullAllowed() - { - return $this->Get("is_null_allowed"); - } - - - public function GetBasicFilterOperators() - { - return parent::GetBasicFilterOperators(); - } - public function GetBasicFilterLooseOperator() - { - return parent::GetBasicFilterLooseOperator(); - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return parent::GetBasicFilterSQLExpr($sOpCode, $value); - } - - public function GetNullValue() - { - return 0; - } - - public function IsNull($proposedValue) - { - return ($proposedValue == 0); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return 0; - if ($proposedValue === '') return 0; - if (MetaModel::IsValidObject($proposedValue)) { - /** @var \DBObject $proposedValue */ - return $proposedValue->GetKey(); - } - return (int)$proposedValue; - } -} - -/** - * Display an integer between 0 and 100 as a percentage / horizontal bar graph - * - * @package iTopORM - */ -class AttributePercentage extends AttributeInteger -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - $iWidth = 5; // Total width of the percentage bar graph, in em... - $iValue = (int)$sValue; - if ($iValue > 100) - { - $iValue = 100; - } - else if ($iValue < 0) - { - $iValue = 0; - } - if ($iValue > 90) - { - $sColor = "#cc3300"; - } - else if ($iValue > 50) - { - $sColor = "#cccc00"; - } - else - { - $sColor = "#33cc00"; - } - $iPercentWidth = ($iWidth * $iValue) / 100; - return "
     
     $sValue %"; - } -} - -/** - * Map a decimal value column (suitable for financial computations) to an attribute - * internally in PHP such numbers are represented as string. Should you want to perform - * a calculation on them, it is recommended to use the BC Math functions in order to - * retain the precision - * - * @package iTopORM - */ -class AttributeDecimal extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */)); - } - - public function GetEditClass() {return "String";} - protected function GetSQLCol($bFullSpec = false) - { - return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")".($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetValidationPattern() - { - $iNbDigits = $this->Get('digits'); - $iPrecision = $this->Get('decimals'); - $iNbIntegerDigits = $iNbDigits - $iPrecision - 1; // -1 because the first digit is treated separately in the pattern below - return "^[-+]?[0-9]\d{0,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$"; - } - - public function GetBasicFilterOperators() - { - return array( - "!="=>"differs from", - "="=>"equals", - ">"=>"greater (strict) than", - ">="=>"greater than", - "<"=>"less (strict) than", - "<="=>"less than", - "in"=>"in" - ); - } - public function GetBasicFilterLooseOperator() - { - // Unless we implement an "equals approximately..." or "same order of magnitude" - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '>': - return $this->GetSQLExpr()." > $sQValue"; - break; - case '>=': - return $this->GetSQLExpr()." >= $sQValue"; - break; - case '<': - return $this->GetSQLExpr()." < $sQValue"; - break; - case '<=': - return $this->GetSQLExpr()." <= $sQValue"; - break; - case 'in': - if (!is_array($value)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); - return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; - break; - - case '=': - default: - return $this->GetSQLExpr()." = \"$value\""; - } - } - - public function GetNullValue() - { - return null; - } - public function IsNull($proposedValue) - { - return is_null($proposedValue); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return null; - if ($proposedValue === '') return null; - return (string)$proposedValue; - } - - public function ScalarToSQL($value) - { - assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value)); - return $value; // null or string - } -} - -/** - * Map a boolean column to an attribute - * - * @package iTopORM - */ -class AttributeBoolean extends AttributeInteger -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "Integer";} - protected function GetSQLCol($bFullSpec = false) {return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : '');} - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return null; - if ($proposedValue === '') return null; - if ((int)$proposedValue) return true; - return false; - } - - public function ScalarToSQL($value) - { - if ($value) return 1; - return 0; - } - - public function GetValueLabel($bValue) - { - if (is_null($bValue)) - { - $sLabel = Dict::S('Core:'.get_class($this).'/Value:null'); - } - else - { - $sValue = $bValue ? 'yes' : 'no'; - $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue); - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/); - } - return $sLabel; - } - - public function GetValueDescription($bValue) - { - if (is_null($bValue)) - { - $sDescription = Dict::S('Core:'.get_class($this).'/Value:null+'); - } - else - { - $sValue = $bValue ? 'yes' : 'no'; - $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+'); - $sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault, true /*user lang*/); - } - return $sDescription; - } - - public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true) - { - if (is_null($bValue)) - { - $sRes = ''; - } - elseif ($bLocalize) - { - $sLabel = $this->GetValueLabel($bValue); - $sDescription = $this->GetValueDescription($bValue); - // later, we could imagine a detailed description in the title - $sRes = "".parent::GetAsHtml($sLabel).""; - } - else - { - $sRes = $bValue ? 'yes' : 'no'; - } - return $sRes; - } - - public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true) - { - if (is_null($bValue)) - { - $sFinalValue = ''; - } - elseif ($bLocalize) - { - $sFinalValue = $this->GetValueLabel($bValue); - } - else - { - $sFinalValue = $bValue ? 'yes' : 'no'; - } - $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); - return $sRes; - } - - public function GetAsCSV($bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - if (is_null($bValue)) - { - $sFinalValue = ''; - } - elseif ($bLocalize) - { - $sFinalValue = $this->GetValueLabel($bValue); - } - else - { - $sFinalValue = $bValue ? 'yes' : 'no'; - } - $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); - return $sRes; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\SelectField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\SelectField $oFormField - * - * @return \Combodo\iTop\Form\Field\SelectField - * @throws \CoreException - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - $oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false))); - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - public function GetEditValue($value, $oHostObj = null) - { - if (is_null($value)) - { - return ''; - } - else - { - return $this->GetValueLabel($value); - } - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - * - * @param $value - * - * @return bool - */ - public function GetForJSON($value) - { - return (bool)$value; - } - - public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) - { - $sInput = strtolower(trim($sProposedValue)); - if ($bLocalizedValue) - { - switch ($sInput) - { - case '1': // backward compatibility - case $this->GetValueLabel(true): - $value = true; - break; - case '0': // backward compatibility - case 'no': - case $this->GetValueLabel(false): - $value = false; - break; - default: - $value = null; - } - } - else - { - switch ($sInput) - { - case '1': // backward compatibility - case 'yes': - $value = true; - break; - case '0': // backward compatibility - case 'no': - $value = false; - break; - default: - $value = null; - } - } - return $value; - } -} - -/** - * Map a varchar column (size < ?) to an attribute - * - * @package iTopORM - */ -class AttributeString extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "String";} - - protected function GetSQLCol($bFullSpec = false) - { - return 'VARCHAR(255)' - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetValidationPattern() - { - $sPattern = $this->GetOptional('validation_pattern', ''); - if (empty($sPattern)) - { - return parent::GetValidationPattern(); - } - else - { - return $sPattern; - } - } - - public function CheckFormat($value) - { - $sRegExp = $this->GetValidationPattern(); - if (empty($sRegExp)) - { - return true; - } - else - { - $sRegExp = str_replace('/', '\\/', $sRegExp); - return preg_match("/$sRegExp/", $value); - } - } - - public function GetMaxSize() - { - return 255; - } - - public function GetBasicFilterOperators() - { - return array( - "="=>"equals", - "!="=>"differs from", - "Like"=>"equals (no case)", - "NotLike"=>"differs from (no case)", - "Contains"=>"contains", - "Begins with"=>"begins with", - "Finishes with"=>"finishes with" - ); - } - public function GetBasicFilterLooseOperator() - { - return "Contains"; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '=': - case '!=': - return $this->GetSQLExpr()." $sOpCode $sQValue"; - case 'Begins with': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("$value%"); - case 'Finishes with': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value"); - case 'Contains': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); - case 'NotLike': - return $this->GetSQLExpr()." NOT LIKE $sQValue"; - case 'Like': - default: - return $this->GetSQLExpr()." LIKE $sQValue"; - } - } - - public function GetNullValue() - { - return ''; - } - - public function IsNull($proposedValue) - { - return ($proposedValue == ''); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return ''; - return (string)$proposedValue; - } - - public function ScalarToSQL($value) - { - if (!is_string($value) && !is_null($value)) - { - throw new CoreWarning('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetHostClass(), 'attribute' => $this->GetCode())); - } - return $value; - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); - return $sTextQualifier.$sEscaped.$sTextQualifier; - } - - public function GetDisplayStyle() - { - return $this->GetOptional('display_style', 'select'); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\StringField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - -} - -/** - * An attibute that matches an object class - * - * @package iTopORM - */ -class AttributeClass extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("class_category", "more_values")); - } - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']); - parent::__construct($sCode, $aParams); - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - $sDefault = parent::GetDefaultValue($oHostObject); - if (!$this->IsNullAllowed() && $this->IsNull($sDefault)) - { - // For this kind of attribute specifying null as default value - // is authorized even if null is not allowed - - // Pick the first one... - $aClasses = $this->GetAllowedValues(); - $sDefault = key($aClasses); - } - return $sDefault; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) return ''; - return MetaModel::GetName($sValue); - } - - public function RequiresIndex() - { - return true; - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } - -} - -/** - * An attibute that matches one of the language codes availables in the dictionnary - * - * @package iTopORM - */ -class AttributeApplicationLanguage extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - } - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $aAvailableLanguages = Dict::GetLanguages(); - $aLanguageCodes = array(); - foreach($aAvailableLanguages as $sLangCode => $aInfo) - { - $aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')'; - } - $aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes); - parent::__construct($sCode, $aParams); - } - - public function RequiresIndex() - { - return true; - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } -} - -/** - * The attribute dedicated to the finalclass automatic attribute - * - * @package iTopORM - */ -class AttributeFinalClass extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - public $m_sValue; - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $aParams["allowed_values"] = null; - parent::__construct($sCode, $aParams); - - $this->m_sValue = $this->Get("default_value"); - } - - public function IsWritable() - { - return false; - } - public function IsMagic() - { - return true; - } - - public function RequiresIndex() - { - return true; - } - - public function SetFixedValue($sValue) - { - $this->m_sValue = $sValue; - } - public function GetDefaultValue(DBObject $oHostObject = null) - { - return $this->m_sValue; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) return ''; - if ($bLocalize) - { - return MetaModel::GetName($sValue); - } - else - { - return $sValue; - } - } - - /** - * An enum can be localized - * - * @param string $sProposedValue - * @param bool $bLocalizedValue - * @param string $sSepItem - * @param string $sSepAttribute - * @param string $sSepValue - * @param string $sAttributeQualifier - * - * @return mixed|null|string - * @throws \CoreException - * @throws \OQLException - */ - public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) - { - if ($bLocalizedValue) - { - // Lookup for the value matching the input - // - $sFoundValue = null; - $aRawValues = self::GetAllowedValues(); - if (!is_null($aRawValues)) - { - foreach ($aRawValues as $sKey => $sValue) - { - if ($sProposedValue == $sValue) - { - $sFoundValue = $sKey; - break; - } - } - } - if (is_null($sFoundValue)) - { - return null; - } - return $this->MakeRealValue($sFoundValue, null); - } - else - { - return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); - } - } - - - // Because this is sometimes used to get a localized/string version of an attribute... - public function GetEditValue($sValue, $oHostObj = null) - { - if (empty($sValue)) return ''; - return MetaModel::GetName($sValue); - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - * - * @param $value - * - * @return string - */ - public function GetForJSON($value) - { - // JSON values are NOT localized - return $value; - } - - /** - * @param $value - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return string - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function GetAsCSV( - $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false - ) - { - if ($bLocalize && $value != '') - { - $sRawValue = MetaModel::GetName($value); - } - else - { - $sRawValue = $value; - } - return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText); - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - if (empty($value)) return ''; - if ($bLocalize) - { - $sRawValue = MetaModel::GetName($value); - } - else - { - $sRawValue = $value; - } - return Str::pure2xml($sRawValue); - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetValueLabel($sValue) - { - if (empty($sValue)) return ''; - return MetaModel::GetName($sValue); - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $aRawValues = MetaModel::EnumChildClasses($this->GetHostClass(), ENUM_CHILD_CLASSES_ALL); - $aLocalizedValues = array(); - foreach ($aRawValues as $sClass) - { - $aLocalizedValues[$sClass] = MetaModel::GetName($sClass); - } - return $aLocalizedValues; - } -} - - -/** - * Map a varchar column (size < ?) to an attribute that must never be shown to the user - * - * @package iTopORM - */ -class AttributePassword extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "Password";} - - protected function GetSQLCol($bFullSpec = false) - { - return "VARCHAR(64)" - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetMaxSize() - { - return 64; - } - - public function GetFilterDefinitions() - { - // Note: due to this, you will get an error if a password is being declared as a search criteria (see ZLists) - // not allowed to search on passwords! - return array(); - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (strlen($sValue) == 0) - { - return ''; - } - else - { - return '******'; - } - } - - public function IsPartOfFingerprint() { return false; } // Cannot reliably compare two encrypted passwords since the same password will be encrypted in diffferent manners depending on the random 'salt' -} - -/** - * Map a text column (size < 255) to an attribute that is encrypted in the database - * The encryption is based on a key set per iTop instance. Thus if you export your - * database (in SQL) to someone else without providing the key at the same time - * the encrypted fields will remain encrypted - * - * @package iTopORM - */ -class AttributeEncryptedString extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static $sKey = null; // Encryption key used for all encrypted fields - static $sLibrary = null; // Encryption library used for all encrypted fields - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - if (self::$sKey == null) - { - self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); - } - if(self::$sLibrary == null) - { - self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary(); - } - } - /** - * When the attribute definitions are stored in APC cache: - * 1) The static class variable $sKey is NOT serialized - * 2) The object's constructor is NOT called upon wakeup - * 3) mcrypt may crash the server if passed an empty key !! - * - * So let's restore the key (if needed) when waking up - **/ - public function __wakeup() - { - if (self::$sKey == null) - { - self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); - } - if(self::$sLibrary == null) - { - self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary(); - } - } - - - protected function GetSQLCol($bFullSpec = false) {return "TINYBLOB";} - - public function GetMaxSize() - { - return 255; - } - - public function GetFilterDefinitions() - { - // Note: due to this, you will get an error if a an encrypted field is declared as a search criteria (see ZLists) - // not allowed to search on encrypted fields ! - return array(); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return null; - return (string)$proposedValue; - } - - /** - * Decrypt the value when reading from the database - * - * @param array $aCols - * @param string $sPrefix - * - * @return string - * @throws \Exception - */ - public function FromSQLToValue($aCols, $sPrefix = '') - { - $oSimpleCrypt = new SimpleCrypt(self::$sLibrary); - $sValue = $oSimpleCrypt->Decrypt(self::$sKey, $aCols[$sPrefix]); - return $sValue; - } - - /** - * Encrypt the value before storing it in the database - * - * @param $value - * - * @return array - * @throws \Exception - */ - public function GetSQLValues($value) - { - $oSimpleCrypt = new SimpleCrypt(self::$sLibrary); - $encryptedValue = $oSimpleCrypt->Encrypt(self::$sKey, $value); - - $aValues = array(); - $aValues[$this->Get("sql")] = $encryptedValue; - return $aValues; - } -} - - -// Wiki formatting - experimental -// -// [[:]] -// Example: [[Server:db1.tnut.com]] -define('WIKI_OBJECT_REGEXP', '/\[\[(.+):(.+)\]\]/U'); - - -/** - * Map a text column (size > ?) to an attribute - * - * @package iTopORM - */ -class AttributeText extends AttributeString -{ - public function GetEditClass() {return ($this->GetFormat() == 'text') ? 'Text' : "HTML";} - - protected function GetSQLCol($bFullSpec = false) - { - return "TEXT".CMDBSource::GetSqlStringColumnDefinition(); - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->Get('sql')] = $this->GetSQLCol($bFullSpec); - if ($this->GetOptional('format', null) != null ) - { - // Add the extra column only if the property 'format' is specified for the attribute - $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')".CMDBSource::GetSqlStringColumnDefinition(); - if ($bFullSpec) - { - $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'text'"; // default 'text' is for migrating old records - } - } - return $aColumns; - } - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - $sPrefix = $this->Get('sql'); - } - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $sPrefix; - if ($this->GetOptional('format', null) != null ) - { - // Add the extra column only if the property 'format' is specified for the attribute - $aColumns['_format'] = $sPrefix.'_format'; - } - return $aColumns; - } - - public function GetMaxSize() - { - // Is there a way to know the current limitation for mysql? - // See mysql_field_len() - return 65535; - } - - static public function RenderWikiHtml($sText, $bWikiOnly = false) - { - if (!$bWikiOnly) - { - $sPattern = '/'.str_replace('/', '\/', utils::GetConfig()->Get('url_validation_pattern')).'/i'; - if (preg_match_all($sPattern, $sText, $aAllMatches, PREG_SET_ORDER /* important !*/ |PREG_OFFSET_CAPTURE /* important ! */)) - { - $i = count($aAllMatches); - // Replace the URLs by an actual hyperlink ... - // Let's do it backwards so that the initial positions are not modified by the replacement - // This works if the matches are captured: in the order they occur in the string AND - // with their offset (i.e. position) inside the string - while($i > 0) - { - $i--; - $sUrl = $aAllMatches[$i][0][0]; // String corresponding to the main pattern - $iPos = $aAllMatches[$i][0][1]; // Position of the main pattern - $sText = substr_replace($sText, "$sUrl", $iPos, strlen($sUrl)); - - } - } - } - if (preg_match_all(WIKI_OBJECT_REGEXP, $sText, $aAllMatches, PREG_SET_ORDER)) - { - foreach($aAllMatches as $iPos => $aMatches) - { - $sClass = trim($aMatches[1]); - $sName = trim($aMatches[2]); - - if (MetaModel::IsValidClass($sClass)) - { - $oObj = MetaModel::GetObjectByName($sClass, $sName, false /* MustBeFound */); - if (is_object($oObj)) - { - // Propose a std link to the object - $sText = str_replace($aMatches[0], $oObj->GetHyperlink(), $sText); - } - else - { - // Propose a std link to the object - $sClassLabel = MetaModel::GetName($sClass); - $sText = str_replace($aMatches[0], "$sClassLabel:$sName", $sText); - // Later: propose a link to create a new object - // Anyhow... there is no easy way to suggest default values based on the given FRIENDLY name - //$sText = preg_replace('/\[\[(.+):(.+)\]\]/', ''.$sName.'', $sText); - } - } - } - } - return $sText; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - $aStyles = array(); - if ($this->GetWidth() != '') - { - $aStyles[] = 'width:'.$this->GetWidth(); - } - if ($this->GetHeight() != '') - { - $aStyles[] = 'height:'.$this->GetHeight(); - } - $sStyle = ''; - if (count($aStyles) > 0) - { - $aStyles[] = 'overflow:auto'; - $sStyle = 'style="'.implode(';', $aStyles).'"'; - } - - if ($this->GetFormat() == 'text') - { - $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize); - $sValue = self::RenderWikiHtml($sValue); - return "
    ".str_replace("\n", "
    \n", $sValue).'
    '; - } - else - { - $sValue = self::RenderWikiHtml($sValue, true /* wiki only */); - return "
    ".InlineImage::FixUrls($sValue).'
    '; - } - - } - - public function GetEditValue($sValue, $oHostObj = null) - { - if ($this->GetFormat() == 'text') - { - if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) - { - foreach($aAllMatches as $iPos => $aMatches) - { - $sClass = $aMatches[1]; - $sName = $aMatches[2]; - - if (MetaModel::IsValidClass($sClass)) - { - $sClassLabel = MetaModel::GetName($sClass); - $sValue = str_replace($aMatches[0], "[[$sClassLabel:$sName]]", $sValue); - } - } - } - } - else - { - $sValue = str_replace('&', '&', $sValue); - } - return $sValue; - } - - /** - * For fields containing a potential markup, return the value without this markup - * - * @param string $sValue - * @param \DBObject $oHostObj - * - * @return string - */ - public function GetAsPlainText($sValue, $oHostObj = null) - { - if ($this->GetFormat() == 'html') - { - return (string) utils::HtmlToText($this->GetEditValue($sValue, $oHostObj)); - } - else - { - return parent::GetAsPlainText($sValue, $oHostObj); - } - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - $sValue = $proposedValue; - switch ($this->GetFormat()) - { - case 'html': - if (($sValue !== null) && ($sValue !== '')) - { - $sValue = HTMLSanitizer::Sanitize($sValue); - } - break; - - case 'text': - default: - if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) - { - foreach($aAllMatches as $iPos => $aMatches) - { - $sClassLabel = $aMatches[1]; - $sName = $aMatches[2]; - - if (!MetaModel::IsValidClass($sClassLabel)) - { - $sClass = MetaModel::GetClassFromLabel($sClassLabel); - if ($sClass) - { - $sValue = str_replace($aMatches[0], "[[$sClass:$sName]]", $sValue); - } - } - } - } - } - return $sValue; - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - return Str::pure2xml($value); - } - - public function GetWidth() - { - return $this->GetOptional('width', ''); - } - - public function GetHeight() - { - return $this->GetOptional('height', ''); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\TextAreaField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\TextAreaField $oFormField - * - * @return \Combodo\iTop\Form\Field\TextAreaField - * @throws \CoreException - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - /** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */ - $oFormField = new $sFormFieldClass($this->GetCode(), null, $oObject); - $oFormField->SetFormat($this->GetFormat()); - } - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - /** - * The actual formatting of the field: either text (=plain text) or html (= text with HTML markup) - * @return string - */ - public function GetFormat() - { - return $this->GetOptional('format', 'text'); - } - - /** - * Read the value from the row returned by the SQL query and transorms it to the appropriate - * internal format (either text or html) - * - * @see AttributeDBFieldVoid::FromSQLToValue() - * - * @param array $aCols - * @param string $sPrefix - * - * @return string - */ - public function FromSQLToValue($aCols, $sPrefix = '') - { - $value = $aCols[$sPrefix.'']; - if ($this->GetOptional('format', null) != null ) - { - // Read from the extra column only if the property 'format' is specified for the attribute - $sFormat = $aCols[$sPrefix.'_format']; - } - else - { - $sFormat = $this->GetFormat(); - } - - switch($sFormat) - { - case 'text': - if ($this->GetFormat() == 'html') - { - $value = utils::TextToHtml($value); - } - break; - - case 'html': - if ($this->GetFormat() == 'text') - { - $value = utils::HtmlToText($value); - } - else - { - $value = InlineImage::FixUrls((string)$value); - } - break; - - default: - // unknown format ?? - } - return $value; - } - - public function GetSQLValues($value) - { - $aValues = array(); - $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); - if ($this->GetOptional('format', null) != null ) - { - // Add the extra column only if the property 'format' is specified for the attribute - $aValues[$this->Get("sql").'_format'] = $this->GetFormat(); - } - return $aValues; - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - switch($this->GetFormat()) - { - case 'html': - if ($bConvertToPlainText) - { - $sValue = utils::HtmlToText((string)$sValue); - } - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); - return $sTextQualifier.$sEscaped.$sTextQualifier; - break; - - case 'text': - default: - return parent::GetAsCSV($sValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, $bConvertToPlainText); - } - } -} - -/** - * Map a log to an attribute - * - * @package iTopORM - */ -class AttributeLongText extends AttributeText -{ - protected function GetSQLCol($bFullSpec = false) - { - return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); - } - - public function GetMaxSize() - { - // Is there a way to know the current limitation for mysql? - // See mysql_field_len() - return 65535*1024; // Limited... still 64 Mb! - } -} - -/** - * An attibute that stores a case log (i.e journal) - * - * @package iTopORM - */ -class AttributeCaseLog extends AttributeLongText -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - public function GetNullValue() - { - return ''; - } - - public function IsNull($proposedValue) - { - if (!($proposedValue instanceof ormCaseLog)) - { - return ($proposedValue == ''); - } - return ($proposedValue->GetText() == ''); - } - - public function ScalarToSQL($value) - { - if (!is_string($value) && !is_null($value)) - { - throw new CoreWarning('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetCode(), 'attribute' => $this->GetHostClass())); - } - return $value; - } - public function GetEditClass() {return "CaseLog";} - - public function GetEditValue($sValue, $oHostObj = null) - { - if (!($sValue instanceOf ormCaseLog)) - { - return ''; - } - return $sValue->GetModifiedEntry(); - } - - /** - * For fields containing a potential markup, return the value without this markup - * - * @param mixed $value - * @param \DBObject $oHostObj - * - * @return string - */ - public function GetAsPlainText($value, $oHostObj = null) - { - if ($value instanceOf ormCaseLog) - { - /** ormCaseLog $value */ - return $value->GetAsPlainText(); - } - else - { - return (string) $value; - } - } - - public function GetDefaultValue(DBObject $oHostObject = null) {return new ormCaseLog();} - public function Equals($val1, $val2) {return ($val1->GetText() == $val2->GetText());} - - - /** - * Facilitate things: allow the user to Set the value from a string - * - * @param $proposedValue - * @param \DBObject $oHostObj - * - * @return mixed|null|\ormCaseLog|string - * @throws \Exception - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - if ($proposedValue instanceof ormCaseLog) - { - // Passthrough - $ret = clone $proposedValue; - } - else - { - // Append the new value if an instance of the object is supplied - // - $oPreviousLog = null; - if ($oHostObj != null) - { - $oPreviousLog = $oHostObj->Get($this->GetCode()); - if (!is_object($oPreviousLog)) - { - $oPreviousLog = $oHostObj->GetOriginal($this->GetCode());; - } - - } - if (is_object($oPreviousLog)) - { - $oCaseLog = clone($oPreviousLog); - } - else - { - $oCaseLog = new ormCaseLog(); - } - - if ($proposedValue instanceof stdClass) - { - $oCaseLog->AddLogEntryFromJSON($proposedValue); - } - else - { - if (strlen($proposedValue) > 0) - { - $oCaseLog->AddLogEntry($proposedValue); - } - } - $ret = $oCaseLog; - } - return $ret; - } - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - $sPrefix = $this->Get('sql'); - } - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $sPrefix; - $aColumns['_index'] = $sPrefix.'_index'; - return $aColumns; - } - - /** - * @param array $aCols - * @param string $sPrefix - * - * @return \ormCaseLog - * @throws \MissingColumnException - */ - public function FromSQLToValue($aCols, $sPrefix = '') - { - if (!array_key_exists($sPrefix, $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); - } - $sLog = $aCols[$sPrefix]; - - if (isset($aCols[$sPrefix.'_index'])) - { - $sIndex = $aCols[$sPrefix.'_index']; - } - else - { - // For backward compatibility, allow the current state to be: 1 log, no index - $sIndex = ''; - } - - if (strlen($sIndex) > 0) - { - $aIndex = unserialize($sIndex); - $value = new ormCaseLog($sLog, $aIndex); - } - else - { - $value = new ormCaseLog($sLog); - } - return $value; - } - - public function GetSQLValues($value) - { - if (!($value instanceOf ormCaseLog)) - { - $value = new ormCaseLog(''); - } - $aValues = array(); - $aValues[$this->GetCode()] = $value->GetText(); - $aValues[$this->GetCode().'_index'] = serialize($value->GetIndex()); - - return $aValues; - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->GetCode()] = 'LONGTEXT' // 2^32 (4 Gb) - .CMDBSource::GetSqlStringColumnDefinition(); - $aColumns[$this->GetCode().'_index'] = 'BLOB'; - return $aColumns; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - if ($value instanceOf ormCaseLog) - { - $sContent = $value->GetAsHTML(null, false, array(__class__, 'RenderWikiHtml')); - } - else - { - $sContent = ''; - } - $aStyles = array(); - if ($this->GetWidth() != '') - { - $aStyles[] = 'width:'.$this->GetWidth(); - } - if ($this->GetHeight() != '') - { - $aStyles[] = 'height:'.$this->GetHeight(); - } - $sStyle = ''; - if (count($aStyles) > 0) - { - $sStyle = 'style="'.implode(';', $aStyles).'"'; - } - return "
    ".$sContent.'
    '; } - - - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - if ($value instanceOf ormCaseLog) - { - return parent::GetAsCSV($value->GetText($bConvertToPlainText), $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, $bConvertToPlainText); - } - else - { - return ''; - } - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - if ($value instanceOf ormCaseLog) - { - return parent::GetAsXML($value->GetText(), $oHostObject, $bLocalize); - } - else - { - return ''; - } - } - - /** - * List the available verbs for 'GetForTemplate' - */ - public function EnumTemplateVerbs() - { - return array( - '' => 'Plain text representation of all the log entries', - 'head' => 'Plain text representation of the latest entry', - 'head_html' => 'HTML representation of the latest entry', - 'html' => 'HTML representation of all the log entries', - ); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * - * @param $value mixed The current value of the field - * @param $sVerb string The verb specifying the representation of the value - * @param $oHostObject DBObject The object - * @param $bLocalize bool Whether or not to localize the value - * - * @return mixed - * @throws \Exception - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - switch($sVerb) - { - case '': - return $value->GetText(true); - - case 'head': - return $value->GetLatestEntry('text'); - - case 'head_html': - return $value->GetLatestEntry('html'); - - case 'html': - return $value->GetAsEmailHtml(); - - default: - throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); - } - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - */ - public function GetForJSON($value) - { - return $value->GetForJSON(); - } - - /** - * Helper to form a value, given JSON decoded data - * The operation is the opposite to GetForJSON - */ - public function FromJSONToValue($json) - { - if (is_string($json)) - { - // Will be correctly handled in MakeRealValue - $ret = $json; - } - else - { - if (isset($json->add_item)) - { - // Will be correctly handled in MakeRealValue - $ret = $json->add_item; - if (!isset($ret->message)) - { - throw new Exception("Missing mandatory entry: 'message'"); - } - } - else - { - $ret = ormCaseLog::FromJSON($json); - } - } - return $ret; - } - - public function Fingerprint($value) - { - $sFingerprint = ''; - if ($value instanceOf ormCaseLog) - { - $sFingerprint = $value->GetText(); - } - return $sFingerprint; - } - - /** - * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) - * @return string - */ - public function GetFormat() - { - return $this->GetOptional('format', 'html'); // default format for case logs is now HTML - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\CaseLogField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - // First we call the parent so the field is build - $oFormField = parent::MakeFormField($oObject, $oFormField); - // Then only we set the value - $oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode()))); - // And we set the entries - $oFormField->SetEntries($oObject->Get($this->GetCode())->GetAsArray()); - - return $oFormField; - } -} - -/** - * Map a text column (size > ?), containing HTML code, to an attribute - * - * @package iTopORM - */ -class AttributeHTML extends AttributeLongText -{ - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->Get('sql')] = $this->GetSQLCol(); - if ($this->GetOptional('format', null) != null ) - { - // Add the extra column only if the property 'format' is specified for the attribute - $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; - if ($bFullSpec) - { - $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'html'"; // default 'html' is for migrating old records - } - } - return $aColumns; - } - - /** - * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) - * @return string - */ - public function GetFormat() - { - return $this->GetOptional('format', 'html'); // Defaults to HTML - } -} - -/** - * Specialization of a string: email - * - * @package iTopORM - */ -class AttributeEmailAddress extends AttributeString -{ - public function GetValidationPattern() - { - return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('email_validation_pattern').'$'); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\EmailField'; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) return ''; - - $sUrlDecorationClass = utils::GetConfig()->Get('email_decoration_class'); - - return ''.parent::GetAsHTML($sValue).''; - } -} - -/** - * Specialization of a string: IP address - * - * @package iTopORM - */ -class AttributeIPAddress extends AttributeString -{ - public function GetValidationPattern() - { - $sNum = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])'; - return "^($sNum\\.$sNum\\.$sNum\\.$sNum)$"; - } - - public function GetOrderBySQLExpressions($sClassAlias) - { - // Note: This is the responsibility of this function to place backticks around column aliases - return array('INET_ATON(`'.$sClassAlias.$this->GetCode().'`)'); - } -} - -/** - * Specialization of a string: phone number - * - * @package iTopORM - */ -class AttributePhoneNumber extends AttributeString -{ - public function GetValidationPattern() - { - return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('phone_number_validation_pattern').'$'); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\PhoneField'; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) return ''; - - $sUrlDecorationClass = utils::GetConfig()->Get('phone_number_decoration_class'); - $sUrlPattern = utils::GetConfig()->Get('phone_number_url_pattern'); - $sUrl = sprintf($sUrlPattern, $sValue); - - return ''.parent::GetAsHTML($sValue).''; - } -} - -/** - * Specialization of a string: OQL expression - * - * @package iTopORM - */ -class AttributeOQL extends AttributeText -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - public function GetEditClass() {return "OQLExpression";} -} - -/** - * Specialization of a string: template (contains iTop placeholders like $current_contact_id$ or $this->name$) - * - * @package iTopORM - */ -class AttributeTemplateString extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; -} - -/** - * Specialization of a text: template (contains iTop placeholders like $current_contact_id$ or $this->name$) - * - * @package iTopORM - */ -class AttributeTemplateText extends AttributeText -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; -} - -/** - * Specialization of a HTML: template (contains iTop placeholders like $current_contact_id$ or $this->name$) - * - * @package iTopORM - */ -class AttributeTemplateHTML extends AttributeText -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->Get('sql')] = $this->GetSQLCol(); - if ($this->GetOptional('format', null) != null ) - { - // Add the extra column only if the property 'format' is specified for the attribute - $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; - if ($bFullSpec) - { - $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'html'"; // default 'html' is for migrating old records - } - } - return $aColumns; - } - - /** - * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) - * @return string - */ - public function GetFormat() - { - return $this->GetOptional('format', 'html'); // Defaults to HTML - } -} - - -/** - * Map a enum column to an attribute - * - * @package iTopORM - */ -class AttributeEnum extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "String";} - protected function GetSQLCol($bFullSpec = false) - { - $oValDef = $this->GetValuesDef(); - if ($oValDef) - { - $aValues = CMDBSource::Quote(array_keys($oValDef->GetValues(array(), "")), true); - } - else - { - $aValues = array(); - } - if (count($aValues) > 0) - { - // The syntax used here do matters - // In particular, I had to remove unnecessary spaces to - // make sure that this string will match the field type returned by the DB - // (used to perform a comparison between the current DB format and the data model) - return "ENUM(".implode(",", $aValues).")" - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - else - { - return "VARCHAR(255)" - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? " DEFAULT ''" : ""); // ENUM() is not an allowed syntax! - } - } - - protected function GetSQLColSpec() - { - $default = $this->ScalarToSQL($this->GetDefaultValue()); - if (is_null($default)) - { - $sRet = ''; - } - else - { - // ENUMs values are strings so the default value must be a string as well, - // otherwise MySQL interprets the number as the zero-based index of the value in the list (i.e. the nth value in the list) - $sRet = " DEFAULT ".CMDBSource::Quote($default); - } - return $sRet; - } - - public function ScalarToSQL($value) - { - // Note: for strings, the null value is an empty string and it is recorded as such in the DB - // but that wasn't working for enums, because '' is NOT one of the allowed values - // that's why a null value must be forced to a real null - $value = parent::ScalarToSQL($value); - if ($this->IsNull($value)) - { - return null; - } - else - { - return $value; - } - } - - public function RequiresIndex() - { - return false; - } - - public function GetBasicFilterOperators() - { - return parent::GetBasicFilterOperators(); - } - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return parent::GetBasicFilterSQLExpr($sOpCode, $value); - } - - public function GetValueLabel($sValue) - { - if (is_null($sValue)) - { - // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label - $sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, Dict::S('Enum:Undefined')); - } - else - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, null, true /*user lang*/); - if (is_null($sLabel)) - { - $sDefault = str_replace('_', ' ', $sValue); - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, false); - } - } - return $sLabel; - } - - public function GetValueDescription($sValue) - { - if (is_null($sValue)) - { - // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label - $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', Dict::S('Enum:Undefined')); - } - else - { - $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', '', true /* user language only */); - if (strlen($sDescription) == 0) - { - $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); - if ($sParentClass) - { - if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) - { - $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); - $sDescription = $oAttDef->GetValueDescription($sValue); - } - } - } - } - return $sDescription; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if ($bLocalize) - { - $sLabel = $this->GetValueLabel($sValue); - $sDescription = $this->GetValueDescription($sValue); - // later, we could imagine a detailed description in the title - $sRes = "".parent::GetAsHtml($sLabel).""; - } - else - { - $sRes = parent::GetAsHtml($sValue, $oHostObject, $bLocalize); - } - return $sRes; - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - if (is_null($value)) - { - $sFinalValue = ''; - } - elseif ($bLocalize) - { - $sFinalValue = $this->GetValueLabel($value); - } - else - { - $sFinalValue = $value; - } - $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); - return $sRes; - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - if (is_null($sValue)) - { - $sFinalValue = ''; - } - elseif ($bLocalize) - { - $sFinalValue = $this->GetValueLabel($sValue); - } - else - { - $sFinalValue = $sValue; - } - $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); - return $sRes; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\SelectField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - // TODO : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - $oFormField->SetChoices($this->GetAllowedValues($oObject->ToArgsForQuery())); - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - public function GetEditValue($sValue, $oHostObj = null) - { - if (is_null($sValue)) - { - return ''; - } - else - { - return $this->GetValueLabel($sValue); - } - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - */ - public function GetForJSON($value) - { - return $value; - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $aRawValues = parent::GetAllowedValues($aArgs, $sContains); - if (is_null($aRawValues)) return null; - $aLocalizedValues = array(); - foreach ($aRawValues as $sKey => $sValue) - { - $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey); - } - return $aLocalizedValues; - } - - public function GetMaxSize() - { - return null; - } - - /** - * An enum can be localized - */ - public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) - { - if ($bLocalizedValue) - { - // Lookup for the value matching the input - // - $sFoundValue = null; - $aRawValues = parent::GetAllowedValues(); - if (!is_null($aRawValues)) - { - foreach ($aRawValues as $sKey => $sValue) - { - $sRefValue = $this->GetValueLabel($sKey); - if ($sProposedValue == $sRefValue) - { - $sFoundValue = $sKey; - break; - } - } - } - if (is_null($sFoundValue)) - { - return null; - } - return $this->MakeRealValue($sFoundValue, null); - } - else - { - return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); - } - } - - /** - * Processes the input value to align it with the values supported - * by this type of attribute. In this case: turns empty strings into nulls - * @param mixed $proposedValue The value to be set for the attribute - * @return mixed The actual value that will be set - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - if ($proposedValue == '') return null; - return parent::MakeRealValue($proposedValue, $oHostObj); - } - - public function GetOrderByHint() - { - $aValues = $this->GetAllowedValues(); - - return Dict::Format('UI:OrderByHint_Values', implode(', ', $aValues)); - } -} - -/** - * A meta enum is an aggregation of enum from subclasses into an enum of a base class - * It has been designed is to cope with the fact that statuses must be defined in leaf classes, while it makes sense to - * have a superstatus available on the root classe(s) - * - * @package iTopORM - */ -class AttributeMetaEnum extends AttributeEnum -{ - static public function ListExpectedParams() - { - return array('allowed_values', 'sql', 'default_value', 'mapping'); - } - - public function IsNullAllowed() - { - return false; // Well... this actually depends on the mapping - } - - public function IsWritable() - { - return false; - } - - public function RequiresIndex() - { - return true; - } - - public function GetPrerequisiteAttributes($sClass = null) - { - if (is_null($sClass)) - { - $sClass = $this->GetHostClass(); - } - $aMappingData = $this->GetMapRule($sClass); - if ($aMappingData == null) - { - $aRet = array(); - } - else - { - $aRet = array($aMappingData['attcode']); - } - return $aRet; - } - - /** - * Overload the standard so as to leave the data unsorted - * - * @param array $aArgs - * @param string $sContains - * @return array|null - */ - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $oValSetDef = $this->GetValuesDef(); - if (!$oValSetDef) return null; - $aRawValues = $oValSetDef->GetValueList(); - - if (is_null($aRawValues)) return null; - $aLocalizedValues = array(); - foreach ($aRawValues as $sKey => $sValue) - { - $aLocalizedValues[$sKey] = Str::pure2html($this->GetValueLabel($sKey)); - } - return $aLocalizedValues; - } - - /** - * Returns the meta value for the given object. - * See also MetaModel::RebuildMetaEnums() that must be maintained when MapValue changes - * - * @param $oObject - * @return mixed - * @throws Exception - */ - public function MapValue($oObject) - { - $aMappingData = $this->GetMapRule(get_class($oObject)); - if ($aMappingData == null) - { - $sRet = $this->GetDefaultValue(); - } - else - { - $sAttCode = $aMappingData['attcode']; - $value = $oObject->Get($sAttCode); - if (array_key_exists($value, $aMappingData['values'])) - { - $sRet = $aMappingData['values'][$value]; - } - elseif ($this->GetDefaultValue() != '') - { - $sRet = $this->GetDefaultValue(); - } - else - { - throw new Exception('AttributeMetaEnum::MapValue(): mapping not found for value "'.$value.'" in '.get_class($oObject).', on attribute '.MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()).'::'.$this->GetCode()); - } - } - return $sRet; - } - - public function GetMapRule($sClass) - { - $aMappings = $this->Get('mapping'); - if (array_key_exists($sClass, $aMappings)) - { - $aMappingData = $aMappings[$sClass]; - } - else - { - $sParent = MetaModel::GetParentClass($sClass); - if (is_null($sParent)) - { - $aMappingData = null; - } - else - { - $aMappingData = $this->GetMapRule($sParent); - } - } - - return $aMappingData; - } -} -/** - * Map a date+time column to an attribute - * - * @package iTopORM - */ -class AttributeDateTime extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE_TIME; - - static $oFormat = null; - - /** - * - * @return DateTimeFormat - */ - static public function GetFormat() - { - if (self::$oFormat == null) - { - static::LoadFormatFromConfig(); - } - return self::$oFormat; - } - - /** - * Load the 3 settings: date format, time format and data_time format from the configuration - */ - protected static function LoadFormatFromConfig() - { - $aFormats = MetaModel::GetConfig()->Get('date_and_time_format'); - $sLang = Dict::GetUserLanguage(); - $sDateFormat = isset($aFormats[$sLang]['date']) ? $aFormats[$sLang]['date'] : (isset($aFormats['default']['date']) ? $aFormats['default']['date'] : 'Y-m-d'); - $sTimeFormat = isset($aFormats[$sLang]['time']) ? $aFormats[$sLang]['time'] : (isset($aFormats['default']['time']) ? $aFormats['default']['time'] : 'H:i:s'); - $sDateAndTimeFormat = isset($aFormats[$sLang]['date_time']) ? $aFormats[$sLang]['date_time'] : (isset($aFormats['default']['date_time']) ? $aFormats['default']['date_time'] : '$date $time'); - - $sFullFormat = str_replace(array('$date', '$time'), array($sDateFormat, $sTimeFormat), $sDateAndTimeFormat); - - self::SetFormat(new DateTimeFormat($sFullFormat)); - AttributeDate::SetFormat(new DateTimeFormat($sDateFormat)); - } - - /** - * Returns the format string used for the date & time stored in memory - * @return string - */ - static public function GetInternalFormat() - { - return 'Y-m-d H:i:s'; - } - - /** - * Returns the format string used for the date & time written to MySQL - * @return string - */ - static public function GetSQLFormat() - { - return 'Y-m-d H:i:s'; - } - - static public function SetFormat(DateTimeFormat $oDateTimeFormat) - { - self::$oFormat = $oDateTimeFormat; - } - - static public function GetSQLTimeFormat() - { - return 'H:i:s'; - } - - /** - * Parses a search string coming from user input - * @param string $sSearchString - * @return string - */ - public function ParseSearchString($sSearchString) - { - try - { - $oDateTime = $this->GetFormat()->Parse($sSearchString); - $sSearchString = $oDateTime->format($this->GetInternalFormat()); - } - catch(Exception $e) - { - $sFormatString = '!'.(string)AttributeDate::GetFormat(); // BEWARE: ! is needed to set non-parsed fields to zero !!! - $oDateTime = DateTime::createFromFormat($sFormatString, $sSearchString); - if ($oDateTime !== false) - { - $sSearchString = $oDateTime->format($this->GetInternalFormat()); - } - } - return $sSearchString; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\DateTimeField'; - } - - /** - * Override to specify Field class - * - * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behave more like a Prepare. - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - $oFormField->SetPHPDateTimeFormat((string) $this->GetFormat()); - $oFormField->SetJSDateTimeFormat($this->GetFormat()->ToMomentJS()); - - $oFormField = parent::MakeFormField($oObject, $oFormField); - - // After call to the parent as it sets the current value - $oFormField->SetCurrentValue($this->GetFormat()->Format($oObject->Get($this->GetCode()))); - - return $oFormField; - } - - /** - * @inheritdoc - */ - public function EnumTemplateVerbs() - { - return array( - '' => 'Formatted representation', - 'raw' => 'Not formatted representation', - ); - } - - /** - * @inheritdoc - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - switch ($sVerb) - { - case '': - case 'text': - return static::GetFormat()->format($value); - break; - case 'html': - // Note: Not passing formatted value as the method will format it. - return $this->GetAsHTML($value); - break; - case 'raw': - return $value; - break; - default: - return parent::GetForTemplate($value, $sVerb, $oHostObject, $bLocalize); - break; - } - } - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "DateTime";} - - - public function GetEditValue($sValue, $oHostObj = null) - { - return (string)static::GetFormat()->format($sValue); - } - public function GetValueLabel($sValue, $oHostObj = null) - { - return (string)static::GetFormat()->format($sValue); - } - - protected function GetSQLCol($bFullSpec = false) {return "DATETIME";} - - public function GetImportColumns() - { - // Allow an empty string to be a valid value (synonym for "reset") - $aColumns = array(); - $aColumns[$this->GetCode()] = 'VARCHAR(19)'; - return $aColumns; - } - - public static function GetAsUnixSeconds($value) - { - $oDeadlineDateTime = new DateTime($value); - $iUnixSeconds = $oDeadlineDateTime->format('U'); - return $iUnixSeconds; - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - // null value will be replaced by the current date, if not already set, in DoComputeValues - return $this->GetNullValue(); - } - - public function GetValidationPattern() - { - return static::GetFormat()->ToRegExpr(); - } - - public function GetBasicFilterOperators() - { - return array( - "="=>"equals", - "!="=>"differs from", - "<"=>"before", - "<="=>"before", - ">"=>"after (strictly)", - ">="=>"after", - "SameDay"=>"same day (strip time)", - "SameMonth"=>"same year/month", - "SameYear"=>"same year", - "Today"=>"today", - ">|"=>"after today + N days", - "<|"=>"before today + N days", - "=|"=>"equals today + N days", - ); - } - public function GetBasicFilterLooseOperator() - { - // Unless we implement a "same xxx, depending on given precision" ! - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - - switch ($sOpCode) - { - case '=': - case '!=': - case '<': - case '<=': - case '>': - case '>=': - return $this->GetSQLExpr()." $sOpCode $sQValue"; - case 'SameDay': - return "DATE(".$this->GetSQLExpr().") = DATE($sQValue)"; - case 'SameMonth': - return "DATE_FORMAT(".$this->GetSQLExpr().", '%Y-%m') = DATE_FORMAT($sQValue, '%Y-%m')"; - case 'SameYear': - return "MONTH(".$this->GetSQLExpr().") = MONTH($sQValue)"; - case 'Today': - return "DATE(".$this->GetSQLExpr().") = CURRENT_DATE()"; - case '>|': - return "DATE(".$this->GetSQLExpr().") > DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; - case '<|': - return "DATE(".$this->GetSQLExpr().") < DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; - case '=|': - return "DATE(".$this->GetSQLExpr().") = DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; - default: - return $this->GetSQLExpr()." = $sQValue"; - } - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return null; - } - if (is_string($proposedValue) && ($proposedValue == "") && $this->IsNullAllowed()) - { - return null; - } - if (!is_numeric($proposedValue)) - { - // Check the format - try - { - $oFormat = new DateTimeFormat($this->GetInternalFormat()); - $oFormat->Parse($proposedValue); - } - catch (Exception $e) - { - throw new Exception('Wrong format for date attribute '.$this->GetCode().', expecting "'.$this->GetInternalFormat().'" and got "'.$proposedValue.'"'); - } - - return $proposedValue; - } - - return date(static::GetInternalFormat(), $proposedValue); - } - - public function ScalarToSQL($value) - { - if (empty($value)) - { - return null; - } - return $value; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - return Str::pure2html(static::GetFormat()->format($value)); - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - return Str::pure2xml($value); - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - if (empty($sValue) || ($sValue === '0000-00-00 00:00:00') || ($sValue === '0000-00-00')) - { - return ''; - } - else if ((string)static::GetFormat() !== static::GetInternalFormat()) - { - // Format conversion - $oDate = new DateTime($sValue); - if ($oDate !== false) - { - $sValue = static::GetFormat()->format($oDate); - } - } - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); - return $sTextQualifier.$sEscaped.$sTextQualifier; - } - - /** - * Parses a string to find some smart search patterns and build the corresponding search/OQL condition - * Each derived class is reponsible for defining and processing their own smart patterns, the base class - * does nothing special, and just calls the default (loose) operator - * - * @param string $sSearchText The search string to analyze for smart patterns - * @param FieldExpression $oField The FieldExpression representing the atttribute code in this OQL query - * @param array $aParams Values of the query parameters - * @param bool $bParseSearchString - * - * @return Expression The search condition to be added (AND) to the current search - * @throws \CoreException - */ - public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams, $bParseSearchString = false) - { - // Possible smart patterns - $aPatterns = array( - 'between' => array('pattern' => '/^\[(.*),(.*)\]$/', 'operator' => 'n/a'), - 'greater than or equal' => array('pattern' => '/^>=(.*)$/', 'operator' => '>='), - 'greater than' => array('pattern' => '/^>(.*)$/', 'operator' => '>'), - 'less than or equal' => array('pattern' => '/^<=(.*)$/', 'operator' => '<='), - 'less than' => array('pattern' => '/^<(.*)$/', 'operator' => '<'), - ); - - $sPatternFound = ''; - $aMatches = array(); - foreach($aPatterns as $sPatName => $sPattern) - { - if (preg_match($sPattern['pattern'], $sSearchText, $aMatches)) - { - $sPatternFound = $sPatName; - break; - } - } - - switch($sPatternFound) - { - case 'between': - - $sParamName1 = $oField->GetParent().'_'.$oField->GetName().'_1'; - $oRightExpr = new VariableExpression($sParamName1); - if ($bParseSearchString) - { - $aParams[$sParamName1] = $this->ParseSearchString($aMatches[1]); - } - else - { - $aParams[$sParamName1] = $aMatches[1]; - } - $oCondition1 = new BinaryExpression($oField, '>=', $oRightExpr); - - $sParamName2 = $oField->GetParent().'_'.$oField->GetName().'_2'; - $oRightExpr = new VariableExpression($sParamName2); - if ($bParseSearchString) - { - $aParams[$sParamName2] = $this->ParseSearchString($aMatches[2]); - } - else - { - $aParams[$sParamName2] = $aMatches[2]; - } - $oCondition2 = new BinaryExpression($oField, '<=', $oRightExpr); - - $oNewCondition = new BinaryExpression($oCondition1, 'AND', $oCondition2); - break; - - case 'greater than': - case 'greater than or equal': - case 'less than': - case 'less than or equal': - $sSQLOperator = $aPatterns[$sPatternFound]['operator']; - $sParamName = $oField->GetParent().'_'.$oField->GetName(); - $oRightExpr = new VariableExpression($sParamName); - if ($bParseSearchString) - { - $aParams[$sParamName] = $this->ParseSearchString($aMatches[1]); - } - else - { - $aParams[$sParamName] = $aMatches[1]; - } - $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); - - break; - - default: - $oNewCondition = parent::GetSmartConditionExpression($sSearchText, $oField, $aParams); - - } - - return $oNewCondition; - } - - - public function GetHelpOnSmartSearch() - { - $sDict = parent::GetHelpOnSmartSearch(); - - $oFormat = static::GetFormat(); - $sExample = $oFormat->Format(new DateTime('2015-07-19 18:40:00')); - return vsprintf($sDict, array($oFormat->ToPlaceholder(), $sExample)); - } -} - -/** - * Store a duration as a number of seconds - * - * @package iTopORM - */ -class AttributeDuration extends AttributeInteger -{ - public function GetEditClass() {return "Duration";} - protected function GetSQLCol($bFullSpec = false) {return "INT(11) UNSIGNED";} - - public function GetNullValue() {return '0';} - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return null; - if (!is_numeric($proposedValue)) return null; - if ( ((int)$proposedValue) < 0) return null; - - return (int)$proposedValue; - } - - public function ScalarToSQL($value) - { - if (is_null($value)) - { - return null; - } - return $value; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - return Str::pure2html(self::FormatDuration($value)); - } - - public static function FormatDuration($duration) - { - $aDuration = self::SplitDuration($duration); - - if ($duration < 60) - { - // Less than 1 min - $sResult = Dict::Format('Core:Duration_Seconds', $aDuration['seconds']); - } - else if ($duration < 3600) - { - // less than 1 hour, display it in minutes/seconds - $sResult = Dict::Format('Core:Duration_Minutes_Seconds', $aDuration['minutes'], $aDuration['seconds']); - } - else if ($duration < 86400) - { - // Less than 1 day, display it in hours/minutes/seconds - $sResult = Dict::Format('Core:Duration_Hours_Minutes_Seconds', $aDuration['hours'], $aDuration['minutes'], $aDuration['seconds']); - } - else - { - // more than 1 day, display it in days/hours/minutes/seconds - $sResult = Dict::Format('Core:Duration_Days_Hours_Minutes_Seconds', $aDuration['days'], $aDuration['hours'], $aDuration['minutes'], $aDuration['seconds']); - } - return $sResult; - } - - static function SplitDuration($duration) - { - $duration = (int) $duration; - $days = floor($duration / 86400); - $hours = floor(($duration - (86400*$days)) / 3600); - $minutes = floor(($duration - (86400*$days + 3600*$hours)) / 60); - $seconds = ($duration % 60); // modulo - return array( 'days' => $days, 'hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds ); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\DurationField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - parent::MakeFormField($oObject, $oFormField); - - // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition - $sAttCode = $this->GetCode(); - $oFormField->SetCurrentValue($oObject->Get($sAttCode)); - $oFormField->SetReadOnly(true); - - return $oFormField; - } - -} -/** - * Map a date+time column to an attribute - * - * @package iTopORM - */ -class AttributeDate extends AttributeDateTime -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE; - - static $oDateFormat = null; - - static public function GetFormat() - { - if (self::$oDateFormat == null) - { - AttributeDateTime::LoadFormatFromConfig(); - } - return self::$oDateFormat; - } - - static public function SetFormat(DateTimeFormat $oDateFormat) - { - self::$oDateFormat = $oDateFormat; - } - - /** - * Returns the format string used for the date & time stored in memory - * @return string - */ - static public function GetInternalFormat() - { - return 'Y-m-d'; - } - - /** - * Returns the format string used for the date & time written to MySQL - * @return string - */ - static public function GetSQLFormat() - { - return 'Y-m-d'; - } - - static public function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() {return "Date";} - protected function GetSQLCol($bFullSpec = false) {return "DATE";} - public function GetImportColumns() - { - // Allow an empty string to be a valid value (synonym for "reset") - $aColumns = array(); - $aColumns[$this->GetCode()] = 'VARCHAR(10)'; - return $aColumns; - } - - - /** - * Override to specify Field class - * - * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behave more like a Prepare. - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - $oFormField = parent::MakeFormField($oObject, $oFormField); - $oFormField->SetDateOnly(true); - - return $oFormField; - } - -} - -/** - * A dead line stored as a date & time - * The only difference with the DateTime attribute is the display: - * relative to the current time - */ -class AttributeDeadline extends AttributeDateTime -{ - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - $sResult = self::FormatDeadline($value); - return $sResult; - } - - public static function FormatDeadline($value) - { - $sResult = ''; - if ($value !== null) - { - $iValue = AttributeDateTime::GetAsUnixSeconds($value); - $sDate = AttributeDateTime::GetFormat()->Format($value); - $difference = $iValue - time(); - - if ($difference >= 0) - { - $sDifference = self::FormatDuration($difference); - } - else - { - $sDifference = Dict::Format('UI:DeadlineMissedBy_duration', self::FormatDuration(-$difference)); - } - $sFormat = MetaModel::GetConfig()->Get('deadline_format'); - $sResult = str_replace(array('$date$', '$difference$'), array($sDate, $sDifference), $sFormat); - } - - return $sResult; - } - - static function FormatDuration($duration) - { - $days = floor($duration / 86400); - $hours = floor(($duration - (86400*$days)) / 3600); - $minutes = floor(($duration - (86400*$days + 3600*$hours)) / 60); - - if ($duration < 60) - { - // Less than 1 min - $sResult =Dict::S('UI:Deadline_LessThan1Min'); - } - else if ($duration < 3600) - { - // less than 1 hour, display it in minutes - $sResult =Dict::Format('UI:Deadline_Minutes', $minutes); - } - else if ($duration < 86400) - { - // Less that 1 day, display it in hours/minutes - $sResult =Dict::Format('UI:Deadline_Hours_Minutes', $hours, $minutes); - } - else - { - // Less that 1 day, display it in hours/minutes - $sResult =Dict::Format('UI:Deadline_Days_Hours_Minutes', $days, $hours, $minutes); - } - return $sResult; - } -} - -/** - * Map a foreign key to an attribute - * AttributeExternalKey and AttributeExternalField may be an external key - * the difference is that AttributeExternalKey corresponds to a column into the defined table - * where an AttributeExternalField corresponds to a column into another table (class) - * - * @package iTopORM - */ -class AttributeExternalKey extends AttributeDBFieldVoid -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; - - - /** - * Return the search widget type corresponding to this attribute - * - * @return string - */ - public function GetSearchType() - { - try - { - $oRemoteAtt = $this->GetFinalAttDef(); - $sTargetClass = $oRemoteAtt->GetTargetClass(); - if (MetaModel::IsHierarchicalClass($sTargetClass)) - { - return self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; - } - return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; - } - catch (CoreException $e) - { - } - - return self::SEARCH_WIDGET_TYPE_RAW; - } - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete")); - } - - public function GetEditClass() {return "ExtKey";} - protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");} - public function RequiresIndex() - { - return true; - } - - public function IsExternalKey($iType = EXTKEY_RELATIVE) {return true;} - public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->Get("targetclass");} - public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;} - public function GetKeyAttCode() {return $this->GetCode();} - public function GetDisplayStyle() { return $this->GetOptional('display_style', 'select'); } - - - public function GetDefaultValue(DBObject $oHostObject = null) {return 0;} - public function IsNullAllowed() - { - if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys')) - { - return true; - } - return $this->Get("is_null_allowed"); - } - - - public function GetBasicFilterOperators() - { - return parent::GetBasicFilterOperators(); - } - public function GetBasicFilterLooseOperator() - { - return parent::GetBasicFilterLooseOperator(); - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return parent::GetBasicFilterSQLExpr($sOpCode, $value); - } - - // overloaded here so that an ext key always have the answer to - // "what are your possible values?" - public function GetValuesDef() - { - $oValSetDef = $this->Get("allowed_values"); - if (!$oValSetDef) - { - // Let's propose every existing value - $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); - } - return $oValSetDef; - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - //throw new Exception("GetAllowedValues on ext key has been deprecated"); - try - { - return parent::GetAllowedValues($aArgs, $sContains); - } - catch (MissingQueryArgument $e) //FIXME never enters here... - { - // Some required arguments could not be found, enlarge to any existing value - $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); - return $oValSetDef->GetValues($aArgs, $sContains); - } - } - - public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) - { - $oValSetDef = $this->GetValuesDef(); - $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue); - return $oSet; - } - - public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) - { - return DBObjectSearch::FromOQL($this->GetValuesDef()->GetFilterExpression()); - } - - public function GetDeletionPropagationOption() - { - return $this->Get("on_target_delete"); - } - - public function GetNullValue() - { - return 0; - } - - public function IsNull($proposedValue) - { - return ($proposedValue == 0); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return 0; - if ($proposedValue === '') return 0; - if (MetaModel::IsValidObject($proposedValue)) return $proposedValue->GetKey(); - return (int)$proposedValue; - } - - public function GetMaximumComboLength() - { - return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length')); - } - - public function GetMinAutoCompleteChars() - { - return $this->GetOptional('min_autocomplete_chars', MetaModel::GetConfig()->Get('min_autocomplete_chars')); - } - - public function AllowTargetCreation() - { - return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation')); - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * @return null | AttributeDefinition - * @throws \CoreException - */ - public function GetMirrorLinkAttribute() - { - $oRet = null; - $sRemoteClass = $this->GetTargetClass(); - foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) - { - if (!$oRemoteAttDef->IsLinkSet()) continue; - if (!is_subclass_of($this->GetHostClass(), $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $this->GetHostClass()) continue; - if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetCode()) continue; - $oRet = $oRemoteAttDef; - break; - } - return $oRet; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - // TODO : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - // Setting params - $oFormField->SetMaximumComboLength($this->GetMaximumComboLength()); - $oFormField->SetMinAutoCompleteChars($this->GetMinAutoCompleteChars()); - $oFormField->SetHierarchical(MetaModel::IsHierarchicalClass($this->GetTargetClass())); - // Setting choices regarding the field dependencies - $aFieldDependencies = $this->GetPrerequisiteAttributes(); - if (!empty($aFieldDependencies)) - { - $oTmpAttDef = $this; - $oTmpField = $oFormField; - $oFormField->SetOnFinalizeCallback(function() use ($oTmpField, $oTmpAttDef, $oObject) - { - /** @var $oTmpField \Combodo\iTop\Form\Field\Field */ - /** @var $oTmpAttDef \AttributeDefinition */ - /** @var $oObject \DBObject */ - - // We set search object only if it has not already been set (overrided) - if ($oTmpField->GetSearch() === null) - { - $oSearch = DBSearch::FromOQL($oTmpAttDef->GetValuesDef()->GetFilterExpression()); - $oSearch->SetInternalParams(array('this' => $oObject)); - $oTmpField->SetSearch($oSearch); - } - }); - } - else - { - $oSearch = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression()); - $oSearch->SetInternalParams(array('this' => $oObject)); - $oFormField->SetSearch($oSearch); - } - - // If ExtKey is mandatory, we add a validator to ensure that the value 0 is not selected - if ($oObject->GetAttributeFlags($this->GetCode()) & OPT_ATT_MANDATORY) - { - $oFormField->AddValidator(new \Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator()); - } - - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - -} - -/** - * Special kind of External Key to manage a hierarchy of objects - */ -class AttributeHierarchicalKey extends AttributeExternalKey -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; - - protected $m_sTargetClass; - - static public function ListExpectedParams() - { - $aParams = parent::ListExpectedParams(); - $idx = array_search('targetclass', $aParams); - unset($aParams[$idx]); - $idx = array_search('jointype', $aParams); - unset($aParams[$idx]); - return $aParams; // TODO: mettre les bons parametres ici !! - } - - public function GetEditClass() {return "ExtKey";} - public function RequiresIndex() - { - return true; - } - - /* - * The target class is the class for which the attribute has been defined first - */ - public function SetHostClass($sHostClass) - { - if (!isset($this->m_sTargetClass)) - { - $this->m_sTargetClass = $sHostClass; - } - parent::SetHostClass($sHostClass); - } - - static public function IsHierarchicalKey() {return true;} - public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->m_sTargetClass;} - public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;} - public function GetKeyAttCode() {return $this->GetCode();} - - public function GetBasicFilterOperators() - { - return parent::GetBasicFilterOperators(); - } - public function GetBasicFilterLooseOperator() - { - return parent::GetBasicFilterLooseOperator(); - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->GetCode()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); - $aColumns[$this->GetSQLLeft()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); - $aColumns[$this->GetSQLRight()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); - return $aColumns; - } - public function GetSQLRight() - { - return $this->GetCode().'_right'; - } - public function GetSQLLeft() - { - return $this->GetCode().'_left'; - } - - public function GetSQLValues($value) - { - if (!is_array($value)) - { - $aValues[$this->GetCode()] = $value; - } - else - { - $aValues = array(); - $aValues[$this->GetCode()] = $value[$this->GetCode()]; - $aValues[$this->GetSQLRight()] = $value[$this->GetSQLRight()]; - $aValues[$this->GetSQLLeft()] = $value[$this->GetSQLLeft()]; - } - return $aValues; - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $oFilter = $this->GetHierachicalFilter($aArgs, $sContains); - if ($oFilter) - { - $oValSetDef = $this->GetValuesDef(); - $oValSetDef->AddCondition($oFilter); - return $oValSetDef->GetValues($aArgs, $sContains); - } - else - { - return parent::GetAllowedValues($aArgs, $sContains); - } - } - - public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) - { - $oValSetDef = $this->GetValuesDef(); - $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue); - if ($oFilter) - { - $oValSetDef->AddCondition($oFilter); - } - $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue); - return $oSet; - } - - public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) - { - $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue); - if ($oFilter) - { - return $oFilter; - } - return parent::GetAllowedValuesAsFilter($aArgs, $sContains, $iAdditionalValue); - } - - private function GetHierachicalFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) - { - if (array_key_exists('this', $aArgs)) - { - // Hierarchical keys have one more constraint: the "parent value" cannot be - // "under" themselves - $iRootId = $aArgs['this']->GetKey(); - if ($iRootId > 0) // ignore objects that do no exist in the database... - { - $sClass = $this->m_sTargetClass; - return DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node.".$this->GetCode()." NOT BELOW root.id WHERE root.id = $iRootId"); - } - } - return false; - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * @return null | AttributeDefinition - */ - public function GetMirrorLinkAttribute() - { - return null; - } -} - -/** - * An attribute which corresponds to an external key (direct or indirect) - * - * @package iTopORM - */ -class AttributeExternalField extends AttributeDefinition -{ - /** - * Return the search widget type corresponding to this attribute - * - * @return string - * @throws \CoreException - */ - public function GetSearchType() - { - // Not necessary the external key is already present - if ($this->IsFriendlyName()) - { - return self::SEARCH_WIDGET_TYPE_RAW; - } - - try - { - $oRemoteAtt = $this->GetFinalAttDef(); - switch (true) - { - case ($oRemoteAtt instanceof AttributeString): - return self::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD; - case ($oRemoteAtt instanceof AttributeExternalKey): - return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; - } - } - catch (CoreException $e) - { - } - - return self::SEARCH_WIDGET_TYPE_RAW; - } - - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode")); - } - - public function GetEditClass() {return "ExtField";} - - /** - * @return \AttributeDefinition - * @throws \CoreException - */ - public function GetFinalAttDef() - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetFinalAttDef(); - } - - protected function GetSQLCol($bFullSpec = false) - { - // throw new CoreException("external attribute: does it make any sense to request its type ?"); - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetSQLCol($bFullSpec); - } - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - return array('' => $this->GetCode()); // Warning: Use GetCode() since AttributeExternalField does not have any 'sql' property - } - else - { - return $sPrefix; - } - } - - public function GetLabel($sDefault = null) - { - if ($this->IsFriendlyName()) - { - $sKeyAttCode = $this->Get("extkey_attcode"); - $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); - $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); - } - else - { - $sLabel = parent::GetLabel(''); - if (strlen($sLabel) == 0) - { - $oRemoteAtt = $this->GetExtAttDef(); - $sLabel = $oRemoteAtt->GetLabel($this->m_sCode); - } - } - return $sLabel; - } - - public function GetLabelForSearchField() - { - $sLabel = parent::GetLabel(''); - if (strlen($sLabel) == 0) - { - $sKeyAttCode = $this->Get("extkey_attcode"); - $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); - $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); - - $oRemoteAtt = $this->GetExtAttDef(); - $sLabel .= '->'.$oRemoteAtt->GetLabel($this->m_sCode); - } - - return $sLabel; - } - - public function GetDescription($sDefault = null) - { - $sLabel = parent::GetDescription(''); - if (strlen($sLabel) == 0) - { - $oRemoteAtt = $this->GetExtAttDef(); - $sLabel = $oRemoteAtt->GetDescription(''); - } - return $sLabel; - } - public function GetHelpOnEdition($sDefault = null) - { - $sLabel = parent::GetHelpOnEdition(''); - if (strlen($sLabel) == 0) - { - $oRemoteAtt = $this->GetExtAttDef(); - $sLabel = $oRemoteAtt->GetHelpOnEdition(''); - } - return $sLabel; - } - - public function IsExternalKey($iType = EXTKEY_RELATIVE) - { - switch($iType) - { - case EXTKEY_ABSOLUTE: - // see further - $oRemoteAtt = $this->GetExtAttDef(); - return $oRemoteAtt->IsExternalKey($iType); - - case EXTKEY_RELATIVE: - return false; - - default: - throw new CoreException("Unexpected value for argument iType: '$iType'"); - } - } - - /** - * @return bool - * @throws \CoreException - */ - public function IsFriendlyName() - { - $oRemoteAtt = $this->GetExtAttDef(); - if ($oRemoteAtt instanceof AttributeExternalField) - { - $bRet = $oRemoteAtt->IsFriendlyName(); - } - elseif ($oRemoteAtt instanceof AttributeFriendlyName) - { - $bRet = true; - } - else - { - $bRet = false; - } - return $bRet; - } - - public function GetTargetClass($iType = EXTKEY_RELATIVE) - { - return $this->GetKeyAttDef($iType)->GetTargetClass(); - } - - static public function IsExternalField() {return true;} - - public function GetKeyAttCode() - { - return $this->Get("extkey_attcode"); - } - - public function GetExtAttCode() - { - return $this->Get("target_attcode"); - } - - /** - * @param int $iType - * - * @return \AttributeExternalKey - * @throws \CoreException - * @throws \Exception - */ - public function GetKeyAttDef($iType = EXTKEY_RELATIVE) - { - switch($iType) - { - case EXTKEY_ABSOLUTE: - // see further - /** @var \AttributeExternalKey $oRemoteAtt */ - $oRemoteAtt = $this->GetExtAttDef(); - if ($oRemoteAtt->IsExternalField()) - { - return $oRemoteAtt->GetKeyAttDef(EXTKEY_ABSOLUTE); - } - else if ($oRemoteAtt->IsExternalKey()) - { - return $oRemoteAtt; - } - return $this->GetKeyAttDef(EXTKEY_RELATIVE); // which corresponds to the code hereafter ! - - case EXTKEY_RELATIVE: - return MetaModel::GetAttributeDef($this->GetHostClass(), $this->Get("extkey_attcode")); - - default: - throw new CoreException("Unexpected value for argument iType: '$iType'"); - } - } - - public function GetPrerequisiteAttributes($sClass = null) - { - return array($this->Get("extkey_attcode")); - } - - - /** - * @return \AttributeExternalField - * @throws \CoreException - * @throws \Exception - */ - public function GetExtAttDef() - { - $oKeyAttDef = $this->GetKeyAttDef(); - /** @var \AttributeExternalField $oExtAttDef */ - $oExtAttDef = MetaModel::GetAttributeDef($oKeyAttDef->GetTargetClass(), $this->Get("target_attcode")); - if (!is_object($oExtAttDef)) throw new CoreException("Invalid external field ".$this->GetCode()." in class ".$this->GetHostClass().". The class ".$oKeyAttDef->GetTargetClass()." has no attribute ".$this->Get("target_attcode")); - return $oExtAttDef; - } - - /** - * @return mixed - * @throws \CoreException - */ - public function GetSQLExpr() - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetSQLExpr(); - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetDefaultValue(); - } - public function IsNullAllowed() - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->IsNullAllowed(); - } - - static public function IsScalar() - { - return true; - } - - public function GetFilterDefinitions() - { - return array($this->GetCode() => new FilterFromAttribute($this)); - } - - public function GetBasicFilterOperators() - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetBasicFilterOperators(); - } - public function GetBasicFilterLooseOperator() - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetBasicFilterLooseOperator(); - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value); - } - - public function GetNullValue() - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetNullValue(); - } - - public function IsNull($proposedValue) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->IsNull($proposedValue); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->MakeRealValue($proposedValue, $oHostObj); - } - - public function ScalarToSQL($value) - { - // This one could be used in case of filtering only - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->ScalarToSQL($value); - } - - - // Do not overload GetSQLExpression here because this is handled in the joins - //public function GetSQLExpressions($sPrefix = '') {return array();} - - // Here, we get the data... - public function FromSQLToValue($aCols, $sPrefix = '') - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->FromSQLToValue($aCols, $sPrefix); - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsHTML($value, null, $bLocalize); - } - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsXML($value, null, $bLocalize); - } - public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize, $bConvertToPlainText); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\LabelField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\Field $oFormField - * - * @return null - * @throws \CoreException - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - // Retrieving AttDef from the remote attribute - $oRemoteAttDef = $this->GetExtAttDef(); - - if ($oFormField === null) - { - // ExternalField's FormField are actually based on the FormField from the target attribute. - // Except for the AttributeExternalKey because we have no OQL and stuff - if($oRemoteAttDef instanceof AttributeExternalKey) - { - $sFormFieldClass = static::GetFormFieldClass(); - } - else - { - $sFormFieldClass = $oRemoteAttDef::GetFormFieldClass(); - } - $oFormField = new $sFormFieldClass($this->GetCode()); - } - parent::MakeFormField($oObject, $oFormField); - - // Manually setting for remote ExternalKey, otherwise, the id would be displayed. - if($oRemoteAttDef instanceof AttributeExternalKey) - { - $oFormField->SetCurrentValue($oObject->Get($this->GetCode().'_friendlyname')); - } - - // Readonly field because we can't update external fields - $oFormField->SetReadOnly(true); - - return $oFormField; - } - - public function IsPartOfFingerprint() - { - return false; - } - -} - - -/** - * Multi value list of tags - * - * @see TagSetFieldData - * @since 2.6 N°931 tag fields - */ -class AttributeTagSet extends AttributeString -{ - //TODO SQL type length (nb of tags per record, max tag length) - //TODO implement ?? - //TODO specific filters - public function RequiresIndex() - { - return true; - } - - public function RequiresFullTextIndex() - { - return true; - } - - public function IsNullAllowed() - { - return true; - } -} - -/** - * Map a varchar column to an URL (formats the ouput in HMTL) - * - * @package iTopORM - */ -class AttributeURL extends AttributeString -{ - static public function ListExpectedParams() - { - //return parent::ListExpectedParams(); - return array_merge(parent::ListExpectedParams(), array("target")); - } - - protected function GetSQLCol($bFullSpec = false) - { - return "VARCHAR(2048)" - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetMaxSize() - { - return 2048; - } - - public function GetEditClass() {return "String";} - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - $sTarget = $this->Get("target"); - if (empty($sTarget)) $sTarget = "_blank"; - $sLabel = Str::pure2html($sValue); - if (strlen($sLabel) > 128) - { - // Truncate the length to 128 characters, by removing the middle - $sLabel = substr($sLabel, 0, 100).'.....'.substr($sLabel, -20); - } - return "$sLabel"; - } - - public function GetValidationPattern() - { - return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('url_validation_pattern').'$'); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\UrlField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\UrlField $oFormField - * - * @return null - * @throws \CoreException - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - parent::MakeFormField($oObject, $oFormField); - - $oFormField->SetTarget($this->Get('target')); - - return $oFormField; - } -} - -/** - * A blob is an ormDocument, it is stored as several columns in the database - * - * @package iTopORM - */ -class AttributeBlob extends AttributeDefinition -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("depends_on")); - } - - public function GetEditClass() {return "Document";} - - static public function IsBasedOnDBColumns() {return true;} - static public function IsScalar() {return true;} - public function IsWritable() {return true;} - public function GetDefaultValue(DBObject $oHostObject = null) {return "";} - public function IsNullAllowed(DBObject $oHostObject = null) {return $this->GetOptional("is_null_allowed", false);} - - public function GetEditValue($sValue, $oHostObj = null) - { - return ''; - } - - /** - * Users can provide the document from an URL (including an URL on iTop itself) - * for CSV import. Administrators can even provide the path to a local file - * {@inheritDoc} - * @see AttributeDefinition::MakeRealValue() - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - if ($proposedValue === null) return null; - - if (is_object($proposedValue)) - { - $proposedValue = clone $proposedValue; - } - else - { - try - { - // Read the file from iTop, an URL (or the local file system - for admins only) - $proposedValue = Utils::FileGetContentsAndMIMEType($proposedValue); - } - catch(Exception $e) - { - IssueLog::Warning(get_class($this)."::MakeRealValue - ".$e->getMessage()); - // Not a real document !! store is as text !!! (This was the default behavior before) - $proposedValue = new ormDocument($e->getMessage()." \n".$proposedValue, 'text/plain'); - } - } - return $proposedValue; - } - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - $sPrefix = $this->GetCode(); - } - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $sPrefix.'_mimetype'; - $aColumns['_data'] = $sPrefix.'_data'; - $aColumns['_filename'] = $sPrefix.'_filename'; - return $aColumns; - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - if (!array_key_exists($sPrefix, $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); - } - $sMimeType = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; - - if (!array_key_exists($sPrefix.'_data', $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}"); - } - $data = isset($aCols[$sPrefix.'_data']) ? $aCols[$sPrefix.'_data'] : null; - - if (!array_key_exists($sPrefix.'_filename', $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}"); - } - $sFileName = isset($aCols[$sPrefix.'_filename']) ? $aCols[$sPrefix.'_filename'] : ''; - - $value = new ormDocument($data, $sMimeType, $sFileName); - return $value; - } - - public function GetSQLValues($value) - { - // #@# Optimization: do not load blobs anytime - // As per mySQL doc, selecting blob columns will prevent mySQL from - // using memory in case a temporary table has to be created - // (temporary tables created on disk) - // We will have to remove the blobs from the list of attributes when doing the select - // then the use of Get() should finalize the load - if ($value instanceOf ormDocument && !$value->IsEmpty()) - { - $aValues = array(); - $aValues[$this->GetCode().'_data'] = $value->GetData(); - $aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType(); - $aValues[$this->GetCode().'_filename'] = $value->GetFileName(); - } - else - { - $aValues = array(); - $aValues[$this->GetCode().'_data'] = ''; - $aValues[$this->GetCode().'_mimetype'] = ''; - $aValues[$this->GetCode().'_filename'] = ''; - } - return $aValues; - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb) - $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); - $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); - return $aColumns; - } - - public function GetFilterDefinitions() - { - return array(); - } - - public function GetBasicFilterOperators() - { - return array(); - } - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return 'true'; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - if (is_object($value)) - { - return $value->GetAsHTML(); - } - return ''; - } - - /** - * @param string $sValue - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return string - */ - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $sAttCode = $this->GetCode(); - if ($sValue instanceof ormDocument && !$sValue->IsEmpty()) - { - return $sValue->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $sAttCode); - } - return ''; // Not exportable in CSV ! - } - - /** - * @param $value - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return mixed|string - */ - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - $sRet = ''; - if (is_object($value)) - { - if (!$value->IsEmpty()) - { - $sRet = ''.$value->GetMimeType().''; - $sRet .= ''.$value->GetFileName().''; - $sRet .= ''.base64_encode($value->GetData()).''; - } - } - return $sRet; - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - */ - public function GetForJSON($value) - { - if ($value instanceOf ormDocument) - { - $aValues = array(); - $aValues['data'] = base64_encode($value->GetData()); - $aValues['mimetype'] = $value->GetMimeType(); - $aValues['filename'] = $value->GetFileName(); - } - else - { - $aValues = null; - } - return $aValues; - } - - /** - * Helper to form a value, given JSON decoded data - * The operation is the opposite to GetForJSON - */ - public function FromJSONToValue($json) - { - if (isset($json->data)) - { - $data = base64_decode($json->data); - $value = new ormDocument($data, $json->mimetype, $json->filename); - } - else - { - $value = null; - } - return $value; - } - - public function Fingerprint($value) - { - $sFingerprint = ''; - if ($value instanceOf ormDocument) - { - $sFingerprint = md5($value->GetData()); - } - return $sFingerprint; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\BlobField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - // Note: As of today we want this field to always be read-only - $oFormField->SetReadOnly(true); - - // Generating urls - $value = $oObject->Get($this->GetCode()); - $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); - $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); - - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - -} - -/** - * An image is a specific type of document, it is stored as several columns in the database - * - * @package iTopORM - */ -class AttributeImage extends AttributeBlob -{ - public function GetEditClass() {return "Image";} - - /** - * {@inheritDoc} - * @see AttributeBlob::MakeRealValue() - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - $oDoc = parent::MakeRealValue($proposedValue, $oHostObj); - // The validation of the MIME Type is done by CheckFormat below - return $oDoc; - } - - /** - * Check that the supplied ormDocument actually contains an image - * {@inheritDoc} - * @see AttributeDefinition::CheckFormat() - */ - public function CheckFormat($value) - { - if ($value instanceof ormDocument && !$value->IsEmpty()) - { - return ($value->GetMainMimeType() == 'image'); - } - return true; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - $iMaxWidthPx = $this->Get('display_max_width').'px'; - $iMaxHeightPx = $this->Get('display_max_height').'px'; - $sUrl = $this->Get('default_image'); - $sRet = ($sUrl !== null) ? '' : ''; - if (is_object($value) && !$value->IsEmpty()) - { - if ($oHostObject->IsNew() || ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges())))) - { - // If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline - // otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it - $sUrl = 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData()); - } - else - { - $sUrl = $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode()); - } - $sRet = ''; - } - return '
    '.$sRet.'
    '; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\ImageField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - parent::MakeFormField($oObject, $oFormField); - - // Generating urls - $value = $oObject->Get($this->GetCode()); - if (is_object($value) && !$value->IsEmpty()) - { - $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); - $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); - } - else - { - $oFormField->SetDownloadUrl($this->Get('default_image')); - $oFormField->SetDisplayUrl($this->Get('default_image')); - } - - return $oFormField; - } -} -/** - * A stop watch is an ormStopWatch object, it is stored as several columns in the database - * - * @package iTopORM - */ -class AttributeStopWatch extends AttributeDefinition -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - // The list of thresholds must be an array of iPercent => array of 'option' => value - return array_merge(parent::ListExpectedParams(), array("states", "goal_computing", "working_time_computing", "thresholds")); - } - - public function GetEditClass() {return "StopWatch";} - - static public function IsBasedOnDBColumns() {return true;} - static public function IsScalar() {return true;} - public function IsWritable() {return true;} - public function GetDefaultValue(DBObject $oHostObject = null) {return $this->NewStopWatch();} - - /** - * @param \ormStopWatch $value - * @param \DBObject $oHostObj - * - * @return string - */ - public function GetEditValue($value, $oHostObj = null) - { - return $value->GetTimeSpent(); - } - - public function GetStates() - { - return $this->Get('states'); - } - - public function AlwaysLoadInTables() - { - // Each and every stop watch is accessed for computing the highlight code (DBObject::GetHighlightCode()) - return true; - } - - /** - * Construct a brand new (but configured) stop watch - */ - public function NewStopWatch() - { - $oSW = new ormStopWatch(); - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $oSW->DefineThreshold($iThreshold); - } - return $oSW; - } - - // Facilitate things: allow the user to Set the value from a string - public function MakeRealValue($proposedValue, $oHostObj) - { - if (!$proposedValue instanceof ormStopWatch) - { - return $this->NewStopWatch(); - } - return $proposedValue; - } - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - $sPrefix = $this->GetCode(); // Warning: a stopwatch does not have any 'sql' property, so its SQL column is equal to its attribute code !! - } - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $sPrefix.'_timespent'; - $aColumns['_started'] = $sPrefix.'_started'; - $aColumns['_laststart'] = $sPrefix.'_laststart'; - $aColumns['_stopped'] = $sPrefix.'_stopped'; - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = '_'.$iThreshold; - $aColumns[$sThPrefix.'_deadline'] = $sPrefix.$sThPrefix.'_deadline'; - $aColumns[$sThPrefix.'_passed'] = $sPrefix.$sThPrefix.'_passed'; - $aColumns[$sThPrefix.'_triggered'] = $sPrefix.$sThPrefix.'_triggered'; - $aColumns[$sThPrefix.'_overrun'] = $sPrefix.$sThPrefix.'_overrun'; - } - return $aColumns; - } - - public static function DateToSeconds($sDate) - { - if (is_null($sDate)) - { - return null; - } - $oDateTime = new DateTime($sDate); - $iSeconds = $oDateTime->format('U'); - return $iSeconds; - } - - public static function SecondsToDate($iSeconds) - { - if (is_null($iSeconds)) - { - return null; - } - return date("Y-m-d H:i:s", $iSeconds); - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - $aExpectedCols = array($sPrefix, $sPrefix.'_started', $sPrefix.'_laststart', $sPrefix.'_stopped'); - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = '_'.$iThreshold; - $aExpectedCols[] = $sPrefix.$sThPrefix.'_deadline'; - $aExpectedCols[] = $sPrefix.$sThPrefix.'_passed'; - $aExpectedCols[] = $sPrefix.$sThPrefix.'_triggered'; - $aExpectedCols[] = $sPrefix.$sThPrefix.'_overrun'; - } - foreach ($aExpectedCols as $sExpectedCol) - { - if (!array_key_exists($sExpectedCol, $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '$sExpectedCol' from {$sAvailable}"); - } - } - - $value = new ormStopWatch( - $aCols[$sPrefix], - self::DateToSeconds($aCols[$sPrefix.'_started']), - self::DateToSeconds($aCols[$sPrefix.'_laststart']), - self::DateToSeconds($aCols[$sPrefix.'_stopped']) - ); - - foreach ($this->ListThresholds() as $iThreshold => $aDefinition) - { - $sThPrefix = '_'.$iThreshold; - $value->DefineThreshold( - $iThreshold, - self::DateToSeconds($aCols[$sPrefix.$sThPrefix.'_deadline']), - (bool)($aCols[$sPrefix.$sThPrefix.'_passed'] == 1), - (bool)($aCols[$sPrefix.$sThPrefix.'_triggered'] == 1), - $aCols[$sPrefix.$sThPrefix.'_overrun'], - array_key_exists('highlight', $aDefinition) ? $aDefinition['highlight'] : null - ); - } - - return $value; - } - - public function GetSQLValues($value) - { - if ($value instanceOf ormStopWatch) - { - $aValues = array(); - $aValues[$this->GetCode().'_timespent'] = $value->GetTimeSpent(); - $aValues[$this->GetCode().'_started'] = self::SecondsToDate($value->GetStartDate()); - $aValues[$this->GetCode().'_laststart'] = self::SecondsToDate($value->GetLastStartDate()); - $aValues[$this->GetCode().'_stopped'] = self::SecondsToDate($value->GetStopDate()); - - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sPrefix = $this->GetCode().'_'.$iThreshold; - $aValues[$sPrefix.'_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold)); - $aValues[$sPrefix.'_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0'; - $aValues[$sPrefix.'_triggered'] = $value->IsThresholdTriggered($iThreshold) ? '1' : '0'; - $aValues[$sPrefix.'_overrun'] = $value->GetOverrun($iThreshold); - } - } - else - { - $aValues = array(); - $aValues[$this->GetCode().'_timespent'] = ''; - $aValues[$this->GetCode().'_started'] = ''; - $aValues[$this->GetCode().'_laststart'] = ''; - $aValues[$this->GetCode().'_stopped'] = ''; - } - return $aValues; - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->GetCode().'_timespent'] = 'INT(11) UNSIGNED'; - $aColumns[$this->GetCode().'_started'] = 'DATETIME'; - $aColumns[$this->GetCode().'_laststart'] = 'DATETIME'; - $aColumns[$this->GetCode().'_stopped'] = 'DATETIME'; - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sPrefix = $this->GetCode().'_'.$iThreshold; - $aColumns[$sPrefix.'_deadline'] = 'DATETIME'; - $aColumns[$sPrefix.'_passed'] = 'TINYINT(1) UNSIGNED'; - $aColumns[$sPrefix.'_triggered'] = 'TINYINT(1)'; - $aColumns[$sPrefix.'_overrun'] = 'INT(11) UNSIGNED'; - } - return $aColumns; - } - - public function GetFilterDefinitions() - { - $aRes = array( - $this->GetCode() => new FilterFromAttribute($this), - $this->GetCode().'_started' => new FilterFromAttribute($this, '_started'), - $this->GetCode().'_laststart' => new FilterFromAttribute($this, '_laststart'), - $this->GetCode().'_stopped' => new FilterFromAttribute($this, '_stopped') - ); - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sPrefix = $this->GetCode().'_'.$iThreshold; - $aRes[$sPrefix.'_deadline'] = new FilterFromAttribute($this, '_deadline'); - $aRes[$sPrefix.'_passed'] = new FilterFromAttribute($this, '_passed'); - $aRes[$sPrefix.'_triggered'] = new FilterFromAttribute($this, '_triggered'); - $aRes[$sPrefix.'_overrun'] = new FilterFromAttribute($this, '_overrun'); - } - return $aRes; - } - - public function GetBasicFilterOperators() - { - return array(); - } - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return 'true'; - } - - /** - * @param \ormStopWatch $value - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - */ - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - if (is_object($value)) - { - return $value->GetAsHTML($this, $oHostObject); - } - return ''; - } - - /** - * @param ormStopWatch $value - * @param string $sSeparator - * @param string $sTextQualifier - * @param null $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return string - */ - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - return $value->GetTimeSpent(); - } - - /** - * @param \ormStopWatch $value - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return mixed - */ - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - return $value->GetTimeSpent(); - } - - public function ListThresholds() - { - return $this->Get('thresholds'); - } - - public function Fingerprint($value) - { - $sFingerprint = ''; - if (is_object($value)) - { - $sFingerprint = $value->GetAsHTML($this); - } - return $sFingerprint; - } - - /** - * To expose internal values: Declare an attribute AttributeSubItem - * and implement the GetSubItemXXXX verbs - * - * @param string $sItemCode - * - * @return array - * @throws \CoreException - */ - public function GetSubItemSQLExpression($sItemCode) - { - $sPrefix = $this->GetCode(); - switch($sItemCode) - { - case 'timespent': - return array('' => $sPrefix.'_timespent'); - case 'started': - return array('' => $sPrefix.'_started'); - case 'laststart': - return array('' => $sPrefix.'_laststart'); - case 'stopped': - return array('' => $sPrefix.'_stopped'); - } - - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold.'_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch($sThresholdCode) - { - case 'deadline': - return array('' => $sPrefix.'_'.$iThreshold.'_deadline'); - case 'passed': - return array('' => $sPrefix.'_'.$iThreshold.'_passed'); - case 'triggered': - return array('' => $sPrefix.'_'.$iThreshold.'_triggered'); - case 'overrun': - return array('' => $sPrefix.'_'.$iThreshold.'_overrun'); - } - } - } - throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode()); - } - - /** - * @param string $sItemCode - * @param \ormStopWatch $value - * @param \DBObject $oHostObject - * - * @return mixed - * @throws \CoreException - */ - public function GetSubItemValue($sItemCode, $value, $oHostObject = null) - { - $oStopWatch = $value; - switch($sItemCode) - { - case 'timespent': - return $oStopWatch->GetTimeSpent(); - case 'started': - return $oStopWatch->GetStartDate(); - case 'laststart': - return $oStopWatch->GetLastStartDate(); - case 'stopped': - return $oStopWatch->GetStopDate(); - } - - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold.'_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch($sThresholdCode) - { - case 'deadline': - return $oStopWatch->GetThresholdDate($iThreshold); - case 'passed': - return $oStopWatch->IsThresholdPassed($iThreshold); - case 'triggered': - return $oStopWatch->IsThresholdTriggered($iThreshold); - case 'overrun': - return $oStopWatch->GetOverrun($iThreshold); - } - } - } - - throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode()); - } - - protected function GetBooleanLabel($bValue) - { - $sDictKey = $bValue ? 'yes' : 'no'; - return Dict::S('BooleanLabel:'.$sDictKey, 'def:'.$sDictKey); - } - - public function GetSubItemAsHTMLForHistory($sItemCode, $sValue) - { - $sHtml = null; - switch($sItemCode) - { - case 'timespent': - $sHtml = (int)$sValue ? Str::pure2html(AttributeDuration::FormatDuration($sValue)) : null; - break; - case 'started': - case 'laststart': - case 'stopped': - $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null; - break; - - default: - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold.'_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch($sThresholdCode) - { - case 'deadline': - $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null; - break; - case 'passed': - $sHtml = $this->GetBooleanLabel((int)$sValue); - break; - case 'triggered': - $sHtml = $this->GetBooleanLabel((int)$sValue); - break; - case 'overrun': - $sHtml = (int)$sValue > 0 ? Str::pure2html(AttributeDuration::FormatDuration((int)$sValue)) : ''; - } - } - } - } - return $sHtml; - } - - public function GetSubItemAsPlainText($sItemCode, $value) - { - $sRet = $value; - - switch ($sItemCode) - { - case 'timespent': - $sRet = AttributeDuration::FormatDuration($value); - break; - case 'started': - case 'laststart': - case 'stopped': - if (is_null($value)) - { - $sRet = ''; // Undefined - } - else - { - $oDateTime = new DateTime(); - $oDateTime->setTimestamp($value); - $oDateTimeFormat = AttributeDateTime::GetFormat(); - $sRet = $oDateTimeFormat->Format($oDateTime); - } - break; - - default: - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold . '_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch ($sThresholdCode) - { - case 'deadline': - if ($value) - { - $sDate = date(AttributeDateTime::GetInternalFormat(), $value); - $sRet = AttributeDeadline::FormatDeadline($sDate); - } - else - { - $sRet = ''; - } - break; - case 'passed': - case 'triggered': - $sRet = $this->GetBooleanLabel($value); - break; - case 'overrun': - $sRet = AttributeDuration::FormatDuration($value); - break; - } - } - } - } - return $sRet; - } - - public function GetSubItemAsHTML($sItemCode, $value) - { - $sHtml = $value; - - switch ($sItemCode) - { - case 'timespent': - $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value)); - break; - case 'started': - case 'laststart': - case 'stopped': - if (is_null($value)) - { - $sHtml = ''; // Undefined - } - else - { - $oDateTime = new DateTime(); - $oDateTime->setTimestamp($value); - $oDateTimeFormat = AttributeDateTime::GetFormat(); - $sHtml = Str::pure2html($oDateTimeFormat->Format($oDateTime)); - } - break; - - default: - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold . '_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch ($sThresholdCode) - { - case 'deadline': - if ($value) - { - $sDate = date(AttributeDateTime::GetInternalFormat(), $value); - $sHtml = Str::pure2html(AttributeDeadline::FormatDeadline($sDate)); - } - else - { - $sHtml = ''; - } - break; - case 'passed': - case 'triggered': - $sHtml = $this->GetBooleanLabel($value); - break; - case 'overrun': - $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value)); - break; - } - } - } - } - return $sHtml; - } - - public function GetSubItemAsCSV($sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"', $bConvertToPlainText = false) - { - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$value); - $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; - - switch($sItemCode) - { - case 'timespent': - $sRet = $sTextQualifier . AttributeDuration::FormatDuration($value) . $sTextQualifier; - break; - case 'started': - case 'laststart': - case 'stopped': - if ($value !== null) - { - $oDateTime = new DateTime(); - $oDateTime->setTimestamp($value); - $oDateTimeFormat = AttributeDateTime::GetFormat(); - $sRet = $sTextQualifier . $oDateTimeFormat->Format($oDateTime) . $sTextQualifier; - } - break; - - default: - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold.'_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch($sThresholdCode) - { - case 'deadline': - if ($value != '') - { - $oDateTime = new DateTime(); - $oDateTime->setTimestamp($value); - $oDateTimeFormat = AttributeDateTime::GetFormat(); - $sRet = $sTextQualifier . $oDateTimeFormat->Format($oDateTime) . $sTextQualifier; - } - break; - - case 'passed': - case 'triggered': - $sRet = $sTextQualifier . $this->GetBooleanLabel($value) . $sTextQualifier; - break; - - case 'overrun': - $sRet = $sTextQualifier . AttributeDuration::FormatDuration($value) . $sTextQualifier; - break; - } - } - } - } - return $sRet; - } - - public function GetSubItemAsXML($sItemCode, $value) - { - $sRet = Str::pure2xml((string)$value); - - switch($sItemCode) - { - case 'timespent': - case 'started': - case 'laststart': - case 'stopped': - break; - - default: - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold.'_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch($sThresholdCode) - { - case 'deadline': - break; - - case 'passed': - case 'triggered': - $sRet = $this->GetBooleanLabel($value); - break; - - case 'overrun': - break; - } - } - } - } - return $sRet; - } - - /** - * Implemented for the HTML spreadsheet format! - * - * @param string $sItemCode - * @param \ormStopWatch $value - * - * @return false|string - */ - public function GetSubItemAsEditValue($sItemCode, $value) - { - $sRet = $value; - - switch($sItemCode) - { - case 'timespent': - break; - - case 'started': - case 'laststart': - case 'stopped': - if (is_null($value)) - { - $sRet = ''; // Undefined - } - else - { - $sRet = date((string)AttributeDateTime::GetFormat(), $value); - } - break; - - default: - foreach ($this->ListThresholds() as $iThreshold => $aFoo) - { - $sThPrefix = $iThreshold.'_'; - if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) - { - // The current threshold is concerned - $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); - switch($sThresholdCode) - { - case 'deadline': - if ($value) - { - $sRet = date((string)AttributeDateTime::GetFormat(), $value); - } - else - { - $sRet = ''; - } - break; - case 'passed': - case 'triggered': - $sRet = $this->GetBooleanLabel($value); - break; - case 'overrun': - break; - } - } - } - } - return $sRet; - } -} - -/** - * View of a subvalue of another attribute - * If an attribute implements the verbs GetSubItem.... then it can expose - * internal values, each of them being an attribute and therefore they - * can be displayed at different times in the object lifecycle, and used for - * reporting (as a condition in OQL, or as an additional column in an export) - * Known usages: Stop Watches can expose threshold statuses - */ -class AttributeSubItem extends AttributeDefinition -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code')); - } - - public function GetParentAttCode() {return $this->Get("target_attcode");} - - /** - * Helper : get the attribute definition to which the execution will be forwarded - */ - public function GetTargetAttDef() - { - $sClass = $this->GetHostClass(); - $oParentAttDef = MetaModel::GetAttributeDef($sClass, $this->Get('target_attcode')); - return $oParentAttDef; - } - - public function GetEditClass() {return "";} - - public function GetValuesDef() {return null;} - - static public function IsBasedOnDBColumns() {return true;} - static public function IsScalar() {return true;} - public function IsWritable() {return false;} - public function GetDefaultValue(DBObject $oHostObject = null) {return null;} -// public function IsNullAllowed() {return false;} - - static public function LoadInObject() {return false;} // if this verb returns false, then GetValue must be implemented - - /** - * Used by DBOBject::Get() - * - * @param \DBObject $oHostObject - * - * @return \AttributeSubItem - * @throws \CoreException - */ - public function GetValue($oHostObject) - { - /** @var \AttributeStopWatch $oParent */ - $oParent = $this->GetTargetAttDef(); - $parentValue = $oHostObject->GetStrict($oParent->GetCode()); - $res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject); - return $res; - } - - // -// protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) - - public function FromSQLToValue($aCols, $sPrefix = '') - { - } - - public function GetSQLColumns($bFullSpec = false) - { - return array(); - } - - public function GetFilterDefinitions() - { - return array($this->GetCode() => new FilterFromAttribute($this)); - } - - public function GetBasicFilterOperators() - { - return array(); - } - public function GetBasicFilterLooseOperator() - { - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '=': - default: - return $this->GetSQLExpr()." = $sQValue"; - } - } - - public function GetSQLExpressions($sPrefix = '') - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemSQLExpression($this->Get('item_code')); - return $res; - } - - public function GetAsPlainText($value, $oHostObject = null, $bLocalize = true) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsPlainText($this->Get('item_code'), $value); - return $res; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value); - return $res; - } - - public function GetAsHTMLForHistory($value, $oHostObject = null, $bLocalize = true) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsHTMLForHistory($this->Get('item_code'), $value); - return $res; - } - - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier, $bConvertToPlainText); - return $res; - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value); - return $res; - } - - /** - * As of now, this function must be implemented to have the value in spreadsheet format - */ - public function GetEditValue($value, $oHostObj = null) - { - $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsEditValue($this->Get('item_code'), $value); - return $res; - } - - public function IsPartOfFingerprint() - { - return false; - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\LabelField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - parent::MakeFormField($oObject, $oFormField); - - // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition - $sAttCode = $this->GetCode(); - $oFormField->SetCurrentValue(html_entity_decode($oObject->GetAsHTML($sAttCode), ENT_QUOTES, 'UTF-8')); - $oFormField->SetReadOnly(true); - - return $oFormField; - } - -} - -/** - * One way encrypted (hashed) password - */ -class AttributeOneWayPassword extends AttributeDefinition -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("depends_on")); - } - - public function GetEditClass() {return "One Way Password";} - - static public function IsBasedOnDBColumns() {return true;} - static public function IsScalar() {return true;} - public function IsWritable() {return true;} - public function GetDefaultValue(DBObject $oHostObject = null) {return "";} - public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);} - - // Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted) - public function MakeRealValue($proposedValue, $oHostObj) - { - $oPassword = $proposedValue; - if (is_object($oPassword)) - { - $oPassword = clone $proposedValue; - } - else - { - $oPassword = new ormPassword('', ''); - $oPassword->SetPassword($proposedValue); - } - return $oPassword; - } - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - $sPrefix = $this->GetCode(); // Warning: AttributeOneWayPassword does not have any sql property so code = sql ! - } - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $sPrefix.'_hash'; - $aColumns['_salt'] = $sPrefix.'_salt'; - return $aColumns; - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - if (!array_key_exists($sPrefix, $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); - } - $hashed = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; - - if (!array_key_exists($sPrefix.'_salt', $aCols)) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '".$sPrefix."_salt' from {$sAvailable}"); - } - $sSalt = isset($aCols[$sPrefix.'_salt']) ? $aCols[$sPrefix.'_salt'] : ''; - - $value = new ormPassword($hashed, $sSalt); - return $value; - } - - public function GetSQLValues($value) - { - // #@# Optimization: do not load blobs anytime - // As per mySQL doc, selecting blob columns will prevent mySQL from - // using memory in case a temporary table has to be created - // (temporary tables created on disk) - // We will have to remove the blobs from the list of attributes when doing the select - // then the use of Get() should finalize the load - if ($value instanceOf ormPassword) - { - $aValues = array(); - $aValues[$this->GetCode().'_hash'] = $value->GetHash(); - $aValues[$this->GetCode().'_salt'] = $value->GetSalt(); - } - else - { - $aValues = array(); - $aValues[$this->GetCode().'_hash'] = ''; - $aValues[$this->GetCode().'_salt'] = ''; - } - return $aValues; - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->GetCode().'_hash'] = 'TINYBLOB'; - $aColumns[$this->GetCode().'_salt'] = 'TINYBLOB'; - return $aColumns; - } - - public function GetImportColumns() - { - $aColumns = array(); - $aColumns[$this->GetCode()] = 'TINYTEXT'.CMDBSource::GetSqlStringColumnDefinition(); - return $aColumns; - } - - public function FromImportToValue($aCols, $sPrefix = '') - { - if (!isset($aCols[$sPrefix])) - { - $sAvailable = implode(', ', array_keys($aCols)); - throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); - } - $sClearPwd = $aCols[$sPrefix]; - - $oPassword = new ormPassword('', ''); - $oPassword->SetPassword($sClearPwd); - return $oPassword; - } - - public function GetFilterDefinitions() - { - return array(); - // still not working... see later... - } - - public function GetBasicFilterOperators() - { - return array(); - } - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return 'true'; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - if (is_object($value)) - { - return $value->GetAsHTML(); - } - return ''; - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - return ''; // Not exportable in CSV - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - return ''; // Not exportable in XML - } - - public function GetValueLabel($sValue, $oHostObj = null) - { - // Don't display anything in "group by" reports - return '*****'; - } - -} - -// Indexed array having two dimensions -class AttributeTable extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - public function GetEditClass() {return "Table";} - - protected function GetSQLCol($bFullSpec = false) - { - return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); - } - - public function GetMaxSize() - { - return null; - } - - public function GetNullValue() - { - return array(); - } - - public function IsNull($proposedValue) - { - return (count($proposedValue) == 0); - } - - public function GetEditValue($sValue, $oHostObj = null) - { - return ''; - } - - // Facilitate things: allow the user to Set the value from a string - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return array(); - } - else if (!is_array($proposedValue)) - { - return array(0 => array(0 => $proposedValue)); - } - return $proposedValue; - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - try - { - $value = @unserialize($aCols[$sPrefix.'']); - if ($value === false) - { - $value = $this->MakeRealValue($aCols[$sPrefix.''], null); - } - } - catch(Exception $e) - { - $value = $this->MakeRealValue($aCols[$sPrefix.''], null); - } - - return $value; - } - - public function GetSQLValues($value) - { - $aValues = array(); - $aValues[$this->Get("sql")] = serialize($value); - return $aValues; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - if (!is_array($value)) - { - throw new CoreException('Expecting an array', array('found' => get_class($value))); - } - if (count($value) == 0) - { - return ""; - } - - $sRes = ""; - $sRes .= ""; - foreach($value as $iRow => $aRawData) - { - $sRes .= ""; - foreach ($aRawData as $iCol => $cell) - { - // Note: avoid the warning in case the cell is made of an array - $sCell = @Str::pure2html((string)$cell); - $sCell = str_replace("\n", "
    \n", $sCell); - $sRes .= ""; - } - $sRes .= ""; - } - $sRes .= ""; - $sRes .= "
    $sCell
    "; - return $sRes; - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - // Not implemented - return ''; - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - if (count($value) == 0) - { - return ""; - } - - $sRes = ""; - foreach($value as $iRow => $aRawData) - { - $sRes .= ""; - foreach ($aRawData as $iCol => $cell) - { - $sCell = Str::pure2xml((string)$cell); - $sRes .= "$sCell"; - } - $sRes .= ""; - } - return $sRes; - } -} - -// The PHP value is a hash array, it is stored as a TEXT column -class AttributePropertySet extends AttributeTable -{ - public function GetEditClass() {return "PropertySet";} - - // Facilitate things: allow the user to Set the value from a string - public function MakeRealValue($proposedValue, $oHostObj) - { - if (!is_array($proposedValue)) - { - return array('?' => (string)$proposedValue); - } - return $proposedValue; - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - if (!is_array($value)) - { - throw new CoreException('Expecting an array', array('found' => get_class($value))); - } - if (count($value) == 0) - { - return ""; - } - - $sRes = ""; - $sRes .= ""; - foreach($value as $sProperty => $sValue) - { - if ($sProperty == 'auth_pwd') - { - $sValue = '*****'; - } - $sRes .= ""; - $sCell = str_replace("\n", "
    \n", Str::pure2html((string)$sValue)); - $sRes .= ""; - $sRes .= ""; - } - $sRes .= ""; - $sRes .= "
    $sProperty$sCell
    "; - return $sRes; - } - - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - if (count($value) == 0) - { - return ""; - } - - $aRes = array(); - foreach($value as $sProperty => $sValue) - { - if ($sProperty == 'auth_pwd') - { - $sValue = '*****'; - } - $sFrom = array(',', '='); - $sTo = array('\,', '\='); - $aRes[] = $sProperty.'='.str_replace($sFrom, $sTo, (string)$sValue); - } - $sRaw = implode(',', $aRes); - - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, $sRaw); - return $sTextQualifier.$sEscaped.$sTextQualifier; - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - if (count($value) == 0) - { - return ""; - } - - $sRes = ""; - foreach($value as $sProperty => $sValue) - { - if ($sProperty == 'auth_pwd') - { - $sValue = '*****'; - } - $sRes .= ""; - $sRes .= Str::pure2xml((string)$sValue); - $sRes .= ""; - } - return $sRes; - } -} - -/** - * The attribute dedicated to the friendly name automatic attribute (not written) - * - * @package iTopORM - */ - -/** - * The attribute dedicated to the friendly name automatic attribute (not written) - * - * @package iTopORM - */ -class AttributeFriendlyName extends AttributeDefinition -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - public $m_sValue; - - public function __construct($sCode) - { - $this->m_sCode = $sCode; - $aParams = array(); - $aParams["default_value"] = ''; - parent::__construct($sCode, $aParams); - - $this->m_sValue = $this->Get("default_value"); - } - - - public function GetEditClass() {return "";} - - public function GetValuesDef() {return null;} - public function GetPrerequisiteAttributes($sClass = null) {return $this->GetOptional("depends_on", array());} - - static public function IsScalar() {return true;} - public function IsNullAllowed() {return false;} - - public function GetSQLExpressions($sPrefix = '') - { - if ($sPrefix == '') - { - $sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property - } - return array('' => $sPrefix); - } - - static public function IsBasedOnOQLExpression() {return true;} - public function GetOQLExpression() - { - return MetaModel::GetNameExpression($this->GetHostClass()); - } - - public function GetLabel($sDefault = null) - { - $sLabel = parent::GetLabel(''); - if (strlen($sLabel) == 0) - { - $sLabel = Dict::S('Core:FriendlyName-Label'); - } - return $sLabel; - } - public function GetDescription($sDefault = null) - { - $sLabel = parent::GetDescription(''); - if (strlen($sLabel) == 0) - { - $sLabel = Dict::S('Core:FriendlyName-Description'); - } - return $sLabel; - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - $sValue = $aCols[$sPrefix]; - return $sValue; - } - - public function IsWritable() - { - return false; - } - public function IsMagic() - { - return true; - } - - static public function IsBasedOnDBColumns() - { - return false; - } - - public function SetFixedValue($sValue) - { - $this->m_sValue = $sValue; - } - public function GetDefaultValue(DBObject $oHostObject = null) - { - return $this->m_sValue; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - return Str::pure2html((string)$sValue); - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); - return $sTextQualifier.$sEscaped.$sTextQualifier; - } - - static function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\StringField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - $oFormField->SetReadOnly(true); - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - // Do not display friendly names in the history of change - public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) - { - return ''; - } - - public function GetFilterDefinitions() - { - return array($this->GetCode() => new FilterFromAttribute($this)); - } - - public function GetBasicFilterOperators() - { - return array("="=>"equals", "!="=>"differs from"); - } - - public function GetBasicFilterLooseOperator() - { - return "Contains"; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '=': - case '!=': - return $this->GetSQLExpr()." $sOpCode $sQValue"; - case 'Contains': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); - case 'NotLike': - return $this->GetSQLExpr()." NOT LIKE $sQValue"; - case 'Like': - default: - return $this->GetSQLExpr()." LIKE $sQValue"; - } - } - - public function IsPartOfFingerprint() { return false; } -} - -/** - * Holds the setting for the redundancy on a specific relation - * Its value is a string, containing either: - * - 'disabled' - * - 'n', where n is a positive integer value giving the minimum count of items upstream - * - 'n%', where n is a positive integer value, giving the minimum as a percentage of the total count of items upstream - * - * @package iTopORM - */ -class AttributeRedundancySettings extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return array('sql', 'relation_code', 'from_class', 'neighbour_id', 'enabled', 'enabled_mode', 'min_up', 'min_up_type', 'min_up_mode'); - } - - public function GetValuesDef() {return null;} - public function GetPrerequisiteAttributes($sClass = null) {return array();} - - public function GetEditClass() {return "RedundancySetting";} - protected function GetSQLCol($bFullSpec = false) - { - return "VARCHAR(20)" - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - - public function GetValidationPattern() - { - return "^[0-9]{1,3}|[0-9]{1,2}%|disabled$"; - } - - public function GetMaxSize() - { - return 20; - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - $sRet = 'disabled'; - if ($this->Get('enabled')) - { - if ($this->Get('min_up_type') == 'count') - { - $sRet = (string) $this->Get('min_up'); - } - else // percent - { - $sRet = $this->Get('min_up').'%'; - } - } - return $sRet; - } - - public function IsNullAllowed() - { - return false; - } - - public function GetNullValue() - { - return ''; - } - - public function IsNull($proposedValue) - { - return ($proposedValue == ''); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) return ''; - return (string)$proposedValue; - } - - public function ScalarToSQL($value) - { - if (!is_string($value)) - { - throw new CoreException('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetHostClass(), 'attribute' => $this->GetCode())); - } - return $value; - } - - public function GetRelationQueryData() - { - foreach (MetaModel::EnumRelationQueries($this->GetHostClass(), $this->Get('relation_code'), false) as $sDummy => $aQueryInfo) - { - if ($aQueryInfo['sFromClass'] == $this->Get('from_class')) - { - if ($aQueryInfo['sNeighbour'] == $this->Get('neighbour_id')) - { - return $aQueryInfo; - } - } - } - return array(); - } - - /** - * Find the user option label - * - * @param string $sUserOption possible values : disabled|cout|percent - * @param string $sDefault - * - * @return string - * @throws \Exception - */ - public function GetUserOptionFormat($sUserOption, $sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = str_replace('_', ' ', $this->m_sCode.':'.$sUserOption.'(%1$s)'); - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, $sDefault, false); - } - return $sLabel; - } - - /** - * Override to display the value in the GUI - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - $sCurrentOption = $this->GetCurrentOption($sValue); - $sClass = $oHostObject ? get_class($oHostObject) : $this->m_sHostClass; - return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue), MetaModel::GetName($sClass)); - } - - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); - return $sTextQualifier.$sEscaped.$sTextQualifier; - } - - /** - * Helper to interpret the value, given the current settings and string representation of the attribute - */ - public function IsEnabled($sValue) - { - if ($this->get('enabled_mode') == 'fixed') - { - $bRet = $this->get('enabled'); - } - else - { - $bRet = ($sValue != 'disabled'); - } - return $bRet; - } - - /** - * Helper to interpret the value, given the current settings and string representation of the attribute - */ - public function GetMinUpType($sValue) - { - if ($this->get('min_up_mode') == 'fixed') - { - $sRet = $this->get('min_up_type'); - } - else - { - $sRet = 'count'; - if (substr(trim($sValue), -1, 1) == '%') - { - $sRet = 'percent'; - } - } - return $sRet; - } - - /** - * Helper to interpret the value, given the current settings and string representation of the attribute - */ - public function GetMinUpValue($sValue) - { - if ($this->get('min_up_mode') == 'fixed') - { - $iRet = (int) $this->Get('min_up'); - } - else - { - $sRefValue = $sValue; - if (substr(trim($sValue), -1, 1) == '%') - { - $sRefValue = substr(trim($sValue), 0, -1); - } - $iRet = (int) trim($sRefValue); - } - return $iRet; - } - - /** - * Helper to determine if the redundancy can be viewed/edited by the end-user - */ - public function IsVisible() - { - $bRet = false; - if ($this->Get('enabled_mode') == 'fixed') - { - $bRet = $this->Get('enabled'); - } - elseif ($this->Get('enabled_mode') == 'user') - { - $bRet = true; - } - return $bRet; - } - - public function IsWritable() - { - if (($this->Get('enabled_mode') == 'fixed') && ($this->Get('min_up_mode') == 'fixed')) - { - return false; - } - return true; - } - - /** - * Returns an HTML form that can be read by ReadValueFromPostedForm - */ - public function GetDisplayForm($sCurrentValue, $oPage, $bEditMode = false, $sFormPrefix = '') - { - $sRet = ''; - $aUserOptions = $this->GetUserOptions($sCurrentValue); - if (count($aUserOptions) < 2) - { - $bEditOption = false; - } - else - { - $bEditOption = $bEditMode; - } - $sCurrentOption = $this->GetCurrentOption($sCurrentValue); - foreach($aUserOptions as $sUserOption) - { - $bSelected = ($sUserOption == $sCurrentOption); - $sRet .= '
    '; - $sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption, $bSelected); - $sRet .= '
    '; - } - return $sRet; - } - - const USER_OPTION_DISABLED = 'disabled'; - const USER_OPTION_ENABLED_COUNT = 'count'; - const USER_OPTION_ENABLED_PERCENT = 'percent'; - - /** - * Depending on the xxx_mode parameters, build the list of options that are allowed to the end-user - */ - protected function GetUserOptions($sValue) - { - $aRet = array(); - if ($this->Get('enabled_mode') == 'user') - { - $aRet[] = self::USER_OPTION_DISABLED; - } - - if ($this->Get('min_up_mode') == 'user') - { - $aRet[] = self::USER_OPTION_ENABLED_COUNT; - $aRet[] = self::USER_OPTION_ENABLED_PERCENT; - } - else - { - if ($this->GetMinUpType($sValue) == 'count') - { - $aRet[] = self::USER_OPTION_ENABLED_COUNT; - } - else - { - $aRet[] = self::USER_OPTION_ENABLED_PERCENT; - } - } - return $aRet; - } - - /** - * Convert the string representation into one of the existing options - */ - protected function GetCurrentOption($sValue) - { - $sRet = self::USER_OPTION_DISABLED; - if ($this->IsEnabled($sValue)) - { - if ($this->GetMinUpType($sValue) == 'count') - { - $sRet = self::USER_OPTION_ENABLED_COUNT; - } - else - { - $sRet = self::USER_OPTION_ENABLED_PERCENT; - } - } - return $sRet; - } - - /** - * Display an option (form, or current value) - * - * @param string $sCurrentValue - * @param \WebPage $oPage - * @param string $sFormPrefix - * @param bool $bEditMode - * @param string $sUserOption - * @param bool $bSelected - * - * @return string - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \Exception - */ - protected function GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditMode, $sUserOption, $bSelected = true) - { - $sRet = ''; - - $iCurrentValue = $this->GetMinUpValue($sCurrentValue); - if ($bEditMode) - { - $sValue = null; - $sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id'); - switch ($sUserOption) - { - case self::USER_OPTION_DISABLED: - $sValue = ''; // Empty placeholder - break; - - case self::USER_OPTION_ENABLED_COUNT: - if ($bEditMode) - { - $sName = $sHtmlNamesPrefix.'_min_up_count'; - $sEditValue = $bSelected ? $iCurrentValue : ''; - $sValue = ''; - // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option) - $oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});"); - } - else - { - $sValue = $iCurrentValue; - } - break; - - case self::USER_OPTION_ENABLED_PERCENT: - if ($bEditMode) - { - $sName = $sHtmlNamesPrefix.'_min_up_percent'; - $sEditValue = $bSelected ? $iCurrentValue : ''; - $sValue = ''; - // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option) - $oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});"); - } - else - { - $sValue = $iCurrentValue; - } - break; - } - $sLabel = sprintf($this->GetUserOptionFormat($sUserOption), $sValue, MetaModel::GetName($this->GetHostClass())); - - $sOptionName = $sHtmlNamesPrefix.'_user_option'; - $sOptionId = $sOptionName.'_'.$sUserOption; - $sChecked = $bSelected ? 'checked' : ''; - $sRet = ' '; - } - else - { - // Read-only: display only the currently selected option - if ($bSelected) - { - $sRet = sprintf($this->GetUserOptionFormat($sUserOption), $iCurrentValue, MetaModel::GetName($this->GetHostClass())); - } - } - return $sRet; - } - - /** - * Makes the string representation out of the values given by the form defined in GetDisplayForm - */ - public function ReadValueFromPostedForm($sFormPrefix) - { - $sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id'); - - $iMinUpCount = (int) utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_count', null, 'raw_data'); - $iMinUpPercent = (int) utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_percent', null, 'raw_data'); - $sSelectedOption = utils::ReadPostedParam($sHtmlNamesPrefix.'_user_option', null, 'raw_data'); - switch ($sSelectedOption) - { - case self::USER_OPTION_ENABLED_COUNT: - $sRet = $iMinUpCount; - break; - - case self::USER_OPTION_ENABLED_PERCENT: - $sRet = $iMinUpPercent.'%'; - break; - - case self::USER_OPTION_DISABLED: - default: - $sRet = 'disabled'; - break; - } - return $sRet; - } -} - -/** - * Custom fields managed by an external implementation - * - * @package iTopORM - */ -class AttributeCustomFields extends AttributeDefinition -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - static public function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("handler_class")); - } - - public function GetEditClass() {return "CustomFields";} - public function IsWritable() {return true;} - static public function LoadFromDB() {return false;} // See ReadValue... - - public function GetDefaultValue(DBObject $oHostObject = null) - { - return new ormCustomFieldsValue($oHostObject, $this->GetCode()); - } - - public function GetBasicFilterOperators() {return array();} - public function GetBasicFilterLooseOperator() {return '';} - public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} - - /** - * @param DBObject $oHostObject - * @param array|null $aValues - * @return CustomFieldsHandler - */ - public function GetHandler($aValues = null) - { - $sHandlerClass = $this->Get('handler_class'); - $oHandler = new $sHandlerClass($this->GetCode()); - if (!is_null($aValues)) - { - $oHandler->SetCurrentValues($aValues); - } - return $oHandler; - } - - public function GetPrerequisiteAttributes($sClass = null) - { - $sHandlerClass = $this->Get('handler_class'); - return $sHandlerClass::GetPrerequisiteAttributes($sClass); - } - - public function GetEditValue($sValue, $oHostObj = null) - { - return $this->GetForTemplate($sValue, '', $oHostObj, true); - } - - /** - * Makes the string representation out of the values given by the form defined in GetDisplayForm - */ - public function ReadValueFromPostedForm($oHostObject, $sFormPrefix) - { - $aRawData = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->GetCode()}", '{}', 'raw_data'), true); - return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aRawData); - } - - public function MakeRealValue($proposedValue, $oHostObject) - { - if (is_object($proposedValue) && ($proposedValue instanceof ormCustomFieldsValue)) - { - return $proposedValue; - } - elseif (is_string($proposedValue)) - { - $aValues = json_decode($proposedValue, true); - return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); - } - elseif (is_array($proposedValue)) - { - return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $proposedValue); - } - elseif (is_null($proposedValue)) - { - return new ormCustomFieldsValue($oHostObject, $this->GetCode()); - } - throw new Exception('Unexpected type for the value of a custom fields attribute: '.gettype($proposedValue)); - } - - static public function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\SubFormField'; - } - - /** - * Override to build the relevant form field - * - * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behaves more like a Prepare. - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - $oFormField->SetForm($this->GetForm($oObject)); - } - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - /** - * @param DBObject $oHostObject - * @param null $sFormPrefix - * @return Combodo\iTop\Form\Form - * @throws \Exception - */ - public function GetForm(DBObject $oHostObject, $sFormPrefix = null) - { - try - { - $oValue = $oHostObject->Get($this->GetCode()); - $oHandler = $this->GetHandler($oValue->GetValues()); - $sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode(); - $oHandler->BuildForm($oHostObject, $sFormId); - $oForm = $oHandler->GetForm(); - } - catch (Exception $e) - { - $oForm = new \Combodo\iTop\Form\Form(''); - $oField = new \Combodo\iTop\Form\Field\LabelField(''); - $oField->SetLabel('Custom field error: '.$e->getMessage()); - $oForm->AddField($oField); - $oForm->Finalize(); - } - return $oForm; - } - - /** - * Read the data from where it has been stored. This verb must be implemented as soon as LoadFromDB returns false and LoadInObject returns true - * @param $oHostObject - * @return ormCustomFieldsValue - */ - public function ReadValue($oHostObject) - { - try - { - $oHandler = $this->GetHandler(); - $aValues = $oHandler->ReadValues($oHostObject); - $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); - } - catch (Exception $e) - { - $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode()); - } - return $oRet; - } - - /** - * Record the data (currently in the processing of recording the host object) - * It is assumed that the data has been checked prior to calling Write() - * @param DBObject $oHostObject - * @param ormCustomFieldsValue|null $oValue (null is the default value) - */ - public function WriteValue(DBObject $oHostObject, ormCustomFieldsValue $oValue = null) - { - if (is_null($oValue)) - { - $oHandler = $this->GetHandler(); - $aValues = array(); - } - else - { - // Pass the values through the form to make sure that they are correct - $oHandler = $this->GetHandler($oValue->GetValues()); - $oHandler->BuildForm($oHostObject, ''); - $oForm = $oHandler->GetForm(); - $aValues = $oForm->GetCurrentValues(); - } - return $oHandler->WriteValues($oHostObject, $aValues); - } - - /** - * The part of the current attribute in the object's signature, for the supplied value - * @param ormCustomFieldsValue $value The value of this attribute for the object - * @return string The "signature" for this field/attribute - */ - public function Fingerprint($value) - { - $oHandler = $this->GetHandler($value->GetValues()); - return $oHandler->GetValueFingerprint(); - } - - /** - * Check the validity of the data - * @param DBObject $oHostObject - * @param $value - * @return bool|string true or error message - */ - public function CheckValue(DBObject $oHostObject, $value) - { - try - { - $oHandler = $this->GetHandler($value->GetValues()); - $oHandler->BuildForm($oHostObject, ''); - $oForm = $oHandler->GetForm(); - $oForm->Validate(); - if ($oForm->GetValid()) - { - $ret = true; - } - else - { - $aMessages = array(); - foreach ($oForm->GetErrorMessages() as $sFieldId => $aFieldMessages) - { - $aMessages[] = $sFieldId.': '.implode(', ', $aFieldMessages); - } - $ret = 'Invalid value: '.implode(', ', $aMessages); - } - } - catch (Exception $e) - { - $ret = $e->getMessage(); - } - return $ret; - } - - /** - * Cleanup data upon object deletion (object id still available here) - * @param DBObject $oHostObject - * @return - * @throws \CoreException - */ - public function DeleteValue(DBObject $oHostObject) - { - $oValue = $oHostObject->Get($this->GetCode()); - $oHandler = $this->GetHandler($oValue->GetValues()); - return $oHandler->DeleteValues($oHostObject); - } - - public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) - { - try - { - $sRet = $value->GetAsHTML($bLocalize); - } - catch (Exception $e) - { - $sRet = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); - } - return $sRet; - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - try - { - $sRet = $value->GetAsXML($bLocalize); - } - catch (Exception $e) - { - $sRet = Str::pure2xml('Custom field error: '.$e->getMessage()); - } - return $sRet; - } - - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) - { - try - { - $sRet = $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText); - } - catch (Exception $e) - { - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, 'Custom field error: '.$e->getMessage()); - $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; - } - return $sRet; - } - - /** - * List the available verbs for 'GetForTemplate' - */ - public function EnumTemplateVerbs() - { - $sHandlerClass = $this->Get('handler_class'); - return $sHandlerClass::EnumTemplateVerbs(); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * - * @param $value mixed The current value of the field - * @param $sVerb string The verb specifying the representation of the value - * @param $oHostObject DBObject The object - * @param $bLocalize bool Whether or not to localize the value - * - * @return string - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - try - { - $sRet = $value->GetForTemplate($sVerb, $bLocalize); - } - catch (Exception $e) - { - $sRet = 'Custom field error: '.$e->getMessage(); - } - return $sRet; - } - - public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) - { - return null; - } - - /** - * Helper to get a value that will be JSON encoded - * The operation is the opposite to FromJSONToValue - * - * @param $value - * - * @return string - */ - public function GetForJSON($value) - { - return null; - } - - /** - * Helper to form a value, given JSON decoded data - * The operation is the opposite to GetForJSON - * - * @param string $json - * - * @return array - */ - public function FromJSONToValue($json) - { - return null; - } - - public function Equals($val1, $val2) - { - try - { - $bEquals = $val1->Equals($val2); - } - catch (Exception $e) - { - $bEquals = false; - } - return $bEquals; - } -} - -class AttributeArchiveFlag extends AttributeBoolean -{ - public function __construct($sCode) - { - parent::__construct($sCode, array("allowed_values" => null, "sql" => $sCode, "default_value" => false, "is_null_allowed" => false, "depends_on" => array())); - } - public function RequiresIndex() - { - return true; - } - public function CopyOnAllTables() - { - return true; - } - public function IsWritable() - { - return false; - } - public function IsMagic() - { - return true; - } - public function GetLabel($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeArchiveFlag/Label', $sDefault); - return parent::GetLabel($sDefault); - } - public function GetDescription($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeArchiveFlag/Label+', $sDefault); - return parent::GetDescription($sDefault); - } -} -class AttributeArchiveDate extends AttributeDate -{ - public function GetLabel($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeArchiveDate/Label', $sDefault); - return parent::GetLabel($sDefault); - } - public function GetDescription($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeArchiveDate/Label+', $sDefault); - return parent::GetDescription($sDefault); - } -} - -class AttributeObsolescenceFlag extends AttributeBoolean -{ - public function __construct($sCode) - { - parent::__construct($sCode, array("allowed_values"=>null, "sql"=>$sCode, "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())); - } - public function IsWritable() - { - return false; - } - public function IsMagic() - { - return true; - } - - static public function IsBasedOnDBColumns() {return false;} - /** - * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via GetOQLExpression) - * @return bool - */ - static public function IsBasedOnOQLExpression() {return true;} - public function GetOQLExpression() - { - return MetaModel::GetObsolescenceExpression($this->GetHostClass()); - } - - public function GetSQLExpressions($sPrefix = '') - { - return array(); - } - public function GetSQLColumns($bFullSpec = false) {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) - public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update) - - public function GetEditClass() {return "";} - - public function GetValuesDef() {return null;} - public function GetPrerequisiteAttributes($sClass = null) {return $this->GetOptional("depends_on", array());} - - public function IsDirectField() {return true;} - static public function IsScalar() {return true;} - public function GetSQLExpr() - { - return null; - } - - public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);} - public function IsNullAllowed() {return false;} - - public function GetLabel($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label', $sDefault); - return parent::GetLabel($sDefault); - } - public function GetDescription($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label+', $sDefault); - return parent::GetDescription($sDefault); - } -} - -class AttributeObsolescenceDate extends AttributeDate -{ - public function GetLabel($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeObsolescenceDate/Label', $sDefault); - return parent::GetLabel($sDefault); - } - public function GetDescription($sDefault = null) - { - $sDefault = Dict::S('Core:AttributeObsolescenceDate/Label+', $sDefault); - return parent::GetDescription($sDefault); - } -} + + + +/** + * Typology for the attributes + * + * @copyright Copyright (C) 2010-2018 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +require_once('MyHelpers.class.inc.php'); +require_once('ormdocument.class.inc.php'); +require_once('ormstopwatch.class.inc.php'); +require_once('ormpassword.class.inc.php'); +require_once('ormcaselog.class.inc.php'); +require_once('ormlinkset.class.inc.php'); +require_once('htmlsanitizer.class.inc.php'); +require_once(APPROOT.'sources/autoload.php'); +require_once('customfieldshandler.class.inc.php'); +require_once('ormcustomfieldsvalue.class.inc.php'); +require_once('datetimeformat.class.inc.php'); +// This should be changed to a use when we go full-namespace +require_once(APPROOT . 'sources/form/validator/validator.class.inc.php'); +require_once(APPROOT . 'sources/form/validator/notemptyextkeyvalidator.class.inc.php'); + +/** + * MissingColumnException - sent if an attribute is being created but the column is missing in the row + * + * @package iTopORM + */ +class MissingColumnException extends Exception +{} + +/** + * add some description here... + * + * @package iTopORM + */ +define('EXTKEY_RELATIVE', 1); + +/** + * add some description here... + * + * @package iTopORM + */ +define('EXTKEY_ABSOLUTE', 2); + +/** + * Propagation of the deletion through an external key - ask the user to delete the referencing object + * + * @package iTopORM + */ +define('DEL_MANUAL', 1); + +/** + * Propagation of the deletion through an external key - ask the user to delete the referencing object + * + * @package iTopORM + */ +define('DEL_AUTO', 2); +/** + * Fully silent delete... not yet implemented + */ +define('DEL_SILENT', 2); +/** + * For HierarchicalKeys only: move all the children up one level automatically + */ +define('DEL_MOVEUP', 3); + + +/** + * For Link sets: tracking_level + * + * @package iTopORM + */ +define('ATTRIBUTE_TRACKING_NONE', 0); // Do not track changes of the attribute +define('ATTRIBUTE_TRACKING_ALL', 3); // Do track all changes of the attribute +define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set +define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items +define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items +define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items + +define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from inside this object +define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object +define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu +define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place +define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place + + +/** + * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) + * + * @package iTopORM + */ +abstract class AttributeDefinition +{ + const SEARCH_WIDGET_TYPE_RAW = 'raw'; + const SEARCH_WIDGET_TYPE_STRING = 'string'; + const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric'; + const SEARCH_WIDGET_TYPE_ENUM = 'enum'; + const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key'; + const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key'; + const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field'; + const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time'; + const SEARCH_WIDGET_TYPE_DATE = 'date'; + + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + const INDEX_LENGTH = 95; + + public function GetType() + { + return Dict::S('Core:'.get_class($this)); + } + public function GetTypeDesc() + { + return Dict::S('Core:'.get_class($this).'+'); + } + + abstract public function GetEditClass(); + + /** + * Return the search widget type corresponding to this attribute + * + * @return string + */ + public function GetSearchType() + { + return static::SEARCH_WIDGET_TYPE; + } + + /** + * @return bool + */ + public function IsSearchable() + { + return static::SEARCH_WIDGET_TYPE != static::SEARCH_WIDGET_TYPE_RAW; + } + + protected $m_sCode; + private $m_aParams = array(); + protected $m_sHostClass = '!undefined!'; + public function Get($sParamName) {return $this->m_aParams[$sParamName];} + + public function GetIndexLength() { + $iMaxLength = $this->GetMaxSize(); + if (is_null($iMaxLength)) + { + return null; + } + if ($iMaxLength > static::INDEX_LENGTH) + { + return static::INDEX_LENGTH; + } + return $iMaxLength; + } + + public function IsParam($sParamName) {return (array_key_exists($sParamName, $this->m_aParams));} + + protected function GetOptional($sParamName, $default) + { + if (array_key_exists($sParamName, $this->m_aParams)) + { + return $this->m_aParams[$sParamName]; + } + else + { + return $default; + } + } + + /** + * AttributeDefinition constructor. + * + * @param string $sCode + * @param array $aParams + * + * @throws \Exception + */ + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $this->m_aParams = $aParams; + $this->ConsistencyCheck(); + } + + public function GetParams() + { + return $this->m_aParams; + } + + public function HasParam($sParam) + { + return array_key_exists($sParam, $this->m_aParams); + } + + public function SetHostClass($sHostClass) + { + $this->m_sHostClass = $sHostClass; + } + public function GetHostClass() + { + return $this->m_sHostClass; + } + + /** + * @return array + * + * @throws \CoreException + */ + public function ListSubItems() + { + $aSubItems = array(); + foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeSubItem) + { + if ($oAttDef->Get('target_attcode') == $this->m_sCode) + { + $aSubItems[$sAttCode] = $oAttDef; + } + } + } + return $aSubItems; + } + + // Note: I could factorize this code with the parameter management made for the AttributeDef class + // to be overloaded + static public function ListExpectedParams() + { + return array(); + } + + /** + * @throws \Exception + */ + private function ConsistencyCheck() + { + // Check that any mandatory param has been specified + // + $aExpectedParams = $this->ListExpectedParams(); + foreach($aExpectedParams as $sParamName) + { + if (!array_key_exists($sParamName, $this->m_aParams)) + { + $aBacktrace = debug_backtrace(); + $sTargetClass = $aBacktrace[2]["class"]; + $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; + throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); + } + } + } + + /** + * Check the validity of the given value + * + * @param \DBObject $oHostObject + * @param $value An error if any, null otherwise + * + * @return bool + */ + public function CheckValue(DBObject $oHostObject, $value) + { + // todo: factorize here the cases implemented into DBObject + return true; + } + + // table, key field, name field + public function ListDBJoins() + { + return ""; + // e.g: return array("Site", "infrid", "name"); + } + + public function GetFinalAttDef() + { + return $this; + } + + /** + * Deprecated - use IsBasedOnDBColumns instead + * @return bool + */ + public function IsDirectField() {return static::IsBasedOnDBColumns();} + + /** + * Returns true if the attribute value is built after DB columns + * @return bool + */ + static public function IsBasedOnDBColumns() {return false;} + /** + * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via GetOQLExpression) + * @return bool + */ + static public function IsBasedOnOQLExpression() {return false;} + /** + * Returns true if the attribute value can be shown as a string + * @return bool + */ + static public function IsScalar() {return false;} + /** + * Returns true if the attribute value is a set of related objects (1-N or N-N) + * @return bool + */ + static public function IsLinkSet() {return false;} + + /** + * @param int $iType + * + * @return bool true if the attribute is an external key, either directly (RELATIVE to the host class), or indirectly (ABSOLUTELY) + */ + public function IsExternalKey($iType = EXTKEY_RELATIVE) + { + return false; + } + /** + * @return bool true if the attribute value is an external key, pointing to the host class + */ + static public function IsHierarchicalKey() {return false;} + /** + * @return bool true if the attribute value is stored on an object pointed to be an external key + */ + static public function IsExternalField() {return false;} + /** + * @return bool true if the attribute can be written (by essence : metamodel field option) + * @see \DBObject::IsAttributeReadOnlyForCurrentState() for a specific object instance (depending on its workflow) + */ + public function IsWritable() {return false;} + /** + * @return bool true if the attribute has been added automatically by the framework + */ + public function IsMagic() {return $this->GetOptional('magic', false);} + /** + * @return bool true if the attribute value is kept in the loaded object (in memory) + */ + static public function LoadInObject() {return true;} + /** + * @return bool true if the attribute value comes from the database in one way or another + */ + static public function LoadFromDB() {return true;} + /** + * @return bool true if the attribute should be loaded anytime (in addition to the column selected by the user) + */ + public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);} + + /** + * @param \DBObject $oHostObject + * + * @return mixed Must return the value if LoadInObject returns false + */ + public function GetValue($oHostObject) + { + return null; + } + + /** + * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull()) + * @return bool + */ + public function IsNullAllowed() {return true;} + /** + * Returns the attribute code (identifies the attribute in the host class) + * @return string + */ + public function GetCode() {return $this->m_sCode;} + + /** + * Find the corresponding "link" attribute on the target class, if any + * @return null | AttributeDefinition + */ + public function GetMirrorLinkAttribute() {return null;} + + /** + * Helper to browse the hierarchy of classes, searching for a label + * + * @param string $sDictEntrySuffix + * @param string $sDefault + * @param bool $bUserLanguageOnly + * + * @return string + * @throws \Exception + */ + protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly) + { + $sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly); + if (strlen($sLabel) == 0) + { + // Nothing found: go higher in the hierarchy (if possible) + // + $sLabel = $sDefault; + $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); + if ($sParentClass) + { + if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) + { + $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); + $sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly); + } + } + } + return $sLabel; + } + + /** + * @param string|null $sDefault + * + * @return string + * + * @throws \Exception + */ + public function GetLabel($sDefault = null) + { + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, null, true /*user lang*/); + if (is_null($sLabel)) + { + // If no default value is specified, let's define the most relevant one for developping purposes + if (is_null($sDefault)) + { + $sDefault = str_replace('_', ' ', $this->m_sCode); + } + // Browse the hierarchy again, accepting default (english) translations + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false); + } + return $sLabel; + } + + /** + * To be overloaded for localized enums + * + * @param string $sValue + * + * @return string label corresponding to the given value (in plain text) + */ + public function GetValueLabel($sValue) + { + return $sValue; + } + + /** + * Get the value from a given string (plain text, CSV import) + * + * @param string $sProposedValue + * @param bool $bLocalizedValue + * @param string $sSepItem + * @param string $sSepAttribute + * @param string $sSepValue + * @param string $sAttributeQualifier + * + * @return mixed null if no match could be found + */ + public function MakeValueFromString( + $sProposedValue, + $bLocalizedValue = false, + $sSepItem = null, + $sSepAttribute = null, + $sSepValue = null, + $sAttributeQualifier = null + ) + { + return $this->MakeRealValue($sProposedValue, null); + } + + /** + * Parses a search string coming from user input + * @param string $sSearchString + * @return string + */ + public function ParseSearchString($sSearchString) + { + return $sSearchString; + } + + /** + * @return string + * + * @throws \Exception + */ + public function GetLabel_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('label', $this->m_aParams)) + { + return $this->m_aParams['label']; + } + else + { + return $this->GetLabel(); + } + } + + /** + * @param string|null $sDefault + * + * @return string + * + * @throws \Exception + */ + public function GetDescription($sDefault = null) + { + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', null, true /*user lang*/); + if (is_null($sLabel)) + { + // If no default value is specified, let's define the most relevant one for developping purposes + if (is_null($sDefault)) + { + $sDefault = ''; + } + // Browse the hierarchy again, accepting default (english) translations + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false); + } + return $sLabel; + } + + /** + * @param string|null $sDefault + * + * @return string + * + * @throws \Exception + */ + public function GetHelpOnEdition($sDefault = null) + { + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', null, true /*user lang*/); + if (is_null($sLabel)) + { + // If no default value is specified, let's define the most relevant one for developping purposes + if (is_null($sDefault)) + { + $sDefault = ''; + } + // Browse the hierarchy again, accepting default (english) translations + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false); + } + return $sLabel; + } + + public function GetHelpOnSmartSearch() + { + $aParents = array_merge(array(get_class($this) => get_class($this)), class_parents($this)); + foreach ($aParents as $sClass) + { + $sHelp = Dict::S("Core:$sClass?SmartSearch", '-missing-'); + if ($sHelp != '-missing-') + { + return $sHelp; + } + } + return ''; + } + + /** + * @return string + * + * @throws \Exception + */ + public function GetDescription_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('description', $this->m_aParams)) + { + return $this->m_aParams['description']; + } + else + { + return $this->GetDescription(); + } + } + + public function GetTrackingLevel() + { + return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL); + } + + /** + * @return \ValueSetObjects + */ + public function GetValuesDef() {return null;} + + public function GetPrerequisiteAttributes($sClass = null) + { + return array(); + } + + public function GetNullValue() {return null;} + + public function IsNull($proposedValue) + { + return is_null($proposedValue); + } + + /** + * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! + * + * @param $proposedValue + * @param $oHostObj + * + * @return mixed + */ + public function MakeRealValue($proposedValue, $oHostObj) + { + return $proposedValue; + } + + public function Equals($val1, $val2) {return ($val1 == $val2);} + + /** + * @param string $sPrefix + * + * @return array suffix/expression pairs (1 in most of the cases), for READING (Select) + */ + public function GetSQLExpressions($sPrefix = '') + { + return array(); + } + + /** + * @param array $aCols + * @param string $sPrefix + * + * @return mixed a value out of suffix/value pairs, for SELECT result interpretation + */ + public function FromSQLToValue($aCols, $sPrefix = '') + { + return null; + } + + /** + * @param bool $bFullSpec + * + * @return array column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) + * @see \CMDBSource::GetFieldSpec() + */ + public function GetSQLColumns($bFullSpec = false) + { + return array(); + } + + /** + * @param $value + * + * @return array column/value pairs (1 in most of the cases), for WRITING (Insert, Update) + */ + public function GetSQLValues($value) + { + return array(); + } + public function RequiresIndex() {return false;} + + public function RequiresFullTextIndex() + { + return false; + } + public function CopyOnAllTables() {return false;} + + public function GetOrderBySQLExpressions($sClassAlias) + { + // Note: This is the responsibility of this function to place backticks around column aliases + return array('`'.$sClassAlias.$this->GetCode().'`'); + } + + public function GetOrderByHint() + { + return ''; + } + + // Import - differs slightly from SQL input, but identical in most cases + // + public function GetImportColumns() {return $this->GetSQLColumns();} + public function FromImportToValue($aCols, $sPrefix = '') + { + $aValues = array(); + foreach ($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr) + { + // This is working, based on the assumption that importable fields + // are not computed fields => the expression is the name of a column + $aValues[$sPrefix.$sAlias] = $aCols[$sExpr]; + } + return $this->FromSQLToValue($aValues, $sPrefix); + } + + public function GetValidationPattern() + { + return ''; + } + + public function CheckFormat($value) + { + return true; + } + + public function GetMaxSize() + { + return null; + } + + public function MakeValue() + { + $sComputeFunc = $this->Get("compute_func"); + if (empty($sComputeFunc)) return null; + + return call_user_func($sComputeFunc); + } + + abstract public function GetDefaultValue(DBObject $oHostObject = null); + + // + // To be overloaded in subclasses + // + + abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description" + abstract public function GetBasicFilterLooseOperator(); // returns an "opCode" + //abstract protected GetBasicFilterHTMLInput(); + abstract public function GetBasicFilterSQLExpr($sOpCode, $value); + + public function GetFilterDefinitions() + { + return array(); + } + + public function GetEditValue($sValue, $oHostObj = null) + { + return (string)$sValue; + } + + /** + * For fields containing a potential markup, return the value without this markup + * + * @param string $sValue + * @param \DBObject $oHostObj + * + * @return string + */ + public function GetAsPlainText($sValue, $oHostObj = null) + { + return (string) $this->GetEditValue($sValue, $oHostObj); + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + * + * @param $value + * + * @return string + */ + public function GetForJSON($value) + { + // In most of the cases, that will be the expected behavior... + return $this->GetEditValue($value); + } + + /** + * Helper to form a value, given JSON decoded data + * The operation is the opposite to GetForJSON + * + * @param $json + * + * @return mixed + */ + public function FromJSONToValue($json) + { + // Passthrough in most of the cases + return $json; + } + + /** + * Override to display the value in the GUI + * + * @param string $sValue + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return string + */ + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + return Str::pure2html((string)$sValue); + } + + /** + * Override to export the value in XML + * + * @param string $sValue + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return mixed + */ + public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) + { + return Str::pure2xml((string)$sValue); + } + + /** + * Override to escape the value when read by DBObject::GetAsCSV() + * + * @param string $sValue + * @param string $sSeparator + * @param string $sTextQualifier + * @param \DBObject $oHostObject + * @param bool $bLocalize + * @param bool $bConvertToPlainText + * + * @return string + */ + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + return (string)$sValue; + } + + /** + * Override to differentiate a value displayed in the UI or in the history + * + * @param string $sValue + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return string + */ + public function GetAsHTMLForHistory($sValue, $oHostObject = null, $bLocalize = true) + { + return $this->GetAsHTML($sValue, $oHostObject, $bLocalize); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\StringField'; + } + + /** + * Override to specify Field class + * + * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is + * passed, MakeFormField behave more like a Prepare. + * + * @param \DBObject $oObject + * @param \Combodo\iTop\Form\Field\Field $oFormField + * + * @return null + * @throws \CoreException + * @throws \Exception + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + // This is a fallback in case the AttributeDefinition subclass has no overloading of this function. + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + //$oFormField->SetReadOnly(true); + } + + $oFormField->SetLabel($this->GetLabel()); + + // Attributes flags + // - Retrieving flags for the current object + if ($oObject->IsNew()) + { + $iFlags = $oObject->GetInitialStateAttributeFlags($this->GetCode()); + } + else + { + $iFlags = $oObject->GetAttributeFlags($this->GetCode()); + } + + // - Comparing flags + if ($this->IsWritable() && (!$this->IsNullAllowed() || (($iFlags & OPT_ATT_MANDATORY) === OPT_ATT_MANDATORY))) + { + $oFormField->SetMandatory(true); + } + if ((!$oObject->IsNew() || !$oFormField->GetMandatory()) && (($iFlags & OPT_ATT_READONLY) === OPT_ATT_READONLY)) + { + $oFormField->SetReadOnly(true); + } + + // CurrentValue + $oFormField->SetCurrentValue($oObject->Get($this->GetCode())); + + // Validation pattern + if ($this->GetValidationPattern() !== '') + { + $oFormField->AddValidator(new \Combodo\iTop\Form\Validator\Validator($this->GetValidationPattern())); + } + + return $oFormField; + } + + /** + * List the available verbs for 'GetForTemplate' + */ + public function EnumTemplateVerbs() + { + return array( + '' => 'Plain text (unlocalized) representation', + 'html' => 'HTML representation', + 'label' => 'Localized representation', + 'text' => 'Plain text representation (without any markup)', + ); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * + * @param mixed $value The current value of the field + * @param string $sVerb The verb specifying the representation of the value + * @param \DBObject $oHostObject + * @param bool $bLocalize Whether or not to localize the value + * + * @return mixed|null|string + * + * @throws \Exception + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + if ($this->IsScalar()) + { + switch ($sVerb) + { + case '': + return $value; + + case 'html': + return $this->GetAsHtml($value, $oHostObject, $bLocalize); + + case 'label': + return $this->GetEditValue($value); + + case 'text': + return $this->GetAsPlainText($value); + break; + + default: + throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); + } + } + return null; + } + + /** + * @param array $aArgs + * @param string $sContains + * + * @return array|null + * @throws \CoreException + * @throws \OQLException + */ + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $oValSetDef = $this->GetValuesDef(); + if (!$oValSetDef) return null; + return $oValSetDef->GetValues($aArgs, $sContains); + } + + /** + * Explain the change of the attribute (history) + * + * @param string $sOldValue + * @param string $sNewValue + * @param string $sLabel + * + * @return string + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \OQLException + * @throws \Exception + */ + public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) + { + if (is_null($sLabel)) + { + $sLabel = $this->GetLabel(); + } + + $sNewValueHtml = $this->GetAsHTMLForHistory($sNewValue); + $sOldValueHtml = $this->GetAsHTMLForHistory($sOldValue); + + if($this->IsExternalKey()) + { + /** @var \AttributeExternalKey $this */ + $sTargetClass = $this->GetTargetClass(); + $sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null; + $sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null; + } + if ( (($this->GetType() == 'String') || ($this->GetType() == 'Text')) && + (strlen($sNewValue) > strlen($sOldValue)) ) + { + // Check if some text was not appended to the field + if (substr($sNewValue,0, strlen($sOldValue)) == $sOldValue) // Text added at the end + { + $sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue))); + $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); + } + else if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning + { + $sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue))); + $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); + } + else + { + if (strlen($sOldValue) == 0) + { + $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); + } + else + { + if (is_null($sNewValue)) + { + $sNewValueHtml = Dict::S('UI:UndefinedObject'); + } + $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, $sOldValueHtml); + } + } + } + else + { + if (strlen($sOldValue) == 0) + { + $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); + } + else + { + if (is_null($sNewValue)) + { + $sNewValueHtml = Dict::S('UI:UndefinedObject'); + } + $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, $sOldValueHtml); + } + } + return $sResult; + } + + + /** + * Parses a string to find some smart search patterns and build the corresponding search/OQL condition + * Each derived class is reponsible for defining and processing their own smart patterns, the base class + * does nothing special, and just calls the default (loose) operator + * + * @param string $sSearchText The search string to analyze for smart patterns + * @param \FieldExpression $oField + * @param array $aParams Values of the query parameters + * + * @return \Expression The search condition to be added (AND) to the current search + * + * @throws \CoreException + */ + public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams) + { + $sParamName = $oField->GetParent().'_'.$oField->GetName(); + $oRightExpr = new VariableExpression($sParamName); + $sOperator = $this->GetBasicFilterLooseOperator(); + switch ($sOperator) + { + case 'Contains': + $aParams[$sParamName] = "%$sSearchText%"; + $sSQLOperator = 'LIKE'; + break; + + default: + $sSQLOperator = $sOperator; + $aParams[$sParamName] = $sSearchText; + } + $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); + return $oNewCondition; + } + + /** + * Tells if an attribute is part of the unique fingerprint of the object (used for comparing two objects) + * All attributes which value is not based on a value from the object itself (like ExternalFields or LinkedSet) + * must be excluded from the object's signature + * @return boolean + */ + public function IsPartOfFingerprint() + { + return true; + } + + /** + * The part of the current attribute in the object's signature, for the supplied value + * @param mixed $value The value of this attribute for the object + * @return string The "signature" for this field/attribute + */ + public function Fingerprint($value) + { + return (string)$value; + } +} + +/** + * Set of objects directly linked to an object, and being part of its definition + * + * @package iTopORM + */ +class AttributeLinkedSet extends AttributeDefinition +{ + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max")); + } + + public function GetEditClass() {return "LinkedSet";} + + public function IsWritable() {return true;} + static public function IsLinkSet() {return true;} + public function IsIndirect() {return false;} + + public function GetValuesDef() {return $this->Get("allowed_values");} + public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");} + + /** + * @param \DBObject|null $oHostObject + * + * @return \ormLinkSet + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreWarning + */ + public function GetDefaultValue(DBObject $oHostObject = null) + { + $sLinkClass = $this->GetLinkedClass(); + $sExtKeyToMe = $this->GetExtKeyToMe(); + + // The class to target is not the current class, because if this is a derived class, + // it may differ from the target class, then things start to become confusing + /** @var \AttributeExternalKey $oRemoteExtKeyAtt */ + $oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe); + $sMyClass = $oRemoteExtKeyAtt->GetTargetClass(); + + $oMyselfSearch = new DBObjectSearch($sMyClass); + if ($oHostObject !== null) + { + $oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '='); + } + + $oLinkSearch = new DBObjectSearch($sLinkClass); + $oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe); + if ($this->IsIndirect()) + { + // Join the remote class so that the archive flag will be taken into account + /** @var \AttributeLinkedSetIndirect $this */ + $sExtKeyToRemote = $this->GetExtKeyToRemote(); + /** @var \AttributeExternalKey $oExtKeyToRemote */ + $oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote); + $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); + if (MetaModel::IsArchivable($sRemoteClass)) + { + $oRemoteSearch = new DBObjectSearch($sRemoteClass); + /** @var \AttributeLinkedSetIndirect $this */ + $oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote()); + } + } + $oLinks = new DBObjectSet($oLinkSearch); + $oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks); + return $oLinkSet; + } + + public function GetTrackingLevel() + { + return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default')); + } + + public function GetEditMode() + { + return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS); + } + + public function GetLinkedClass() {return $this->Get('linked_class');} + public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');} + + public function GetBasicFilterOperators() {return array();} + public function GetBasicFilterLooseOperator() {return '';} + public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} + + /** + * @param string $sValue + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return string|null + * + * @throws \CoreException + */ + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if (is_object($sValue) && ($sValue instanceof ormLinkSet)) + { + $sValue->Rewind(); + $aItems = array(); + while ($oObj = $sValue->Fetch()) + { + // Show only relevant information (hide the external key to the current object) + $aAttributes = array(); + foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) + { + if ($sAttCode == $this->GetExtKeyToMe()) continue; + if ($oAttDef->IsExternalField()) continue; + $sAttValue = $oObj->GetAsHTML($sAttCode); + if (strlen($sAttValue) > 0) + { + $aAttributes[] = $sAttValue; + } + } + $sAttributes = implode(', ', $aAttributes); + $aItems[] = $sAttributes; + } + return implode('
    ', $aItems); + } + return null; + } + + /** + * @param string $sValue + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return string + * + * @throws \CoreException + */ + public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) + { + if (is_object($sValue) && ($sValue instanceof ormLinkSet)) + { + $sValue->Rewind(); + $sRes = "\n"; + while ($oObj = $sValue->Fetch()) + { + $sObjClass = get_class($oObj); + $sRes .= "<$sObjClass id=\"".$oObj->GetKey()."\">\n"; + // Show only relevant information (hide the external key to the current object) + foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) + { + if ($sAttCode == 'finalclass') + { + if ($sObjClass == $this->GetLinkedClass()) + { + // Simplify the output if the exact class could be determined implicitely + continue; + } + } + if ($sAttCode == $this->GetExtKeyToMe()) continue; + if ($oAttDef->IsExternalField()) + { + /** @var \AttributeExternalField $oAttDef */ + if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) continue; + /** @var AttributeExternalField $oAttDef */ + if ($oAttDef->IsFriendlyName()) continue; + } + if ($oAttDef instanceof AttributeFriendlyName) continue; + if (!$oAttDef->IsScalar()) continue; + $sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize); + $sRes .= "<$sAttCode>$sAttValue\n"; + } + $sRes .= "\n"; + } + $sRes .= "\n"; + } + else + { + $sRes = ''; + } + return $sRes; + } + + /** + * @param $sValue + * @param string $sSeparator + * @param string $sTextQualifier + * @param \DBObject $oHostObject + * @param bool $bLocalize + * @param bool $bConvertToPlainText + * + * @return mixed|string + * @throws \CoreException + */ + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); + $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); + $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); + $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); + + if (is_object($sValue) && ($sValue instanceof ormLinkSet)) + { + $sValue->Rewind(); + $aItems = array(); + while ($oObj = $sValue->Fetch()) + { + $sObjClass = get_class($oObj); + // Show only relevant information (hide the external key to the current object) + $aAttributes = array(); + foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) + { + if ($sAttCode == 'finalclass') + { + if ($sObjClass == $this->GetLinkedClass()) + { + // Simplify the output if the exact class could be determined implicitely + continue; + } + } + if ($sAttCode == $this->GetExtKeyToMe()) continue; + if ($oAttDef->IsExternalField()) continue; + if (!$oAttDef->IsBasedOnDBColumns()) continue; + if (!$oAttDef->IsScalar()) continue; + $sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize); + if (strlen($sAttValue) > 0) + { + $sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier, $sAttCode.$sSepValue.$sAttValue); + $aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier; + } + } + $sAttributes = implode($sSepAttribute, $aAttributes); + $aItems[] = $sAttributes; + } + $sRes = implode($sSepItem, $aItems); + } + else + { + $sRes = ''; + } + $sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes); + $sRes = $sTextQualifier.$sRes.$sTextQualifier; + return $sRes; + } + + /** + * List the available verbs for 'GetForTemplate' + */ + public function EnumTemplateVerbs() + { + return array( + '' => 'Plain text (unlocalized) representation', + 'html' => 'HTML representation (unordered list)', + ); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * + * @param mixed $value The current value of the field + * @param string $sVerb The verb specifying the representation of the value + * @param DBObject $oHostObject The object + * @param bool $bLocalize Whether or not to localize the value + * + * @return string + * @throws \Exception + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + $sRemoteName = $this->IsIndirect() ? + /** @var \AttributeLinkedSetIndirect $this */ + $this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname'; + + $oLinkSet = clone $value; // Workaround/Safety net for Trac #887 + $iLimit = MetaModel::GetConfig()->Get('max_linkset_output'); + $iCount = 0; + $aNames = array(); + foreach($oLinkSet as $oItem) + { + if (($iLimit > 0) && ($iCount == $iLimit)) + { + $iTotal = $oLinkSet->Count(); + $aNames[] = '... '.Dict::Format('UI:TruncatedResults', $iCount, $iTotal); + break; + } + $aNames[] = $oItem->Get($sRemoteName); + $iCount++; + } + + switch($sVerb) + { + case '': + return implode("\n", $aNames); + + case 'html': + return '
    • '.implode("
    • ", $aNames).'
    '; + + default: + throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); + } + } + + public function DuplicatesAllowed() {return false;} // No duplicates for 1:n links, never + + public function GetImportColumns() + { + $aColumns = array(); + $aColumns[$this->GetCode()] = 'TEXT'; + return $aColumns; + } + + /** + * @param string $sProposedValue + * @param bool $bLocalizedValue + * @param string $sSepItem + * @param string $sSepAttribute + * @param string $sSepValue + * @param string $sAttributeQualifier + * + * @return \DBObjectSet|mixed + * @throws \CSVParserException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \Exception + */ + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + if (is_null($sSepItem) || empty($sSepItem)) + { + $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); + } + if (is_null($sSepAttribute) || empty($sSepAttribute)) + { + $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); + } + if (is_null($sSepValue) || empty($sSepValue)) + { + $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); + } + if (is_null($sAttributeQualifier) || empty($sAttributeQualifier)) + { + $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); + } + + $sTargetClass = $this->Get('linked_class'); + + $sInput = str_replace($sSepItem, "\n", $sProposedValue); + $oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier); + + $aInput = $oCSVParser->ToArray(0 /* do not skip lines */); + + $aLinks = array(); + foreach($aInput as $aRow) + { + // 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value + $aExtKeys = array(); + $aValues = array(); + foreach($aRow as $sCell) + { + $iSepPos = strpos($sCell, $sSepValue); + if ($iSepPos === false) + { + // Houston... + throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell)); + } + + $sAttCode = trim(substr($sCell, 0, $iSepPos)); + $sValue = substr($sCell, $iSepPos + strlen($sSepValue)); + + if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches)) + { + $sKeyAttCode = $aMatches[1]; + $sRemoteAttCode = $aMatches[2]; + $aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue; + if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode)) + { + throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sKeyAttCode)); + } + /** @var \AttributeExternalKey $oKeyAttDef */ + $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); + $sRemoteClass = $oKeyAttDef->GetTargetClass(); + if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode)) + { + throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode)); + } + } + else + { + if(!MetaModel::IsValidAttCode($sTargetClass, $sAttCode)) + { + throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sAttCode)); + } + $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode); + $aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); + } + } + + // 2nd - Instanciate the object and set the value + if (isset($aValues['finalclass'])) + { + $sLinkClass = $aValues['finalclass']; + if (!is_subclass_of($sLinkClass, $sTargetClass)) + { + throw new CoreException('Wrong class for link attribute specification', array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); + } + } + elseif (MetaModel::IsAbstract($sTargetClass)) + { + throw new CoreException('Missing finalclass for link attribute specification'); + } + else + { + $sLinkClass = $sTargetClass; + } + + $oLink = MetaModel::NewObject($sLinkClass); + foreach ($aValues as $sAttCode => $sValue) + { + $oLink->Set($sAttCode, $sValue); + } + + // 3rd - Set external keys from search conditions + foreach ($aExtKeys as $sKeyAttCode => $aReconciliation) + { + $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); + $sKeyClass = $oKeyAttDef->GetTargetClass(); + $oExtKeyFilter = new DBObjectSearch($sKeyClass); + $aReconciliationDesc = array(); + foreach($aReconciliation as $sRemoteAttCode => $sValue) + { + $oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '='); + $aReconciliationDesc[] = "$sRemoteAttCode=$sValue"; + } + $oExtKeySet = new DBObjectSet($oExtKeyFilter); + switch($oExtKeySet->Count()) + { + case 0: + $sReconciliationDesc = implode(', ', $aReconciliationDesc); + throw new CoreException("Found no match", array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); + break; + case 1: + $oRemoteObj = $oExtKeySet->Fetch(); + $oLink->Set($sKeyAttCode, $oRemoteObj->GetKey()); + break; + default: + $sReconciliationDesc = implode(', ', $aReconciliationDesc); + throw new CoreException("Found several matches", array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); + // Found several matches, ambiguous + } + } + + // Check (roughly) if such a link is valid + $aErrors = array(); + foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsExternalKey()) + { + /** @var \AttributeExternalKey $oAttDef */ + if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), $oAttDef->GetTargetClass()))) + { + continue; // Don't check the key to self + } + } + + if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) + { + $aErrors[] = $sAttCode; + } + } + if (count($aErrors) > 0) + { + throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); + } + + $aLinks[] = $oLink; + } + $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); + return $oSet; + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + * + * @param \ormLinkSet $value + * + * @return array + * @throws \CoreException + */ + public function GetForJSON($value) + { + $aRet = array(); + if (is_object($value) && ($value instanceof ormLinkSet)) + { + $value->Rewind(); + while ($oObj = $value->Fetch()) + { + $sObjClass = get_class($oObj); + // Show only relevant information (hide the external key to the current object) + $aAttributes = array(); + foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) + { + if ($sAttCode == 'finalclass') + { + if ($sObjClass == $this->GetLinkedClass()) + { + // Simplify the output if the exact class could be determined implicitely + continue; + } + } + if ($sAttCode == $this->GetExtKeyToMe()) continue; + if ($oAttDef->IsExternalField()) continue; + if (!$oAttDef->IsBasedOnDBColumns()) continue; + if (!$oAttDef->IsScalar()) continue; + $attValue = $oObj->Get($sAttCode); + $aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue); + } + $aRet[] = $aAttributes; + } + } + return $aRet; + } + + /** + * Helper to form a value, given JSON decoded data + * The operation is the opposite to GetForJSON + * + * @param $json + * + * @return \DBObjectSet + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \Exception + */ + public function FromJSONToValue($json) + { + $sTargetClass = $this->Get('linked_class'); + + $aLinks = array(); + foreach($json as $aValues) + { + if (isset($aValues['finalclass'])) + { + $sLinkClass = $aValues['finalclass']; + if (!is_subclass_of($sLinkClass, $sTargetClass)) + { + throw new CoreException('Wrong class for link attribute specification', array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); + } + } + elseif (MetaModel::IsAbstract($sTargetClass)) + { + throw new CoreException('Missing finalclass for link attribute specification'); + } + else + { + $sLinkClass = $sTargetClass; + } + + $oLink = MetaModel::NewObject($sLinkClass); + foreach ($aValues as $sAttCode => $sValue) + { + $oLink->Set($sAttCode, $sValue); + } + + // Check (roughly) if such a link is valid + $aErrors = array(); + foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsExternalKey()) + { + /** @var AttributeExternalKey $oAttDef */ + if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), $oAttDef->GetTargetClass()))) + { + continue; // Don't check the key to self + } + } + + if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) + { + $aErrors[] = $sAttCode; + } + } + if (count($aErrors) > 0) + { + throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); + } + + $aLinks[] = $oLink; + } + $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); + return $oSet; + } + + /** + * @param $proposedValue + * @param $oHostObj + * + * @return mixed + * @throws \Exception + */ + public function MakeRealValue($proposedValue, $oHostObj){ + if($proposedValue === null) + { + $sLinkedClass = $this->GetLinkedClass(); + $aLinkedObjectsArray = array(); + $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); + + return new ormLinkSet( + get_class($oHostObj), + $this->GetCode(), + $oSet + ); + } + + return $proposedValue; + } + + /** + * @param ormLinkSet $val1 + * @param ormLinkSet $val2 + * @return bool + */ + public function Equals($val1, $val2) + { + if ($val1 === $val2) + { + $bAreEquivalent = true; + } + else + { + $bAreEquivalent = ($val2->HasDelta() === false); + } + return $bAreEquivalent; + } + + /** + * Find the corresponding "link" attribute on the target class, if any + * + * @return null | AttributeDefinition + * @throws \Exception + */ + public function GetMirrorLinkAttribute() + { + $oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe()); + return $oRemoteAtt; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField'; + } + + /** + * @param \DBObject $oObject + * @param \Combodo\iTop\Form\Field\LinkedSetField $oFormField + * + * @return \Combodo\iTop\Form\Field\LinkedSetField + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \Exception + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + // Setting target class + if (!$this->IsIndirect()) + { + $sTargetClass = $this->GetLinkedClass(); + } + else + { + /** @var \AttributeExternalKey $oRemoteAttDef */ + /** @var \AttributeLinkedSetIndirect $this */ + $oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); + $sTargetClass = $oRemoteAttDef->GetTargetClass(); + + /** @var \AttributeLinkedSetIndirect $this */ + $oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote()); + } + $oFormField->SetTargetClass($sTargetClass); + $oFormField->SetIndirect($this->IsIndirect()); + // Setting attcodes to display + $aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list')); + // - Adding friendlyname attribute to the list is not already in it + $sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass); + if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) + { + $aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay); + } + // - Adding attribute labels + $aAttributesToDisplay = array(); + foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) + { + $oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay); + $aAttributesToDisplay[$sAttCodeToDisplay] = $oAttDefToDisplay->GetLabel(); + } + $oFormField->SetAttributesToDisplay($aAttributesToDisplay); + + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + public function IsPartOfFingerprint() { return false; } +} + +/** + * Set of objects linked to an object (n-n), and being part of its definition + * + * @package iTopORM + */ +class AttributeLinkedSetIndirect extends AttributeLinkedSet +{ + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote")); + } + + public function IsIndirect() + { + return true; + } + + public function GetExtKeyToRemote() { return $this->Get('ext_key_to_remote'); } + public function GetEditClass() {return "LinkedSet";} + public function DuplicatesAllowed() {return $this->GetOptional("duplicates", false);} // The same object may be linked several times... or not... + + public function GetTrackingLevel() + { + return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default')); + } + + /** + * Find the corresponding "link" attribute on the target class, if any + * @return null | AttributeDefinition + * @throws \CoreException + */ + public function GetMirrorLinkAttribute() + { + $oRet = null; + /** @var \AttributeExternalKey $oExtKeyToRemote */ + $oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); + $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); + foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) + { + if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect) continue; + if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass()) continue; + if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote()) continue; + if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe()) continue; + $oRet = $oRemoteAttDef; + break; + } + return $oRet; + } +} + +/** + * Abstract class implementing default filters for a DB column + * + * @package iTopORM + */ +class AttributeDBFieldVoid extends AttributeDefinition +{ + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql")); + } + + // To be overriden, used in GetSQLColumns + protected function GetSQLCol($bFullSpec = false) + { + return 'VARCHAR(255)' + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? $this->GetSQLColSpec() : ''); + } + protected function GetSQLColSpec() + { + $default = $this->ScalarToSQL($this->GetDefaultValue()); + if (is_null($default)) + { + $sRet = ''; + } + else + { + if (is_numeric($default)) + { + // Though it is a string in PHP, it will be considered as a numeric value in MySQL + // Then it must not be quoted here, to preserve the compatibility with the value returned by CMDBSource::GetFieldSpec + $sRet = " DEFAULT $default"; + } + else + { + $sRet = " DEFAULT ".CMDBSource::Quote($default); + } + } + return $sRet; + } + + public function GetEditClass() {return "String";} + + public function GetValuesDef() {return $this->Get("allowed_values");} + public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");} + + static public function IsBasedOnDBColumns() {return true;} + static public function IsScalar() {return true;} + public function IsWritable() {return !$this->IsMagic();} + public function GetSQLExpr() + { + return $this->Get("sql"); + } + + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);} + public function IsNullAllowed() {return false;} + + // + protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) + + public function GetSQLExpressions($sPrefix = '') + { + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $this->Get("sql"); + return $aColumns; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + $value = $this->MakeRealValue($aCols[$sPrefix.''], null); + return $value; + } + public function GetSQLValues($value) + { + $aValues = array(); + $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); + return $aValues; + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->Get("sql")] = $this->GetSQLCol($bFullSpec); + return $aColumns; + } + + public function GetFilterDefinitions() + { + return array($this->GetCode() => new FilterFromAttribute($this)); + } + + public function GetBasicFilterOperators() + { + return array("="=>"equals", "!="=>"differs from"); + } + public function GetBasicFilterLooseOperator() + { + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '=': + default: + return $this->GetSQLExpr()." = $sQValue"; + } + } +} + +/** + * Base class for all kind of DB attributes, with the exception of external keys + * + * @package iTopORM + */ +class AttributeDBField extends AttributeDBFieldVoid +{ + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed")); + } + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue($this->Get("default_value"), $oHostObject);} + public function IsNullAllowed() {return $this->Get("is_null_allowed");} +} + +/** + * Map an integer column to an attribute + * + * @package iTopORM + */ +class AttributeInteger extends AttributeDBField +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? $this->GetSQLColSpec() : '');} + + public function GetValidationPattern() + { + return "^[0-9]+$"; + } + + public function GetBasicFilterOperators() + { + return array( + "!="=>"differs from", + "="=>"equals", + ">"=>"greater (strict) than", + ">="=>"greater than", + "<"=>"less (strict) than", + "<="=>"less than", + "in"=>"in" + ); + } + public function GetBasicFilterLooseOperator() + { + // Unless we implement an "equals approximately..." or "same order of magnitude" + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '>': + return $this->GetSQLExpr()." > $sQValue"; + break; + case '>=': + return $this->GetSQLExpr()." >= $sQValue"; + break; + case '<': + return $this->GetSQLExpr()." < $sQValue"; + break; + case '<=': + return $this->GetSQLExpr()." <= $sQValue"; + break; + case 'in': + if (!is_array($value)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); + return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; + break; + + case '=': + default: + return $this->GetSQLExpr()." = \"$value\""; + } + } + + public function GetNullValue() + { + return null; + } + public function IsNull($proposedValue) + { + return is_null($proposedValue); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return null; + if ($proposedValue === '') return null; // 0 is transformed into '' ! + return (int)$proposedValue; + } + + public function ScalarToSQL($value) + { + assert(is_numeric($value) || is_null($value)); + return $value; // supposed to be an int + } +} + +/** + * An external key for which the class is defined as the value of another attribute + * + * @package iTopORM + */ +class AttributeObjectKey extends AttributeDBFieldVoid +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed')); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");} + + public function GetDefaultValue(DBObject $oHostObject = null) {return 0;} + public function IsNullAllowed() + { + return $this->Get("is_null_allowed"); + } + + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + return parent::GetBasicFilterLooseOperator(); + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return parent::GetBasicFilterSQLExpr($sOpCode, $value); + } + + public function GetNullValue() + { + return 0; + } + + public function IsNull($proposedValue) + { + return ($proposedValue == 0); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return 0; + if ($proposedValue === '') return 0; + if (MetaModel::IsValidObject($proposedValue)) { + /** @var \DBObject $proposedValue */ + return $proposedValue->GetKey(); + } + return (int)$proposedValue; + } +} + +/** + * Display an integer between 0 and 100 as a percentage / horizontal bar graph + * + * @package iTopORM + */ +class AttributePercentage extends AttributeInteger +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + $iWidth = 5; // Total width of the percentage bar graph, in em... + $iValue = (int)$sValue; + if ($iValue > 100) + { + $iValue = 100; + } + else if ($iValue < 0) + { + $iValue = 0; + } + if ($iValue > 90) + { + $sColor = "#cc3300"; + } + else if ($iValue > 50) + { + $sColor = "#cccc00"; + } + else + { + $sColor = "#33cc00"; + } + $iPercentWidth = ($iWidth * $iValue) / 100; + return "
     
     $sValue %"; + } +} + +/** + * Map a decimal value column (suitable for financial computations) to an attribute + * internally in PHP such numbers are represented as string. Should you want to perform + * a calculation on them, it is recommended to use the BC Math functions in order to + * retain the precision + * + * @package iTopORM + */ +class AttributeDecimal extends AttributeDBField +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */)); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol($bFullSpec = false) + { + return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")".($bFullSpec ? $this->GetSQLColSpec() : ''); + } + + public function GetValidationPattern() + { + $iNbDigits = $this->Get('digits'); + $iPrecision = $this->Get('decimals'); + $iNbIntegerDigits = $iNbDigits - $iPrecision - 1; // -1 because the first digit is treated separately in the pattern below + return "^[-+]?[0-9]\d{0,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$"; + } + + public function GetBasicFilterOperators() + { + return array( + "!="=>"differs from", + "="=>"equals", + ">"=>"greater (strict) than", + ">="=>"greater than", + "<"=>"less (strict) than", + "<="=>"less than", + "in"=>"in" + ); + } + public function GetBasicFilterLooseOperator() + { + // Unless we implement an "equals approximately..." or "same order of magnitude" + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '>': + return $this->GetSQLExpr()." > $sQValue"; + break; + case '>=': + return $this->GetSQLExpr()." >= $sQValue"; + break; + case '<': + return $this->GetSQLExpr()." < $sQValue"; + break; + case '<=': + return $this->GetSQLExpr()." <= $sQValue"; + break; + case 'in': + if (!is_array($value)) throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); + return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; + break; + + case '=': + default: + return $this->GetSQLExpr()." = \"$value\""; + } + } + + public function GetNullValue() + { + return null; + } + public function IsNull($proposedValue) + { + return is_null($proposedValue); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return null; + if ($proposedValue === '') return null; + return (string)$proposedValue; + } + + public function ScalarToSQL($value) + { + assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value)); + return $value; // null or string + } +} + +/** + * Map a boolean column to an attribute + * + * @package iTopORM + */ +class AttributeBoolean extends AttributeInteger +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "Integer";} + protected function GetSQLCol($bFullSpec = false) {return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : '');} + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return null; + if ($proposedValue === '') return null; + if ((int)$proposedValue) return true; + return false; + } + + public function ScalarToSQL($value) + { + if ($value) return 1; + return 0; + } + + public function GetValueLabel($bValue) + { + if (is_null($bValue)) + { + $sLabel = Dict::S('Core:'.get_class($this).'/Value:null'); + } + else + { + $sValue = $bValue ? 'yes' : 'no'; + $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue); + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/); + } + return $sLabel; + } + + public function GetValueDescription($bValue) + { + if (is_null($bValue)) + { + $sDescription = Dict::S('Core:'.get_class($this).'/Value:null+'); + } + else + { + $sValue = $bValue ? 'yes' : 'no'; + $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+'); + $sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault, true /*user lang*/); + } + return $sDescription; + } + + public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true) + { + if (is_null($bValue)) + { + $sRes = ''; + } + elseif ($bLocalize) + { + $sLabel = $this->GetValueLabel($bValue); + $sDescription = $this->GetValueDescription($bValue); + // later, we could imagine a detailed description in the title + $sRes = "".parent::GetAsHtml($sLabel).""; + } + else + { + $sRes = $bValue ? 'yes' : 'no'; + } + return $sRes; + } + + public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true) + { + if (is_null($bValue)) + { + $sFinalValue = ''; + } + elseif ($bLocalize) + { + $sFinalValue = $this->GetValueLabel($bValue); + } + else + { + $sFinalValue = $bValue ? 'yes' : 'no'; + } + $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); + return $sRes; + } + + public function GetAsCSV($bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + if (is_null($bValue)) + { + $sFinalValue = ''; + } + elseif ($bLocalize) + { + $sFinalValue = $this->GetValueLabel($bValue); + } + else + { + $sFinalValue = $bValue ? 'yes' : 'no'; + } + $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); + return $sRes; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\SelectField'; + } + + /** + * @param \DBObject $oObject + * @param \Combodo\iTop\Form\Field\SelectField $oFormField + * + * @return \Combodo\iTop\Form\Field\SelectField + * @throws \CoreException + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + $oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false))); + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + public function GetEditValue($value, $oHostObj = null) + { + if (is_null($value)) + { + return ''; + } + else + { + return $this->GetValueLabel($value); + } + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + * + * @param $value + * + * @return bool + */ + public function GetForJSON($value) + { + return (bool)$value; + } + + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + $sInput = strtolower(trim($sProposedValue)); + if ($bLocalizedValue) + { + switch ($sInput) + { + case '1': // backward compatibility + case $this->GetValueLabel(true): + $value = true; + break; + case '0': // backward compatibility + case 'no': + case $this->GetValueLabel(false): + $value = false; + break; + default: + $value = null; + } + } + else + { + switch ($sInput) + { + case '1': // backward compatibility + case 'yes': + $value = true; + break; + case '0': // backward compatibility + case 'no': + $value = false; + break; + default: + $value = null; + } + } + return $value; + } +} + +/** + * Map a varchar column (size < ?) to an attribute + * + * @package iTopORM + */ +class AttributeString extends AttributeDBField +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "String";} + + protected function GetSQLCol($bFullSpec = false) + { + return 'VARCHAR(255)' + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? $this->GetSQLColSpec() : ''); + } + + public function GetValidationPattern() + { + $sPattern = $this->GetOptional('validation_pattern', ''); + if (empty($sPattern)) + { + return parent::GetValidationPattern(); + } + else + { + return $sPattern; + } + } + + public function CheckFormat($value) + { + $sRegExp = $this->GetValidationPattern(); + if (empty($sRegExp)) + { + return true; + } + else + { + $sRegExp = str_replace('/', '\\/', $sRegExp); + return preg_match("/$sRegExp/", $value); + } + } + + public function GetMaxSize() + { + return 255; + } + + public function GetBasicFilterOperators() + { + return array( + "="=>"equals", + "!="=>"differs from", + "Like"=>"equals (no case)", + "NotLike"=>"differs from (no case)", + "Contains"=>"contains", + "Begins with"=>"begins with", + "Finishes with"=>"finishes with" + ); + } + public function GetBasicFilterLooseOperator() + { + return "Contains"; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '=': + case '!=': + return $this->GetSQLExpr()." $sOpCode $sQValue"; + case 'Begins with': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("$value%"); + case 'Finishes with': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value"); + case 'Contains': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); + case 'NotLike': + return $this->GetSQLExpr()." NOT LIKE $sQValue"; + case 'Like': + default: + return $this->GetSQLExpr()." LIKE $sQValue"; + } + } + + public function GetNullValue() + { + return ''; + } + + public function IsNull($proposedValue) + { + return ($proposedValue == ''); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return ''; + return (string)$proposedValue; + } + + public function ScalarToSQL($value) + { + if (!is_string($value) && !is_null($value)) + { + throw new CoreWarning('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetHostClass(), 'attribute' => $this->GetCode())); + } + return $value; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return $sTextQualifier.$sEscaped.$sTextQualifier; + } + + public function GetDisplayStyle() + { + return $this->GetOptional('display_style', 'select'); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\StringField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + +} + +/** + * An attibute that matches an object class + * + * @package iTopORM + */ +class AttributeClass extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("class_category", "more_values")); + } + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']); + parent::__construct($sCode, $aParams); + } + + public function GetDefaultValue(DBObject $oHostObject = null) + { + $sDefault = parent::GetDefaultValue($oHostObject); + if (!$this->IsNullAllowed() && $this->IsNull($sDefault)) + { + // For this kind of attribute specifying null as default value + // is authorized even if null is not allowed + + // Pick the first one... + $aClasses = $this->GetAllowedValues(); + $sDefault = key($aClasses); + } + return $sDefault; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if (empty($sValue)) return ''; + return MetaModel::GetName($sValue); + } + + public function RequiresIndex() + { + return true; + } + + public function GetBasicFilterLooseOperator() + { + return '='; + } + +} + +/** + * An attibute that matches one of the language codes availables in the dictionnary + * + * @package iTopORM + */ +class AttributeApplicationLanguage extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + } + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $aAvailableLanguages = Dict::GetLanguages(); + $aLanguageCodes = array(); + foreach($aAvailableLanguages as $sLangCode => $aInfo) + { + $aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')'; + } + $aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes); + parent::__construct($sCode, $aParams); + } + + public function RequiresIndex() + { + return true; + } + + public function GetBasicFilterLooseOperator() + { + return '='; + } +} + +/** + * The attribute dedicated to the finalclass automatic attribute + * + * @package iTopORM + */ +class AttributeFinalClass extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + public $m_sValue; + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $aParams["allowed_values"] = null; + parent::__construct($sCode, $aParams); + + $this->m_sValue = $this->Get("default_value"); + } + + public function IsWritable() + { + return false; + } + public function IsMagic() + { + return true; + } + + public function RequiresIndex() + { + return true; + } + + public function SetFixedValue($sValue) + { + $this->m_sValue = $sValue; + } + public function GetDefaultValue(DBObject $oHostObject = null) + { + return $this->m_sValue; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if (empty($sValue)) return ''; + if ($bLocalize) + { + return MetaModel::GetName($sValue); + } + else + { + return $sValue; + } + } + + /** + * An enum can be localized + * + * @param string $sProposedValue + * @param bool $bLocalizedValue + * @param string $sSepItem + * @param string $sSepAttribute + * @param string $sSepValue + * @param string $sAttributeQualifier + * + * @return mixed|null|string + * @throws \CoreException + * @throws \OQLException + */ + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + if ($bLocalizedValue) + { + // Lookup for the value matching the input + // + $sFoundValue = null; + $aRawValues = self::GetAllowedValues(); + if (!is_null($aRawValues)) + { + foreach ($aRawValues as $sKey => $sValue) + { + if ($sProposedValue == $sValue) + { + $sFoundValue = $sKey; + break; + } + } + } + if (is_null($sFoundValue)) + { + return null; + } + return $this->MakeRealValue($sFoundValue, null); + } + else + { + return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); + } + } + + + // Because this is sometimes used to get a localized/string version of an attribute... + public function GetEditValue($sValue, $oHostObj = null) + { + if (empty($sValue)) return ''; + return MetaModel::GetName($sValue); + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + * + * @param $value + * + * @return string + */ + public function GetForJSON($value) + { + // JSON values are NOT localized + return $value; + } + + /** + * @param $value + * @param string $sSeparator + * @param string $sTextQualifier + * @param \DBObject $oHostObject + * @param bool $bLocalize + * @param bool $bConvertToPlainText + * + * @return string + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function GetAsCSV( + $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false + ) + { + if ($bLocalize && $value != '') + { + $sRawValue = MetaModel::GetName($value); + } + else + { + $sRawValue = $value; + } + return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText); + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if (empty($value)) return ''; + if ($bLocalize) + { + $sRawValue = MetaModel::GetName($value); + } + else + { + $sRawValue = $value; + } + return Str::pure2xml($sRawValue); + } + + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetValueLabel($sValue) + { + if (empty($sValue)) return ''; + return MetaModel::GetName($sValue); + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $aRawValues = MetaModel::EnumChildClasses($this->GetHostClass(), ENUM_CHILD_CLASSES_ALL); + $aLocalizedValues = array(); + foreach ($aRawValues as $sClass) + { + $aLocalizedValues[$sClass] = MetaModel::GetName($sClass); + } + return $aLocalizedValues; + } +} + + +/** + * Map a varchar column (size < ?) to an attribute that must never be shown to the user + * + * @package iTopORM + */ +class AttributePassword extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "Password";} + + protected function GetSQLCol($bFullSpec = false) + { + return "VARCHAR(64)" + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? $this->GetSQLColSpec() : ''); + } + + public function GetMaxSize() + { + return 64; + } + + public function GetFilterDefinitions() + { + // Note: due to this, you will get an error if a password is being declared as a search criteria (see ZLists) + // not allowed to search on passwords! + return array(); + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if (strlen($sValue) == 0) + { + return ''; + } + else + { + return '******'; + } + } + + public function IsPartOfFingerprint() { return false; } // Cannot reliably compare two encrypted passwords since the same password will be encrypted in diffferent manners depending on the random 'salt' +} + +/** + * Map a text column (size < 255) to an attribute that is encrypted in the database + * The encryption is based on a key set per iTop instance. Thus if you export your + * database (in SQL) to someone else without providing the key at the same time + * the encrypted fields will remain encrypted + * + * @package iTopORM + */ +class AttributeEncryptedString extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static $sKey = null; // Encryption key used for all encrypted fields + static $sLibrary = null; // Encryption library used for all encrypted fields + public function __construct($sCode, $aParams) + { + parent::__construct($sCode, $aParams); + if (self::$sKey == null) + { + self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); + } + if(self::$sLibrary == null) + { + self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary(); + } + } + /** + * When the attribute definitions are stored in APC cache: + * 1) The static class variable $sKey is NOT serialized + * 2) The object's constructor is NOT called upon wakeup + * 3) mcrypt may crash the server if passed an empty key !! + * + * So let's restore the key (if needed) when waking up + **/ + public function __wakeup() + { + if (self::$sKey == null) + { + self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); + } + if(self::$sLibrary == null) + { + self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary(); + } + } + + + protected function GetSQLCol($bFullSpec = false) {return "TINYBLOB";} + + public function GetMaxSize() + { + return 255; + } + + public function GetFilterDefinitions() + { + // Note: due to this, you will get an error if a an encrypted field is declared as a search criteria (see ZLists) + // not allowed to search on encrypted fields ! + return array(); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return null; + return (string)$proposedValue; + } + + /** + * Decrypt the value when reading from the database + * + * @param array $aCols + * @param string $sPrefix + * + * @return string + * @throws \Exception + */ + public function FromSQLToValue($aCols, $sPrefix = '') + { + $oSimpleCrypt = new SimpleCrypt(self::$sLibrary); + $sValue = $oSimpleCrypt->Decrypt(self::$sKey, $aCols[$sPrefix]); + return $sValue; + } + + /** + * Encrypt the value before storing it in the database + * + * @param $value + * + * @return array + * @throws \Exception + */ + public function GetSQLValues($value) + { + $oSimpleCrypt = new SimpleCrypt(self::$sLibrary); + $encryptedValue = $oSimpleCrypt->Encrypt(self::$sKey, $value); + + $aValues = array(); + $aValues[$this->Get("sql")] = $encryptedValue; + return $aValues; + } +} + + +// Wiki formatting - experimental +// +// [[:]] +// Example: [[Server:db1.tnut.com]] +define('WIKI_OBJECT_REGEXP', '/\[\[(.+):(.+)\]\]/U'); + + +/** + * Map a text column (size > ?) to an attribute + * + * @package iTopORM + */ +class AttributeText extends AttributeString +{ + public function GetEditClass() {return ($this->GetFormat() == 'text') ? 'Text' : "HTML";} + + protected function GetSQLCol($bFullSpec = false) + { + return "TEXT".CMDBSource::GetSqlStringColumnDefinition(); + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->Get('sql')] = $this->GetSQLCol($bFullSpec); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')".CMDBSource::GetSqlStringColumnDefinition(); + if ($bFullSpec) + { + $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'text'"; // default 'text' is for migrating old records + } + } + return $aColumns; + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->Get('sql'); + } + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $sPrefix; + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns['_format'] = $sPrefix.'_format'; + } + return $aColumns; + } + + public function GetMaxSize() + { + // Is there a way to know the current limitation for mysql? + // See mysql_field_len() + return 65535; + } + + static public function RenderWikiHtml($sText, $bWikiOnly = false) + { + if (!$bWikiOnly) + { + $sPattern = '/'.str_replace('/', '\/', utils::GetConfig()->Get('url_validation_pattern')).'/i'; + if (preg_match_all($sPattern, $sText, $aAllMatches, PREG_SET_ORDER /* important !*/ |PREG_OFFSET_CAPTURE /* important ! */)) + { + $i = count($aAllMatches); + // Replace the URLs by an actual hyperlink ... + // Let's do it backwards so that the initial positions are not modified by the replacement + // This works if the matches are captured: in the order they occur in the string AND + // with their offset (i.e. position) inside the string + while($i > 0) + { + $i--; + $sUrl = $aAllMatches[$i][0][0]; // String corresponding to the main pattern + $iPos = $aAllMatches[$i][0][1]; // Position of the main pattern + $sText = substr_replace($sText, "$sUrl", $iPos, strlen($sUrl)); + + } + } + } + if (preg_match_all(WIKI_OBJECT_REGEXP, $sText, $aAllMatches, PREG_SET_ORDER)) + { + foreach($aAllMatches as $iPos => $aMatches) + { + $sClass = trim($aMatches[1]); + $sName = trim($aMatches[2]); + + if (MetaModel::IsValidClass($sClass)) + { + $oObj = MetaModel::GetObjectByName($sClass, $sName, false /* MustBeFound */); + if (is_object($oObj)) + { + // Propose a std link to the object + $sText = str_replace($aMatches[0], $oObj->GetHyperlink(), $sText); + } + else + { + // Propose a std link to the object + $sClassLabel = MetaModel::GetName($sClass); + $sText = str_replace($aMatches[0], "$sClassLabel:$sName", $sText); + // Later: propose a link to create a new object + // Anyhow... there is no easy way to suggest default values based on the given FRIENDLY name + //$sText = preg_replace('/\[\[(.+):(.+)\]\]/', ''.$sName.'', $sText); + } + } + } + } + return $sText; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + $aStyles = array(); + if ($this->GetWidth() != '') + { + $aStyles[] = 'width:'.$this->GetWidth(); + } + if ($this->GetHeight() != '') + { + $aStyles[] = 'height:'.$this->GetHeight(); + } + $sStyle = ''; + if (count($aStyles) > 0) + { + $aStyles[] = 'overflow:auto'; + $sStyle = 'style="'.implode(';', $aStyles).'"'; + } + + if ($this->GetFormat() == 'text') + { + $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize); + $sValue = self::RenderWikiHtml($sValue); + return "
    ".str_replace("\n", "
    \n", $sValue).'
    '; + } + else + { + $sValue = self::RenderWikiHtml($sValue, true /* wiki only */); + return "
    ".InlineImage::FixUrls($sValue).'
    '; + } + + } + + public function GetEditValue($sValue, $oHostObj = null) + { + if ($this->GetFormat() == 'text') + { + if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) + { + foreach($aAllMatches as $iPos => $aMatches) + { + $sClass = $aMatches[1]; + $sName = $aMatches[2]; + + if (MetaModel::IsValidClass($sClass)) + { + $sClassLabel = MetaModel::GetName($sClass); + $sValue = str_replace($aMatches[0], "[[$sClassLabel:$sName]]", $sValue); + } + } + } + } + else + { + $sValue = str_replace('&', '&', $sValue); + } + return $sValue; + } + + /** + * For fields containing a potential markup, return the value without this markup + * + * @param string $sValue + * @param \DBObject $oHostObj + * + * @return string + */ + public function GetAsPlainText($sValue, $oHostObj = null) + { + if ($this->GetFormat() == 'html') + { + return (string) utils::HtmlToText($this->GetEditValue($sValue, $oHostObj)); + } + else + { + return parent::GetAsPlainText($sValue, $oHostObj); + } + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + $sValue = $proposedValue; + switch ($this->GetFormat()) + { + case 'html': + if (($sValue !== null) && ($sValue !== '')) + { + $sValue = HTMLSanitizer::Sanitize($sValue); + } + break; + + case 'text': + default: + if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) + { + foreach($aAllMatches as $iPos => $aMatches) + { + $sClassLabel = $aMatches[1]; + $sName = $aMatches[2]; + + if (!MetaModel::IsValidClass($sClassLabel)) + { + $sClass = MetaModel::GetClassFromLabel($sClassLabel); + if ($sClass) + { + $sValue = str_replace($aMatches[0], "[[$sClass:$sName]]", $sValue); + } + } + } + } + } + return $sValue; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + return Str::pure2xml($value); + } + + public function GetWidth() + { + return $this->GetOptional('width', ''); + } + + public function GetHeight() + { + return $this->GetOptional('height', ''); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\TextAreaField'; + } + + /** + * @param \DBObject $oObject + * @param \Combodo\iTop\Form\Field\TextAreaField $oFormField + * + * @return \Combodo\iTop\Form\Field\TextAreaField + * @throws \CoreException + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + /** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */ + $oFormField = new $sFormFieldClass($this->GetCode(), null, $oObject); + $oFormField->SetFormat($this->GetFormat()); + } + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + /** + * The actual formatting of the field: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'text'); + } + + /** + * Read the value from the row returned by the SQL query and transorms it to the appropriate + * internal format (either text or html) + * + * @see AttributeDBFieldVoid::FromSQLToValue() + * + * @param array $aCols + * @param string $sPrefix + * + * @return string + */ + public function FromSQLToValue($aCols, $sPrefix = '') + { + $value = $aCols[$sPrefix.'']; + if ($this->GetOptional('format', null) != null ) + { + // Read from the extra column only if the property 'format' is specified for the attribute + $sFormat = $aCols[$sPrefix.'_format']; + } + else + { + $sFormat = $this->GetFormat(); + } + + switch($sFormat) + { + case 'text': + if ($this->GetFormat() == 'html') + { + $value = utils::TextToHtml($value); + } + break; + + case 'html': + if ($this->GetFormat() == 'text') + { + $value = utils::HtmlToText($value); + } + else + { + $value = InlineImage::FixUrls((string)$value); + } + break; + + default: + // unknown format ?? + } + return $value; + } + + public function GetSQLValues($value) + { + $aValues = array(); + $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aValues[$this->Get("sql").'_format'] = $this->GetFormat(); + } + return $aValues; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + switch($this->GetFormat()) + { + case 'html': + if ($bConvertToPlainText) + { + $sValue = utils::HtmlToText((string)$sValue); + } + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return $sTextQualifier.$sEscaped.$sTextQualifier; + break; + + case 'text': + default: + return parent::GetAsCSV($sValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, $bConvertToPlainText); + } + } +} + +/** + * Map a log to an attribute + * + * @package iTopORM + */ +class AttributeLongText extends AttributeText +{ + protected function GetSQLCol($bFullSpec = false) + { + return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); + } + + public function GetMaxSize() + { + // Is there a way to know the current limitation for mysql? + // See mysql_field_len() + return 65535*1024; // Limited... still 64 Mb! + } +} + +/** + * An attibute that stores a case log (i.e journal) + * + * @package iTopORM + */ +class AttributeCaseLog extends AttributeLongText +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + + public function GetNullValue() + { + return ''; + } + + public function IsNull($proposedValue) + { + if (!($proposedValue instanceof ormCaseLog)) + { + return ($proposedValue == ''); + } + return ($proposedValue->GetText() == ''); + } + + public function ScalarToSQL($value) + { + if (!is_string($value) && !is_null($value)) + { + throw new CoreWarning('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetCode(), 'attribute' => $this->GetHostClass())); + } + return $value; + } + public function GetEditClass() {return "CaseLog";} + + public function GetEditValue($sValue, $oHostObj = null) + { + if (!($sValue instanceOf ormCaseLog)) + { + return ''; + } + return $sValue->GetModifiedEntry(); + } + + /** + * For fields containing a potential markup, return the value without this markup + * + * @param mixed $value + * @param \DBObject $oHostObj + * + * @return string + */ + public function GetAsPlainText($value, $oHostObj = null) + { + if ($value instanceOf ormCaseLog) + { + /** ormCaseLog $value */ + return $value->GetAsPlainText(); + } + else + { + return (string) $value; + } + } + + public function GetDefaultValue(DBObject $oHostObject = null) {return new ormCaseLog();} + public function Equals($val1, $val2) {return ($val1->GetText() == $val2->GetText());} + + + /** + * Facilitate things: allow the user to Set the value from a string + * + * @param $proposedValue + * @param \DBObject $oHostObj + * + * @return mixed|null|\ormCaseLog|string + * @throws \Exception + */ + public function MakeRealValue($proposedValue, $oHostObj) + { + if ($proposedValue instanceof ormCaseLog) + { + // Passthrough + $ret = clone $proposedValue; + } + else + { + // Append the new value if an instance of the object is supplied + // + $oPreviousLog = null; + if ($oHostObj != null) + { + $oPreviousLog = $oHostObj->Get($this->GetCode()); + if (!is_object($oPreviousLog)) + { + $oPreviousLog = $oHostObj->GetOriginal($this->GetCode());; + } + + } + if (is_object($oPreviousLog)) + { + $oCaseLog = clone($oPreviousLog); + } + else + { + $oCaseLog = new ormCaseLog(); + } + + if ($proposedValue instanceof stdClass) + { + $oCaseLog->AddLogEntryFromJSON($proposedValue); + } + else + { + if (strlen($proposedValue) > 0) + { + $oCaseLog->AddLogEntry($proposedValue); + } + } + $ret = $oCaseLog; + } + return $ret; + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->Get('sql'); + } + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $sPrefix; + $aColumns['_index'] = $sPrefix.'_index'; + return $aColumns; + } + + /** + * @param array $aCols + * @param string $sPrefix + * + * @return \ormCaseLog + * @throws \MissingColumnException + */ + public function FromSQLToValue($aCols, $sPrefix = '') + { + if (!array_key_exists($sPrefix, $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); + } + $sLog = $aCols[$sPrefix]; + + if (isset($aCols[$sPrefix.'_index'])) + { + $sIndex = $aCols[$sPrefix.'_index']; + } + else + { + // For backward compatibility, allow the current state to be: 1 log, no index + $sIndex = ''; + } + + if (strlen($sIndex) > 0) + { + $aIndex = unserialize($sIndex); + $value = new ormCaseLog($sLog, $aIndex); + } + else + { + $value = new ormCaseLog($sLog); + } + return $value; + } + + public function GetSQLValues($value) + { + if (!($value instanceOf ormCaseLog)) + { + $value = new ormCaseLog(''); + } + $aValues = array(); + $aValues[$this->GetCode()] = $value->GetText(); + $aValues[$this->GetCode().'_index'] = serialize($value->GetIndex()); + + return $aValues; + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->GetCode()] = 'LONGTEXT' // 2^32 (4 Gb) + .CMDBSource::GetSqlStringColumnDefinition(); + $aColumns[$this->GetCode().'_index'] = 'BLOB'; + return $aColumns; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + if ($value instanceOf ormCaseLog) + { + $sContent = $value->GetAsHTML(null, false, array(__class__, 'RenderWikiHtml')); + } + else + { + $sContent = ''; + } + $aStyles = array(); + if ($this->GetWidth() != '') + { + $aStyles[] = 'width:'.$this->GetWidth(); + } + if ($this->GetHeight() != '') + { + $aStyles[] = 'height:'.$this->GetHeight(); + } + $sStyle = ''; + if (count($aStyles) > 0) + { + $sStyle = 'style="'.implode(';', $aStyles).'"'; + } + return "
    ".$sContent.'
    '; } + + + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + if ($value instanceOf ormCaseLog) + { + return parent::GetAsCSV($value->GetText($bConvertToPlainText), $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, $bConvertToPlainText); + } + else + { + return ''; + } + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if ($value instanceOf ormCaseLog) + { + return parent::GetAsXML($value->GetText(), $oHostObject, $bLocalize); + } + else + { + return ''; + } + } + + /** + * List the available verbs for 'GetForTemplate' + */ + public function EnumTemplateVerbs() + { + return array( + '' => 'Plain text representation of all the log entries', + 'head' => 'Plain text representation of the latest entry', + 'head_html' => 'HTML representation of the latest entry', + 'html' => 'HTML representation of all the log entries', + ); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $oHostObject DBObject The object + * @param $bLocalize bool Whether or not to localize the value + * + * @return mixed + * @throws \Exception + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + switch($sVerb) + { + case '': + return $value->GetText(true); + + case 'head': + return $value->GetLatestEntry('text'); + + case 'head_html': + return $value->GetLatestEntry('html'); + + case 'html': + return $value->GetAsEmailHtml(); + + default: + throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); + } + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + */ + public function GetForJSON($value) + { + return $value->GetForJSON(); + } + + /** + * Helper to form a value, given JSON decoded data + * The operation is the opposite to GetForJSON + */ + public function FromJSONToValue($json) + { + if (is_string($json)) + { + // Will be correctly handled in MakeRealValue + $ret = $json; + } + else + { + if (isset($json->add_item)) + { + // Will be correctly handled in MakeRealValue + $ret = $json->add_item; + if (!isset($ret->message)) + { + throw new Exception("Missing mandatory entry: 'message'"); + } + } + else + { + $ret = ormCaseLog::FromJSON($json); + } + } + return $ret; + } + + public function Fingerprint($value) + { + $sFingerprint = ''; + if ($value instanceOf ormCaseLog) + { + $sFingerprint = $value->GetText(); + } + return $sFingerprint; + } + + /** + * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'html'); // default format for case logs is now HTML + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\CaseLogField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + // First we call the parent so the field is build + $oFormField = parent::MakeFormField($oObject, $oFormField); + // Then only we set the value + $oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode()))); + // And we set the entries + $oFormField->SetEntries($oObject->Get($this->GetCode())->GetAsArray()); + + return $oFormField; + } +} + +/** + * Map a text column (size > ?), containing HTML code, to an attribute + * + * @package iTopORM + */ +class AttributeHTML extends AttributeLongText +{ + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->Get('sql')] = $this->GetSQLCol(); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; + if ($bFullSpec) + { + $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'html'"; // default 'html' is for migrating old records + } + } + return $aColumns; + } + + /** + * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'html'); // Defaults to HTML + } +} + +/** + * Specialization of a string: email + * + * @package iTopORM + */ +class AttributeEmailAddress extends AttributeString +{ + public function GetValidationPattern() + { + return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('email_validation_pattern').'$'); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\EmailField'; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if (empty($sValue)) return ''; + + $sUrlDecorationClass = utils::GetConfig()->Get('email_decoration_class'); + + return ''.parent::GetAsHTML($sValue).''; + } +} + +/** + * Specialization of a string: IP address + * + * @package iTopORM + */ +class AttributeIPAddress extends AttributeString +{ + public function GetValidationPattern() + { + $sNum = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])'; + return "^($sNum\\.$sNum\\.$sNum\\.$sNum)$"; + } + + public function GetOrderBySQLExpressions($sClassAlias) + { + // Note: This is the responsibility of this function to place backticks around column aliases + return array('INET_ATON(`'.$sClassAlias.$this->GetCode().'`)'); + } +} + +/** + * Specialization of a string: phone number + * + * @package iTopORM + */ +class AttributePhoneNumber extends AttributeString +{ + public function GetValidationPattern() + { + return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('phone_number_validation_pattern').'$'); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\PhoneField'; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if (empty($sValue)) return ''; + + $sUrlDecorationClass = utils::GetConfig()->Get('phone_number_decoration_class'); + $sUrlPattern = utils::GetConfig()->Get('phone_number_url_pattern'); + $sUrl = sprintf($sUrlPattern, $sValue); + + return ''.parent::GetAsHTML($sValue).''; + } +} + +/** + * Specialization of a string: OQL expression + * + * @package iTopORM + */ +class AttributeOQL extends AttributeText +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + + public function GetEditClass() {return "OQLExpression";} +} + +/** + * Specialization of a string: template (contains iTop placeholders like $current_contact_id$ or $this->name$) + * + * @package iTopORM + */ +class AttributeTemplateString extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; +} + +/** + * Specialization of a text: template (contains iTop placeholders like $current_contact_id$ or $this->name$) + * + * @package iTopORM + */ +class AttributeTemplateText extends AttributeText +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; +} + +/** + * Specialization of a HTML: template (contains iTop placeholders like $current_contact_id$ or $this->name$) + * + * @package iTopORM + */ +class AttributeTemplateHTML extends AttributeText +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->Get('sql')] = $this->GetSQLCol(); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; + if ($bFullSpec) + { + $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'html'"; // default 'html' is for migrating old records + } + } + return $aColumns; + } + + /** + * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'html'); // Defaults to HTML + } +} + + +/** + * Map a enum column to an attribute + * + * @package iTopORM + */ +class AttributeEnum extends AttributeString +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "String";} + protected function GetSQLCol($bFullSpec = false) + { + $oValDef = $this->GetValuesDef(); + if ($oValDef) + { + $aValues = CMDBSource::Quote(array_keys($oValDef->GetValues(array(), "")), true); + } + else + { + $aValues = array(); + } + if (count($aValues) > 0) + { + // The syntax used here do matters + // In particular, I had to remove unnecessary spaces to + // make sure that this string will match the field type returned by the DB + // (used to perform a comparison between the current DB format and the data model) + return "ENUM(".implode(",", $aValues).")" + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? $this->GetSQLColSpec() : ''); + } + else + { + return "VARCHAR(255)" + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? " DEFAULT ''" : ""); // ENUM() is not an allowed syntax! + } + } + + protected function GetSQLColSpec() + { + $default = $this->ScalarToSQL($this->GetDefaultValue()); + if (is_null($default)) + { + $sRet = ''; + } + else + { + // ENUMs values are strings so the default value must be a string as well, + // otherwise MySQL interprets the number as the zero-based index of the value in the list (i.e. the nth value in the list) + $sRet = " DEFAULT ".CMDBSource::Quote($default); + } + return $sRet; + } + + public function ScalarToSQL($value) + { + // Note: for strings, the null value is an empty string and it is recorded as such in the DB + // but that wasn't working for enums, because '' is NOT one of the allowed values + // that's why a null value must be forced to a real null + $value = parent::ScalarToSQL($value); + if ($this->IsNull($value)) + { + return null; + } + else + { + return $value; + } + } + + public function RequiresIndex() + { + return false; + } + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return parent::GetBasicFilterSQLExpr($sOpCode, $value); + } + + public function GetValueLabel($sValue) + { + if (is_null($sValue)) + { + // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label + $sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, Dict::S('Enum:Undefined')); + } + else + { + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, null, true /*user lang*/); + if (is_null($sLabel)) + { + $sDefault = str_replace('_', ' ', $sValue); + // Browse the hierarchy again, accepting default (english) translations + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, false); + } + } + return $sLabel; + } + + public function GetValueDescription($sValue) + { + if (is_null($sValue)) + { + // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label + $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', Dict::S('Enum:Undefined')); + } + else + { + $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', '', true /* user language only */); + if (strlen($sDescription) == 0) + { + $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); + if ($sParentClass) + { + if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) + { + $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); + $sDescription = $oAttDef->GetValueDescription($sValue); + } + } + } + } + return $sDescription; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if ($bLocalize) + { + $sLabel = $this->GetValueLabel($sValue); + $sDescription = $this->GetValueDescription($sValue); + // later, we could imagine a detailed description in the title + $sRes = "".parent::GetAsHtml($sLabel).""; + } + else + { + $sRes = parent::GetAsHtml($sValue, $oHostObject, $bLocalize); + } + return $sRes; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if (is_null($value)) + { + $sFinalValue = ''; + } + elseif ($bLocalize) + { + $sFinalValue = $this->GetValueLabel($value); + } + else + { + $sFinalValue = $value; + } + $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); + return $sRes; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + if (is_null($sValue)) + { + $sFinalValue = ''; + } + elseif ($bLocalize) + { + $sFinalValue = $this->GetValueLabel($sValue); + } + else + { + $sFinalValue = $sValue; + } + $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); + return $sRes; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\SelectField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + // TODO : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + $oFormField->SetChoices($this->GetAllowedValues($oObject->ToArgsForQuery())); + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + public function GetEditValue($sValue, $oHostObj = null) + { + if (is_null($sValue)) + { + return ''; + } + else + { + return $this->GetValueLabel($sValue); + } + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + */ + public function GetForJSON($value) + { + return $value; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $aRawValues = parent::GetAllowedValues($aArgs, $sContains); + if (is_null($aRawValues)) return null; + $aLocalizedValues = array(); + foreach ($aRawValues as $sKey => $sValue) + { + $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey); + } + return $aLocalizedValues; + } + + public function GetMaxSize() + { + return null; + } + + /** + * An enum can be localized + */ + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + if ($bLocalizedValue) + { + // Lookup for the value matching the input + // + $sFoundValue = null; + $aRawValues = parent::GetAllowedValues(); + if (!is_null($aRawValues)) + { + foreach ($aRawValues as $sKey => $sValue) + { + $sRefValue = $this->GetValueLabel($sKey); + if ($sProposedValue == $sRefValue) + { + $sFoundValue = $sKey; + break; + } + } + } + if (is_null($sFoundValue)) + { + return null; + } + return $this->MakeRealValue($sFoundValue, null); + } + else + { + return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, $sAttributeQualifier); + } + } + + /** + * Processes the input value to align it with the values supported + * by this type of attribute. In this case: turns empty strings into nulls + * @param mixed $proposedValue The value to be set for the attribute + * @return mixed The actual value that will be set + */ + public function MakeRealValue($proposedValue, $oHostObj) + { + if ($proposedValue == '') return null; + return parent::MakeRealValue($proposedValue, $oHostObj); + } + + public function GetOrderByHint() + { + $aValues = $this->GetAllowedValues(); + + return Dict::Format('UI:OrderByHint_Values', implode(', ', $aValues)); + } +} + +/** + * A meta enum is an aggregation of enum from subclasses into an enum of a base class + * It has been designed is to cope with the fact that statuses must be defined in leaf classes, while it makes sense to + * have a superstatus available on the root classe(s) + * + * @package iTopORM + */ +class AttributeMetaEnum extends AttributeEnum +{ + static public function ListExpectedParams() + { + return array('allowed_values', 'sql', 'default_value', 'mapping'); + } + + public function IsNullAllowed() + { + return false; // Well... this actually depends on the mapping + } + + public function IsWritable() + { + return false; + } + + public function RequiresIndex() + { + return true; + } + + public function GetPrerequisiteAttributes($sClass = null) + { + if (is_null($sClass)) + { + $sClass = $this->GetHostClass(); + } + $aMappingData = $this->GetMapRule($sClass); + if ($aMappingData == null) + { + $aRet = array(); + } + else + { + $aRet = array($aMappingData['attcode']); + } + return $aRet; + } + + /** + * Overload the standard so as to leave the data unsorted + * + * @param array $aArgs + * @param string $sContains + * @return array|null + */ + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $oValSetDef = $this->GetValuesDef(); + if (!$oValSetDef) return null; + $aRawValues = $oValSetDef->GetValueList(); + + if (is_null($aRawValues)) return null; + $aLocalizedValues = array(); + foreach ($aRawValues as $sKey => $sValue) + { + $aLocalizedValues[$sKey] = Str::pure2html($this->GetValueLabel($sKey)); + } + return $aLocalizedValues; + } + + /** + * Returns the meta value for the given object. + * See also MetaModel::RebuildMetaEnums() that must be maintained when MapValue changes + * + * @param $oObject + * @return mixed + * @throws Exception + */ + public function MapValue($oObject) + { + $aMappingData = $this->GetMapRule(get_class($oObject)); + if ($aMappingData == null) + { + $sRet = $this->GetDefaultValue(); + } + else + { + $sAttCode = $aMappingData['attcode']; + $value = $oObject->Get($sAttCode); + if (array_key_exists($value, $aMappingData['values'])) + { + $sRet = $aMappingData['values'][$value]; + } + elseif ($this->GetDefaultValue() != '') + { + $sRet = $this->GetDefaultValue(); + } + else + { + throw new Exception('AttributeMetaEnum::MapValue(): mapping not found for value "'.$value.'" in '.get_class($oObject).', on attribute '.MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()).'::'.$this->GetCode()); + } + } + return $sRet; + } + + public function GetMapRule($sClass) + { + $aMappings = $this->Get('mapping'); + if (array_key_exists($sClass, $aMappings)) + { + $aMappingData = $aMappings[$sClass]; + } + else + { + $sParent = MetaModel::GetParentClass($sClass); + if (is_null($sParent)) + { + $aMappingData = null; + } + else + { + $aMappingData = $this->GetMapRule($sParent); + } + } + + return $aMappingData; + } +} +/** + * Map a date+time column to an attribute + * + * @package iTopORM + */ +class AttributeDateTime extends AttributeDBField +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE_TIME; + + static $oFormat = null; + + /** + * + * @return DateTimeFormat + */ + static public function GetFormat() + { + if (self::$oFormat == null) + { + static::LoadFormatFromConfig(); + } + return self::$oFormat; + } + + /** + * Load the 3 settings: date format, time format and data_time format from the configuration + */ + protected static function LoadFormatFromConfig() + { + $aFormats = MetaModel::GetConfig()->Get('date_and_time_format'); + $sLang = Dict::GetUserLanguage(); + $sDateFormat = isset($aFormats[$sLang]['date']) ? $aFormats[$sLang]['date'] : (isset($aFormats['default']['date']) ? $aFormats['default']['date'] : 'Y-m-d'); + $sTimeFormat = isset($aFormats[$sLang]['time']) ? $aFormats[$sLang]['time'] : (isset($aFormats['default']['time']) ? $aFormats['default']['time'] : 'H:i:s'); + $sDateAndTimeFormat = isset($aFormats[$sLang]['date_time']) ? $aFormats[$sLang]['date_time'] : (isset($aFormats['default']['date_time']) ? $aFormats['default']['date_time'] : '$date $time'); + + $sFullFormat = str_replace(array('$date', '$time'), array($sDateFormat, $sTimeFormat), $sDateAndTimeFormat); + + self::SetFormat(new DateTimeFormat($sFullFormat)); + AttributeDate::SetFormat(new DateTimeFormat($sDateFormat)); + } + + /** + * Returns the format string used for the date & time stored in memory + * @return string + */ + static public function GetInternalFormat() + { + return 'Y-m-d H:i:s'; + } + + /** + * Returns the format string used for the date & time written to MySQL + * @return string + */ + static public function GetSQLFormat() + { + return 'Y-m-d H:i:s'; + } + + static public function SetFormat(DateTimeFormat $oDateTimeFormat) + { + self::$oFormat = $oDateTimeFormat; + } + + static public function GetSQLTimeFormat() + { + return 'H:i:s'; + } + + /** + * Parses a search string coming from user input + * @param string $sSearchString + * @return string + */ + public function ParseSearchString($sSearchString) + { + try + { + $oDateTime = $this->GetFormat()->Parse($sSearchString); + $sSearchString = $oDateTime->format($this->GetInternalFormat()); + } + catch(Exception $e) + { + $sFormatString = '!'.(string)AttributeDate::GetFormat(); // BEWARE: ! is needed to set non-parsed fields to zero !!! + $oDateTime = DateTime::createFromFormat($sFormatString, $sSearchString); + if ($oDateTime !== false) + { + $sSearchString = $oDateTime->format($this->GetInternalFormat()); + } + } + return $sSearchString; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\DateTimeField'; + } + + /** + * Override to specify Field class + * + * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behave more like a Prepare. + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + $oFormField->SetPHPDateTimeFormat((string) $this->GetFormat()); + $oFormField->SetJSDateTimeFormat($this->GetFormat()->ToMomentJS()); + + $oFormField = parent::MakeFormField($oObject, $oFormField); + + // After call to the parent as it sets the current value + $oFormField->SetCurrentValue($this->GetFormat()->Format($oObject->Get($this->GetCode()))); + + return $oFormField; + } + + /** + * @inheritdoc + */ + public function EnumTemplateVerbs() + { + return array( + '' => 'Formatted representation', + 'raw' => 'Not formatted representation', + ); + } + + /** + * @inheritdoc + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + switch ($sVerb) + { + case '': + case 'text': + return static::GetFormat()->format($value); + break; + case 'html': + // Note: Not passing formatted value as the method will format it. + return $this->GetAsHTML($value); + break; + case 'raw': + return $value; + break; + default: + return parent::GetForTemplate($value, $sVerb, $oHostObject, $bLocalize); + break; + } + } + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "DateTime";} + + + public function GetEditValue($sValue, $oHostObj = null) + { + return (string)static::GetFormat()->format($sValue); + } + public function GetValueLabel($sValue, $oHostObj = null) + { + return (string)static::GetFormat()->format($sValue); + } + + protected function GetSQLCol($bFullSpec = false) {return "DATETIME";} + + public function GetImportColumns() + { + // Allow an empty string to be a valid value (synonym for "reset") + $aColumns = array(); + $aColumns[$this->GetCode()] = 'VARCHAR(19)'; + return $aColumns; + } + + public static function GetAsUnixSeconds($value) + { + $oDeadlineDateTime = new DateTime($value); + $iUnixSeconds = $oDeadlineDateTime->format('U'); + return $iUnixSeconds; + } + + public function GetDefaultValue(DBObject $oHostObject = null) + { + // null value will be replaced by the current date, if not already set, in DoComputeValues + return $this->GetNullValue(); + } + + public function GetValidationPattern() + { + return static::GetFormat()->ToRegExpr(); + } + + public function GetBasicFilterOperators() + { + return array( + "="=>"equals", + "!="=>"differs from", + "<"=>"before", + "<="=>"before", + ">"=>"after (strictly)", + ">="=>"after", + "SameDay"=>"same day (strip time)", + "SameMonth"=>"same year/month", + "SameYear"=>"same year", + "Today"=>"today", + ">|"=>"after today + N days", + "<|"=>"before today + N days", + "=|"=>"equals today + N days", + ); + } + public function GetBasicFilterLooseOperator() + { + // Unless we implement a "same xxx, depending on given precision" ! + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + + switch ($sOpCode) + { + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + return $this->GetSQLExpr()." $sOpCode $sQValue"; + case 'SameDay': + return "DATE(".$this->GetSQLExpr().") = DATE($sQValue)"; + case 'SameMonth': + return "DATE_FORMAT(".$this->GetSQLExpr().", '%Y-%m') = DATE_FORMAT($sQValue, '%Y-%m')"; + case 'SameYear': + return "MONTH(".$this->GetSQLExpr().") = MONTH($sQValue)"; + case 'Today': + return "DATE(".$this->GetSQLExpr().") = CURRENT_DATE()"; + case '>|': + return "DATE(".$this->GetSQLExpr().") > DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; + case '<|': + return "DATE(".$this->GetSQLExpr().") < DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; + case '=|': + return "DATE(".$this->GetSQLExpr().") = DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; + default: + return $this->GetSQLExpr()." = $sQValue"; + } + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) + { + return null; + } + if (is_string($proposedValue) && ($proposedValue == "") && $this->IsNullAllowed()) + { + return null; + } + if (!is_numeric($proposedValue)) + { + // Check the format + try + { + $oFormat = new DateTimeFormat($this->GetInternalFormat()); + $oFormat->Parse($proposedValue); + } + catch (Exception $e) + { + throw new Exception('Wrong format for date attribute '.$this->GetCode().', expecting "'.$this->GetInternalFormat().'" and got "'.$proposedValue.'"'); + } + + return $proposedValue; + } + + return date(static::GetInternalFormat(), $proposedValue); + } + + public function ScalarToSQL($value) + { + if (empty($value)) + { + return null; + } + return $value; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + return Str::pure2html(static::GetFormat()->format($value)); + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + return Str::pure2xml($value); + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + if (empty($sValue) || ($sValue === '0000-00-00 00:00:00') || ($sValue === '0000-00-00')) + { + return ''; + } + else if ((string)static::GetFormat() !== static::GetInternalFormat()) + { + // Format conversion + $oDate = new DateTime($sValue); + if ($oDate !== false) + { + $sValue = static::GetFormat()->format($oDate); + } + } + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return $sTextQualifier.$sEscaped.$sTextQualifier; + } + + /** + * Parses a string to find some smart search patterns and build the corresponding search/OQL condition + * Each derived class is reponsible for defining and processing their own smart patterns, the base class + * does nothing special, and just calls the default (loose) operator + * + * @param string $sSearchText The search string to analyze for smart patterns + * @param FieldExpression $oField The FieldExpression representing the atttribute code in this OQL query + * @param array $aParams Values of the query parameters + * @param bool $bParseSearchString + * + * @return Expression The search condition to be added (AND) to the current search + * @throws \CoreException + */ + public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams, $bParseSearchString = false) + { + // Possible smart patterns + $aPatterns = array( + 'between' => array('pattern' => '/^\[(.*),(.*)\]$/', 'operator' => 'n/a'), + 'greater than or equal' => array('pattern' => '/^>=(.*)$/', 'operator' => '>='), + 'greater than' => array('pattern' => '/^>(.*)$/', 'operator' => '>'), + 'less than or equal' => array('pattern' => '/^<=(.*)$/', 'operator' => '<='), + 'less than' => array('pattern' => '/^<(.*)$/', 'operator' => '<'), + ); + + $sPatternFound = ''; + $aMatches = array(); + foreach($aPatterns as $sPatName => $sPattern) + { + if (preg_match($sPattern['pattern'], $sSearchText, $aMatches)) + { + $sPatternFound = $sPatName; + break; + } + } + + switch($sPatternFound) + { + case 'between': + + $sParamName1 = $oField->GetParent().'_'.$oField->GetName().'_1'; + $oRightExpr = new VariableExpression($sParamName1); + if ($bParseSearchString) + { + $aParams[$sParamName1] = $this->ParseSearchString($aMatches[1]); + } + else + { + $aParams[$sParamName1] = $aMatches[1]; + } + $oCondition1 = new BinaryExpression($oField, '>=', $oRightExpr); + + $sParamName2 = $oField->GetParent().'_'.$oField->GetName().'_2'; + $oRightExpr = new VariableExpression($sParamName2); + if ($bParseSearchString) + { + $aParams[$sParamName2] = $this->ParseSearchString($aMatches[2]); + } + else + { + $aParams[$sParamName2] = $aMatches[2]; + } + $oCondition2 = new BinaryExpression($oField, '<=', $oRightExpr); + + $oNewCondition = new BinaryExpression($oCondition1, 'AND', $oCondition2); + break; + + case 'greater than': + case 'greater than or equal': + case 'less than': + case 'less than or equal': + $sSQLOperator = $aPatterns[$sPatternFound]['operator']; + $sParamName = $oField->GetParent().'_'.$oField->GetName(); + $oRightExpr = new VariableExpression($sParamName); + if ($bParseSearchString) + { + $aParams[$sParamName] = $this->ParseSearchString($aMatches[1]); + } + else + { + $aParams[$sParamName] = $aMatches[1]; + } + $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); + + break; + + default: + $oNewCondition = parent::GetSmartConditionExpression($sSearchText, $oField, $aParams); + + } + + return $oNewCondition; + } + + + public function GetHelpOnSmartSearch() + { + $sDict = parent::GetHelpOnSmartSearch(); + + $oFormat = static::GetFormat(); + $sExample = $oFormat->Format(new DateTime('2015-07-19 18:40:00')); + return vsprintf($sDict, array($oFormat->ToPlaceholder(), $sExample)); + } +} + +/** + * Store a duration as a number of seconds + * + * @package iTopORM + */ +class AttributeDuration extends AttributeInteger +{ + public function GetEditClass() {return "Duration";} + protected function GetSQLCol($bFullSpec = false) {return "INT(11) UNSIGNED";} + + public function GetNullValue() {return '0';} + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return null; + if (!is_numeric($proposedValue)) return null; + if ( ((int)$proposedValue) < 0) return null; + + return (int)$proposedValue; + } + + public function ScalarToSQL($value) + { + if (is_null($value)) + { + return null; + } + return $value; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + return Str::pure2html(self::FormatDuration($value)); + } + + public static function FormatDuration($duration) + { + $aDuration = self::SplitDuration($duration); + + if ($duration < 60) + { + // Less than 1 min + $sResult = Dict::Format('Core:Duration_Seconds', $aDuration['seconds']); + } + else if ($duration < 3600) + { + // less than 1 hour, display it in minutes/seconds + $sResult = Dict::Format('Core:Duration_Minutes_Seconds', $aDuration['minutes'], $aDuration['seconds']); + } + else if ($duration < 86400) + { + // Less than 1 day, display it in hours/minutes/seconds + $sResult = Dict::Format('Core:Duration_Hours_Minutes_Seconds', $aDuration['hours'], $aDuration['minutes'], $aDuration['seconds']); + } + else + { + // more than 1 day, display it in days/hours/minutes/seconds + $sResult = Dict::Format('Core:Duration_Days_Hours_Minutes_Seconds', $aDuration['days'], $aDuration['hours'], $aDuration['minutes'], $aDuration['seconds']); + } + return $sResult; + } + + static function SplitDuration($duration) + { + $duration = (int) $duration; + $days = floor($duration / 86400); + $hours = floor(($duration - (86400*$days)) / 3600); + $minutes = floor(($duration - (86400*$days + 3600*$hours)) / 60); + $seconds = ($duration % 60); // modulo + return array( 'days' => $days, 'hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds ); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\DurationField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + parent::MakeFormField($oObject, $oFormField); + + // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition + $sAttCode = $this->GetCode(); + $oFormField->SetCurrentValue($oObject->Get($sAttCode)); + $oFormField->SetReadOnly(true); + + return $oFormField; + } + +} +/** + * Map a date+time column to an attribute + * + * @package iTopORM + */ +class AttributeDate extends AttributeDateTime +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE; + + static $oDateFormat = null; + + static public function GetFormat() + { + if (self::$oDateFormat == null) + { + AttributeDateTime::LoadFormatFromConfig(); + } + return self::$oDateFormat; + } + + static public function SetFormat(DateTimeFormat $oDateFormat) + { + self::$oDateFormat = $oDateFormat; + } + + /** + * Returns the format string used for the date & time stored in memory + * @return string + */ + static public function GetInternalFormat() + { + return 'Y-m-d'; + } + + /** + * Returns the format string used for the date & time written to MySQL + * @return string + */ + static public function GetSQLFormat() + { + return 'Y-m-d'; + } + + static public function ListExpectedParams() + { + return parent::ListExpectedParams(); + //return array_merge(parent::ListExpectedParams(), array()); + } + + public function GetEditClass() {return "Date";} + protected function GetSQLCol($bFullSpec = false) {return "DATE";} + public function GetImportColumns() + { + // Allow an empty string to be a valid value (synonym for "reset") + $aColumns = array(); + $aColumns[$this->GetCode()] = 'VARCHAR(10)'; + return $aColumns; + } + + + /** + * Override to specify Field class + * + * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behave more like a Prepare. + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + $oFormField = parent::MakeFormField($oObject, $oFormField); + $oFormField->SetDateOnly(true); + + return $oFormField; + } + +} + +/** + * A dead line stored as a date & time + * The only difference with the DateTime attribute is the display: + * relative to the current time + */ +class AttributeDeadline extends AttributeDateTime +{ + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + $sResult = self::FormatDeadline($value); + return $sResult; + } + + public static function FormatDeadline($value) + { + $sResult = ''; + if ($value !== null) + { + $iValue = AttributeDateTime::GetAsUnixSeconds($value); + $sDate = AttributeDateTime::GetFormat()->Format($value); + $difference = $iValue - time(); + + if ($difference >= 0) + { + $sDifference = self::FormatDuration($difference); + } + else + { + $sDifference = Dict::Format('UI:DeadlineMissedBy_duration', self::FormatDuration(-$difference)); + } + $sFormat = MetaModel::GetConfig()->Get('deadline_format'); + $sResult = str_replace(array('$date$', '$difference$'), array($sDate, $sDifference), $sFormat); + } + + return $sResult; + } + + static function FormatDuration($duration) + { + $days = floor($duration / 86400); + $hours = floor(($duration - (86400*$days)) / 3600); + $minutes = floor(($duration - (86400*$days + 3600*$hours)) / 60); + + if ($duration < 60) + { + // Less than 1 min + $sResult =Dict::S('UI:Deadline_LessThan1Min'); + } + else if ($duration < 3600) + { + // less than 1 hour, display it in minutes + $sResult =Dict::Format('UI:Deadline_Minutes', $minutes); + } + else if ($duration < 86400) + { + // Less that 1 day, display it in hours/minutes + $sResult =Dict::Format('UI:Deadline_Hours_Minutes', $hours, $minutes); + } + else + { + // Less that 1 day, display it in hours/minutes + $sResult =Dict::Format('UI:Deadline_Days_Hours_Minutes', $days, $hours, $minutes); + } + return $sResult; + } +} + +/** + * Map a foreign key to an attribute + * AttributeExternalKey and AttributeExternalField may be an external key + * the difference is that AttributeExternalKey corresponds to a column into the defined table + * where an AttributeExternalField corresponds to a column into another table (class) + * + * @package iTopORM + */ +class AttributeExternalKey extends AttributeDBFieldVoid +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; + + + /** + * Return the search widget type corresponding to this attribute + * + * @return string + */ + public function GetSearchType() + { + try + { + $oRemoteAtt = $this->GetFinalAttDef(); + $sTargetClass = $oRemoteAtt->GetTargetClass(); + if (MetaModel::IsHierarchicalClass($sTargetClass)) + { + return self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; + } + return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; + } + catch (CoreException $e) + { + } + + return self::SEARCH_WIDGET_TYPE_RAW; + } + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete")); + } + + public function GetEditClass() {return "ExtKey";} + protected function GetSQLCol($bFullSpec = false) {return "INT(11)".($bFullSpec ? " DEFAULT 0" : "");} + public function RequiresIndex() + { + return true; + } + + public function IsExternalKey($iType = EXTKEY_RELATIVE) {return true;} + public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->Get("targetclass");} + public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;} + public function GetKeyAttCode() {return $this->GetCode();} + public function GetDisplayStyle() { return $this->GetOptional('display_style', 'select'); } + + + public function GetDefaultValue(DBObject $oHostObject = null) {return 0;} + public function IsNullAllowed() + { + if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys')) + { + return true; + } + return $this->Get("is_null_allowed"); + } + + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + return parent::GetBasicFilterLooseOperator(); + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return parent::GetBasicFilterSQLExpr($sOpCode, $value); + } + + // overloaded here so that an ext key always have the answer to + // "what are your possible values?" + public function GetValuesDef() + { + $oValSetDef = $this->Get("allowed_values"); + if (!$oValSetDef) + { + // Let's propose every existing value + $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); + } + return $oValSetDef; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + //throw new Exception("GetAllowedValues on ext key has been deprecated"); + try + { + return parent::GetAllowedValues($aArgs, $sContains); + } + catch (MissingQueryArgument $e) //FIXME never enters here... + { + // Some required arguments could not be found, enlarge to any existing value + $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); + return $oValSetDef->GetValues($aArgs, $sContains); + } + } + + public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) + { + $oValSetDef = $this->GetValuesDef(); + $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue); + return $oSet; + } + + public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) + { + return DBObjectSearch::FromOQL($this->GetValuesDef()->GetFilterExpression()); + } + + public function GetDeletionPropagationOption() + { + return $this->Get("on_target_delete"); + } + + public function GetNullValue() + { + return 0; + } + + public function IsNull($proposedValue) + { + return ($proposedValue == 0); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return 0; + if ($proposedValue === '') return 0; + if (MetaModel::IsValidObject($proposedValue)) return $proposedValue->GetKey(); + return (int)$proposedValue; + } + + public function GetMaximumComboLength() + { + return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length')); + } + + public function GetMinAutoCompleteChars() + { + return $this->GetOptional('min_autocomplete_chars', MetaModel::GetConfig()->Get('min_autocomplete_chars')); + } + + public function AllowTargetCreation() + { + return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation')); + } + + /** + * Find the corresponding "link" attribute on the target class, if any + * @return null | AttributeDefinition + * @throws \CoreException + */ + public function GetMirrorLinkAttribute() + { + $oRet = null; + $sRemoteClass = $this->GetTargetClass(); + foreach (MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) + { + if (!$oRemoteAttDef->IsLinkSet()) continue; + if (!is_subclass_of($this->GetHostClass(), $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $this->GetHostClass()) continue; + if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetCode()) continue; + $oRet = $oRemoteAttDef; + break; + } + return $oRet; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + // TODO : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + // Setting params + $oFormField->SetMaximumComboLength($this->GetMaximumComboLength()); + $oFormField->SetMinAutoCompleteChars($this->GetMinAutoCompleteChars()); + $oFormField->SetHierarchical(MetaModel::IsHierarchicalClass($this->GetTargetClass())); + // Setting choices regarding the field dependencies + $aFieldDependencies = $this->GetPrerequisiteAttributes(); + if (!empty($aFieldDependencies)) + { + $oTmpAttDef = $this; + $oTmpField = $oFormField; + $oFormField->SetOnFinalizeCallback(function() use ($oTmpField, $oTmpAttDef, $oObject) + { + /** @var $oTmpField \Combodo\iTop\Form\Field\Field */ + /** @var $oTmpAttDef \AttributeDefinition */ + /** @var $oObject \DBObject */ + + // We set search object only if it has not already been set (overrided) + if ($oTmpField->GetSearch() === null) + { + $oSearch = DBSearch::FromOQL($oTmpAttDef->GetValuesDef()->GetFilterExpression()); + $oSearch->SetInternalParams(array('this' => $oObject)); + $oTmpField->SetSearch($oSearch); + } + }); + } + else + { + $oSearch = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression()); + $oSearch->SetInternalParams(array('this' => $oObject)); + $oFormField->SetSearch($oSearch); + } + + // If ExtKey is mandatory, we add a validator to ensure that the value 0 is not selected + if ($oObject->GetAttributeFlags($this->GetCode()) & OPT_ATT_MANDATORY) + { + $oFormField->AddValidator(new \Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator()); + } + + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + +} + +/** + * Special kind of External Key to manage a hierarchy of objects + */ +class AttributeHierarchicalKey extends AttributeExternalKey +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; + + protected $m_sTargetClass; + + static public function ListExpectedParams() + { + $aParams = parent::ListExpectedParams(); + $idx = array_search('targetclass', $aParams); + unset($aParams[$idx]); + $idx = array_search('jointype', $aParams); + unset($aParams[$idx]); + return $aParams; // TODO: mettre les bons parametres ici !! + } + + public function GetEditClass() {return "ExtKey";} + public function RequiresIndex() + { + return true; + } + + /* + * The target class is the class for which the attribute has been defined first + */ + public function SetHostClass($sHostClass) + { + if (!isset($this->m_sTargetClass)) + { + $this->m_sTargetClass = $sHostClass; + } + parent::SetHostClass($sHostClass); + } + + static public function IsHierarchicalKey() {return true;} + public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->m_sTargetClass;} + public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;} + public function GetKeyAttCode() {return $this->GetCode();} + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + return parent::GetBasicFilterLooseOperator(); + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->GetCode()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); + $aColumns[$this->GetSQLLeft()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); + $aColumns[$this->GetSQLRight()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); + return $aColumns; + } + public function GetSQLRight() + { + return $this->GetCode().'_right'; + } + public function GetSQLLeft() + { + return $this->GetCode().'_left'; + } + + public function GetSQLValues($value) + { + if (!is_array($value)) + { + $aValues[$this->GetCode()] = $value; + } + else + { + $aValues = array(); + $aValues[$this->GetCode()] = $value[$this->GetCode()]; + $aValues[$this->GetSQLRight()] = $value[$this->GetSQLRight()]; + $aValues[$this->GetSQLLeft()] = $value[$this->GetSQLLeft()]; + } + return $aValues; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $oFilter = $this->GetHierachicalFilter($aArgs, $sContains); + if ($oFilter) + { + $oValSetDef = $this->GetValuesDef(); + $oValSetDef->AddCondition($oFilter); + return $oValSetDef->GetValues($aArgs, $sContains); + } + else + { + return parent::GetAllowedValues($aArgs, $sContains); + } + } + + public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) + { + $oValSetDef = $this->GetValuesDef(); + $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue); + if ($oFilter) + { + $oValSetDef->AddCondition($oFilter); + } + $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue); + return $oSet; + } + + public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) + { + $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue); + if ($oFilter) + { + return $oFilter; + } + return parent::GetAllowedValuesAsFilter($aArgs, $sContains, $iAdditionalValue); + } + + private function GetHierachicalFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) + { + if (array_key_exists('this', $aArgs)) + { + // Hierarchical keys have one more constraint: the "parent value" cannot be + // "under" themselves + $iRootId = $aArgs['this']->GetKey(); + if ($iRootId > 0) // ignore objects that do no exist in the database... + { + $sClass = $this->m_sTargetClass; + return DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node.".$this->GetCode()." NOT BELOW root.id WHERE root.id = $iRootId"); + } + } + return false; + } + + /** + * Find the corresponding "link" attribute on the target class, if any + * @return null | AttributeDefinition + */ + public function GetMirrorLinkAttribute() + { + return null; + } +} + +/** + * An attribute which corresponds to an external key (direct or indirect) + * + * @package iTopORM + */ +class AttributeExternalField extends AttributeDefinition +{ + /** + * Return the search widget type corresponding to this attribute + * + * @return string + * @throws \CoreException + */ + public function GetSearchType() + { + // Not necessary the external key is already present + if ($this->IsFriendlyName()) + { + return self::SEARCH_WIDGET_TYPE_RAW; + } + + try + { + $oRemoteAtt = $this->GetFinalAttDef(); + switch (true) + { + case ($oRemoteAtt instanceof AttributeString): + return self::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD; + case ($oRemoteAtt instanceof AttributeExternalKey): + return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; + } + } + catch (CoreException $e) + { + } + + return self::SEARCH_WIDGET_TYPE_RAW; + } + + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode")); + } + + public function GetEditClass() {return "ExtField";} + + /** + * @return \AttributeDefinition + * @throws \CoreException + */ + public function GetFinalAttDef() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetFinalAttDef(); + } + + protected function GetSQLCol($bFullSpec = false) + { + // throw new CoreException("external attribute: does it make any sense to request its type ?"); + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetSQLCol($bFullSpec); + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + return array('' => $this->GetCode()); // Warning: Use GetCode() since AttributeExternalField does not have any 'sql' property + } + else + { + return $sPrefix; + } + } + + public function GetLabel($sDefault = null) + { + if ($this->IsFriendlyName()) + { + $sKeyAttCode = $this->Get("extkey_attcode"); + $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); + $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); + } + else + { + $sLabel = parent::GetLabel(''); + if (strlen($sLabel) == 0) + { + $oRemoteAtt = $this->GetExtAttDef(); + $sLabel = $oRemoteAtt->GetLabel($this->m_sCode); + } + } + return $sLabel; + } + + public function GetLabelForSearchField() + { + $sLabel = parent::GetLabel(''); + if (strlen($sLabel) == 0) + { + $sKeyAttCode = $this->Get("extkey_attcode"); + $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); + $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); + + $oRemoteAtt = $this->GetExtAttDef(); + $sLabel .= '->'.$oRemoteAtt->GetLabel($this->m_sCode); + } + + return $sLabel; + } + + public function GetDescription($sDefault = null) + { + $sLabel = parent::GetDescription(''); + if (strlen($sLabel) == 0) + { + $oRemoteAtt = $this->GetExtAttDef(); + $sLabel = $oRemoteAtt->GetDescription(''); + } + return $sLabel; + } + public function GetHelpOnEdition($sDefault = null) + { + $sLabel = parent::GetHelpOnEdition(''); + if (strlen($sLabel) == 0) + { + $oRemoteAtt = $this->GetExtAttDef(); + $sLabel = $oRemoteAtt->GetHelpOnEdition(''); + } + return $sLabel; + } + + public function IsExternalKey($iType = EXTKEY_RELATIVE) + { + switch($iType) + { + case EXTKEY_ABSOLUTE: + // see further + $oRemoteAtt = $this->GetExtAttDef(); + return $oRemoteAtt->IsExternalKey($iType); + + case EXTKEY_RELATIVE: + return false; + + default: + throw new CoreException("Unexpected value for argument iType: '$iType'"); + } + } + + /** + * @return bool + * @throws \CoreException + */ + public function IsFriendlyName() + { + $oRemoteAtt = $this->GetExtAttDef(); + if ($oRemoteAtt instanceof AttributeExternalField) + { + $bRet = $oRemoteAtt->IsFriendlyName(); + } + elseif ($oRemoteAtt instanceof AttributeFriendlyName) + { + $bRet = true; + } + else + { + $bRet = false; + } + return $bRet; + } + + public function GetTargetClass($iType = EXTKEY_RELATIVE) + { + return $this->GetKeyAttDef($iType)->GetTargetClass(); + } + + static public function IsExternalField() {return true;} + + public function GetKeyAttCode() + { + return $this->Get("extkey_attcode"); + } + + public function GetExtAttCode() + { + return $this->Get("target_attcode"); + } + + /** + * @param int $iType + * + * @return \AttributeExternalKey + * @throws \CoreException + * @throws \Exception + */ + public function GetKeyAttDef($iType = EXTKEY_RELATIVE) + { + switch($iType) + { + case EXTKEY_ABSOLUTE: + // see further + /** @var \AttributeExternalKey $oRemoteAtt */ + $oRemoteAtt = $this->GetExtAttDef(); + if ($oRemoteAtt->IsExternalField()) + { + return $oRemoteAtt->GetKeyAttDef(EXTKEY_ABSOLUTE); + } + else if ($oRemoteAtt->IsExternalKey()) + { + return $oRemoteAtt; + } + return $this->GetKeyAttDef(EXTKEY_RELATIVE); // which corresponds to the code hereafter ! + + case EXTKEY_RELATIVE: + return MetaModel::GetAttributeDef($this->GetHostClass(), $this->Get("extkey_attcode")); + + default: + throw new CoreException("Unexpected value for argument iType: '$iType'"); + } + } + + public function GetPrerequisiteAttributes($sClass = null) + { + return array($this->Get("extkey_attcode")); + } + + + /** + * @return \AttributeExternalField + * @throws \CoreException + * @throws \Exception + */ + public function GetExtAttDef() + { + $oKeyAttDef = $this->GetKeyAttDef(); + /** @var \AttributeExternalField $oExtAttDef */ + $oExtAttDef = MetaModel::GetAttributeDef($oKeyAttDef->GetTargetClass(), $this->Get("target_attcode")); + if (!is_object($oExtAttDef)) throw new CoreException("Invalid external field ".$this->GetCode()." in class ".$this->GetHostClass().". The class ".$oKeyAttDef->GetTargetClass()." has no attribute ".$this->Get("target_attcode")); + return $oExtAttDef; + } + + /** + * @return mixed + * @throws \CoreException + */ + public function GetSQLExpr() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetSQLExpr(); + } + + public function GetDefaultValue(DBObject $oHostObject = null) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetDefaultValue(); + } + public function IsNullAllowed() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->IsNullAllowed(); + } + + static public function IsScalar() + { + return true; + } + + public function GetFilterDefinitions() + { + return array($this->GetCode() => new FilterFromAttribute($this)); + } + + public function GetBasicFilterOperators() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetBasicFilterOperators(); + } + public function GetBasicFilterLooseOperator() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetBasicFilterLooseOperator(); + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value); + } + + public function GetNullValue() + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetNullValue(); + } + + public function IsNull($proposedValue) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->IsNull($proposedValue); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->MakeRealValue($proposedValue, $oHostObj); + } + + public function ScalarToSQL($value) + { + // This one could be used in case of filtering only + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->ScalarToSQL($value); + } + + + // Do not overload GetSQLExpression here because this is handled in the joins + //public function GetSQLExpressions($sPrefix = '') {return array();} + + // Here, we get the data... + public function FromSQLToValue($aCols, $sPrefix = '') + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->FromSQLToValue($aCols, $sPrefix); + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetAsHTML($value, null, $bLocalize); + } + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetAsXML($value, null, $bLocalize); + } + public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $oExtAttDef = $this->GetExtAttDef(); + return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize, $bConvertToPlainText); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\LabelField'; + } + + /** + * @param \DBObject $oObject + * @param \Combodo\iTop\Form\Field\Field $oFormField + * + * @return null + * @throws \CoreException + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + // Retrieving AttDef from the remote attribute + $oRemoteAttDef = $this->GetExtAttDef(); + + if ($oFormField === null) + { + // ExternalField's FormField are actually based on the FormField from the target attribute. + // Except for the AttributeExternalKey because we have no OQL and stuff + if($oRemoteAttDef instanceof AttributeExternalKey) + { + $sFormFieldClass = static::GetFormFieldClass(); + } + else + { + $sFormFieldClass = $oRemoteAttDef::GetFormFieldClass(); + } + $oFormField = new $sFormFieldClass($this->GetCode()); + } + parent::MakeFormField($oObject, $oFormField); + + // Manually setting for remote ExternalKey, otherwise, the id would be displayed. + if($oRemoteAttDef instanceof AttributeExternalKey) + { + $oFormField->SetCurrentValue($oObject->Get($this->GetCode().'_friendlyname')); + } + + // Readonly field because we can't update external fields + $oFormField->SetReadOnly(true); + + return $oFormField; + } + + public function IsPartOfFingerprint() + { + return false; + } + +} + + +/** + * Multi value list of tags + * + * @see TagSetFieldData + * @since 2.6 N°931 tag fields + */ +class AttributeTagSet extends AttributeString +{ + //TODO SQL type length (nb of tags per record, max tag length) + //TODO implement ?? + //TODO specific filters + public function RequiresIndex() + { + return true; + } + + public function RequiresFullTextIndex() + { + return true; + } + + public function IsNullAllowed() + { + return true; + } +} + +/** + * Map a varchar column to an URL (formats the ouput in HMTL) + * + * @package iTopORM + */ +class AttributeURL extends AttributeString +{ + static public function ListExpectedParams() + { + //return parent::ListExpectedParams(); + return array_merge(parent::ListExpectedParams(), array("target")); + } + + protected function GetSQLCol($bFullSpec = false) + { + return "VARCHAR(2048)" + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? $this->GetSQLColSpec() : ''); + } + + public function GetMaxSize() + { + return 2048; + } + + public function GetEditClass() {return "String";} + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + $sTarget = $this->Get("target"); + if (empty($sTarget)) $sTarget = "_blank"; + $sLabel = Str::pure2html($sValue); + if (strlen($sLabel) > 128) + { + // Truncate the length to 128 characters, by removing the middle + $sLabel = substr($sLabel, 0, 100).'.....'.substr($sLabel, -20); + } + return "$sLabel"; + } + + public function GetValidationPattern() + { + return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('url_validation_pattern').'$'); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\UrlField'; + } + + /** + * @param \DBObject $oObject + * @param \Combodo\iTop\Form\Field\UrlField $oFormField + * + * @return null + * @throws \CoreException + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + parent::MakeFormField($oObject, $oFormField); + + $oFormField->SetTarget($this->Get('target')); + + return $oFormField; + } +} + +/** + * A blob is an ormDocument, it is stored as several columns in the database + * + * @package iTopORM + */ +class AttributeBlob extends AttributeDefinition +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("depends_on")); + } + + public function GetEditClass() {return "Document";} + + static public function IsBasedOnDBColumns() {return true;} + static public function IsScalar() {return true;} + public function IsWritable() {return true;} + public function GetDefaultValue(DBObject $oHostObject = null) {return "";} + public function IsNullAllowed(DBObject $oHostObject = null) {return $this->GetOptional("is_null_allowed", false);} + + public function GetEditValue($sValue, $oHostObj = null) + { + return ''; + } + + /** + * Users can provide the document from an URL (including an URL on iTop itself) + * for CSV import. Administrators can even provide the path to a local file + * {@inheritDoc} + * @see AttributeDefinition::MakeRealValue() + */ + public function MakeRealValue($proposedValue, $oHostObj) + { + if ($proposedValue === null) return null; + + if (is_object($proposedValue)) + { + $proposedValue = clone $proposedValue; + } + else + { + try + { + // Read the file from iTop, an URL (or the local file system - for admins only) + $proposedValue = Utils::FileGetContentsAndMIMEType($proposedValue); + } + catch(Exception $e) + { + IssueLog::Warning(get_class($this)."::MakeRealValue - ".$e->getMessage()); + // Not a real document !! store is as text !!! (This was the default behavior before) + $proposedValue = new ormDocument($e->getMessage()." \n".$proposedValue, 'text/plain'); + } + } + return $proposedValue; + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->GetCode(); + } + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $sPrefix.'_mimetype'; + $aColumns['_data'] = $sPrefix.'_data'; + $aColumns['_filename'] = $sPrefix.'_filename'; + return $aColumns; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + if (!array_key_exists($sPrefix, $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); + } + $sMimeType = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; + + if (!array_key_exists($sPrefix.'_data', $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}"); + } + $data = isset($aCols[$sPrefix.'_data']) ? $aCols[$sPrefix.'_data'] : null; + + if (!array_key_exists($sPrefix.'_filename', $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}"); + } + $sFileName = isset($aCols[$sPrefix.'_filename']) ? $aCols[$sPrefix.'_filename'] : ''; + + $value = new ormDocument($data, $sMimeType, $sFileName); + return $value; + } + + public function GetSQLValues($value) + { + // #@# Optimization: do not load blobs anytime + // As per mySQL doc, selecting blob columns will prevent mySQL from + // using memory in case a temporary table has to be created + // (temporary tables created on disk) + // We will have to remove the blobs from the list of attributes when doing the select + // then the use of Get() should finalize the load + if ($value instanceOf ormDocument && !$value->IsEmpty()) + { + $aValues = array(); + $aValues[$this->GetCode().'_data'] = $value->GetData(); + $aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType(); + $aValues[$this->GetCode().'_filename'] = $value->GetFileName(); + } + else + { + $aValues = array(); + $aValues[$this->GetCode().'_data'] = ''; + $aValues[$this->GetCode().'_mimetype'] = ''; + $aValues[$this->GetCode().'_filename'] = ''; + } + return $aValues; + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb) + $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); + $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); + return $aColumns; + } + + public function GetFilterDefinitions() + { + return array(); + } + + public function GetBasicFilterOperators() + { + return array(); + } + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return 'true'; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + if (is_object($value)) + { + return $value->GetAsHTML(); + } + return ''; + } + + /** + * @param string $sValue + * @param string $sSeparator + * @param string $sTextQualifier + * @param \DBObject $oHostObject + * @param bool $bLocalize + * @param bool $bConvertToPlainText + * + * @return string + */ + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $sAttCode = $this->GetCode(); + if ($sValue instanceof ormDocument && !$sValue->IsEmpty()) + { + return $sValue->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $sAttCode); + } + return ''; // Not exportable in CSV ! + } + + /** + * @param $value + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return mixed|string + */ + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + $sRet = ''; + if (is_object($value)) + { + if (!$value->IsEmpty()) + { + $sRet = ''.$value->GetMimeType().''; + $sRet .= ''.$value->GetFileName().''; + $sRet .= ''.base64_encode($value->GetData()).''; + } + } + return $sRet; + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + */ + public function GetForJSON($value) + { + if ($value instanceOf ormDocument) + { + $aValues = array(); + $aValues['data'] = base64_encode($value->GetData()); + $aValues['mimetype'] = $value->GetMimeType(); + $aValues['filename'] = $value->GetFileName(); + } + else + { + $aValues = null; + } + return $aValues; + } + + /** + * Helper to form a value, given JSON decoded data + * The operation is the opposite to GetForJSON + */ + public function FromJSONToValue($json) + { + if (isset($json->data)) + { + $data = base64_decode($json->data); + $value = new ormDocument($data, $json->mimetype, $json->filename); + } + else + { + $value = null; + } + return $value; + } + + public function Fingerprint($value) + { + $sFingerprint = ''; + if ($value instanceOf ormDocument) + { + $sFingerprint = md5($value->GetData()); + } + return $sFingerprint; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\BlobField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + // Note: As of today we want this field to always be read-only + $oFormField->SetReadOnly(true); + + // Generating urls + $value = $oObject->Get($this->GetCode()); + $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); + $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); + + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + +} + +/** + * An image is a specific type of document, it is stored as several columns in the database + * + * @package iTopORM + */ +class AttributeImage extends AttributeBlob +{ + public function GetEditClass() {return "Image";} + + /** + * {@inheritDoc} + * @see AttributeBlob::MakeRealValue() + */ + public function MakeRealValue($proposedValue, $oHostObj) + { + $oDoc = parent::MakeRealValue($proposedValue, $oHostObj); + // The validation of the MIME Type is done by CheckFormat below + return $oDoc; + } + + /** + * Check that the supplied ormDocument actually contains an image + * {@inheritDoc} + * @see AttributeDefinition::CheckFormat() + */ + public function CheckFormat($value) + { + if ($value instanceof ormDocument && !$value->IsEmpty()) + { + return ($value->GetMainMimeType() == 'image'); + } + return true; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + $iMaxWidthPx = $this->Get('display_max_width').'px'; + $iMaxHeightPx = $this->Get('display_max_height').'px'; + $sUrl = $this->Get('default_image'); + $sRet = ($sUrl !== null) ? '' : ''; + if (is_object($value) && !$value->IsEmpty()) + { + if ($oHostObject->IsNew() || ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges())))) + { + // If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline + // otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it + $sUrl = 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData()); + } + else + { + $sUrl = $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode()); + } + $sRet = ''; + } + return '
    '.$sRet.'
    '; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\ImageField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + parent::MakeFormField($oObject, $oFormField); + + // Generating urls + $value = $oObject->Get($this->GetCode()); + if (is_object($value) && !$value->IsEmpty()) + { + $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); + $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); + } + else + { + $oFormField->SetDownloadUrl($this->Get('default_image')); + $oFormField->SetDisplayUrl($this->Get('default_image')); + } + + return $oFormField; + } +} +/** + * A stop watch is an ormStopWatch object, it is stored as several columns in the database + * + * @package iTopORM + */ +class AttributeStopWatch extends AttributeDefinition +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + // The list of thresholds must be an array of iPercent => array of 'option' => value + return array_merge(parent::ListExpectedParams(), array("states", "goal_computing", "working_time_computing", "thresholds")); + } + + public function GetEditClass() {return "StopWatch";} + + static public function IsBasedOnDBColumns() {return true;} + static public function IsScalar() {return true;} + public function IsWritable() {return true;} + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->NewStopWatch();} + + /** + * @param \ormStopWatch $value + * @param \DBObject $oHostObj + * + * @return string + */ + public function GetEditValue($value, $oHostObj = null) + { + return $value->GetTimeSpent(); + } + + public function GetStates() + { + return $this->Get('states'); + } + + public function AlwaysLoadInTables() + { + // Each and every stop watch is accessed for computing the highlight code (DBObject::GetHighlightCode()) + return true; + } + + /** + * Construct a brand new (but configured) stop watch + */ + public function NewStopWatch() + { + $oSW = new ormStopWatch(); + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $oSW->DefineThreshold($iThreshold); + } + return $oSW; + } + + // Facilitate things: allow the user to Set the value from a string + public function MakeRealValue($proposedValue, $oHostObj) + { + if (!$proposedValue instanceof ormStopWatch) + { + return $this->NewStopWatch(); + } + return $proposedValue; + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->GetCode(); // Warning: a stopwatch does not have any 'sql' property, so its SQL column is equal to its attribute code !! + } + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $sPrefix.'_timespent'; + $aColumns['_started'] = $sPrefix.'_started'; + $aColumns['_laststart'] = $sPrefix.'_laststart'; + $aColumns['_stopped'] = $sPrefix.'_stopped'; + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = '_'.$iThreshold; + $aColumns[$sThPrefix.'_deadline'] = $sPrefix.$sThPrefix.'_deadline'; + $aColumns[$sThPrefix.'_passed'] = $sPrefix.$sThPrefix.'_passed'; + $aColumns[$sThPrefix.'_triggered'] = $sPrefix.$sThPrefix.'_triggered'; + $aColumns[$sThPrefix.'_overrun'] = $sPrefix.$sThPrefix.'_overrun'; + } + return $aColumns; + } + + public static function DateToSeconds($sDate) + { + if (is_null($sDate)) + { + return null; + } + $oDateTime = new DateTime($sDate); + $iSeconds = $oDateTime->format('U'); + return $iSeconds; + } + + public static function SecondsToDate($iSeconds) + { + if (is_null($iSeconds)) + { + return null; + } + return date("Y-m-d H:i:s", $iSeconds); + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + $aExpectedCols = array($sPrefix, $sPrefix.'_started', $sPrefix.'_laststart', $sPrefix.'_stopped'); + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = '_'.$iThreshold; + $aExpectedCols[] = $sPrefix.$sThPrefix.'_deadline'; + $aExpectedCols[] = $sPrefix.$sThPrefix.'_passed'; + $aExpectedCols[] = $sPrefix.$sThPrefix.'_triggered'; + $aExpectedCols[] = $sPrefix.$sThPrefix.'_overrun'; + } + foreach ($aExpectedCols as $sExpectedCol) + { + if (!array_key_exists($sExpectedCol, $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sExpectedCol' from {$sAvailable}"); + } + } + + $value = new ormStopWatch( + $aCols[$sPrefix], + self::DateToSeconds($aCols[$sPrefix.'_started']), + self::DateToSeconds($aCols[$sPrefix.'_laststart']), + self::DateToSeconds($aCols[$sPrefix.'_stopped']) + ); + + foreach ($this->ListThresholds() as $iThreshold => $aDefinition) + { + $sThPrefix = '_'.$iThreshold; + $value->DefineThreshold( + $iThreshold, + self::DateToSeconds($aCols[$sPrefix.$sThPrefix.'_deadline']), + (bool)($aCols[$sPrefix.$sThPrefix.'_passed'] == 1), + (bool)($aCols[$sPrefix.$sThPrefix.'_triggered'] == 1), + $aCols[$sPrefix.$sThPrefix.'_overrun'], + array_key_exists('highlight', $aDefinition) ? $aDefinition['highlight'] : null + ); + } + + return $value; + } + + public function GetSQLValues($value) + { + if ($value instanceOf ormStopWatch) + { + $aValues = array(); + $aValues[$this->GetCode().'_timespent'] = $value->GetTimeSpent(); + $aValues[$this->GetCode().'_started'] = self::SecondsToDate($value->GetStartDate()); + $aValues[$this->GetCode().'_laststart'] = self::SecondsToDate($value->GetLastStartDate()); + $aValues[$this->GetCode().'_stopped'] = self::SecondsToDate($value->GetStopDate()); + + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sPrefix = $this->GetCode().'_'.$iThreshold; + $aValues[$sPrefix.'_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold)); + $aValues[$sPrefix.'_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0'; + $aValues[$sPrefix.'_triggered'] = $value->IsThresholdTriggered($iThreshold) ? '1' : '0'; + $aValues[$sPrefix.'_overrun'] = $value->GetOverrun($iThreshold); + } + } + else + { + $aValues = array(); + $aValues[$this->GetCode().'_timespent'] = ''; + $aValues[$this->GetCode().'_started'] = ''; + $aValues[$this->GetCode().'_laststart'] = ''; + $aValues[$this->GetCode().'_stopped'] = ''; + } + return $aValues; + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->GetCode().'_timespent'] = 'INT(11) UNSIGNED'; + $aColumns[$this->GetCode().'_started'] = 'DATETIME'; + $aColumns[$this->GetCode().'_laststart'] = 'DATETIME'; + $aColumns[$this->GetCode().'_stopped'] = 'DATETIME'; + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sPrefix = $this->GetCode().'_'.$iThreshold; + $aColumns[$sPrefix.'_deadline'] = 'DATETIME'; + $aColumns[$sPrefix.'_passed'] = 'TINYINT(1) UNSIGNED'; + $aColumns[$sPrefix.'_triggered'] = 'TINYINT(1)'; + $aColumns[$sPrefix.'_overrun'] = 'INT(11) UNSIGNED'; + } + return $aColumns; + } + + public function GetFilterDefinitions() + { + $aRes = array( + $this->GetCode() => new FilterFromAttribute($this), + $this->GetCode().'_started' => new FilterFromAttribute($this, '_started'), + $this->GetCode().'_laststart' => new FilterFromAttribute($this, '_laststart'), + $this->GetCode().'_stopped' => new FilterFromAttribute($this, '_stopped') + ); + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sPrefix = $this->GetCode().'_'.$iThreshold; + $aRes[$sPrefix.'_deadline'] = new FilterFromAttribute($this, '_deadline'); + $aRes[$sPrefix.'_passed'] = new FilterFromAttribute($this, '_passed'); + $aRes[$sPrefix.'_triggered'] = new FilterFromAttribute($this, '_triggered'); + $aRes[$sPrefix.'_overrun'] = new FilterFromAttribute($this, '_overrun'); + } + return $aRes; + } + + public function GetBasicFilterOperators() + { + return array(); + } + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return 'true'; + } + + /** + * @param \ormStopWatch $value + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return string + */ + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + if (is_object($value)) + { + return $value->GetAsHTML($this, $oHostObject); + } + return ''; + } + + /** + * @param ormStopWatch $value + * @param string $sSeparator + * @param string $sTextQualifier + * @param null $oHostObject + * @param bool $bLocalize + * @param bool $bConvertToPlainText + * + * @return string + */ + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + return $value->GetTimeSpent(); + } + + /** + * @param \ormStopWatch $value + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return mixed + */ + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + return $value->GetTimeSpent(); + } + + public function ListThresholds() + { + return $this->Get('thresholds'); + } + + public function Fingerprint($value) + { + $sFingerprint = ''; + if (is_object($value)) + { + $sFingerprint = $value->GetAsHTML($this); + } + return $sFingerprint; + } + + /** + * To expose internal values: Declare an attribute AttributeSubItem + * and implement the GetSubItemXXXX verbs + * + * @param string $sItemCode + * + * @return array + * @throws \CoreException + */ + public function GetSubItemSQLExpression($sItemCode) + { + $sPrefix = $this->GetCode(); + switch($sItemCode) + { + case 'timespent': + return array('' => $sPrefix.'_timespent'); + case 'started': + return array('' => $sPrefix.'_started'); + case 'laststart': + return array('' => $sPrefix.'_laststart'); + case 'stopped': + return array('' => $sPrefix.'_stopped'); + } + + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold.'_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch($sThresholdCode) + { + case 'deadline': + return array('' => $sPrefix.'_'.$iThreshold.'_deadline'); + case 'passed': + return array('' => $sPrefix.'_'.$iThreshold.'_passed'); + case 'triggered': + return array('' => $sPrefix.'_'.$iThreshold.'_triggered'); + case 'overrun': + return array('' => $sPrefix.'_'.$iThreshold.'_overrun'); + } + } + } + throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode()); + } + + /** + * @param string $sItemCode + * @param \ormStopWatch $value + * @param \DBObject $oHostObject + * + * @return mixed + * @throws \CoreException + */ + public function GetSubItemValue($sItemCode, $value, $oHostObject = null) + { + $oStopWatch = $value; + switch($sItemCode) + { + case 'timespent': + return $oStopWatch->GetTimeSpent(); + case 'started': + return $oStopWatch->GetStartDate(); + case 'laststart': + return $oStopWatch->GetLastStartDate(); + case 'stopped': + return $oStopWatch->GetStopDate(); + } + + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold.'_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch($sThresholdCode) + { + case 'deadline': + return $oStopWatch->GetThresholdDate($iThreshold); + case 'passed': + return $oStopWatch->IsThresholdPassed($iThreshold); + case 'triggered': + return $oStopWatch->IsThresholdTriggered($iThreshold); + case 'overrun': + return $oStopWatch->GetOverrun($iThreshold); + } + } + } + + throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode()); + } + + protected function GetBooleanLabel($bValue) + { + $sDictKey = $bValue ? 'yes' : 'no'; + return Dict::S('BooleanLabel:'.$sDictKey, 'def:'.$sDictKey); + } + + public function GetSubItemAsHTMLForHistory($sItemCode, $sValue) + { + $sHtml = null; + switch($sItemCode) + { + case 'timespent': + $sHtml = (int)$sValue ? Str::pure2html(AttributeDuration::FormatDuration($sValue)) : null; + break; + case 'started': + case 'laststart': + case 'stopped': + $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null; + break; + + default: + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold.'_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch($sThresholdCode) + { + case 'deadline': + $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null; + break; + case 'passed': + $sHtml = $this->GetBooleanLabel((int)$sValue); + break; + case 'triggered': + $sHtml = $this->GetBooleanLabel((int)$sValue); + break; + case 'overrun': + $sHtml = (int)$sValue > 0 ? Str::pure2html(AttributeDuration::FormatDuration((int)$sValue)) : ''; + } + } + } + } + return $sHtml; + } + + public function GetSubItemAsPlainText($sItemCode, $value) + { + $sRet = $value; + + switch ($sItemCode) + { + case 'timespent': + $sRet = AttributeDuration::FormatDuration($value); + break; + case 'started': + case 'laststart': + case 'stopped': + if (is_null($value)) + { + $sRet = ''; // Undefined + } + else + { + $oDateTime = new DateTime(); + $oDateTime->setTimestamp($value); + $oDateTimeFormat = AttributeDateTime::GetFormat(); + $sRet = $oDateTimeFormat->Format($oDateTime); + } + break; + + default: + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold . '_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch ($sThresholdCode) + { + case 'deadline': + if ($value) + { + $sDate = date(AttributeDateTime::GetInternalFormat(), $value); + $sRet = AttributeDeadline::FormatDeadline($sDate); + } + else + { + $sRet = ''; + } + break; + case 'passed': + case 'triggered': + $sRet = $this->GetBooleanLabel($value); + break; + case 'overrun': + $sRet = AttributeDuration::FormatDuration($value); + break; + } + } + } + } + return $sRet; + } + + public function GetSubItemAsHTML($sItemCode, $value) + { + $sHtml = $value; + + switch ($sItemCode) + { + case 'timespent': + $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value)); + break; + case 'started': + case 'laststart': + case 'stopped': + if (is_null($value)) + { + $sHtml = ''; // Undefined + } + else + { + $oDateTime = new DateTime(); + $oDateTime->setTimestamp($value); + $oDateTimeFormat = AttributeDateTime::GetFormat(); + $sHtml = Str::pure2html($oDateTimeFormat->Format($oDateTime)); + } + break; + + default: + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold . '_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch ($sThresholdCode) + { + case 'deadline': + if ($value) + { + $sDate = date(AttributeDateTime::GetInternalFormat(), $value); + $sHtml = Str::pure2html(AttributeDeadline::FormatDeadline($sDate)); + } + else + { + $sHtml = ''; + } + break; + case 'passed': + case 'triggered': + $sHtml = $this->GetBooleanLabel($value); + break; + case 'overrun': + $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value)); + break; + } + } + } + } + return $sHtml; + } + + public function GetSubItemAsCSV($sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"', $bConvertToPlainText = false) + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$value); + $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; + + switch($sItemCode) + { + case 'timespent': + $sRet = $sTextQualifier . AttributeDuration::FormatDuration($value) . $sTextQualifier; + break; + case 'started': + case 'laststart': + case 'stopped': + if ($value !== null) + { + $oDateTime = new DateTime(); + $oDateTime->setTimestamp($value); + $oDateTimeFormat = AttributeDateTime::GetFormat(); + $sRet = $sTextQualifier . $oDateTimeFormat->Format($oDateTime) . $sTextQualifier; + } + break; + + default: + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold.'_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch($sThresholdCode) + { + case 'deadline': + if ($value != '') + { + $oDateTime = new DateTime(); + $oDateTime->setTimestamp($value); + $oDateTimeFormat = AttributeDateTime::GetFormat(); + $sRet = $sTextQualifier . $oDateTimeFormat->Format($oDateTime) . $sTextQualifier; + } + break; + + case 'passed': + case 'triggered': + $sRet = $sTextQualifier . $this->GetBooleanLabel($value) . $sTextQualifier; + break; + + case 'overrun': + $sRet = $sTextQualifier . AttributeDuration::FormatDuration($value) . $sTextQualifier; + break; + } + } + } + } + return $sRet; + } + + public function GetSubItemAsXML($sItemCode, $value) + { + $sRet = Str::pure2xml((string)$value); + + switch($sItemCode) + { + case 'timespent': + case 'started': + case 'laststart': + case 'stopped': + break; + + default: + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold.'_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch($sThresholdCode) + { + case 'deadline': + break; + + case 'passed': + case 'triggered': + $sRet = $this->GetBooleanLabel($value); + break; + + case 'overrun': + break; + } + } + } + } + return $sRet; + } + + /** + * Implemented for the HTML spreadsheet format! + * + * @param string $sItemCode + * @param \ormStopWatch $value + * + * @return false|string + */ + public function GetSubItemAsEditValue($sItemCode, $value) + { + $sRet = $value; + + switch($sItemCode) + { + case 'timespent': + break; + + case 'started': + case 'laststart': + case 'stopped': + if (is_null($value)) + { + $sRet = ''; // Undefined + } + else + { + $sRet = date((string)AttributeDateTime::GetFormat(), $value); + } + break; + + default: + foreach ($this->ListThresholds() as $iThreshold => $aFoo) + { + $sThPrefix = $iThreshold.'_'; + if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) + { + // The current threshold is concerned + $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); + switch($sThresholdCode) + { + case 'deadline': + if ($value) + { + $sRet = date((string)AttributeDateTime::GetFormat(), $value); + } + else + { + $sRet = ''; + } + break; + case 'passed': + case 'triggered': + $sRet = $this->GetBooleanLabel($value); + break; + case 'overrun': + break; + } + } + } + } + return $sRet; + } +} + +/** + * View of a subvalue of another attribute + * If an attribute implements the verbs GetSubItem.... then it can expose + * internal values, each of them being an attribute and therefore they + * can be displayed at different times in the object lifecycle, and used for + * reporting (as a condition in OQL, or as an additional column in an export) + * Known usages: Stop Watches can expose threshold statuses + */ +class AttributeSubItem extends AttributeDefinition +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code')); + } + + public function GetParentAttCode() {return $this->Get("target_attcode");} + + /** + * Helper : get the attribute definition to which the execution will be forwarded + */ + public function GetTargetAttDef() + { + $sClass = $this->GetHostClass(); + $oParentAttDef = MetaModel::GetAttributeDef($sClass, $this->Get('target_attcode')); + return $oParentAttDef; + } + + public function GetEditClass() {return "";} + + public function GetValuesDef() {return null;} + + static public function IsBasedOnDBColumns() {return true;} + static public function IsScalar() {return true;} + public function IsWritable() {return false;} + public function GetDefaultValue(DBObject $oHostObject = null) {return null;} +// public function IsNullAllowed() {return false;} + + static public function LoadInObject() {return false;} // if this verb returns false, then GetValue must be implemented + + /** + * Used by DBOBject::Get() + * + * @param \DBObject $oHostObject + * + * @return \AttributeSubItem + * @throws \CoreException + */ + public function GetValue($oHostObject) + { + /** @var \AttributeStopWatch $oParent */ + $oParent = $this->GetTargetAttDef(); + $parentValue = $oHostObject->GetStrict($oParent->GetCode()); + $res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject); + return $res; + } + + // +// protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) + + public function FromSQLToValue($aCols, $sPrefix = '') + { + } + + public function GetSQLColumns($bFullSpec = false) + { + return array(); + } + + public function GetFilterDefinitions() + { + return array($this->GetCode() => new FilterFromAttribute($this)); + } + + public function GetBasicFilterOperators() + { + return array(); + } + public function GetBasicFilterLooseOperator() + { + return "="; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '!=': + return $this->GetSQLExpr()." != $sQValue"; + break; + case '=': + default: + return $this->GetSQLExpr()." = $sQValue"; + } + } + + public function GetSQLExpressions($sPrefix = '') + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemSQLExpression($this->Get('item_code')); + return $res; + } + + public function GetAsPlainText($value, $oHostObject = null, $bLocalize = true) + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemAsPlainText($this->Get('item_code'), $value); + return $res; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value); + return $res; + } + + public function GetAsHTMLForHistory($value, $oHostObject = null, $bLocalize = true) + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemAsHTMLForHistory($this->Get('item_code'), $value); + return $res; + } + + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier, $bConvertToPlainText); + return $res; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value); + return $res; + } + + /** + * As of now, this function must be implemented to have the value in spreadsheet format + */ + public function GetEditValue($value, $oHostObj = null) + { + $oParent = $this->GetTargetAttDef(); + $res = $oParent->GetSubItemAsEditValue($this->Get('item_code'), $value); + return $res; + } + + public function IsPartOfFingerprint() + { + return false; + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\LabelField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + parent::MakeFormField($oObject, $oFormField); + + // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition + $sAttCode = $this->GetCode(); + $oFormField->SetCurrentValue(html_entity_decode($oObject->GetAsHTML($sAttCode), ENT_QUOTES, 'UTF-8')); + $oFormField->SetReadOnly(true); + + return $oFormField; + } + +} + +/** + * One way encrypted (hashed) password + */ +class AttributeOneWayPassword extends AttributeDefinition +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("depends_on")); + } + + public function GetEditClass() {return "One Way Password";} + + static public function IsBasedOnDBColumns() {return true;} + static public function IsScalar() {return true;} + public function IsWritable() {return true;} + public function GetDefaultValue(DBObject $oHostObject = null) {return "";} + public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);} + + // Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted) + public function MakeRealValue($proposedValue, $oHostObj) + { + $oPassword = $proposedValue; + if (is_object($oPassword)) + { + $oPassword = clone $proposedValue; + } + else + { + $oPassword = new ormPassword('', ''); + $oPassword->SetPassword($proposedValue); + } + return $oPassword; + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->GetCode(); // Warning: AttributeOneWayPassword does not have any sql property so code = sql ! + } + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $sPrefix.'_hash'; + $aColumns['_salt'] = $sPrefix.'_salt'; + return $aColumns; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + if (!array_key_exists($sPrefix, $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); + } + $hashed = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; + + if (!array_key_exists($sPrefix.'_salt', $aCols)) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_salt' from {$sAvailable}"); + } + $sSalt = isset($aCols[$sPrefix.'_salt']) ? $aCols[$sPrefix.'_salt'] : ''; + + $value = new ormPassword($hashed, $sSalt); + return $value; + } + + public function GetSQLValues($value) + { + // #@# Optimization: do not load blobs anytime + // As per mySQL doc, selecting blob columns will prevent mySQL from + // using memory in case a temporary table has to be created + // (temporary tables created on disk) + // We will have to remove the blobs from the list of attributes when doing the select + // then the use of Get() should finalize the load + if ($value instanceOf ormPassword) + { + $aValues = array(); + $aValues[$this->GetCode().'_hash'] = $value->GetHash(); + $aValues[$this->GetCode().'_salt'] = $value->GetSalt(); + } + else + { + $aValues = array(); + $aValues[$this->GetCode().'_hash'] = ''; + $aValues[$this->GetCode().'_salt'] = ''; + } + return $aValues; + } + + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->GetCode().'_hash'] = 'TINYBLOB'; + $aColumns[$this->GetCode().'_salt'] = 'TINYBLOB'; + return $aColumns; + } + + public function GetImportColumns() + { + $aColumns = array(); + $aColumns[$this->GetCode()] = 'TINYTEXT'.CMDBSource::GetSqlStringColumnDefinition(); + return $aColumns; + } + + public function FromImportToValue($aCols, $sPrefix = '') + { + if (!isset($aCols[$sPrefix])) + { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); + } + $sClearPwd = $aCols[$sPrefix]; + + $oPassword = new ormPassword('', ''); + $oPassword->SetPassword($sClearPwd); + return $oPassword; + } + + public function GetFilterDefinitions() + { + return array(); + // still not working... see later... + } + + public function GetBasicFilterOperators() + { + return array(); + } + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return 'true'; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + if (is_object($value)) + { + return $value->GetAsHTML(); + } + return ''; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + return ''; // Not exportable in CSV + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + return ''; // Not exportable in XML + } + + public function GetValueLabel($sValue, $oHostObj = null) + { + // Don't display anything in "group by" reports + return '*****'; + } + +} + +// Indexed array having two dimensions +class AttributeTable extends AttributeDBField +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + public function GetEditClass() {return "Table";} + + protected function GetSQLCol($bFullSpec = false) + { + return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); + } + + public function GetMaxSize() + { + return null; + } + + public function GetNullValue() + { + return array(); + } + + public function IsNull($proposedValue) + { + return (count($proposedValue) == 0); + } + + public function GetEditValue($sValue, $oHostObj = null) + { + return ''; + } + + // Facilitate things: allow the user to Set the value from a string + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) + { + return array(); + } + else if (!is_array($proposedValue)) + { + return array(0 => array(0 => $proposedValue)); + } + return $proposedValue; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + try + { + $value = @unserialize($aCols[$sPrefix.'']); + if ($value === false) + { + $value = $this->MakeRealValue($aCols[$sPrefix.''], null); + } + } + catch(Exception $e) + { + $value = $this->MakeRealValue($aCols[$sPrefix.''], null); + } + + return $value; + } + + public function GetSQLValues($value) + { + $aValues = array(); + $aValues[$this->Get("sql")] = serialize($value); + return $aValues; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + if (!is_array($value)) + { + throw new CoreException('Expecting an array', array('found' => get_class($value))); + } + if (count($value) == 0) + { + return ""; + } + + $sRes = ""; + $sRes .= ""; + foreach($value as $iRow => $aRawData) + { + $sRes .= ""; + foreach ($aRawData as $iCol => $cell) + { + // Note: avoid the warning in case the cell is made of an array + $sCell = @Str::pure2html((string)$cell); + $sCell = str_replace("\n", "
    \n", $sCell); + $sRes .= ""; + } + $sRes .= ""; + } + $sRes .= ""; + $sRes .= "
    $sCell
    "; + return $sRes; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + // Not implemented + return ''; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if (count($value) == 0) + { + return ""; + } + + $sRes = ""; + foreach($value as $iRow => $aRawData) + { + $sRes .= ""; + foreach ($aRawData as $iCol => $cell) + { + $sCell = Str::pure2xml((string)$cell); + $sRes .= "$sCell"; + } + $sRes .= ""; + } + return $sRes; + } +} + +// The PHP value is a hash array, it is stored as a TEXT column +class AttributePropertySet extends AttributeTable +{ + public function GetEditClass() {return "PropertySet";} + + // Facilitate things: allow the user to Set the value from a string + public function MakeRealValue($proposedValue, $oHostObj) + { + if (!is_array($proposedValue)) + { + return array('?' => (string)$proposedValue); + } + return $proposedValue; + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + if (!is_array($value)) + { + throw new CoreException('Expecting an array', array('found' => get_class($value))); + } + if (count($value) == 0) + { + return ""; + } + + $sRes = ""; + $sRes .= ""; + foreach($value as $sProperty => $sValue) + { + if ($sProperty == 'auth_pwd') + { + $sValue = '*****'; + } + $sRes .= ""; + $sCell = str_replace("\n", "
    \n", Str::pure2html((string)$sValue)); + $sRes .= ""; + $sRes .= ""; + } + $sRes .= ""; + $sRes .= "
    $sProperty$sCell
    "; + return $sRes; + } + + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + if (count($value) == 0) + { + return ""; + } + + $aRes = array(); + foreach($value as $sProperty => $sValue) + { + if ($sProperty == 'auth_pwd') + { + $sValue = '*****'; + } + $sFrom = array(',', '='); + $sTo = array('\,', '\='); + $aRes[] = $sProperty.'='.str_replace($sFrom, $sTo, (string)$sValue); + } + $sRaw = implode(',', $aRes); + + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, $sRaw); + return $sTextQualifier.$sEscaped.$sTextQualifier; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if (count($value) == 0) + { + return ""; + } + + $sRes = ""; + foreach($value as $sProperty => $sValue) + { + if ($sProperty == 'auth_pwd') + { + $sValue = '*****'; + } + $sRes .= ""; + $sRes .= Str::pure2xml((string)$sValue); + $sRes .= ""; + } + return $sRes; + } +} + +/** + * The attribute dedicated to the friendly name automatic attribute (not written) + * + * @package iTopORM + */ + +/** + * The attribute dedicated to the friendly name automatic attribute (not written) + * + * @package iTopORM + */ +class AttributeFriendlyName extends AttributeDefinition +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; + public $m_sValue; + + public function __construct($sCode) + { + $this->m_sCode = $sCode; + $aParams = array(); + $aParams["default_value"] = ''; + parent::__construct($sCode, $aParams); + + $this->m_sValue = $this->Get("default_value"); + } + + + public function GetEditClass() {return "";} + + public function GetValuesDef() {return null;} + public function GetPrerequisiteAttributes($sClass = null) {return $this->GetOptional("depends_on", array());} + + static public function IsScalar() {return true;} + public function IsNullAllowed() {return false;} + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property + } + return array('' => $sPrefix); + } + + static public function IsBasedOnOQLExpression() {return true;} + public function GetOQLExpression() + { + return MetaModel::GetNameExpression($this->GetHostClass()); + } + + public function GetLabel($sDefault = null) + { + $sLabel = parent::GetLabel(''); + if (strlen($sLabel) == 0) + { + $sLabel = Dict::S('Core:FriendlyName-Label'); + } + return $sLabel; + } + public function GetDescription($sDefault = null) + { + $sLabel = parent::GetDescription(''); + if (strlen($sLabel) == 0) + { + $sLabel = Dict::S('Core:FriendlyName-Description'); + } + return $sLabel; + } + + public function FromSQLToValue($aCols, $sPrefix = '') + { + $sValue = $aCols[$sPrefix]; + return $sValue; + } + + public function IsWritable() + { + return false; + } + public function IsMagic() + { + return true; + } + + static public function IsBasedOnDBColumns() + { + return false; + } + + public function SetFixedValue($sValue) + { + $this->m_sValue = $sValue; + } + public function GetDefaultValue(DBObject $oHostObject = null) + { + return $this->m_sValue; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + return Str::pure2html((string)$sValue); + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return $sTextQualifier.$sEscaped.$sTextQualifier; + } + + static function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\StringField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + $oFormField->SetReadOnly(true); + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + // Do not display friendly names in the history of change + public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) + { + return ''; + } + + public function GetFilterDefinitions() + { + return array($this->GetCode() => new FilterFromAttribute($this)); + } + + public function GetBasicFilterOperators() + { + return array("="=>"equals", "!="=>"differs from"); + } + + public function GetBasicFilterLooseOperator() + { + return "Contains"; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + $sQValue = CMDBSource::Quote($value); + switch ($sOpCode) + { + case '=': + case '!=': + return $this->GetSQLExpr()." $sOpCode $sQValue"; + case 'Contains': + return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); + case 'NotLike': + return $this->GetSQLExpr()." NOT LIKE $sQValue"; + case 'Like': + default: + return $this->GetSQLExpr()." LIKE $sQValue"; + } + } + + public function IsPartOfFingerprint() { return false; } +} + +/** + * Holds the setting for the redundancy on a specific relation + * Its value is a string, containing either: + * - 'disabled' + * - 'n', where n is a positive integer value giving the minimum count of items upstream + * - 'n%', where n is a positive integer value, giving the minimum as a percentage of the total count of items upstream + * + * @package iTopORM + */ +class AttributeRedundancySettings extends AttributeDBField +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return array('sql', 'relation_code', 'from_class', 'neighbour_id', 'enabled', 'enabled_mode', 'min_up', 'min_up_type', 'min_up_mode'); + } + + public function GetValuesDef() {return null;} + public function GetPrerequisiteAttributes($sClass = null) {return array();} + + public function GetEditClass() {return "RedundancySetting";} + protected function GetSQLCol($bFullSpec = false) + { + return "VARCHAR(20)" + .CMDBSource::GetSqlStringColumnDefinition() + .($bFullSpec ? $this->GetSQLColSpec() : ''); + } + + + public function GetValidationPattern() + { + return "^[0-9]{1,3}|[0-9]{1,2}%|disabled$"; + } + + public function GetMaxSize() + { + return 20; + } + + public function GetDefaultValue(DBObject $oHostObject = null) + { + $sRet = 'disabled'; + if ($this->Get('enabled')) + { + if ($this->Get('min_up_type') == 'count') + { + $sRet = (string) $this->Get('min_up'); + } + else // percent + { + $sRet = $this->Get('min_up').'%'; + } + } + return $sRet; + } + + public function IsNullAllowed() + { + return false; + } + + public function GetNullValue() + { + return ''; + } + + public function IsNull($proposedValue) + { + return ($proposedValue == ''); + } + + public function MakeRealValue($proposedValue, $oHostObj) + { + if (is_null($proposedValue)) return ''; + return (string)$proposedValue; + } + + public function ScalarToSQL($value) + { + if (!is_string($value)) + { + throw new CoreException('Expected the attribute value to be a string', array('found_type' => gettype($value), 'value' => $value, 'class' => $this->GetHostClass(), 'attribute' => $this->GetCode())); + } + return $value; + } + + public function GetRelationQueryData() + { + foreach (MetaModel::EnumRelationQueries($this->GetHostClass(), $this->Get('relation_code'), false) as $sDummy => $aQueryInfo) + { + if ($aQueryInfo['sFromClass'] == $this->Get('from_class')) + { + if ($aQueryInfo['sNeighbour'] == $this->Get('neighbour_id')) + { + return $aQueryInfo; + } + } + } + return array(); + } + + /** + * Find the user option label + * + * @param string $sUserOption possible values : disabled|cout|percent + * @param string $sDefault + * + * @return string + * @throws \Exception + */ + public function GetUserOptionFormat($sUserOption, $sDefault = null) + { + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, null, true /*user lang*/); + if (is_null($sLabel)) + { + // If no default value is specified, let's define the most relevant one for developping purposes + if (is_null($sDefault)) + { + $sDefault = str_replace('_', ' ', $this->m_sCode.':'.$sUserOption.'(%1$s)'); + } + // Browse the hierarchy again, accepting default (english) translations + $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, $sDefault, false); + } + return $sLabel; + } + + /** + * Override to display the value in the GUI + * + * @param string $sValue + * @param \DBObject $oHostObject + * @param bool $bLocalize + * + * @return string + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + $sCurrentOption = $this->GetCurrentOption($sValue); + $sClass = $oHostObject ? get_class($oHostObject) : $this->m_sHostClass; + return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue), MetaModel::GetName($sClass)); + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return $sTextQualifier.$sEscaped.$sTextQualifier; + } + + /** + * Helper to interpret the value, given the current settings and string representation of the attribute + */ + public function IsEnabled($sValue) + { + if ($this->get('enabled_mode') == 'fixed') + { + $bRet = $this->get('enabled'); + } + else + { + $bRet = ($sValue != 'disabled'); + } + return $bRet; + } + + /** + * Helper to interpret the value, given the current settings and string representation of the attribute + */ + public function GetMinUpType($sValue) + { + if ($this->get('min_up_mode') == 'fixed') + { + $sRet = $this->get('min_up_type'); + } + else + { + $sRet = 'count'; + if (substr(trim($sValue), -1, 1) == '%') + { + $sRet = 'percent'; + } + } + return $sRet; + } + + /** + * Helper to interpret the value, given the current settings and string representation of the attribute + */ + public function GetMinUpValue($sValue) + { + if ($this->get('min_up_mode') == 'fixed') + { + $iRet = (int) $this->Get('min_up'); + } + else + { + $sRefValue = $sValue; + if (substr(trim($sValue), -1, 1) == '%') + { + $sRefValue = substr(trim($sValue), 0, -1); + } + $iRet = (int) trim($sRefValue); + } + return $iRet; + } + + /** + * Helper to determine if the redundancy can be viewed/edited by the end-user + */ + public function IsVisible() + { + $bRet = false; + if ($this->Get('enabled_mode') == 'fixed') + { + $bRet = $this->Get('enabled'); + } + elseif ($this->Get('enabled_mode') == 'user') + { + $bRet = true; + } + return $bRet; + } + + public function IsWritable() + { + if (($this->Get('enabled_mode') == 'fixed') && ($this->Get('min_up_mode') == 'fixed')) + { + return false; + } + return true; + } + + /** + * Returns an HTML form that can be read by ReadValueFromPostedForm + */ + public function GetDisplayForm($sCurrentValue, $oPage, $bEditMode = false, $sFormPrefix = '') + { + $sRet = ''; + $aUserOptions = $this->GetUserOptions($sCurrentValue); + if (count($aUserOptions) < 2) + { + $bEditOption = false; + } + else + { + $bEditOption = $bEditMode; + } + $sCurrentOption = $this->GetCurrentOption($sCurrentValue); + foreach($aUserOptions as $sUserOption) + { + $bSelected = ($sUserOption == $sCurrentOption); + $sRet .= '
    '; + $sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption, $bSelected); + $sRet .= '
    '; + } + return $sRet; + } + + const USER_OPTION_DISABLED = 'disabled'; + const USER_OPTION_ENABLED_COUNT = 'count'; + const USER_OPTION_ENABLED_PERCENT = 'percent'; + + /** + * Depending on the xxx_mode parameters, build the list of options that are allowed to the end-user + */ + protected function GetUserOptions($sValue) + { + $aRet = array(); + if ($this->Get('enabled_mode') == 'user') + { + $aRet[] = self::USER_OPTION_DISABLED; + } + + if ($this->Get('min_up_mode') == 'user') + { + $aRet[] = self::USER_OPTION_ENABLED_COUNT; + $aRet[] = self::USER_OPTION_ENABLED_PERCENT; + } + else + { + if ($this->GetMinUpType($sValue) == 'count') + { + $aRet[] = self::USER_OPTION_ENABLED_COUNT; + } + else + { + $aRet[] = self::USER_OPTION_ENABLED_PERCENT; + } + } + return $aRet; + } + + /** + * Convert the string representation into one of the existing options + */ + protected function GetCurrentOption($sValue) + { + $sRet = self::USER_OPTION_DISABLED; + if ($this->IsEnabled($sValue)) + { + if ($this->GetMinUpType($sValue) == 'count') + { + $sRet = self::USER_OPTION_ENABLED_COUNT; + } + else + { + $sRet = self::USER_OPTION_ENABLED_PERCENT; + } + } + return $sRet; + } + + /** + * Display an option (form, or current value) + * + * @param string $sCurrentValue + * @param \WebPage $oPage + * @param string $sFormPrefix + * @param bool $bEditMode + * @param string $sUserOption + * @param bool $bSelected + * + * @return string + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \Exception + */ + protected function GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditMode, $sUserOption, $bSelected = true) + { + $sRet = ''; + + $iCurrentValue = $this->GetMinUpValue($sCurrentValue); + if ($bEditMode) + { + $sValue = null; + $sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id'); + switch ($sUserOption) + { + case self::USER_OPTION_DISABLED: + $sValue = ''; // Empty placeholder + break; + + case self::USER_OPTION_ENABLED_COUNT: + if ($bEditMode) + { + $sName = $sHtmlNamesPrefix.'_min_up_count'; + $sEditValue = $bSelected ? $iCurrentValue : ''; + $sValue = ''; + // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option) + $oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});"); + } + else + { + $sValue = $iCurrentValue; + } + break; + + case self::USER_OPTION_ENABLED_PERCENT: + if ($bEditMode) + { + $sName = $sHtmlNamesPrefix.'_min_up_percent'; + $sEditValue = $bSelected ? $iCurrentValue : ''; + $sValue = ''; + // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option) + $oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});"); + } + else + { + $sValue = $iCurrentValue; + } + break; + } + $sLabel = sprintf($this->GetUserOptionFormat($sUserOption), $sValue, MetaModel::GetName($this->GetHostClass())); + + $sOptionName = $sHtmlNamesPrefix.'_user_option'; + $sOptionId = $sOptionName.'_'.$sUserOption; + $sChecked = $bSelected ? 'checked' : ''; + $sRet = ' '; + } + else + { + // Read-only: display only the currently selected option + if ($bSelected) + { + $sRet = sprintf($this->GetUserOptionFormat($sUserOption), $iCurrentValue, MetaModel::GetName($this->GetHostClass())); + } + } + return $sRet; + } + + /** + * Makes the string representation out of the values given by the form defined in GetDisplayForm + */ + public function ReadValueFromPostedForm($sFormPrefix) + { + $sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id'); + + $iMinUpCount = (int) utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_count', null, 'raw_data'); + $iMinUpPercent = (int) utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_percent', null, 'raw_data'); + $sSelectedOption = utils::ReadPostedParam($sHtmlNamesPrefix.'_user_option', null, 'raw_data'); + switch ($sSelectedOption) + { + case self::USER_OPTION_ENABLED_COUNT: + $sRet = $iMinUpCount; + break; + + case self::USER_OPTION_ENABLED_PERCENT: + $sRet = $iMinUpPercent.'%'; + break; + + case self::USER_OPTION_DISABLED: + default: + $sRet = 'disabled'; + break; + } + return $sRet; + } +} + +/** + * Custom fields managed by an external implementation + * + * @package iTopORM + */ +class AttributeCustomFields extends AttributeDefinition +{ + const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; + + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("handler_class")); + } + + public function GetEditClass() {return "CustomFields";} + public function IsWritable() {return true;} + static public function LoadFromDB() {return false;} // See ReadValue... + + public function GetDefaultValue(DBObject $oHostObject = null) + { + return new ormCustomFieldsValue($oHostObject, $this->GetCode()); + } + + public function GetBasicFilterOperators() {return array();} + public function GetBasicFilterLooseOperator() {return '';} + public function GetBasicFilterSQLExpr($sOpCode, $value) {return '';} + + /** + * @param DBObject $oHostObject + * @param array|null $aValues + * @return CustomFieldsHandler + */ + public function GetHandler($aValues = null) + { + $sHandlerClass = $this->Get('handler_class'); + $oHandler = new $sHandlerClass($this->GetCode()); + if (!is_null($aValues)) + { + $oHandler->SetCurrentValues($aValues); + } + return $oHandler; + } + + public function GetPrerequisiteAttributes($sClass = null) + { + $sHandlerClass = $this->Get('handler_class'); + return $sHandlerClass::GetPrerequisiteAttributes($sClass); + } + + public function GetEditValue($sValue, $oHostObj = null) + { + return $this->GetForTemplate($sValue, '', $oHostObj, true); + } + + /** + * Makes the string representation out of the values given by the form defined in GetDisplayForm + */ + public function ReadValueFromPostedForm($oHostObject, $sFormPrefix) + { + $aRawData = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->GetCode()}", '{}', 'raw_data'), true); + return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aRawData); + } + + public function MakeRealValue($proposedValue, $oHostObject) + { + if (is_object($proposedValue) && ($proposedValue instanceof ormCustomFieldsValue)) + { + return $proposedValue; + } + elseif (is_string($proposedValue)) + { + $aValues = json_decode($proposedValue, true); + return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); + } + elseif (is_array($proposedValue)) + { + return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $proposedValue); + } + elseif (is_null($proposedValue)) + { + return new ormCustomFieldsValue($oHostObject, $this->GetCode()); + } + throw new Exception('Unexpected type for the value of a custom fields attribute: '.gettype($proposedValue)); + } + + static public function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\SubFormField'; + } + + /** + * Override to build the relevant form field + * + * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the $oFormField is passed, MakeFormField behaves more like a Prepare. + */ + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + $oFormField->SetForm($this->GetForm($oObject)); + } + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + /** + * @param DBObject $oHostObject + * @param null $sFormPrefix + * @return Combodo\iTop\Form\Form + * @throws \Exception + */ + public function GetForm(DBObject $oHostObject, $sFormPrefix = null) + { + try + { + $oValue = $oHostObject->Get($this->GetCode()); + $oHandler = $this->GetHandler($oValue->GetValues()); + $sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode(); + $oHandler->BuildForm($oHostObject, $sFormId); + $oForm = $oHandler->GetForm(); + } + catch (Exception $e) + { + $oForm = new \Combodo\iTop\Form\Form(''); + $oField = new \Combodo\iTop\Form\Field\LabelField(''); + $oField->SetLabel('Custom field error: '.$e->getMessage()); + $oForm->AddField($oField); + $oForm->Finalize(); + } + return $oForm; + } + + /** + * Read the data from where it has been stored. This verb must be implemented as soon as LoadFromDB returns false and LoadInObject returns true + * @param $oHostObject + * @return ormCustomFieldsValue + */ + public function ReadValue($oHostObject) + { + try + { + $oHandler = $this->GetHandler(); + $aValues = $oHandler->ReadValues($oHostObject); + $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); + } + catch (Exception $e) + { + $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode()); + } + return $oRet; + } + + /** + * Record the data (currently in the processing of recording the host object) + * It is assumed that the data has been checked prior to calling Write() + * @param DBObject $oHostObject + * @param ormCustomFieldsValue|null $oValue (null is the default value) + */ + public function WriteValue(DBObject $oHostObject, ormCustomFieldsValue $oValue = null) + { + if (is_null($oValue)) + { + $oHandler = $this->GetHandler(); + $aValues = array(); + } + else + { + // Pass the values through the form to make sure that they are correct + $oHandler = $this->GetHandler($oValue->GetValues()); + $oHandler->BuildForm($oHostObject, ''); + $oForm = $oHandler->GetForm(); + $aValues = $oForm->GetCurrentValues(); + } + return $oHandler->WriteValues($oHostObject, $aValues); + } + + /** + * The part of the current attribute in the object's signature, for the supplied value + * @param ormCustomFieldsValue $value The value of this attribute for the object + * @return string The "signature" for this field/attribute + */ + public function Fingerprint($value) + { + $oHandler = $this->GetHandler($value->GetValues()); + return $oHandler->GetValueFingerprint(); + } + + /** + * Check the validity of the data + * @param DBObject $oHostObject + * @param $value + * @return bool|string true or error message + */ + public function CheckValue(DBObject $oHostObject, $value) + { + try + { + $oHandler = $this->GetHandler($value->GetValues()); + $oHandler->BuildForm($oHostObject, ''); + $oForm = $oHandler->GetForm(); + $oForm->Validate(); + if ($oForm->GetValid()) + { + $ret = true; + } + else + { + $aMessages = array(); + foreach ($oForm->GetErrorMessages() as $sFieldId => $aFieldMessages) + { + $aMessages[] = $sFieldId.': '.implode(', ', $aFieldMessages); + } + $ret = 'Invalid value: '.implode(', ', $aMessages); + } + } + catch (Exception $e) + { + $ret = $e->getMessage(); + } + return $ret; + } + + /** + * Cleanup data upon object deletion (object id still available here) + * @param DBObject $oHostObject + * @return + * @throws \CoreException + */ + public function DeleteValue(DBObject $oHostObject) + { + $oValue = $oHostObject->Get($this->GetCode()); + $oHandler = $this->GetHandler($oValue->GetValues()); + return $oHandler->DeleteValues($oHostObject); + } + + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) + { + try + { + $sRet = $value->GetAsHTML($bLocalize); + } + catch (Exception $e) + { + $sRet = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); + } + return $sRet; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + try + { + $sRet = $value->GetAsXML($bLocalize); + } + catch (Exception $e) + { + $sRet = Str::pure2xml('Custom field error: '.$e->getMessage()); + } + return $sRet; + } + + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + try + { + $sRet = $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText); + } + catch (Exception $e) + { + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, 'Custom field error: '.$e->getMessage()); + $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; + } + return $sRet; + } + + /** + * List the available verbs for 'GetForTemplate' + */ + public function EnumTemplateVerbs() + { + $sHandlerClass = $this->Get('handler_class'); + return $sHandlerClass::EnumTemplateVerbs(); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $oHostObject DBObject The object + * @param $bLocalize bool Whether or not to localize the value + * + * @return string + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + try + { + $sRet = $value->GetForTemplate($sVerb, $bLocalize); + } + catch (Exception $e) + { + $sRet = 'Custom field error: '.$e->getMessage(); + } + return $sRet; + } + + public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) + { + return null; + } + + /** + * Helper to get a value that will be JSON encoded + * The operation is the opposite to FromJSONToValue + * + * @param $value + * + * @return string + */ + public function GetForJSON($value) + { + return null; + } + + /** + * Helper to form a value, given JSON decoded data + * The operation is the opposite to GetForJSON + * + * @param string $json + * + * @return array + */ + public function FromJSONToValue($json) + { + return null; + } + + public function Equals($val1, $val2) + { + try + { + $bEquals = $val1->Equals($val2); + } + catch (Exception $e) + { + $bEquals = false; + } + return $bEquals; + } +} + +class AttributeArchiveFlag extends AttributeBoolean +{ + public function __construct($sCode) + { + parent::__construct($sCode, array("allowed_values" => null, "sql" => $sCode, "default_value" => false, "is_null_allowed" => false, "depends_on" => array())); + } + public function RequiresIndex() + { + return true; + } + public function CopyOnAllTables() + { + return true; + } + public function IsWritable() + { + return false; + } + public function IsMagic() + { + return true; + } + public function GetLabel($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeArchiveFlag/Label', $sDefault); + return parent::GetLabel($sDefault); + } + public function GetDescription($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeArchiveFlag/Label+', $sDefault); + return parent::GetDescription($sDefault); + } +} +class AttributeArchiveDate extends AttributeDate +{ + public function GetLabel($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeArchiveDate/Label', $sDefault); + return parent::GetLabel($sDefault); + } + public function GetDescription($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeArchiveDate/Label+', $sDefault); + return parent::GetDescription($sDefault); + } +} + +class AttributeObsolescenceFlag extends AttributeBoolean +{ + public function __construct($sCode) + { + parent::__construct($sCode, array("allowed_values"=>null, "sql"=>$sCode, "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())); + } + public function IsWritable() + { + return false; + } + public function IsMagic() + { + return true; + } + + static public function IsBasedOnDBColumns() {return false;} + /** + * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via GetOQLExpression) + * @return bool + */ + static public function IsBasedOnOQLExpression() {return true;} + public function GetOQLExpression() + { + return MetaModel::GetObsolescenceExpression($this->GetHostClass()); + } + + public function GetSQLExpressions($sPrefix = '') + { + return array(); + } + public function GetSQLColumns($bFullSpec = false) {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) + public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update) + + public function GetEditClass() {return "";} + + public function GetValuesDef() {return null;} + public function GetPrerequisiteAttributes($sClass = null) {return $this->GetOptional("depends_on", array());} + + public function IsDirectField() {return true;} + static public function IsScalar() {return true;} + public function GetSQLExpr() + { + return null; + } + + public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);} + public function IsNullAllowed() {return false;} + + public function GetLabel($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label', $sDefault); + return parent::GetLabel($sDefault); + } + public function GetDescription($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label+', $sDefault); + return parent::GetDescription($sDefault); + } +} + +class AttributeObsolescenceDate extends AttributeDate +{ + public function GetLabel($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeObsolescenceDate/Label', $sDefault); + return parent::GetLabel($sDefault); + } + public function GetDescription($sDefault = null) + { + $sDefault = Dict::S('Core:AttributeObsolescenceDate/Label+', $sDefault); + return parent::GetDescription($sDefault); + } +} diff --git a/core/background.inc.php b/core/background.inc.php index 4dacf6d34..d03ee6646 100644 --- a/core/background.inc.php +++ b/core/background.inc.php @@ -1,55 +1,55 @@ - - - -/** - * Tasks performed in the background - * - * @copyright Copyright (C) 2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class ObsolescenceDateUpdater implements iBackgroundProcess -{ - public function GetPeriodicity() - { - return MetaModel::GetConfig()->Get('obsolescence.date_update_interval'); // 10 mn - } - - public function Process($iUnixTimeLimit) - { - $iCountSet = 0; - $iCountReset = 0; - $iClasses = 0; - foreach (MetaModel::EnumObsoletableClasses() as $sClass) - { - $oObsoletedToday = new DBObjectSearch($sClass); - $oObsoletedToday->AddCondition('obsolescence_flag', 1, '='); - $oObsoletedToday->AddCondition('obsolescence_date', null, '='); - $sToday = date(AttributeDate::GetSQLFormat()); - $iCountSet += MetaModel::BulkUpdate($oObsoletedToday, array('obsolescence_date' => $sToday)); - - $oObsoletedToday = new DBObjectSearch($sClass); - $oObsoletedToday->AddCondition('obsolescence_flag', 1, '!='); - $oObsoletedToday->AddCondition('obsolescence_date', null, '!='); - $iCountReset += MetaModel::BulkUpdate($oObsoletedToday, array('obsolescence_date' => null)); - } - return "Obsolescence date updated (classes: $iClasses ; set: $iCountSet ; reset: $iCountReset)\n"; - } -} + + + +/** + * Tasks performed in the background + * + * @copyright Copyright (C) 2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +class ObsolescenceDateUpdater implements iBackgroundProcess +{ + public function GetPeriodicity() + { + return MetaModel::GetConfig()->Get('obsolescence.date_update_interval'); // 10 mn + } + + public function Process($iUnixTimeLimit) + { + $iCountSet = 0; + $iCountReset = 0; + $iClasses = 0; + foreach (MetaModel::EnumObsoletableClasses() as $sClass) + { + $oObsoletedToday = new DBObjectSearch($sClass); + $oObsoletedToday->AddCondition('obsolescence_flag', 1, '='); + $oObsoletedToday->AddCondition('obsolescence_date', null, '='); + $sToday = date(AttributeDate::GetSQLFormat()); + $iCountSet += MetaModel::BulkUpdate($oObsoletedToday, array('obsolescence_date' => $sToday)); + + $oObsoletedToday = new DBObjectSearch($sClass); + $oObsoletedToday->AddCondition('obsolescence_flag', 1, '!='); + $oObsoletedToday->AddCondition('obsolescence_date', null, '!='); + $iCountReset += MetaModel::BulkUpdate($oObsoletedToday, array('obsolescence_date' => null)); + } + return "Obsolescence date updated (classes: $iClasses ; set: $iCountSet ; reset: $iCountReset)\n"; + } +} diff --git a/core/backgroundprocess.inc.php b/core/backgroundprocess.inc.php index f10282933..625385f59 100644 --- a/core/backgroundprocess.inc.php +++ b/core/backgroundprocess.inc.php @@ -1,88 +1,88 @@ - - - -/** - * interface iProcess - * Something that can be executed - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ -interface iProcess -{ - /** - * @param int $iUnixTimeLimit - * - * @return string status message - * @throws \ProcessException - * @throws \ProcessFatalException - * @throws MySQLHasGoneAwayException - */ - public function Process($iUnixTimeLimit); -} - -/** - * interface iBackgroundProcess - * Any extension that must be called regularly to be executed in the background - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ -interface iBackgroundProcess extends iProcess -{ - /** - * @return int repetition rate in seconds - */ - public function GetPeriodicity(); -} - -/** - * interface iScheduledProcess - * A variant of process that must be called at specific times - * - * @copyright Copyright (C) 2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ -interface iScheduledProcess extends iProcess -{ - /** - * @return DateTime exact time at which the process must be run next time - */ - public function GetNextOccurrence(); -} - -/** - * Class ProcessException - * Exception for iProcess implementations.
    - * An error happened during the processing but we can go on with the next implementations. - */ -class ProcessException extends CoreException -{ - -} - -/** - * Class ProcessFatalException - * Exception for iProcess implementations.
    - * A big error occurred, we have to stop the iProcess processing. - */ -class ProcessFatalException extends CoreException -{ - -} + + + +/** + * interface iProcess + * Something that can be executed + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ +interface iProcess +{ + /** + * @param int $iUnixTimeLimit + * + * @return string status message + * @throws \ProcessException + * @throws \ProcessFatalException + * @throws MySQLHasGoneAwayException + */ + public function Process($iUnixTimeLimit); +} + +/** + * interface iBackgroundProcess + * Any extension that must be called regularly to be executed in the background + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ +interface iBackgroundProcess extends iProcess +{ + /** + * @return int repetition rate in seconds + */ + public function GetPeriodicity(); +} + +/** + * interface iScheduledProcess + * A variant of process that must be called at specific times + * + * @copyright Copyright (C) 2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ +interface iScheduledProcess extends iProcess +{ + /** + * @return DateTime exact time at which the process must be run next time + */ + public function GetNextOccurrence(); +} + +/** + * Class ProcessException + * Exception for iProcess implementations.
    + * An error happened during the processing but we can go on with the next implementations. + */ +class ProcessException extends CoreException +{ + +} + +/** + * Class ProcessFatalException + * Exception for iProcess implementations.
    + * A big error occurred, we have to stop the iProcess processing. + */ +class ProcessFatalException extends CoreException +{ + +} diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php index d2f05523a..64f78ae54 100644 --- a/core/bulkchange.class.inc.php +++ b/core/bulkchange.class.inc.php @@ -1,1350 +1,1350 @@ - - - -/** - * Bulk change facility (common to interactive and batch usages) - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -// The BOM is added at the head of exported UTF-8 CSV data, and removed (if present) from input UTF-8 data. -// This helps MS-Excel (Version > 2007, Windows only) in changing its interpretation of a CSV file (by default Excel reads data as ISO-8859-1 -not 100% sure!) -define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF - -/** - * BulkChange - * Interpret a given data set and update the DB accordingly (fake mode avail.) - * - * @package iTopORM - */ - -class BulkChangeException extends CoreException -{ -} - -/** - * CellChangeSpec - * A series of classes, keeping the information about a given cell: could it be changed or not (and why)? - * - * @package iTopORM - */ -abstract class CellChangeSpec -{ - protected $m_proposedValue; - protected $m_sOql; // in case of ambiguity - - public function __construct($proposedValue, $sOql = '') - { - $this->m_proposedValue = $proposedValue; - $this->m_sOql = $sOql; - } - - public function GetPureValue() - { - // Todo - distinguish both values - return $this->m_proposedValue; - } - - public function GetDisplayableValue() - { - return $this->m_proposedValue; - } - - public function GetOql() - { - return $this->m_sOql; - } - - abstract public function GetDescription(); -} - - -class CellStatus_Void extends CellChangeSpec -{ - public function GetDescription() - { - return ''; - } -} - -class CellStatus_Modify extends CellChangeSpec -{ - protected $m_previousValue; - - public function __construct($proposedValue, $previousValue = null) - { - // Unused (could be costly to know -see the case of reconciliation on ext keys) - //$this->m_previousValue = $previousValue; - parent::__construct($proposedValue); - } - - public function GetDescription() - { - return Dict::S('UI:CSVReport-Value-Modified'); - } - - //public function GetPreviousValue() - //{ - // return $this->m_previousValue; - //} -} - -class CellStatus_Issue extends CellStatus_Modify -{ - protected $m_sReason; - - public function __construct($proposedValue, $previousValue, $sReason) - { - $this->m_sReason = $sReason; - parent::__construct($proposedValue, $previousValue); - } - - public function GetDescription() - { - if (is_null($this->m_proposedValue)) - { - return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason); - } - return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason); - } -} - -class CellStatus_SearchIssue extends CellStatus_Issue -{ - public function __construct() - { - parent::__construct(null, null, null); - } - - public function GetDescription() - { - return Dict::S('UI:CSVReport-Value-NoMatch'); - } -} - -class CellStatus_NullIssue extends CellStatus_Issue -{ - public function __construct() - { - parent::__construct(null, null, null); - } - - public function GetDescription() - { - return Dict::S('UI:CSVReport-Value-Missing'); - } -} - - -class CellStatus_Ambiguous extends CellStatus_Issue -{ - protected $m_iCount; - - public function __construct($previousValue, $iCount, $sOql) - { - $this->m_iCount = $iCount; - $this->m_sQuery = $sOql; - parent::__construct(null, $previousValue, ''); - } - - public function GetDescription() - { - $sCount = $this->m_iCount; - return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount); - } -} - - -/** - * RowStatus - * A series of classes, keeping the information about a given row: could it be changed or not (and why)? - * - * @package iTopORM - */ -abstract class RowStatus -{ - public function __construct() - { - } - - abstract public function GetDescription(); -} - -class RowStatus_NoChange extends RowStatus -{ - public function GetDescription() - { - return Dict::S('UI:CSVReport-Row-Unchanged'); - } -} - -class RowStatus_NewObj extends RowStatus -{ - public function GetDescription() - { - return Dict::S('UI:CSVReport-Row-Created'); - } -} - -class RowStatus_Modify extends RowStatus -{ - protected $m_iChanged; - - public function __construct($iChanged) - { - $this->m_iChanged = $iChanged; - } - - public function GetDescription() - { - return Dict::Format('UI:CSVReport-Row-Updated', $this->m_iChanged); - } -} - -class RowStatus_Disappeared extends RowStatus_Modify -{ - public function GetDescription() - { - return Dict::Format('UI:CSVReport-Row-Disappeared', $this->m_iChanged); - } -} - -class RowStatus_Issue extends RowStatus -{ - protected $m_sReason; - - public function __construct($sReason) - { - $this->m_sReason = $sReason; - } - - public function GetDescription() - { - return Dict::Format('UI:CSVReport-Row-Issue', $this->m_sReason); - } -} - - -/** - * BulkChange - * - * @package iTopORM - */ -class BulkChange -{ - protected $m_sClass; - protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string) - // #@# todo: rename the variables to sColIndex - protected $m_aAttList; // attcode => iCol - protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol; - protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey) - protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported - protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined) - protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat - protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum) - protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias - - public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false) - { - $this->m_sClass = $sClass; - $this->m_aData = $aData; - $this->m_aAttList = $aAttList; - $this->m_aReconcilKeys = $aReconcilKeys; - $this->m_aExtKeys = $aExtKeys; - $this->m_sSynchroScope = $sSynchroScope; - $this->m_aOnDisappear = $aOnDisappear; - $this->m_sDateFormat = $sDateFormat; - $this->m_bLocalizedValues = $bLocalize; - $this->m_aExtKeysMappingCache = array(); - } - - protected $m_bReportHtml = false; - protected $m_sReportCsvSep = ','; - protected $m_sReportCsvDelimiter = '"'; - - public function SetReportHtml() - { - $this->m_bReportHtml = true; - } - - public function SetReportCsv($sSeparator = ',', $sDelimiter = '"') - { - $this->m_bReportHtml = false; - $this->m_sReportCsvSep = $sSeparator; - $this->m_sReportCsvDelimiter = $sDelimiter; - } - - protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults) - { - $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); - foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) - { - if ($sForeignAttCode == 'id') - { - $value = (int) $aRowData[$iCol]; - } - else - { - // The foreign attribute is one of our reconciliation key - $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode); - $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - } - $oReconFilter->AddCondition($sForeignAttCode, $value, '='); - $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - - $oExtObjects = new CMDBObjectSet($oReconFilter); - $aKeys = $oExtObjects->ToArray(); - return array($oReconFilter->ToOql(), $aKeys); - } - - // Returns true if the CSV data specifies that the external key must be left undefined - protected function IsNullExternalKeySpec($aRowData, $sAttCode) - { - //$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) - { - // The foreign attribute is one of our reconciliation key - if (strlen($aRowData[$iCol]) > 0) - { - return false; - } - } - return true; - } - - protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors) - { - $aResults = array(); - $aErrors = array(); - - // External keys reconciliation - // - foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) - { - // Skip external keys used for the reconciliation process - // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue; - - $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - - if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) - { - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - // Default reporting - $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - if ($oExtKey->IsNullAllowed()) - { - $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue()); - $aResults[$sAttCode]= new CellStatus_Void($oExtKey->GetNullValue()); - } - else - { - $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-Null'); - $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), Dict::S('UI:CSVReport-Value-Issue-Null')); - } - } - else - { - $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); - - $aCacheKeys = array(); - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - // The foreign attribute is one of our reconciliation key - if ($sForeignAttCode == 'id') - { - $value = $aRowData[$iCol]; - } - else - { - $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode); - $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - } - $aCacheKeys[] = $value; - $oReconFilter->AddCondition($sForeignAttCode, $value, '='); - $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... - $iForeignKey = null; - $sOQL = ''; - // TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case... - if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache)) - { - $this->m_aExtKeysMappingCache[$sAttCode] = array(); - } - if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode])) - { - // Cache hit - $iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c']; - $iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k']; - $sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql']; - // Record the hit - $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++; - } - else - { - // Cache miss, let's initialize it - $oExtObjects = new CMDBObjectSet($oReconFilter); - $iCount = $oExtObjects->Count(); - if ($iCount == 1) - { - $oForeignObj = $oExtObjects->Fetch(); - $iForeignKey = $oForeignObj->GetKey(); - } - $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( - 'c' => $iCount, - 'k' => $iForeignKey, - 'oql' => $oReconFilter->ToOql(), - 'h' => 0, // number of hits on this cache entry - ); - } - switch($iCount) - { - case 0: - $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound'); - $aResults[$sAttCode]= new CellStatus_SearchIssue(); - break; - - case 1: - // Do change the external key attribute - $oTargetObj->Set($sAttCode, $iForeignKey); - break; - - default: - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount); - $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL); - } - } - - // Report - if (!array_key_exists($sAttCode, $aResults)) - { - $iForeignObj = $oTargetObj->Get($sAttCode); - if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) - { - if ($oTargetObj->IsNew()) - { - $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); - } - else - { - $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode)); - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - // Report the change on reconciliation values as well - $aResults[$iCol] = new CellStatus_Modify($aRowData[$iCol]); - } - } - } - else - { - $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); - } - } - } - - // Set the object attributes - // - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - // skip the private key, if any - if ($sAttCode == 'id') continue; - - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - - // skip reconciliation keys - if (!$oAttDef->IsWritable() && in_array($sAttCode, $this->m_aReconcilKeys)){ continue; } - - $aReasons = array(); - $iFlags = $oTargetObj->GetAttributeFlags($sAttCode, $aReasons); - if ( (($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY) && ( $oTargetObj->Get($sAttCode) != $aRowData[$iCol]) ) - { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Readonly', $sAttCode, $oTargetObj->Get($sAttCode), $aRowData[$iCol]); - } - else if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) - { - try - { - $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - $oTargetObj->Set($sAttCode, $oSet); - } - catch(CoreException $e) - { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Format', $e->getMessage()); - } - } - else - { - $value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) - { - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode); - } - else - { - $res = $oTargetObj->CheckValue($sAttCode, $value); - if ($res === true) - { - $oTargetObj->Set($sAttCode, $value); - } - else - { - // $res is a string with the error description - $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Unknown', $sAttCode, $res); - } - } - } - } - - // Reporting on fields - // - $aChangedFields = $oTargetObj->ListChanges(); - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') - { - $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); - } - else - { - if ($this->m_bReportHtml) - { - $sCurValue = $oTargetObj->GetAsHTML($sAttCode, $this->m_bLocalizedValues); - $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode, $this->m_bLocalizedValues); - } - else - { - $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); - $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); - } - if (isset($aErrors[$sAttCode])) - { - $aResults[$iCol]= new CellStatus_Issue($aRowData[$iCol], $sOrigValue, $aErrors[$sAttCode]); - } - elseif (array_key_exists($sAttCode, $aChangedFields)) - { - if ($oTargetObj->IsNew()) - { - $aResults[$iCol]= new CellStatus_Void($sCurValue); - } - else - { - $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue); - } - } - else - { - // By default... nothing happens - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef instanceof AttributeDateTime) - { - $aResults[$iCol]= new CellStatus_Void($oAttDef->GetFormat()->Format($aRowData[$iCol])); - } - else - { - $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); - } - } - } - } - - // Checks - // - $res = $oTargetObj->CheckConsistency(); - if ($res !== true) - { - // $res contains the error description - $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); - } - return $aResults; - } - - protected function PrepareMissingObject(&$oTargetObj, &$aErrors) - { - $aResults = array(); - $aErrors = array(); - - // External keys - // - foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) - { - //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode)); - - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - $aResults[$iCol] = new CellStatus_Void('?'); - } - } - - // Update attributes - // - foreach($this->m_aOnDisappear as $sAttCode => $value) - { - if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode)) - { - throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode)); - } - $oTargetObj->Set($sAttCode, $value); - } - - // Reporting on fields - // - $aChangedFields = $oTargetObj->ListChanges(); - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') - { - $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey()); - } - if (array_key_exists($sAttCode, $aChangedFields)) - { - $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode)); - } - else - { - // By default... nothing happens - $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode)); - } - } - - // Checks - // - $res = $oTargetObj->CheckConsistency(); - if ($res !== true) - { - // $res contains the error description - $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); - } - return $aResults; - } - - - protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null) - { - $oTargetObj = MetaModel::NewObject($this->m_sClass); - - // Populate the cache for hierarchical keys (only if in verify mode) - if (is_null($oChange)) - { - // 1. determine if a hierarchical key exists - foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) - { - $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - if (!$this->IsNullExternalKeySpec($aRowData, $sAttCode) && MetaModel::IsParentClass(get_class($oTargetObj), $this->m_sClass)) - { - // 2. Populate the cache for further checks - $aCacheKeys = array(); - foreach ($aKeyConfig as $sForeignAttCode => $iCol) - { - // The foreign attribute is one of our reconciliation key - if ($sForeignAttCode == 'id') - { - $value = $aRowData[$iCol]; - } - else - { - if (!isset($this->m_aAttList[$sForeignAttCode]) || !isset($aRowData[$this->m_aAttList[$sForeignAttCode]])) - { - // the key is not in the import - break 2; - } - $value = $aRowData[$this->m_aAttList[$sForeignAttCode]]; - } - $aCacheKeys[] = $value; - } - $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... - $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( - 'c' => 1, - 'k' => -1, - 'oql' => '', - 'h' => 0, // number of hits on this cache entry - ); - } - } - } - - $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); - - if (count($aErrors) > 0) - { - $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); - return $oTargetObj; - } - - // Check that any external key will have a value proposed - $aMissingKeys = array(); - foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey) - { - if (!$oExtKey->IsNullAllowed()) - { - if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList)) - { - $aMissingKeys[] = $oExtKey->GetLabel(); - } - } - } - if (count($aMissingKeys) > 0) - { - $sMissingKeys = implode(', ', $aMissingKeys); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-MissingExtKey', $sMissingKeys)); - return $oTargetObj; - } - - // Optionaly record the results - // - if ($oChange) - { - $newID = $oTargetObj->DBInsertTrackedNoReload($oChange); - $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj(); - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void($newID); - } - else - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj(); - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void(0); - } - return $oTargetObj; - } - - protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null) - { - $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); - - // Reporting - // - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); - - if (count($aErrors) > 0) - { - $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); - return; - } - - $aChangedFields = $oTargetObj->ListChanges(); - if (count($aChangedFields) > 0) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields)); - - // Optionaly record the results - // - if ($oChange) - { - try - { - $oTargetObj->DBUpdateTracked($oChange); - } - catch(CoreException $e) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage()); - } - } - } - else - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange(); - } - } - - protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null) - { - $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors); - - // Reporting - // - $aResult[$iRow]["finalclass"] = get_class($oTargetObj); - $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); - - if (count($aErrors) > 0) - { - $sErrors = implode(', ', $aErrors); - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); - return; - } - - $aChangedFields = $oTargetObj->ListChanges(); - if (count($aChangedFields) > 0) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields)); - - // Optionaly record the results - // - if ($oChange) - { - try - { - $oTargetObj->DBUpdateTracked($oChange); - } - catch(CoreException $e) - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage()); - } - } - } - else - { - $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0); - } - } - - public function Process(CMDBChange $oChange = null) - { - // Note: $oChange can be null, in which case the aim is to check what would be done - - // Debug... - // - if (false) - { - echo "
    \n";
    -			echo "Attributes:\n";
    -			print_r($this->m_aAttList);
    -			echo "ExtKeys:\n";
    -			print_r($this->m_aExtKeys);
    -			echo "Reconciliation:\n";
    -			print_r($this->m_aReconcilKeys);
    -			echo "Synchro scope:\n";
    -			print_r($this->m_sSynchroScope);
    -			echo "Synchro changes:\n";
    -			print_r($this->m_aOnDisappear);
    -			//echo "Data:\n";
    -			//print_r($this->m_aData);
    -			echo "
    \n"; - exit; - } - - $aResult = array(); - - if (!is_null($this->m_sDateFormat) && (strlen($this->m_sDateFormat) > 0)) - { - $sDateTimeFormat = $this->m_sDateFormat; // the specified format is actually the date AND time format - $oDateTimeFormat = new DateTimeFormat($sDateTimeFormat); - $sDateFormat = $oDateTimeFormat->ToDateFormat(); - AttributeDateTime::SetFormat($oDateTimeFormat); - AttributeDate::SetFormat(new DateTimeFormat($sDateFormat)); - // Translate dates from the source data - // - foreach ($this->m_aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') continue; - - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime - { - foreach($this->m_aData as $iRow => $aRowData) - { - $sFormat = $sDateTimeFormat; - $sValue = $this->m_aData[$iRow][$iCol]; - if (!empty($sValue)) - { - if ($oAttDef instanceof AttributeDate) - { - $sFormat = $sDateFormat; - } - $oFormat = new DateTimeFormat($sFormat); - $sRegExp = $oFormat->ToRegExpr('/'); - if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol])) - { - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - } - else - { - $oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]); - if ($oDate !== false) - { - $sNewDate = $oDate->format($oAttDef->GetInternalFormat()); - $this->m_aData[$iRow][$iCol] = $sNewDate; - } - else - { - // Leave the cell unchanged - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - $aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, $this->m_aData[$iRow][$iCol], Dict::S('UI:CSVReport-Row-Issue-DateFormat')); - } - } - } - else - { - $this->m_aData[$iRow][$iCol] = ''; - } - } - } - } - } - - // Compute the results - // - if (!is_null($this->m_sSynchroScope)) - { - $aVisited = array(); - } - $iPreviousTimeLimit = ini_get('max_execution_time'); - $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); - foreach($this->m_aData as $iRow => $aRowData) - { - set_time_limit($iLoopTimeLimit); - if (isset($aResult[$iRow]["__STATUS__"])) - { - // An issue at the earlier steps - skip the rest - continue; - } - try - { - $oReconciliationFilter = new DBObjectSearch($this->m_sClass); - $bSkipQuery = false; - foreach($this->m_aReconcilKeys as $sAttCode) - { - $valuecondition = null; - if (array_key_exists($sAttCode, $this->m_aExtKeys)) - { - if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) - { - $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oExtKey->IsNullAllowed()) - { - $valuecondition = $oExtKey->GetNullValue(); - $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue()); - } - else - { - $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue(); - } - } - else - { - // The value has to be found or verified - list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]); - - if (count($aMatches) == 1) - { - $oRemoteObj = reset($aMatches); // first item - $valuecondition = $oRemoteObj->GetKey(); - $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey()); - } - elseif (count($aMatches) == 0) - { - $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue(); - } - else - { - $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery); - } - } - } - else - { - // The value is given in the data row - $iCol = $this->m_aAttList[$sAttCode]; - if ($sAttCode == 'id') - { - $valuecondition = $aRowData[$iCol]; - } - else - { - $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - $valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); - } - } - if (is_null($valuecondition)) - { - $bSkipQuery = true; - } - else - { - $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '='); - } - } - if ($bSkipQuery) - { - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation')); - } - else - { - $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter); - switch($oReconciliationSet->Count()) - { - case 0: - $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange); - // $aResult[$iRow]["__STATUS__"]=> set in CreateObject - $aVisited[] = $oTargetObj->GetKey(); - break; - case 1: - $oTargetObj = $oReconciliationSet->Fetch(); - $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange); - // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject - if (!is_null($this->m_sSynchroScope)) - { - $aVisited[] = $oTargetObj->GetKey(); - } - break; - default: - // Found several matches, ambiguous - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous')); - $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql()); - $aResult[$iRow]["finalclass"]= 'n/a'; - } - } - } - catch (Exception $e) - { - $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage())); - } - } - - if (!is_null($this->m_sSynchroScope)) - { - // Compute the delta between the scope and visited objects - $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope); - $oScopeSet = new DBObjectSet($oScopeSearch); - while ($oObj = $oScopeSet->Fetch()) - { - $iObj = $oObj->GetKey(); - if (!in_array($iObj, $aVisited)) - { - set_time_limit($iLoopTimeLimit); - $iRow++; - $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange); - } - } - } - set_time_limit($iPreviousTimeLimit); - - // Fill in the blanks - the result matrix is expected to be 100% complete - // - foreach($this->m_aData as $iRow => $aRowData) - { - foreach($this->m_aAttList as $iCol) - { - if (!array_key_exists($iCol, $aResult[$iRow])) - { - $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - } - foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts) - { - if (!array_key_exists($sAttCode, $aResult[$iRow])) - { - $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a'); - } - foreach ($aForeignAtts as $sForeignAttCode => $iCol) - { - if (!array_key_exists($iCol, $aResult[$iRow])) - { - // The foreign attribute is one of our reconciliation key - $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); - } - } - } - } - - return $aResult; - } - - /** - * Display the history of bulk imports - */ - static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false) - { - $sAjaxDivId = "CSVImportHistory"; - if (!$bFromAjax) - { - $oPage->add('
    '); - } - - $oPage->p(Dict::S('UI:History:BulkImports+').' '); - - $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE origin IN ('csv-interactive', 'csv-import.php')"); - - $iQueryLimit = $bShowAll ? 0 : appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); - $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit); - - $oAppContext = new ApplicationContext(); - - $bLimitExceeded = false; - if ($oBulkChanges->Count() > (appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()))) - { - $bLimitExceeded = true; - if (!$bShowAll) - { - $iMaxObjects = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); - $oBulkChanges->SetLimit($iMaxObjects); - } - } - $oBulkChanges->Seek(0); - - $aDetails = array(); - while ($oChange = $oBulkChanges->Fetch()) - { - $sDate = ''.$oChange->Get('date').''; - $sUser = $oChange->GetUserName(); - if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches)) - { - $sUser = $aMatches[1]; - } - else - { - $sUser = $oChange->Get('userinfo'); - } - - $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id"); - $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); - $iCreated = $oOpSet->Count(); - - // Get the class from the first item found (assumption: a CSV load is done for a single class) - if ($oCreateOp = $oOpSet->Fetch()) - { - $sClass = $oCreateOp->Get('objclass'); - } - - $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id"); - $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); - - $aModified = array(); - $aAttList = array(); - while ($oModified = $oOpSet->Fetch()) - { - // Get the class (if not done earlier on object creation) - $sClass = $oModified->Get('objclass'); - $iKey = $oModified->Get('objkey'); - $sAttCode = $oModified->Get('attcode'); - - $aAttList[$sClass][$sAttCode] = true; - $aModified["$sClass::$iKey"] = true; - } - $iModified = count($aModified); - - // Assumption: there is only one class of objects being loaded - // Then the last class found gives us the class for every object - if ( ($iModified > 0) || ($iCreated > 0)) - { - $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified); - } - } - - $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')), - 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')), - 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')), - 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')), - 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')), - ); - - if ($bLimitExceeded) - { - if ($bShowAll) - { - // Collapsible list - $oPage->add('

    '.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'  '.Dict::S('UI:CollapseList').'

    '); - } - else - { - // Truncated list - $iMinDisplayLimit = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); - $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count()); - $sLinkLabel = Dict::S('UI:DisplayAll'); - $oPage->add('

    '.$sCollapsedLabel.'  '.$sLinkLabel.'

    '); - - $oPage->add_ready_script( -<<GetForLink(); - $oPage->add_script( -<<'); - $.get(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data) - { - $('#$sAjaxDivId').html(data); - var table = $('#$sAjaxDivId .listResults'); - table.tableHover(); // hover tables - table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables - } - ); - } -EOF - ); - } - } - else - { - // Normal display - full list without any decoration - } - - $oPage->table($aConfig, $aDetails); - - if (!$bFromAjax) - { - $oPage->add('
    '); - } - } - - /** - * Display the details of an import - * @param iTopWebPage $oPage - * @param $iChange - * @throws Exception - */ - static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange) - { - if ($iChange == 0) - { - throw new Exception("Missing parameter changeid"); - } - $oChange = MetaModel::GetObject('CMDBChange', $iChange, false); - if (is_null($oChange)) - { - throw new Exception("Unknown change: $iChange"); - } - $oPage->add("

    ".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."

    \n"); - - // Assumption : change made one single class of objects - $aObjects = array(); - $aAttributes = array(); // array of attcode => occurences - - $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id"); - $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange)); - while ($oOperation = $oOpSet->Fetch()) - { - $sClass = $oOperation->Get('objclass'); - $iKey = $oOperation->Get('objkey'); - $iObjId = "$sClass::$iKey"; - if (!isset($aObjects[$iObjId])) - { - $aObjects[$iObjId] = array(); - $aObjects[$iObjId]['__class__'] = $sClass; - $aObjects[$iObjId]['__id__'] = $iKey; - } - if (get_class($oOperation) == 'CMDBChangeOpCreate') - { - $aObjects[$iObjId]['__created__'] = true; - } - elseif ($oOperation instanceof CMDBChangeOpSetAttribute) - { - $sAttCode = $oOperation->Get('attcode'); - - if ((get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar') || (get_class($oOperation) == 'CMDBChangeOpSetAttributeURL')) - { - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef->IsExternalKey()) - { - $sOldValue = Dict::S('UI:UndefinedObject'); - if ($oOperation->Get('oldvalue') != 0) - { - $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue')); - $sOldValue = $oOldTarget->GetHyperlink(); - } - - $sNewValue = Dict::S('UI:UndefinedObject'); - if ($oOperation->Get('newvalue') != 0) - { - $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue')); - $sNewValue = $oNewTarget->GetHyperlink(); - } - } - else - { - $sOldValue = $oOperation->GetAsHTML('oldvalue'); - $sNewValue = $oOperation->GetAsHTML('newvalue'); - } - $aObjects[$iObjId][$sAttCode] = $sOldValue.' -> '.$sNewValue; - } - else - { - $aObjects[$iObjId][$sAttCode] = 'n/a'; - } - - if (isset($aAttributes[$sAttCode])) - { - $aAttributes[$sAttCode]++; - } - else - { - $aAttributes[$sAttCode] = 1; - } - } - } - - $aDetails = array(); - foreach($aObjects as $iUId => $aObjData) - { - $aRow = array(); - $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false); - if (is_null($oObject)) - { - $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)'; - } - else - { - $aRow['object'] = $oObject->GetHyperlink(); - } - if (isset($aObjData['__created__'])) - { - $aRow['operation'] = Dict::S('Change:ObjectCreated'); - } - else - { - $aRow['operation'] = Dict::S('Change:ObjectModified'); - } - foreach ($aAttributes as $sAttCode => $iOccurences) - { - if (isset($aObjData[$sAttCode])) - { - $aRow[$sAttCode] = $aObjData[$sAttCode]; - } - elseif (!is_null($oObject)) - { - // This is the current vaslue: $oObject->GetAsHtml($sAttCode) - // whereas we are displaying the value that was set at the time - // the object was created - // This requires addtional coding...let's do that later - $aRow[$sAttCode] = ''; - } - else - { - $aRow[$sAttCode] = ''; - } - } - $aDetails[] = $aRow; - } - - $aConfig = array(); - $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass)); - $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+')); - foreach ($aAttributes as $sAttCode => $iOccurences) - { - $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode)); - } - $oPage->table($aConfig, $aDetails); - } -} - + + + +/** + * Bulk change facility (common to interactive and batch usages) + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +// The BOM is added at the head of exported UTF-8 CSV data, and removed (if present) from input UTF-8 data. +// This helps MS-Excel (Version > 2007, Windows only) in changing its interpretation of a CSV file (by default Excel reads data as ISO-8859-1 -not 100% sure!) +define('UTF8_BOM', chr(239).chr(187).chr(191)); // 0xEF, 0xBB, 0xBF + +/** + * BulkChange + * Interpret a given data set and update the DB accordingly (fake mode avail.) + * + * @package iTopORM + */ + +class BulkChangeException extends CoreException +{ +} + +/** + * CellChangeSpec + * A series of classes, keeping the information about a given cell: could it be changed or not (and why)? + * + * @package iTopORM + */ +abstract class CellChangeSpec +{ + protected $m_proposedValue; + protected $m_sOql; // in case of ambiguity + + public function __construct($proposedValue, $sOql = '') + { + $this->m_proposedValue = $proposedValue; + $this->m_sOql = $sOql; + } + + public function GetPureValue() + { + // Todo - distinguish both values + return $this->m_proposedValue; + } + + public function GetDisplayableValue() + { + return $this->m_proposedValue; + } + + public function GetOql() + { + return $this->m_sOql; + } + + abstract public function GetDescription(); +} + + +class CellStatus_Void extends CellChangeSpec +{ + public function GetDescription() + { + return ''; + } +} + +class CellStatus_Modify extends CellChangeSpec +{ + protected $m_previousValue; + + public function __construct($proposedValue, $previousValue = null) + { + // Unused (could be costly to know -see the case of reconciliation on ext keys) + //$this->m_previousValue = $previousValue; + parent::__construct($proposedValue); + } + + public function GetDescription() + { + return Dict::S('UI:CSVReport-Value-Modified'); + } + + //public function GetPreviousValue() + //{ + // return $this->m_previousValue; + //} +} + +class CellStatus_Issue extends CellStatus_Modify +{ + protected $m_sReason; + + public function __construct($proposedValue, $previousValue, $sReason) + { + $this->m_sReason = $sReason; + parent::__construct($proposedValue, $previousValue); + } + + public function GetDescription() + { + if (is_null($this->m_proposedValue)) + { + return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason); + } + return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason); + } +} + +class CellStatus_SearchIssue extends CellStatus_Issue +{ + public function __construct() + { + parent::__construct(null, null, null); + } + + public function GetDescription() + { + return Dict::S('UI:CSVReport-Value-NoMatch'); + } +} + +class CellStatus_NullIssue extends CellStatus_Issue +{ + public function __construct() + { + parent::__construct(null, null, null); + } + + public function GetDescription() + { + return Dict::S('UI:CSVReport-Value-Missing'); + } +} + + +class CellStatus_Ambiguous extends CellStatus_Issue +{ + protected $m_iCount; + + public function __construct($previousValue, $iCount, $sOql) + { + $this->m_iCount = $iCount; + $this->m_sQuery = $sOql; + parent::__construct(null, $previousValue, ''); + } + + public function GetDescription() + { + $sCount = $this->m_iCount; + return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount); + } +} + + +/** + * RowStatus + * A series of classes, keeping the information about a given row: could it be changed or not (and why)? + * + * @package iTopORM + */ +abstract class RowStatus +{ + public function __construct() + { + } + + abstract public function GetDescription(); +} + +class RowStatus_NoChange extends RowStatus +{ + public function GetDescription() + { + return Dict::S('UI:CSVReport-Row-Unchanged'); + } +} + +class RowStatus_NewObj extends RowStatus +{ + public function GetDescription() + { + return Dict::S('UI:CSVReport-Row-Created'); + } +} + +class RowStatus_Modify extends RowStatus +{ + protected $m_iChanged; + + public function __construct($iChanged) + { + $this->m_iChanged = $iChanged; + } + + public function GetDescription() + { + return Dict::Format('UI:CSVReport-Row-Updated', $this->m_iChanged); + } +} + +class RowStatus_Disappeared extends RowStatus_Modify +{ + public function GetDescription() + { + return Dict::Format('UI:CSVReport-Row-Disappeared', $this->m_iChanged); + } +} + +class RowStatus_Issue extends RowStatus +{ + protected $m_sReason; + + public function __construct($sReason) + { + $this->m_sReason = $sReason; + } + + public function GetDescription() + { + return Dict::Format('UI:CSVReport-Row-Issue', $this->m_sReason); + } +} + + +/** + * BulkChange + * + * @package iTopORM + */ +class BulkChange +{ + protected $m_sClass; + protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string) + // #@# todo: rename the variables to sColIndex + protected $m_aAttList; // attcode => iCol + protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol; + protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey) + protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported + protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined) + protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat + protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum) + protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias + + public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false) + { + $this->m_sClass = $sClass; + $this->m_aData = $aData; + $this->m_aAttList = $aAttList; + $this->m_aReconcilKeys = $aReconcilKeys; + $this->m_aExtKeys = $aExtKeys; + $this->m_sSynchroScope = $sSynchroScope; + $this->m_aOnDisappear = $aOnDisappear; + $this->m_sDateFormat = $sDateFormat; + $this->m_bLocalizedValues = $bLocalize; + $this->m_aExtKeysMappingCache = array(); + } + + protected $m_bReportHtml = false; + protected $m_sReportCsvSep = ','; + protected $m_sReportCsvDelimiter = '"'; + + public function SetReportHtml() + { + $this->m_bReportHtml = true; + } + + public function SetReportCsv($sSeparator = ',', $sDelimiter = '"') + { + $this->m_bReportHtml = false; + $this->m_sReportCsvSep = $sSeparator; + $this->m_sReportCsvDelimiter = $sDelimiter; + } + + protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults) + { + $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); + foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) + { + if ($sForeignAttCode == 'id') + { + $value = (int) $aRowData[$iCol]; + } + else + { + // The foreign attribute is one of our reconciliation key + $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode); + $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); + } + $oReconFilter->AddCondition($sForeignAttCode, $value, '='); + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + + $oExtObjects = new CMDBObjectSet($oReconFilter); + $aKeys = $oExtObjects->ToArray(); + return array($oReconFilter->ToOql(), $aKeys); + } + + // Returns true if the CSV data specifies that the external key must be left undefined + protected function IsNullExternalKeySpec($aRowData, $sAttCode) + { + //$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol) + { + // The foreign attribute is one of our reconciliation key + if (strlen($aRowData[$iCol]) > 0) + { + return false; + } + } + return true; + } + + protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors) + { + $aResults = array(); + $aErrors = array(); + + // External keys reconciliation + // + foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) + { + // Skip external keys used for the reconciliation process + // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue; + + $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); + + if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) + { + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + // Default reporting + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + if ($oExtKey->IsNullAllowed()) + { + $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue()); + $aResults[$sAttCode]= new CellStatus_Void($oExtKey->GetNullValue()); + } + else + { + $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-Null'); + $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), Dict::S('UI:CSVReport-Value-Issue-Null')); + } + } + else + { + $oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass()); + + $aCacheKeys = array(); + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + // The foreign attribute is one of our reconciliation key + if ($sForeignAttCode == 'id') + { + $value = $aRowData[$iCol]; + } + else + { + $oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode); + $value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); + } + $aCacheKeys[] = $value; + $oReconFilter->AddCondition($sForeignAttCode, $value, '='); + $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... + $iForeignKey = null; + $sOQL = ''; + // TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case... + if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache)) + { + $this->m_aExtKeysMappingCache[$sAttCode] = array(); + } + if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode])) + { + // Cache hit + $iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c']; + $iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k']; + $sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql']; + // Record the hit + $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++; + } + else + { + // Cache miss, let's initialize it + $oExtObjects = new CMDBObjectSet($oReconFilter); + $iCount = $oExtObjects->Count(); + if ($iCount == 1) + { + $oForeignObj = $oExtObjects->Fetch(); + $iForeignKey = $oForeignObj->GetKey(); + } + $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( + 'c' => $iCount, + 'k' => $iForeignKey, + 'oql' => $oReconFilter->ToOql(), + 'h' => 0, // number of hits on this cache entry + ); + } + switch($iCount) + { + case 0: + $aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound'); + $aResults[$sAttCode]= new CellStatus_SearchIssue(); + break; + + case 1: + // Do change the external key attribute + $oTargetObj->Set($sAttCode, $iForeignKey); + break; + + default: + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount); + $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL); + } + } + + // Report + if (!array_key_exists($sAttCode, $aResults)) + { + $iForeignObj = $oTargetObj->Get($sAttCode); + if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) + { + if ($oTargetObj->IsNew()) + { + $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); + } + else + { + $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode)); + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + // Report the change on reconciliation values as well + $aResults[$iCol] = new CellStatus_Modify($aRowData[$iCol]); + } + } + } + else + { + $aResults[$sAttCode]= new CellStatus_Void($iForeignObj); + } + } + } + + // Set the object attributes + // + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + // skip the private key, if any + if ($sAttCode == 'id') continue; + + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + + // skip reconciliation keys + if (!$oAttDef->IsWritable() && in_array($sAttCode, $this->m_aReconcilKeys)){ continue; } + + $aReasons = array(); + $iFlags = $oTargetObj->GetAttributeFlags($sAttCode, $aReasons); + if ( (($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY) && ( $oTargetObj->Get($sAttCode) != $aRowData[$iCol]) ) + { + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Readonly', $sAttCode, $oTargetObj->Get($sAttCode), $aRowData[$iCol]); + } + else if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) + { + try + { + $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); + $oTargetObj->Set($sAttCode, $oSet); + } + catch(CoreException $e) + { + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Format', $e->getMessage()); + } + } + else + { + $value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); + if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) + { + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode); + } + else + { + $res = $oTargetObj->CheckValue($sAttCode, $value); + if ($res === true) + { + $oTargetObj->Set($sAttCode, $value); + } + else + { + // $res is a string with the error description + $aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-Unknown', $sAttCode, $res); + } + } + } + } + + // Reporting on fields + // + $aChangedFields = $oTargetObj->ListChanges(); + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + if ($sAttCode == 'id') + { + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); + } + else + { + if ($this->m_bReportHtml) + { + $sCurValue = $oTargetObj->GetAsHTML($sAttCode, $this->m_bLocalizedValues); + $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode, $this->m_bLocalizedValues); + } + else + { + $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); + $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter, $this->m_bLocalizedValues); + } + if (isset($aErrors[$sAttCode])) + { + $aResults[$iCol]= new CellStatus_Issue($aRowData[$iCol], $sOrigValue, $aErrors[$sAttCode]); + } + elseif (array_key_exists($sAttCode, $aChangedFields)) + { + if ($oTargetObj->IsNew()) + { + $aResults[$iCol]= new CellStatus_Void($sCurValue); + } + else + { + $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue); + } + } + else + { + // By default... nothing happens + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oAttDef instanceof AttributeDateTime) + { + $aResults[$iCol]= new CellStatus_Void($oAttDef->GetFormat()->Format($aRowData[$iCol])); + } + else + { + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); + } + } + } + } + + // Checks + // + $res = $oTargetObj->CheckConsistency(); + if ($res !== true) + { + // $res contains the error description + $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); + } + return $aResults; + } + + protected function PrepareMissingObject(&$oTargetObj, &$aErrors) + { + $aResults = array(); + $aErrors = array(); + + // External keys + // + foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) + { + //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); + $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode)); + + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + $aResults[$iCol] = new CellStatus_Void('?'); + } + } + + // Update attributes + // + foreach($this->m_aOnDisappear as $sAttCode => $value) + { + if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode)) + { + throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode)); + } + $oTargetObj->Set($sAttCode, $value); + } + + // Reporting on fields + // + $aChangedFields = $oTargetObj->ListChanges(); + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + if ($sAttCode == 'id') + { + $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey()); + } + if (array_key_exists($sAttCode, $aChangedFields)) + { + $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode)); + } + else + { + // By default... nothing happens + $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode)); + } + } + + // Checks + // + $res = $oTargetObj->CheckConsistency(); + if ($res !== true) + { + // $res contains the error description + $aErrors["GLOBAL"] = Dict::Format('UI:CSVReport-Row-Issue-Inconsistent', $res); + } + return $aResults; + } + + + protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null) + { + $oTargetObj = MetaModel::NewObject($this->m_sClass); + + // Populate the cache for hierarchical keys (only if in verify mode) + if (is_null($oChange)) + { + // 1. determine if a hierarchical key exists + foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig) + { + $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); + if (!$this->IsNullExternalKeySpec($aRowData, $sAttCode) && MetaModel::IsParentClass(get_class($oTargetObj), $this->m_sClass)) + { + // 2. Populate the cache for further checks + $aCacheKeys = array(); + foreach ($aKeyConfig as $sForeignAttCode => $iCol) + { + // The foreign attribute is one of our reconciliation key + if ($sForeignAttCode == 'id') + { + $value = $aRowData[$iCol]; + } + else + { + if (!isset($this->m_aAttList[$sForeignAttCode]) || !isset($aRowData[$this->m_aAttList[$sForeignAttCode]])) + { + // the key is not in the import + break 2; + } + $value = $aRowData[$this->m_aAttList[$sForeignAttCode]]; + } + $aCacheKeys[] = $value; + } + $sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query... + $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array( + 'c' => 1, + 'k' => -1, + 'oql' => '', + 'h' => 0, // number of hits on this cache entry + ); + } + } + } + + $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); + + if (count($aErrors) > 0) + { + $sErrors = implode(', ', $aErrors); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); + return $oTargetObj; + } + + // Check that any external key will have a value proposed + $aMissingKeys = array(); + foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey) + { + if (!$oExtKey->IsNullAllowed()) + { + if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList)) + { + $aMissingKeys[] = $oExtKey->GetLabel(); + } + } + } + if (count($aMissingKeys) > 0) + { + $sMissingKeys = implode(', ', $aMissingKeys); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-MissingExtKey', $sMissingKeys)); + return $oTargetObj; + } + + // Optionaly record the results + // + if ($oChange) + { + $newID = $oTargetObj->DBInsertTrackedNoReload($oChange); + $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj(); + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void($newID); + } + else + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj(); + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void(0); + } + return $oTargetObj; + } + + protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null) + { + $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors); + + // Reporting + // + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); + + if (count($aErrors) > 0) + { + $sErrors = implode(', ', $aErrors); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); + return; + } + + $aChangedFields = $oTargetObj->ListChanges(); + if (count($aChangedFields) > 0) + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields)); + + // Optionaly record the results + // + if ($oChange) + { + try + { + $oTargetObj->DBUpdateTracked($oChange); + } + catch(CoreException $e) + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage()); + } + } + } + else + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange(); + } + } + + protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null) + { + $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors); + + // Reporting + // + $aResult[$iRow]["finalclass"] = get_class($oTargetObj); + $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey()); + + if (count($aErrors) > 0) + { + $sErrors = implode(', ', $aErrors); + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute')); + return; + } + + $aChangedFields = $oTargetObj->ListChanges(); + if (count($aChangedFields) > 0) + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields)); + + // Optionaly record the results + // + if ($oChange) + { + try + { + $oTargetObj->DBUpdateTracked($oChange); + } + catch(CoreException $e) + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage()); + } + } + } + else + { + $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0); + } + } + + public function Process(CMDBChange $oChange = null) + { + // Note: $oChange can be null, in which case the aim is to check what would be done + + // Debug... + // + if (false) + { + echo "
    \n";
    +			echo "Attributes:\n";
    +			print_r($this->m_aAttList);
    +			echo "ExtKeys:\n";
    +			print_r($this->m_aExtKeys);
    +			echo "Reconciliation:\n";
    +			print_r($this->m_aReconcilKeys);
    +			echo "Synchro scope:\n";
    +			print_r($this->m_sSynchroScope);
    +			echo "Synchro changes:\n";
    +			print_r($this->m_aOnDisappear);
    +			//echo "Data:\n";
    +			//print_r($this->m_aData);
    +			echo "
    \n"; + exit; + } + + $aResult = array(); + + if (!is_null($this->m_sDateFormat) && (strlen($this->m_sDateFormat) > 0)) + { + $sDateTimeFormat = $this->m_sDateFormat; // the specified format is actually the date AND time format + $oDateTimeFormat = new DateTimeFormat($sDateTimeFormat); + $sDateFormat = $oDateTimeFormat->ToDateFormat(); + AttributeDateTime::SetFormat($oDateTimeFormat); + AttributeDate::SetFormat(new DateTimeFormat($sDateFormat)); + // Translate dates from the source data + // + foreach ($this->m_aAttList as $sAttCode => $iCol) + { + if ($sAttCode == 'id') continue; + + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oAttDef instanceof AttributeDateTime) // AttributeDate is derived from AttributeDateTime + { + foreach($this->m_aData as $iRow => $aRowData) + { + $sFormat = $sDateTimeFormat; + $sValue = $this->m_aData[$iRow][$iCol]; + if (!empty($sValue)) + { + if ($oAttDef instanceof AttributeDate) + { + $sFormat = $sDateFormat; + } + $oFormat = new DateTimeFormat($sFormat); + $sRegExp = $oFormat->ToRegExpr('/'); + if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol])) + { + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); + } + else + { + $oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]); + if ($oDate !== false) + { + $sNewDate = $oDate->format($oAttDef->GetInternalFormat()); + $this->m_aData[$iRow][$iCol] = $sNewDate; + } + else + { + // Leave the cell unchanged + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat')); + $aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, $this->m_aData[$iRow][$iCol], Dict::S('UI:CSVReport-Row-Issue-DateFormat')); + } + } + } + else + { + $this->m_aData[$iRow][$iCol] = ''; + } + } + } + } + } + + // Compute the results + // + if (!is_null($this->m_sSynchroScope)) + { + $aVisited = array(); + } + $iPreviousTimeLimit = ini_get('max_execution_time'); + $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); + foreach($this->m_aData as $iRow => $aRowData) + { + set_time_limit($iLoopTimeLimit); + if (isset($aResult[$iRow]["__STATUS__"])) + { + // An issue at the earlier steps - skip the rest + continue; + } + try + { + $oReconciliationFilter = new DBObjectSearch($this->m_sClass); + $bSkipQuery = false; + foreach($this->m_aReconcilKeys as $sAttCode) + { + $valuecondition = null; + if (array_key_exists($sAttCode, $this->m_aExtKeys)) + { + if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) + { + $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + if ($oExtKey->IsNullAllowed()) + { + $valuecondition = $oExtKey->GetNullValue(); + $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue()); + } + else + { + $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue(); + } + } + else + { + // The value has to be found or verified + list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]); + + if (count($aMatches) == 1) + { + $oRemoteObj = reset($aMatches); // first item + $valuecondition = $oRemoteObj->GetKey(); + $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey()); + } + elseif (count($aMatches) == 0) + { + $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue(); + } + else + { + $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery); + } + } + } + else + { + // The value is given in the data row + $iCol = $this->m_aAttList[$sAttCode]; + if ($sAttCode == 'id') + { + $valuecondition = $aRowData[$iCol]; + } + else + { + $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); + $valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues); + } + } + if (is_null($valuecondition)) + { + $bSkipQuery = true; + } + else + { + $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '='); + } + } + if ($bSkipQuery) + { + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation')); + } + else + { + $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter); + switch($oReconciliationSet->Count()) + { + case 0: + $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange); + // $aResult[$iRow]["__STATUS__"]=> set in CreateObject + $aVisited[] = $oTargetObj->GetKey(); + break; + case 1: + $oTargetObj = $oReconciliationSet->Fetch(); + $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange); + // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject + if (!is_null($this->m_sSynchroScope)) + { + $aVisited[] = $oTargetObj->GetKey(); + } + break; + default: + // Found several matches, ambiguous + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous')); + $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql()); + $aResult[$iRow]["finalclass"]= 'n/a'; + } + } + } + catch (Exception $e) + { + $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage())); + } + } + + if (!is_null($this->m_sSynchroScope)) + { + // Compute the delta between the scope and visited objects + $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope); + $oScopeSet = new DBObjectSet($oScopeSearch); + while ($oObj = $oScopeSet->Fetch()) + { + $iObj = $oObj->GetKey(); + if (!in_array($iObj, $aVisited)) + { + set_time_limit($iLoopTimeLimit); + $iRow++; + $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange); + } + } + } + set_time_limit($iPreviousTimeLimit); + + // Fill in the blanks - the result matrix is expected to be 100% complete + // + foreach($this->m_aData as $iRow => $aRowData) + { + foreach($this->m_aAttList as $iCol) + { + if (!array_key_exists($iCol, $aResult[$iRow])) + { + $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + } + foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts) + { + if (!array_key_exists($sAttCode, $aResult[$iRow])) + { + $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a'); + } + foreach ($aForeignAtts as $sForeignAttCode => $iCol) + { + if (!array_key_exists($iCol, $aResult[$iRow])) + { + // The foreign attribute is one of our reconciliation key + $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]); + } + } + } + } + + return $aResult; + } + + /** + * Display the history of bulk imports + */ + static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false) + { + $sAjaxDivId = "CSVImportHistory"; + if (!$bFromAjax) + { + $oPage->add('
    '); + } + + $oPage->p(Dict::S('UI:History:BulkImports+').' '); + + $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE origin IN ('csv-interactive', 'csv-import.php')"); + + $iQueryLimit = $bShowAll ? 0 : appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); + $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit); + + $oAppContext = new ApplicationContext(); + + $bLimitExceeded = false; + if ($oBulkChanges->Count() > (appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()))) + { + $bLimitExceeded = true; + if (!$bShowAll) + { + $iMaxObjects = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); + $oBulkChanges->SetLimit($iMaxObjects); + } + } + $oBulkChanges->Seek(0); + + $aDetails = array(); + while ($oChange = $oBulkChanges->Fetch()) + { + $sDate = ''.$oChange->Get('date').''; + $sUser = $oChange->GetUserName(); + if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches)) + { + $sUser = $aMatches[1]; + } + else + { + $sUser = $oChange->Get('userinfo'); + } + + $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id"); + $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); + $iCreated = $oOpSet->Count(); + + // Get the class from the first item found (assumption: a CSV load is done for a single class) + if ($oCreateOp = $oOpSet->Fetch()) + { + $sClass = $oCreateOp->Get('objclass'); + } + + $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id"); + $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey())); + + $aModified = array(); + $aAttList = array(); + while ($oModified = $oOpSet->Fetch()) + { + // Get the class (if not done earlier on object creation) + $sClass = $oModified->Get('objclass'); + $iKey = $oModified->Get('objkey'); + $sAttCode = $oModified->Get('attcode'); + + $aAttList[$sClass][$sAttCode] = true; + $aModified["$sClass::$iKey"] = true; + } + $iModified = count($aModified); + + // Assumption: there is only one class of objects being loaded + // Then the last class found gives us the class for every object + if ( ($iModified > 0) || ($iCreated > 0)) + { + $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified); + } + } + + $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')), + 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')), + 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')), + 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')), + 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')), + ); + + if ($bLimitExceeded) + { + if ($bShowAll) + { + // Collapsible list + $oPage->add('

    '.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'  '.Dict::S('UI:CollapseList').'

    '); + } + else + { + // Truncated list + $iMinDisplayLimit = appUserPreferences::GetPref('default_page_size', MetaModel::GetConfig()->GetMinDisplayLimit()); + $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count()); + $sLinkLabel = Dict::S('UI:DisplayAll'); + $oPage->add('

    '.$sCollapsedLabel.'  '.$sLinkLabel.'

    '); + + $oPage->add_ready_script( +<<GetForLink(); + $oPage->add_script( +<<'); + $.get(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data) + { + $('#$sAjaxDivId').html(data); + var table = $('#$sAjaxDivId .listResults'); + table.tableHover(); // hover tables + table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables + } + ); + } +EOF + ); + } + } + else + { + // Normal display - full list without any decoration + } + + $oPage->table($aConfig, $aDetails); + + if (!$bFromAjax) + { + $oPage->add('
    '); + } + } + + /** + * Display the details of an import + * @param iTopWebPage $oPage + * @param $iChange + * @throws Exception + */ + static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange) + { + if ($iChange == 0) + { + throw new Exception("Missing parameter changeid"); + } + $oChange = MetaModel::GetObject('CMDBChange', $iChange, false); + if (is_null($oChange)) + { + throw new Exception("Unknown change: $iChange"); + } + $oPage->add("

    ".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."

    \n"); + + // Assumption : change made one single class of objects + $aObjects = array(); + $aAttributes = array(); // array of attcode => occurences + + $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id"); + $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange)); + while ($oOperation = $oOpSet->Fetch()) + { + $sClass = $oOperation->Get('objclass'); + $iKey = $oOperation->Get('objkey'); + $iObjId = "$sClass::$iKey"; + if (!isset($aObjects[$iObjId])) + { + $aObjects[$iObjId] = array(); + $aObjects[$iObjId]['__class__'] = $sClass; + $aObjects[$iObjId]['__id__'] = $iKey; + } + if (get_class($oOperation) == 'CMDBChangeOpCreate') + { + $aObjects[$iObjId]['__created__'] = true; + } + elseif ($oOperation instanceof CMDBChangeOpSetAttribute) + { + $sAttCode = $oOperation->Get('attcode'); + + if ((get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar') || (get_class($oOperation) == 'CMDBChangeOpSetAttributeURL')) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsExternalKey()) + { + $sOldValue = Dict::S('UI:UndefinedObject'); + if ($oOperation->Get('oldvalue') != 0) + { + $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue')); + $sOldValue = $oOldTarget->GetHyperlink(); + } + + $sNewValue = Dict::S('UI:UndefinedObject'); + if ($oOperation->Get('newvalue') != 0) + { + $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue')); + $sNewValue = $oNewTarget->GetHyperlink(); + } + } + else + { + $sOldValue = $oOperation->GetAsHTML('oldvalue'); + $sNewValue = $oOperation->GetAsHTML('newvalue'); + } + $aObjects[$iObjId][$sAttCode] = $sOldValue.' -> '.$sNewValue; + } + else + { + $aObjects[$iObjId][$sAttCode] = 'n/a'; + } + + if (isset($aAttributes[$sAttCode])) + { + $aAttributes[$sAttCode]++; + } + else + { + $aAttributes[$sAttCode] = 1; + } + } + } + + $aDetails = array(); + foreach($aObjects as $iUId => $aObjData) + { + $aRow = array(); + $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false); + if (is_null($oObject)) + { + $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)'; + } + else + { + $aRow['object'] = $oObject->GetHyperlink(); + } + if (isset($aObjData['__created__'])) + { + $aRow['operation'] = Dict::S('Change:ObjectCreated'); + } + else + { + $aRow['operation'] = Dict::S('Change:ObjectModified'); + } + foreach ($aAttributes as $sAttCode => $iOccurences) + { + if (isset($aObjData[$sAttCode])) + { + $aRow[$sAttCode] = $aObjData[$sAttCode]; + } + elseif (!is_null($oObject)) + { + // This is the current vaslue: $oObject->GetAsHtml($sAttCode) + // whereas we are displaying the value that was set at the time + // the object was created + // This requires addtional coding...let's do that later + $aRow[$sAttCode] = ''; + } + else + { + $aRow[$sAttCode] = ''; + } + } + $aDetails[] = $aRow; + } + + $aConfig = array(); + $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass)); + $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+')); + foreach ($aAttributes as $sAttCode => $iOccurences) + { + $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode)); + } + $oPage->table($aConfig, $aDetails); + } +} + diff --git a/core/cmdbchange.class.inc.php b/core/cmdbchange.class.inc.php index 7304db99c..e0e9dfc65 100644 --- a/core/cmdbchange.class.inc.php +++ b/core/cmdbchange.class.inc.php @@ -1,87 +1,87 @@ - - - -/** - * Persistent class (internal) cmdbChange - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * A change as requested/validated at once by user, may groups many atomic changes - * - * @package iTopORM - */ -class CMDBChange extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "date", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_change", - "db_key_field" => "id", - "db_finalclass_field" => "", - 'indexes' => array( - array('origin'), - ) - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("origin", array("allowed_values"=>new ValueSetEnum('interactive,csv-interactive,csv-import.php,webservice-soap,webservice-rest,synchro-data-source,email-processing,custom-extension'), "sql"=>"origin", "default_value"=>"interactive", "is_null_allowed"=>true, "depends_on"=>array()))); - } - - // Helper to keep track of the author of a given change, - // taking into account a variety of cases (contact attached or not, impersonation) - static public function GetCurrentUserName() - { - if (UserRights::IsImpersonated()) - { - $sUserString = Dict::Format('UI:Archive_User_OnBehalfOf_User', UserRights::GetRealUserFriendlyName(), UserRights::GetUserFriendlyName()); - } - else - { - $sUserString = UserRights::GetUserFriendlyName(); - } - return $sUserString; - } - - public function GetUserName() - { - if (preg_match('/^(.*)\\(CSV\\)$/i', $this->Get('userinfo'), $aMatches)) - { - $sUser = $aMatches[1]; - } - else - { - $sUser = $this->Get('userinfo'); - } - return $sUser; - } -} - -?> + + + +/** + * Persistent class (internal) cmdbChange + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * A change as requested/validated at once by user, may groups many atomic changes + * + * @package iTopORM + */ +class CMDBChange extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "date", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_change", + "db_key_field" => "id", + "db_finalclass_field" => "", + 'indexes' => array( + array('origin'), + ) + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("origin", array("allowed_values"=>new ValueSetEnum('interactive,csv-interactive,csv-import.php,webservice-soap,webservice-rest,synchro-data-source,email-processing,custom-extension'), "sql"=>"origin", "default_value"=>"interactive", "is_null_allowed"=>true, "depends_on"=>array()))); + } + + // Helper to keep track of the author of a given change, + // taking into account a variety of cases (contact attached or not, impersonation) + static public function GetCurrentUserName() + { + if (UserRights::IsImpersonated()) + { + $sUserString = Dict::Format('UI:Archive_User_OnBehalfOf_User', UserRights::GetRealUserFriendlyName(), UserRights::GetUserFriendlyName()); + } + else + { + $sUserString = UserRights::GetUserFriendlyName(); + } + return $sUserString; + } + + public function GetUserName() + { + if (preg_match('/^(.*)\\(CSV\\)$/i', $this->Get('userinfo'), $aMatches)) + { + $sUser = $aMatches[1]; + } + else + { + $sUser = $this->Get('userinfo'); + } + return $sUser; + } +} + +?> diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index f6145083d..3588dd931 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -1,1042 +1,1042 @@ - - - -/** - * Persistent classes (internal) : cmdbChangeOp and derived - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * Various atomic change operations, to be tracked - * - * @package iTopORM - */ - -class CMDBChangeOp extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop", - "db_key_field" => "id", - "db_finalclass_field" => "optype", - 'indexes' => array( - array('objclass', 'objkey'), - ) - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("change", array("allowed_values"=>null, "sql"=>"changeid", "targetclass"=>"CMDBChange", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("date", array("allowed_values"=>null, "extkey_attcode"=>"change", "target_attcode"=>"date"))); - MetaModel::Init_AddAttribute(new AttributeExternalField("userinfo", array("allowed_values"=>null, "extkey_attcode"=>"change", "target_attcode"=>"userinfo"))); - MetaModel::Init_AddAttribute(new AttributeString("objclass", array("allowed_values"=>null, "sql"=>"objclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeObjectKey("objkey", array("allowed_values"=>null, "class_attcode"=>"objclass", "sql"=>"objkey", "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_SetZListItems('details', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - return ''; - } - - /** - * Safety net: in case the change is not given, let's guarantee that it will - * be set to the current ongoing change (or create a new one) - */ - protected function OnInsert() - { - if ($this->Get('change') <= 0) - { - $this->Set('change', CMDBObject::GetCurrentChange()); - } - parent::OnInsert(); - } -} - - - -/** - * Record the creation of an object - * - * @package iTopORM - */ -class CMDBChangeOpCreate extends CMDBChangeOp -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_create", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - return Dict::S('Change:ObjectCreated'); - } -} - - -/** - * Record the deletion of an object - * - * @package iTopORM - */ -class CMDBChangeOpDelete extends CMDBChangeOp -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_delete", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Final class of the object (objclass must be set to the root class for efficiency purposes) - MetaModel::Init_AddAttribute(new AttributeString("fclass", array("allowed_values"=>null, "sql"=>"fclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - // Last friendly name of the object - MetaModel::Init_AddAttribute(new AttributeString("fname", array("allowed_values"=>null, "sql"=>"fname", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - } - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - return Dict::S('Change:ObjectDeleted'); - } -} - - -/** - * Record the modification of an attribute (abstract) - * - * @package iTopORM - */ -class CMDBChangeOpSetAttribute extends CMDBChangeOp -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } -} - -/** - * Record the modification of a scalar attribute - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeScalar extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_scalar", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("oldvalue", array("allowed_values"=>null, "sql"=>"oldvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("newvalue", array("allowed_values"=>null, "sql"=>"newvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... - - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - $sNewValue = $this->Get('newvalue'); - $sOldValue = $this->Get('oldvalue'); - $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue); - } - return $sResult; - } -} -/** - * Record the modification of an URL - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeURL extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_url", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeURL("oldvalue", array("allowed_values"=>null, "sql"=>"oldvalue", "target" => '_blank', "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeURL("newvalue", array("allowed_values"=>null, "sql"=>"newvalue", "target" => '_blank', "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... - - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - $sNewValue = $this->Get('newvalue'); - $sOldValue = $this->Get('oldvalue'); - $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue); - } - return $sResult; - } -} - -/** - * Record the modification of a blob - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_data", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeBlob("prevdata", array("depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - // Temporary, until we change the options of GetDescription() -needs a more global revision - $bIsHtml = true; - - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $oPrevDoc = $this->Get('prevdata'); - if ($oPrevDoc->IsEmpty()) - { - $sPrevious = ''; - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevious); - } - else - { - $sDocView = $oPrevDoc->GetAsHtml(); - $sDocView .= "
    ".Dict::Format('UI:OpenDocumentInNewWindow_', $oPrevDoc->GetDisplayLink(get_class($this), $this->GetKey(), 'prevdata')).", \n"; - $sDocView .= Dict::Format('UI:DownloadDocument_', $oPrevDoc->GetDownloadLink(get_class($this), $this->GetKey(), 'prevdata'))."\n"; - //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sDocView); - } - } - return $sResult; - } -} -/** - * Safely record the modification of one way encrypted password - */ -class CMDBChangeOpSetAttributeOneWayPassword extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_pwd", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeOneWayPassword("prev_pwd", array("sql" => 'data', "default_value" => '', "is_null_allowed"=> true, "allowed_values" => null, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - // Temporary, until we change the options of GetDescription() -needs a more global revision - $bIsHtml = true; - - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $sResult = Dict::Format('Change:AttName_Changed', $sAttName); - } - return $sResult; - } -} - -/** - * Safely record the modification of an encrypted field - */ -class CMDBChangeOpSetAttributeEncrypted extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_encrypted", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeEncryptedString("prevstring", array("sql" => 'data', "default_value" => '', "is_null_allowed"=> true, "allowed_values" => null, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - // Temporary, until we change the options of GetDescription() -needs a more global revision - $bIsHtml = true; - - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $sPrevString = $this->Get('prevstring'); - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevString); - } - return $sResult; - } -} - -/** - * Record the modification of a multiline string (text) - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeText extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_text", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - // Temporary, until we change the options of GetDescription() -needs a more global revision - $bIsHtml = true; - - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $sTextView = '
    '.$this->GetAsHtml('prevdata').'
    '; - - //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); - } - return $sResult; - } -} - -/** - * Record the modification of a multiline string (text) - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeLongText extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_longtext", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $sTextView = '
    '.$this->GetAsHtml('prevdata').'
    '; - - //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); - } - return $sResult; - } -} - -/** - * Record the modification of a multiline string (text) containing some HTML markup - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeHTML extends CMDBChangeOpSetAttributeLongText -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_html", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $sTextView = '
    '.$this->Get('prevdata').'
    '; - - //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); - } - return $sResult; - } -} - -/** - * Record the modification of a caselog (text) - * since the caselog itself stores the history - * of its entries, there is no need to duplicate - * the text here - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeCaseLog extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_log", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeInteger("lastentry", array("allowed_values"=>null, "sql"=>"lastentry", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - // Temporary, until we change the options of GetDescription() -needs a more global revision - $bIsHtml = true; - - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - } - else - { - // The attribute was renamed or removed from the object ? - $sAttName = $this->Get('attcode'); - } - $oObj = $oMonoObjectSet->Fetch(); - $oCaseLog = $oObj->Get($this->Get('attcode')); - $iMaxVisibleLength = MetaModel::getConfig()->Get('max_history_case_log_entry_length', 0); - $sTextEntry = '
    '.$oCaseLog->GetEntryAt($this->Get('lastentry')).'
    '; - - $sResult = Dict::Format('Change:AttName_EntryAdded', $sAttName, $sTextEntry); - } - return $sResult; - } - - protected function ToHtml($sRawText) - { - return str_replace(array("\r\n", "\n", "\r"), "
    ", htmlentities($sRawText, ENT_QUOTES, 'UTF-8')); - } -} - -/** - * Record an action made by a plug-in - * - * @package iTopORM - */ -class CMDBChangeOpPlugin extends CMDBChangeOp -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_plugin", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); - /* May be used later when implementing an extension mechanism that will allow the plug-ins to store some extra information and still degrades gracefully when the plug-in is desinstalled - MetaModel::Init_AddAttribute(new AttributeString("extension_class", array("allowed_values"=>null, "sql"=>"extension_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("extension_id", array("allowed_values"=>null, "sql"=>"extension_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); - */ - MetaModel::Init_InheritAttributes(); - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - return $this->Get('description'); - } -} - -/** - * Record added/removed objects from within a link set - * - * @package iTopORM - */ -abstract class CMDBChangeOpSetAttributeLinks extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_links", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Note: item class/id points to the link class itself in case of a direct link set (e.g. Server::interface_list => Interface) - // item class/id points to the remote class in case of a indirect link set (e.g. Server::contract_list => Contract) - MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values"=>null, "sql"=>"item_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("item_id", array("allowed_values"=>null, "sql"=>"item_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); - } -} - -/** - * Record added/removed objects from within a link set - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeLinksAddRemove extends CMDBChangeOpSetAttributeLinks -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_links_addremove", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeEnum("type", array("allowed_values"=>new ValueSetEnum('added,removed'), "sql"=>"type", "default_value"=>"added", "is_null_allowed"=>false, "depends_on"=>array()))); - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... - - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - - $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id')); - - $sResult = $sAttName.' - '; - switch ($this->Get('type')) - { - case 'added': - $sResult .= Dict::Format('Change:LinkSet:Added', $sItemDesc); - break; - - case 'removed': - $sResult .= Dict::Format('Change:LinkSet:Removed', $sItemDesc); - break; - } - } - return $sResult; - } -} - -/** - * Record attribute changes from within a link set - * A single record redirects to the modifications made within the same change - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeLinksTune extends CMDBChangeOpSetAttributeLinks -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_links_tune", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeInteger("link_id", array("allowed_values"=>null, "sql"=>"link_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... - - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - - $sLinkClass = $oAttDef->GetLinkedClass(); - $aLinkClasses = MetaModel::EnumChildClasses($sLinkClass, ENUM_CHILD_CLASSES_ALL); - - // Search for changes on the corresponding link - // - $oSearch = new DBObjectSearch('CMDBChangeOpSetAttribute'); - $oSearch->AddCondition('change', $this->Get('change'), '='); - $oSearch->AddCondition('objkey', $this->Get('link_id'), '='); - if (count($aLinkClasses) == 1) - { - // Faster than the whole building of the expression below for just one value ?? - $oSearch->AddCondition('objclass', $sLinkClass, '='); - } - else - { - $oField = new FieldExpression('objclass', $oSearch->GetClassAlias()); - $sListExpr = '('.implode(', ', CMDBSource::Quote($aLinkClasses)).')'; - $sOQLCondition = $oField->Render()." IN $sListExpr"; - $oNewCondition = Expression::FromOQL($sOQLCondition); - $oSearch->AddConditionExpression($oNewCondition); - } - $oSet = new DBObjectSet($oSearch); - $aChanges = array(); - while ($oChangeOp = $oSet->Fetch()) - { - $aChanges[] = $oChangeOp->GetDescription(); - } - if (count($aChanges) == 0) - { - return ''; - } - - $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id')); - - $sResult = $sAttName.' - '; - $sResult .= Dict::Format('Change:LinkSet:Modified', $sItemDesc); - $sResult .= ' : '.implode(', ', $aChanges); - } - return $sResult; - } -} - -/** - * Record the modification of custom fields - * - * @package iTopORM - */ -class CMDBChangeOpSetAttributeCustomFields extends CMDBChangeOpSetAttribute -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "", - "name_attcode" => "change", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_changeop_setatt_custfields", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list - } - - /** - * Describe (as a text string) the modifications corresponding to this change - */ - public function GetDescription() - { - $sResult = ''; - if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) - { - $oTargetObjectClass = $this->Get('objclass'); - $oTargetObjectKey = $this->Get('objkey'); - $oTargetSearch = new DBObjectSearch($oTargetObjectClass); - $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); - - $oMonoObjectSet = new DBObjectSet($oTargetSearch); - if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) - { - $aValues = json_decode($this->Get('prevdata'), true); - $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); - $sAttName = $oAttDef->GetLabel(); - - try - { - $oHandler = $oAttDef->GetHandler($aValues); - $sValueDesc = $oHandler->GetAsHTML($aValues); - } - catch (Exception $e) - { - $sValueDesc = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); - } - $sTextView = '
    '.$sValueDesc.'
    '; - - $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); - } - } - return $sResult; - } -} + + + +/** + * Persistent classes (internal) : cmdbChangeOp and derived + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * Various atomic change operations, to be tracked + * + * @package iTopORM + */ + +class CMDBChangeOp extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop", + "db_key_field" => "id", + "db_finalclass_field" => "optype", + 'indexes' => array( + array('objclass', 'objkey'), + ) + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("change", array("allowed_values"=>null, "sql"=>"changeid", "targetclass"=>"CMDBChange", "is_null_allowed"=>false, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("date", array("allowed_values"=>null, "extkey_attcode"=>"change", "target_attcode"=>"date"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("userinfo", array("allowed_values"=>null, "extkey_attcode"=>"change", "target_attcode"=>"userinfo"))); + MetaModel::Init_AddAttribute(new AttributeString("objclass", array("allowed_values"=>null, "sql"=>"objclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeObjectKey("objkey", array("allowed_values"=>null, "class_attcode"=>"objclass", "sql"=>"objkey", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_SetZListItems('details', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('change', 'date', 'userinfo')); // Attributes to be displayed for the complete details + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return ''; + } + + /** + * Safety net: in case the change is not given, let's guarantee that it will + * be set to the current ongoing change (or create a new one) + */ + protected function OnInsert() + { + if ($this->Get('change') <= 0) + { + $this->Set('change', CMDBObject::GetCurrentChange()); + } + parent::OnInsert(); + } +} + + + +/** + * Record the creation of an object + * + * @package iTopORM + */ +class CMDBChangeOpCreate extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_create", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return Dict::S('Change:ObjectCreated'); + } +} + + +/** + * Record the deletion of an object + * + * @package iTopORM + */ +class CMDBChangeOpDelete extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_delete", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Final class of the object (objclass must be set to the root class for efficiency purposes) + MetaModel::Init_AddAttribute(new AttributeString("fclass", array("allowed_values"=>null, "sql"=>"fclass", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + // Last friendly name of the object + MetaModel::Init_AddAttribute(new AttributeString("fname", array("allowed_values"=>null, "sql"=>"fname", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + } + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return Dict::S('Change:ObjectDeleted'); + } +} + + +/** + * Record the modification of an attribute (abstract) + * + * @package iTopORM + */ +class CMDBChangeOpSetAttribute extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } +} + +/** + * Record the modification of a scalar attribute + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeScalar extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_scalar", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("oldvalue", array("allowed_values"=>null, "sql"=>"oldvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("newvalue", array("allowed_values"=>null, "sql"=>"newvalue", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... + + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + $sNewValue = $this->Get('newvalue'); + $sOldValue = $this->Get('oldvalue'); + $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue); + } + return $sResult; + } +} +/** + * Record the modification of an URL + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeURL extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_url", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeURL("oldvalue", array("allowed_values"=>null, "sql"=>"oldvalue", "target" => '_blank', "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeURL("newvalue", array("allowed_values"=>null, "sql"=>"newvalue", "target" => '_blank', "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode', 'oldvalue', 'newvalue')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... + + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + $sNewValue = $this->Get('newvalue'); + $sOldValue = $this->Get('oldvalue'); + $sResult = $oAttDef->DescribeChangeAsHTML($sOldValue, $sNewValue); + } + return $sResult; + } +} + +/** + * Record the modification of a blob + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeBlob extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_data", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeBlob("prevdata", array("depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + // Temporary, until we change the options of GetDescription() -needs a more global revision + $bIsHtml = true; + + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $oPrevDoc = $this->Get('prevdata'); + if ($oPrevDoc->IsEmpty()) + { + $sPrevious = ''; + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevious); + } + else + { + $sDocView = $oPrevDoc->GetAsHtml(); + $sDocView .= "
    ".Dict::Format('UI:OpenDocumentInNewWindow_', $oPrevDoc->GetDisplayLink(get_class($this), $this->GetKey(), 'prevdata')).", \n"; + $sDocView .= Dict::Format('UI:DownloadDocument_', $oPrevDoc->GetDownloadLink(get_class($this), $this->GetKey(), 'prevdata'))."\n"; + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sDocView); + } + } + return $sResult; + } +} +/** + * Safely record the modification of one way encrypted password + */ +class CMDBChangeOpSetAttributeOneWayPassword extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_pwd", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeOneWayPassword("prev_pwd", array("sql" => 'data', "default_value" => '', "is_null_allowed"=> true, "allowed_values" => null, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + // Temporary, until we change the options of GetDescription() -needs a more global revision + $bIsHtml = true; + + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $sResult = Dict::Format('Change:AttName_Changed', $sAttName); + } + return $sResult; + } +} + +/** + * Safely record the modification of an encrypted field + */ +class CMDBChangeOpSetAttributeEncrypted extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_encrypted", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeEncryptedString("prevstring", array("sql" => 'data', "default_value" => '', "is_null_allowed"=> true, "allowed_values" => null, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + // Temporary, until we change the options of GetDescription() -needs a more global revision + $bIsHtml = true; + + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $sPrevString = $this->Get('prevstring'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sPrevString); + } + return $sResult; + } +} + +/** + * Record the modification of a multiline string (text) + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeText extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_text", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + // Temporary, until we change the options of GetDescription() -needs a more global revision + $bIsHtml = true; + + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $sTextView = '
    '.$this->GetAsHtml('prevdata').'
    '; + + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + return $sResult; + } +} + +/** + * Record the modification of a multiline string (text) + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeLongText extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_longtext", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $sTextView = '
    '.$this->GetAsHtml('prevdata').'
    '; + + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + return $sResult; + } +} + +/** + * Record the modification of a multiline string (text) containing some HTML markup + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeHTML extends CMDBChangeOpSetAttributeLongText +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_html", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $sTextView = '
    '.$this->Get('prevdata').'
    '; + + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + return $sResult; + } +} + +/** + * Record the modification of a caselog (text) + * since the caselog itself stores the history + * of its entries, there is no need to duplicate + * the text here + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeCaseLog extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_log", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeInteger("lastentry", array("allowed_values"=>null, "sql"=>"lastentry", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + // Temporary, until we change the options of GetDescription() -needs a more global revision + $bIsHtml = true; + + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $oObj = $oMonoObjectSet->Fetch(); + $oCaseLog = $oObj->Get($this->Get('attcode')); + $iMaxVisibleLength = MetaModel::getConfig()->Get('max_history_case_log_entry_length', 0); + $sTextEntry = '
    '.$oCaseLog->GetEntryAt($this->Get('lastentry')).'
    '; + + $sResult = Dict::Format('Change:AttName_EntryAdded', $sAttName, $sTextEntry); + } + return $sResult; + } + + protected function ToHtml($sRawText) + { + return str_replace(array("\r\n", "\n", "\r"), "
    ", htmlentities($sRawText, ENT_QUOTES, 'UTF-8')); + } +} + +/** + * Record an action made by a plug-in + * + * @package iTopORM + */ +class CMDBChangeOpPlugin extends CMDBChangeOp +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_plugin", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); + /* May be used later when implementing an extension mechanism that will allow the plug-ins to store some extra information and still degrades gracefully when the plug-in is desinstalled + MetaModel::Init_AddAttribute(new AttributeString("extension_class", array("allowed_values"=>null, "sql"=>"extension_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("extension_id", array("allowed_values"=>null, "sql"=>"extension_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + */ + MetaModel::Init_InheritAttributes(); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + return $this->Get('description'); + } +} + +/** + * Record added/removed objects from within a link set + * + * @package iTopORM + */ +abstract class CMDBChangeOpSetAttributeLinks extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_links", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Note: item class/id points to the link class itself in case of a direct link set (e.g. Server::interface_list => Interface) + // item class/id points to the remote class in case of a indirect link set (e.g. Server::contract_list => Contract) + MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values"=>null, "sql"=>"item_class", "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("item_id", array("allowed_values"=>null, "sql"=>"item_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + } +} + +/** + * Record added/removed objects from within a link set + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeLinksAddRemove extends CMDBChangeOpSetAttributeLinks +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_links_addremove", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeEnum("type", array("allowed_values"=>new ValueSetEnum('added,removed'), "sql"=>"type", "default_value"=>"added", "is_null_allowed"=>false, "depends_on"=>array()))); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... + + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + + $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id')); + + $sResult = $sAttName.' - '; + switch ($this->Get('type')) + { + case 'added': + $sResult .= Dict::Format('Change:LinkSet:Added', $sItemDesc); + break; + + case 'removed': + $sResult .= Dict::Format('Change:LinkSet:Removed', $sItemDesc); + break; + } + } + return $sResult; + } +} + +/** + * Record attribute changes from within a link set + * A single record redirects to the modifications made within the same change + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeLinksTune extends CMDBChangeOpSetAttributeLinks +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_links_tune", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeInteger("link_id", array("allowed_values"=>null, "sql"=>"link_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renamed attributes... + + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + + $sLinkClass = $oAttDef->GetLinkedClass(); + $aLinkClasses = MetaModel::EnumChildClasses($sLinkClass, ENUM_CHILD_CLASSES_ALL); + + // Search for changes on the corresponding link + // + $oSearch = new DBObjectSearch('CMDBChangeOpSetAttribute'); + $oSearch->AddCondition('change', $this->Get('change'), '='); + $oSearch->AddCondition('objkey', $this->Get('link_id'), '='); + if (count($aLinkClasses) == 1) + { + // Faster than the whole building of the expression below for just one value ?? + $oSearch->AddCondition('objclass', $sLinkClass, '='); + } + else + { + $oField = new FieldExpression('objclass', $oSearch->GetClassAlias()); + $sListExpr = '('.implode(', ', CMDBSource::Quote($aLinkClasses)).')'; + $sOQLCondition = $oField->Render()." IN $sListExpr"; + $oNewCondition = Expression::FromOQL($sOQLCondition); + $oSearch->AddConditionExpression($oNewCondition); + } + $oSet = new DBObjectSet($oSearch); + $aChanges = array(); + while ($oChangeOp = $oSet->Fetch()) + { + $aChanges[] = $oChangeOp->GetDescription(); + } + if (count($aChanges) == 0) + { + return ''; + } + + $sItemDesc = MetaModel::GetHyperLink($this->Get('item_class'), $this->Get('item_id')); + + $sResult = $sAttName.' - '; + $sResult .= Dict::Format('Change:LinkSet:Modified', $sItemDesc); + $sResult .= ' : '.implode(', ', $aChanges); + } + return $sResult; + } +} + +/** + * Record the modification of custom fields + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeCustomFields extends CMDBChangeOpSetAttribute +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_custfields", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeLongText("prevdata", array("allowed_values"=>null, "sql"=>"prevdata", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + $aValues = json_decode($this->Get('prevdata'), true); + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + + try + { + $oHandler = $oAttDef->GetHandler($aValues); + $sValueDesc = $oHandler->GetAsHTML($aValues); + } + catch (Exception $e) + { + $sValueDesc = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); + } + $sTextView = '
    '.$sValueDesc.'
    '; + + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + } + return $sResult; + } +} diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index 7089057bf..5e6144163 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -1,694 +1,694 @@ - - - -/** - * Class cmdbObject - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * cmdbObjectClass - * the file to include, then the core is yours - * - * @package iTopORM - */ - -require_once('coreexception.class.inc.php'); - -require_once('config.class.inc.php'); -require_once('log.class.inc.php'); -require_once('kpi.class.inc.php'); - -require_once('dict.class.inc.php'); - -require_once('attributedef.class.inc.php'); -require_once('filterdef.class.inc.php'); -require_once('stimulus.class.inc.php'); -require_once('valuesetdef.class.inc.php'); -require_once('MyHelpers.class.inc.php'); - -require_once('oql/expression.class.inc.php'); -require_once('oql/oqlquery.class.inc.php'); -require_once('oql/oqlexception.class.inc.php'); -require_once('oql/oql-parser.php'); -require_once('oql/oql-lexer.php'); -require_once('oql/oqlinterpreter.class.inc.php'); - -require_once('cmdbsource.class.inc.php'); -require_once('sqlquery.class.inc.php'); -require_once('sqlobjectquery.class.inc.php'); -require_once('sqlunionquery.class.inc.php'); - -require_once('dbobject.class.php'); -require_once('dbsearch.class.php'); -require_once('dbobjectset.class.php'); - -require_once('backgroundprocess.inc.php'); -require_once('asynctask.class.inc.php'); -require_once('dbproperty.class.inc.php'); - -// db change tracking data model -require_once('cmdbchange.class.inc.php'); -require_once('cmdbchangeop.class.inc.php'); - -// customization data model -// Romain: temporary moved into application.inc.php (see explanations there) -//require_once('trigger.class.inc.php'); -//require_once('action.class.inc.php'); - -// application log -// Romain: temporary moved into application.inc.php (see explanations there) -//require_once('event.class.inc.php'); - -require_once('templatestring.class.inc.php'); -require_once('csvparser.class.inc.php'); -require_once('bulkchange.class.inc.php'); - -/** - * A persistent object, which changes are accurately recorded - * - * @package iTopORM - */ -abstract class CMDBObject extends DBObject -{ - protected $m_datCreated; - protected $m_datUpdated; - // Note: this value is static, but that could be changed because it is sometimes a real issue (see update of interfaces / connected_to - protected static $m_oCurrChange = null; - protected static $m_sInfo = null; // null => the information is built in a standard way - protected static $m_sOrigin = null; // null => the origin is 'interactive' - - /** - * Specify another change (this is mainly for backward compatibility) - */ - public static function SetCurrentChange(CMDBChange $oChange) - { - self::$m_oCurrChange = $oChange; - } - - // - // Todo: simplify the APIs and do not pass the current change as an argument anymore - // SetTrackInfo to be invoked in very few cases (UI.php, CSV import, Data synchro) - // SetCurrentChange is an alternative to SetTrackInfo (csv ?) - // GetCurrentChange to be called ONCE (!) by CMDBChangeOp::OnInsert ($this->Set('change', ..GetCurrentChange()) - // GetCurrentChange to create a default change if not already done in the current context - // - /** - * Get a change record (create it if not existing) - */ - public static function GetCurrentChange($bAutoCreate = true) - { - if ($bAutoCreate && is_null(self::$m_oCurrChange)) - { - self::CreateChange(); - } - return self::$m_oCurrChange; - } - - /** - * Override the additional information (defaulting to user name) - * A call to this verb should replace every occurence of - * $oMyChange = MetaModel::NewObject("CMDBChange"); - * $oMyChange->Set("date", time()); - * $oMyChange->Set("userinfo", 'this is done by ... for ...'); - * $iChangeId = $oMyChange->DBInsert(); - */ - public static function SetTrackInfo($sInfo) - { - self::$m_sInfo = $sInfo; - } - - /** - * Provides information about the origin of the change - * @param $sOrigin String: one of: interactive, csv-interactive, csv-import.php, webservice-soap, webservice-rest, syncho-data-source, email-processing, custom-extension - */ - public static function SetTrackOrigin($sOrigin) - { - self::$m_sOrigin = $sOrigin; - } - - /** - * Get the additional information (defaulting to user name) - */ - protected static function GetTrackInfo() - { - if (is_null(self::$m_sInfo)) - { - return CMDBChange::GetCurrentUserName(); - } - else - { - return self::$m_sInfo; - } - } - - /** - * Get the 'origin' information (defaulting to 'interactive') - */ - protected static function GetTrackOrigin() - { - if (is_null(self::$m_sOrigin)) - { - return 'interactive'; - } - else - { - return self::$m_sOrigin; - } - } - - /** - * Create a standard change record (done here 99% of the time, and nearly once per page) - */ - protected static function CreateChange() - { - self::$m_oCurrChange = MetaModel::NewObject("CMDBChange"); - self::$m_oCurrChange->Set("date", time()); - self::$m_oCurrChange->Set("userinfo", self::GetTrackInfo()); - self::$m_oCurrChange->Set("origin", self::GetTrackOrigin()); - self::$m_oCurrChange->DBInsert(); - } - - protected function RecordObjCreation() - { - // Delete any existing change tracking about the current object (IDs can be reused due to InnoDb bug; see TRAC #886) - // - // 1 - remove the deletion record(s) - // Note that objclass contain the ROOT class - $oFilter = new DBObjectSearch('CMDBChangeOpDelete'); - $oFilter->AddCondition('objclass', MetaModel::GetRootClass(get_class($this)), '='); - $oFilter->AddCondition('objkey', $this->GetKey(), '='); - MetaModel::PurgeData($oFilter); - // 2 - any other change tracking information left prior to 2.0.3 (when the purge of the history has been implemented in RecordObjDeletion - // In that case, objclass is the final class of the object - $oFilter = new DBObjectSearch('CMDBChangeOp'); - $oFilter->AddCondition('objclass', get_class($this), '='); - $oFilter->AddCondition('objkey', $this->GetKey(), '='); - MetaModel::PurgeData($oFilter); - - parent::RecordObjCreation(); - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - - protected function RecordObjDeletion($objkey) - { - $sRootClass = MetaModel::GetRootClass(get_class($this)); - - // Delete any existing change tracking about the current object - $oFilter = new DBObjectSearch('CMDBChangeOp'); - $oFilter->AddCondition('objclass', get_class($this), '='); - $oFilter->AddCondition('objkey', $objkey, '='); - MetaModel::PurgeData($oFilter); - - parent::RecordObjDeletion($objkey); - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpDelete"); - $oMyChangeOp->Set("objclass", MetaModel::GetRootClass(get_class($this))); - $oMyChangeOp->Set("objkey", $objkey); - $oMyChangeOp->Set("fclass", get_class($this)); - $oMyChangeOp->Set("fname", substr($this->GetRawName(), 0, 255)); // Protect against very long friendly names - $iId = $oMyChangeOp->DBInsertNoReload(); - } - - /** - * @param $sAttCode - * @param $original Original value - * @param $value Current value - */ - protected function RecordAttChange($sAttCode, $original, $value) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if ($oAttDef->IsExternalField()) return; - if ($oAttDef->IsLinkSet()) return; - if ($oAttDef->GetTrackingLevel() == ATTRIBUTE_TRACKING_NONE) return; - - if ($oAttDef instanceOf AttributeOneWayPassword) - { - // One Way encrypted passwords' history is stored -one way- encrypted - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - - if (is_null($original)) - { - $original = ''; - } - $oMyChangeOp->Set("prev_pwd", $original); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeEncryptedString) - { - // Encrypted string history is stored encrypted - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - - if (is_null($original)) - { - $original = ''; - } - $oMyChangeOp->Set("prevstring", $original); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeBlob) - { - // Data blobs - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - - if (is_null($original)) - { - $original = new ormDocument(); - } - $oMyChangeOp->Set("prevdata", $original); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeStopWatch) - { - // Stop watches - record changes for sub items only (they are visible, the rest is not visible) - // - foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef) - { - $item_value = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $value, $this); - $item_original = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $original, $this); - - if ($item_value != $item_original) - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sSubItemAttCode); - - $oMyChangeOp->Set("oldvalue", $item_original); - $oMyChangeOp->Set("newvalue", $item_value); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - } - } - elseif ($oAttDef instanceOf AttributeCaseLog) - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - - $oMyChangeOp->Set("lastentry", $value->GetLatestEntryIndex()); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeLongText) - { - // Data blobs - if ($oAttDef->GetFormat() == 'html') - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML"); - } - else - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeLongText"); - } - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - - if (!is_null($original) && ($original instanceof ormCaseLog)) - { - $original = $original->GetText(); - } - $oMyChangeOp->Set("prevdata", $original); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeText) - { - // Data blobs - if ($oAttDef->GetFormat() == 'html') - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML"); - } - else - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText"); - } - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - - if (!is_null($original) && ($original instanceof ormCaseLog)) - { - $original = $original->GetText(); - } - $oMyChangeOp->Set("prevdata", $original); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeBoolean) - { - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - $oMyChangeOp->Set("oldvalue", $original ? 1 : 0); - $oMyChangeOp->Set("newvalue", $value ? 1 : 0); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeHierarchicalKey) - { - // Hierarchical keys - // - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - $oMyChangeOp->Set("oldvalue", $original); - $oMyChangeOp->Set("newvalue", $value[$sAttCode]); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeCustomFields) - { - // Custom fields - // - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCustomFields"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - $oMyChangeOp->Set("prevdata", json_encode($original->GetValues())); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - elseif ($oAttDef instanceOf AttributeURL) - { - // URLs - // - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeURL"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - $oMyChangeOp->Set("oldvalue", $original); - $oMyChangeOp->Set("newvalue", $value); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - else - { - // Scalars - // - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); - $oMyChangeOp->Set("objclass", get_class($this)); - $oMyChangeOp->Set("objkey", $this->GetKey()); - $oMyChangeOp->Set("attcode", $sAttCode); - $oMyChangeOp->Set("oldvalue", $original); - $oMyChangeOp->Set("newvalue", $value); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - } - - /** - * @param array $aValues - * @param array $aOrigValues - */ - protected function RecordAttChanges(array $aValues, array $aOrigValues) - { - parent::RecordAttChanges($aValues, $aOrigValues); - - // $aValues is an array of $sAttCode => $value - // - foreach ($aValues as $sAttCode=> $value) - { - if (array_key_exists($sAttCode, $aOrigValues)) - { - $original = $aOrigValues[$sAttCode]; - } - else - { - $original = null; - } - $this->RecordAttChange($sAttCode, $original, $value); - } - } - - /** - * Helper to ultimately check user rights before writing (Insert, Update or Delete) - * The check should never fail, because the UI should prevent from such a usage - * Anyhow, if the user has found a workaround... the security gets enforced here - */ - protected function CheckUserRights($bSkipStrongSecurity, $iActionCode) - { - if (is_null($bSkipStrongSecurity)) - { - // This is temporary - // We have implemented this safety net right before releasing iTop 1.0 - // and we decided that it was too risky to activate it - // Anyhow, users willing to have a very strong security could set - // skip_strong_security = 0, in the config file - $bSkipStrongSecurity = MetaModel::GetConfig()->Get('skip_strong_security'); - } - if (!$bSkipStrongSecurity) - { - $sClass = get_class($this); - $oSet = DBObjectSet::FromObject($this); - if (!UserRights::IsActionAllowed($sClass, $iActionCode, $oSet)) - { - // Intrusion detected - throw new SecurityException('You are not allowed to modify objects of class: '.$sClass); - } - } - } - - - public function DBInsertTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) - { - self::SetCurrentChange($oChange); - $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); - $ret = $this->DBInsertTracked_Internal(); - return $ret; - } - - public function DBInsertTrackedNoReload(CMDBChange $oChange, $bSkipStrongSecurity = null) - { - self::SetCurrentChange($oChange); - $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); - $ret = $this->DBInsertTracked_Internal(true); - return $ret; - } - - /** - * To Be Obsoleted: DO NOT rely on an overload of this method since - * DBInsertTracked (resp. DBInsertTrackedNoReload) may call directly - * DBInsert (resp. DBInsertNoReload) in future versions of iTop. - * @param bool $bDoNotReload - * @return integer Identifier of the created object - */ - protected function DBInsertTracked_Internal($bDoNotReload = false) - { - if ($bDoNotReload) - { - $ret = $this->DBInsertNoReload(); - } - else - { - $ret = $this->DBInsert(); - } - return $ret; - } - - public function DBClone($newKey = null) - { - return $this->DBCloneTracked_Internal(); - } - - public function DBCloneTracked(CMDBChange $oChange, $newKey = null) - { - self::SetCurrentChange($oChange); - $this->DBCloneTracked_Internal($newKey); - } - - protected function DBCloneTracked_Internal($newKey = null) - { - $newKey = parent::DBClone($newKey); - $oClone = MetaModel::GetObject(get_class($this), $newKey); - - return $newKey; - } - - public function DBUpdate() - { - // Copy the changes list before the update (the list should be reset afterwards) - $aChanges = $this->ListChanges(); - if (count($aChanges) == 0) - { - return; - } - - $ret = parent::DBUpdate(); - return $ret; - } - - public function DBUpdateTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) - { - self::SetCurrentChange($oChange); - $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); - $this->DBUpdate(); - } - - public function DBDelete(&$oDeletionPlan = null) - { - return $this->DBDeleteTracked_Internal($oDeletionPlan); - } - - public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) - { - self::SetCurrentChange($oChange); - $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_DELETE); - $this->DBDeleteTracked_Internal($oDeletionPlan); - } - - protected function DBDeleteTracked_Internal(&$oDeletionPlan = null) - { - $prevkey = $this->GetKey(); - $ret = parent::DBDelete($oDeletionPlan); - return $ret; - } - - public static function BulkUpdate(DBSearch $oFilter, array $aValues) - { - return static::BulkUpdateTracked_Internal($oFilter, $aValues); - } - - public static function BulkUpdateTracked(CMDBChange $oChange, DBSearch $oFilter, array $aValues) - { - self::SetCurrentChange($oChange); - static::BulkUpdateTracked_Internal($oFilter, $aValues); - } - - protected static function BulkUpdateTracked_Internal(DBSearch $oFilter, array $aValues) - { - // $aValues is an array of $sAttCode => $value - - // Get the list of objects to update (and load it before doing the change) - $oObjSet = new CMDBObjectSet($oFilter); - $oObjSet->Load(); - - // Keep track of the previous values (will be overwritten when the objects are synchronized with the DB) - $aOriginalValues = array(); - $oObjSet->Rewind(); - while ($oItem = $oObjSet->Fetch()) - { - $aOriginalValues[$oItem->GetKey()] = $oItem->m_aOrigValues; - } - - // Update in one single efficient query - $ret = parent::BulkUpdate($oFilter, $aValues); - - // Record... in many queries !!! - $oObjSet->Rewind(); - while ($oItem = $oObjSet->Fetch()) - { - $aChangedValues = $oItem->ListChangedValues($aValues); - $oItem->RecordAttChanges($aChangedValues, $aOriginalValues[$oItem->GetKey()]); - } - return $ret; - } - - public function DBArchive() - { - // Note: do the job anyway, so as to repair any DB discrepancy - $bOriginal = $this->Get('archive_flag'); - parent::DBArchive(); - - if (!$bOriginal) - { - utils::PushArchiveMode(false); - $this->RecordAttChange('archive_flag', false, true); - utils::PopArchiveMode(); - } - } - - public function DBUnarchive() - { - // Note: do the job anyway, so as to repair any DB discrepancy - $bOriginal = $this->Get('archive_flag'); - parent::DBUnarchive(); - - if ($bOriginal) - { - utils::PushArchiveMode(false); - $this->RecordAttChange('archive_flag', true, false); - utils::PopArchiveMode(); - } - } -} - - - -/** - * TODO: investigate how to get rid of this class that was made to workaround some language limitation... or a poor design! - * - * @package iTopORM - */ -class CMDBObjectSet extends DBObjectSet -{ - // this is the public interface (?) - - // I have to define those constructors here... :-( - // just to get the right object class in return. - // I have to think again to those things: maybe it will work fine if a have a constructor define here (?) - - static public function FromScratch($sClass) - { - $oFilter = new DBObjectSearch($sClass); - $oFilter->AddConditionExpression(new FalseExpression()); - $oRetSet = new self($oFilter); - // NOTE: THIS DOES NOT WORK IF m_bLoaded is private in the base class (and you will not get any error message) - $oRetSet->m_bLoaded = true; // no DB load - return $oRetSet; - } - - // create an object set ex nihilo - // input = array of objects - static public function FromArray($sClass, $aObjects) - { - $oRetSet = self::FromScratch($sClass); - $oRetSet->AddObjectArray($aObjects, $sClass); - return $oRetSet; - } - - static public function FromArrayAssoc($aClasses, $aObjects) - { - // In a perfect world, we should create a complete tree of DBObjectSearch, - // but as we lack most of the information related to the objects, - // let's create one search definition - $sClass = reset($aClasses); - $sAlias = key($aClasses); - $oFilter = new DBObjectSearch($sClass, $sAlias); - - $oRetSet = new CMDBObjectSet($oFilter); - $oRetSet->m_bLoaded = true; // no DB load - - foreach($aObjects as $rowIndex => $aObjectsByClassAlias) - { - $oRetSet->AddObjectExtended($aObjectsByClassAlias); - } - return $oRetSet; - } -} + + + +/** + * Class cmdbObject + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * cmdbObjectClass + * the file to include, then the core is yours + * + * @package iTopORM + */ + +require_once('coreexception.class.inc.php'); + +require_once('config.class.inc.php'); +require_once('log.class.inc.php'); +require_once('kpi.class.inc.php'); + +require_once('dict.class.inc.php'); + +require_once('attributedef.class.inc.php'); +require_once('filterdef.class.inc.php'); +require_once('stimulus.class.inc.php'); +require_once('valuesetdef.class.inc.php'); +require_once('MyHelpers.class.inc.php'); + +require_once('oql/expression.class.inc.php'); +require_once('oql/oqlquery.class.inc.php'); +require_once('oql/oqlexception.class.inc.php'); +require_once('oql/oql-parser.php'); +require_once('oql/oql-lexer.php'); +require_once('oql/oqlinterpreter.class.inc.php'); + +require_once('cmdbsource.class.inc.php'); +require_once('sqlquery.class.inc.php'); +require_once('sqlobjectquery.class.inc.php'); +require_once('sqlunionquery.class.inc.php'); + +require_once('dbobject.class.php'); +require_once('dbsearch.class.php'); +require_once('dbobjectset.class.php'); + +require_once('backgroundprocess.inc.php'); +require_once('asynctask.class.inc.php'); +require_once('dbproperty.class.inc.php'); + +// db change tracking data model +require_once('cmdbchange.class.inc.php'); +require_once('cmdbchangeop.class.inc.php'); + +// customization data model +// Romain: temporary moved into application.inc.php (see explanations there) +//require_once('trigger.class.inc.php'); +//require_once('action.class.inc.php'); + +// application log +// Romain: temporary moved into application.inc.php (see explanations there) +//require_once('event.class.inc.php'); + +require_once('templatestring.class.inc.php'); +require_once('csvparser.class.inc.php'); +require_once('bulkchange.class.inc.php'); + +/** + * A persistent object, which changes are accurately recorded + * + * @package iTopORM + */ +abstract class CMDBObject extends DBObject +{ + protected $m_datCreated; + protected $m_datUpdated; + // Note: this value is static, but that could be changed because it is sometimes a real issue (see update of interfaces / connected_to + protected static $m_oCurrChange = null; + protected static $m_sInfo = null; // null => the information is built in a standard way + protected static $m_sOrigin = null; // null => the origin is 'interactive' + + /** + * Specify another change (this is mainly for backward compatibility) + */ + public static function SetCurrentChange(CMDBChange $oChange) + { + self::$m_oCurrChange = $oChange; + } + + // + // Todo: simplify the APIs and do not pass the current change as an argument anymore + // SetTrackInfo to be invoked in very few cases (UI.php, CSV import, Data synchro) + // SetCurrentChange is an alternative to SetTrackInfo (csv ?) + // GetCurrentChange to be called ONCE (!) by CMDBChangeOp::OnInsert ($this->Set('change', ..GetCurrentChange()) + // GetCurrentChange to create a default change if not already done in the current context + // + /** + * Get a change record (create it if not existing) + */ + public static function GetCurrentChange($bAutoCreate = true) + { + if ($bAutoCreate && is_null(self::$m_oCurrChange)) + { + self::CreateChange(); + } + return self::$m_oCurrChange; + } + + /** + * Override the additional information (defaulting to user name) + * A call to this verb should replace every occurence of + * $oMyChange = MetaModel::NewObject("CMDBChange"); + * $oMyChange->Set("date", time()); + * $oMyChange->Set("userinfo", 'this is done by ... for ...'); + * $iChangeId = $oMyChange->DBInsert(); + */ + public static function SetTrackInfo($sInfo) + { + self::$m_sInfo = $sInfo; + } + + /** + * Provides information about the origin of the change + * @param $sOrigin String: one of: interactive, csv-interactive, csv-import.php, webservice-soap, webservice-rest, syncho-data-source, email-processing, custom-extension + */ + public static function SetTrackOrigin($sOrigin) + { + self::$m_sOrigin = $sOrigin; + } + + /** + * Get the additional information (defaulting to user name) + */ + protected static function GetTrackInfo() + { + if (is_null(self::$m_sInfo)) + { + return CMDBChange::GetCurrentUserName(); + } + else + { + return self::$m_sInfo; + } + } + + /** + * Get the 'origin' information (defaulting to 'interactive') + */ + protected static function GetTrackOrigin() + { + if (is_null(self::$m_sOrigin)) + { + return 'interactive'; + } + else + { + return self::$m_sOrigin; + } + } + + /** + * Create a standard change record (done here 99% of the time, and nearly once per page) + */ + protected static function CreateChange() + { + self::$m_oCurrChange = MetaModel::NewObject("CMDBChange"); + self::$m_oCurrChange->Set("date", time()); + self::$m_oCurrChange->Set("userinfo", self::GetTrackInfo()); + self::$m_oCurrChange->Set("origin", self::GetTrackOrigin()); + self::$m_oCurrChange->DBInsert(); + } + + protected function RecordObjCreation() + { + // Delete any existing change tracking about the current object (IDs can be reused due to InnoDb bug; see TRAC #886) + // + // 1 - remove the deletion record(s) + // Note that objclass contain the ROOT class + $oFilter = new DBObjectSearch('CMDBChangeOpDelete'); + $oFilter->AddCondition('objclass', MetaModel::GetRootClass(get_class($this)), '='); + $oFilter->AddCondition('objkey', $this->GetKey(), '='); + MetaModel::PurgeData($oFilter); + // 2 - any other change tracking information left prior to 2.0.3 (when the purge of the history has been implemented in RecordObjDeletion + // In that case, objclass is the final class of the object + $oFilter = new DBObjectSearch('CMDBChangeOp'); + $oFilter->AddCondition('objclass', get_class($this), '='); + $oFilter->AddCondition('objkey', $this->GetKey(), '='); + MetaModel::PurgeData($oFilter); + + parent::RecordObjCreation(); + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpCreate"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + + protected function RecordObjDeletion($objkey) + { + $sRootClass = MetaModel::GetRootClass(get_class($this)); + + // Delete any existing change tracking about the current object + $oFilter = new DBObjectSearch('CMDBChangeOp'); + $oFilter->AddCondition('objclass', get_class($this), '='); + $oFilter->AddCondition('objkey', $objkey, '='); + MetaModel::PurgeData($oFilter); + + parent::RecordObjDeletion($objkey); + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpDelete"); + $oMyChangeOp->Set("objclass", MetaModel::GetRootClass(get_class($this))); + $oMyChangeOp->Set("objkey", $objkey); + $oMyChangeOp->Set("fclass", get_class($this)); + $oMyChangeOp->Set("fname", substr($this->GetRawName(), 0, 255)); // Protect against very long friendly names + $iId = $oMyChangeOp->DBInsertNoReload(); + } + + /** + * @param $sAttCode + * @param $original Original value + * @param $value Current value + */ + protected function RecordAttChange($sAttCode, $original, $value) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef->IsExternalField()) return; + if ($oAttDef->IsLinkSet()) return; + if ($oAttDef->GetTrackingLevel() == ATTRIBUTE_TRACKING_NONE) return; + + if ($oAttDef instanceOf AttributeOneWayPassword) + { + // One Way encrypted passwords' history is stored -one way- encrypted + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeOneWayPassword"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (is_null($original)) + { + $original = ''; + } + $oMyChangeOp->Set("prev_pwd", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeEncryptedString) + { + // Encrypted string history is stored encrypted + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeEncrypted"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (is_null($original)) + { + $original = ''; + } + $oMyChangeOp->Set("prevstring", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeBlob) + { + // Data blobs + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeBlob"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (is_null($original)) + { + $original = new ormDocument(); + } + $oMyChangeOp->Set("prevdata", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeStopWatch) + { + // Stop watches - record changes for sub items only (they are visible, the rest is not visible) + // + foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef) + { + $item_value = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $value, $this); + $item_original = $oAttDef->GetSubItemValue($oSubItemAttDef->Get('item_code'), $original, $this); + + if ($item_value != $item_original) + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sSubItemAttCode); + + $oMyChangeOp->Set("oldvalue", $item_original); + $oMyChangeOp->Set("newvalue", $item_value); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + } + elseif ($oAttDef instanceOf AttributeCaseLog) + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCaseLog"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + $oMyChangeOp->Set("lastentry", $value->GetLatestEntryIndex()); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeLongText) + { + // Data blobs + if ($oAttDef->GetFormat() == 'html') + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML"); + } + else + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeLongText"); + } + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (!is_null($original) && ($original instanceof ormCaseLog)) + { + $original = $original->GetText(); + } + $oMyChangeOp->Set("prevdata", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeText) + { + // Data blobs + if ($oAttDef->GetFormat() == 'html') + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML"); + } + else + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText"); + } + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + + if (!is_null($original) && ($original instanceof ormCaseLog)) + { + $original = $original->GetText(); + } + $oMyChangeOp->Set("prevdata", $original); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeBoolean) + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + $oMyChangeOp->Set("oldvalue", $original ? 1 : 0); + $oMyChangeOp->Set("newvalue", $value ? 1 : 0); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeHierarchicalKey) + { + // Hierarchical keys + // + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + $oMyChangeOp->Set("oldvalue", $original); + $oMyChangeOp->Set("newvalue", $value[$sAttCode]); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeCustomFields) + { + // Custom fields + // + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeCustomFields"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + $oMyChangeOp->Set("prevdata", json_encode($original->GetValues())); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + elseif ($oAttDef instanceOf AttributeURL) + { + // URLs + // + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeURL"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + $oMyChangeOp->Set("oldvalue", $original); + $oMyChangeOp->Set("newvalue", $value); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + else + { + // Scalars + // + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar"); + $oMyChangeOp->Set("objclass", get_class($this)); + $oMyChangeOp->Set("objkey", $this->GetKey()); + $oMyChangeOp->Set("attcode", $sAttCode); + $oMyChangeOp->Set("oldvalue", $original); + $oMyChangeOp->Set("newvalue", $value); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + + /** + * @param array $aValues + * @param array $aOrigValues + */ + protected function RecordAttChanges(array $aValues, array $aOrigValues) + { + parent::RecordAttChanges($aValues, $aOrigValues); + + // $aValues is an array of $sAttCode => $value + // + foreach ($aValues as $sAttCode=> $value) + { + if (array_key_exists($sAttCode, $aOrigValues)) + { + $original = $aOrigValues[$sAttCode]; + } + else + { + $original = null; + } + $this->RecordAttChange($sAttCode, $original, $value); + } + } + + /** + * Helper to ultimately check user rights before writing (Insert, Update or Delete) + * The check should never fail, because the UI should prevent from such a usage + * Anyhow, if the user has found a workaround... the security gets enforced here + */ + protected function CheckUserRights($bSkipStrongSecurity, $iActionCode) + { + if (is_null($bSkipStrongSecurity)) + { + // This is temporary + // We have implemented this safety net right before releasing iTop 1.0 + // and we decided that it was too risky to activate it + // Anyhow, users willing to have a very strong security could set + // skip_strong_security = 0, in the config file + $bSkipStrongSecurity = MetaModel::GetConfig()->Get('skip_strong_security'); + } + if (!$bSkipStrongSecurity) + { + $sClass = get_class($this); + $oSet = DBObjectSet::FromObject($this); + if (!UserRights::IsActionAllowed($sClass, $iActionCode, $oSet)) + { + // Intrusion detected + throw new SecurityException('You are not allowed to modify objects of class: '.$sClass); + } + } + } + + + public function DBInsertTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + self::SetCurrentChange($oChange); + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + $ret = $this->DBInsertTracked_Internal(); + return $ret; + } + + public function DBInsertTrackedNoReload(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + self::SetCurrentChange($oChange); + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + $ret = $this->DBInsertTracked_Internal(true); + return $ret; + } + + /** + * To Be Obsoleted: DO NOT rely on an overload of this method since + * DBInsertTracked (resp. DBInsertTrackedNoReload) may call directly + * DBInsert (resp. DBInsertNoReload) in future versions of iTop. + * @param bool $bDoNotReload + * @return integer Identifier of the created object + */ + protected function DBInsertTracked_Internal($bDoNotReload = false) + { + if ($bDoNotReload) + { + $ret = $this->DBInsertNoReload(); + } + else + { + $ret = $this->DBInsert(); + } + return $ret; + } + + public function DBClone($newKey = null) + { + return $this->DBCloneTracked_Internal(); + } + + public function DBCloneTracked(CMDBChange $oChange, $newKey = null) + { + self::SetCurrentChange($oChange); + $this->DBCloneTracked_Internal($newKey); + } + + protected function DBCloneTracked_Internal($newKey = null) + { + $newKey = parent::DBClone($newKey); + $oClone = MetaModel::GetObject(get_class($this), $newKey); + + return $newKey; + } + + public function DBUpdate() + { + // Copy the changes list before the update (the list should be reset afterwards) + $aChanges = $this->ListChanges(); + if (count($aChanges) == 0) + { + return; + } + + $ret = parent::DBUpdate(); + return $ret; + } + + public function DBUpdateTracked(CMDBChange $oChange, $bSkipStrongSecurity = null) + { + self::SetCurrentChange($oChange); + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_MODIFY); + $this->DBUpdate(); + } + + public function DBDelete(&$oDeletionPlan = null) + { + return $this->DBDeleteTracked_Internal($oDeletionPlan); + } + + public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) + { + self::SetCurrentChange($oChange); + $this->CheckUserRights($bSkipStrongSecurity, UR_ACTION_DELETE); + $this->DBDeleteTracked_Internal($oDeletionPlan); + } + + protected function DBDeleteTracked_Internal(&$oDeletionPlan = null) + { + $prevkey = $this->GetKey(); + $ret = parent::DBDelete($oDeletionPlan); + return $ret; + } + + public static function BulkUpdate(DBSearch $oFilter, array $aValues) + { + return static::BulkUpdateTracked_Internal($oFilter, $aValues); + } + + public static function BulkUpdateTracked(CMDBChange $oChange, DBSearch $oFilter, array $aValues) + { + self::SetCurrentChange($oChange); + static::BulkUpdateTracked_Internal($oFilter, $aValues); + } + + protected static function BulkUpdateTracked_Internal(DBSearch $oFilter, array $aValues) + { + // $aValues is an array of $sAttCode => $value + + // Get the list of objects to update (and load it before doing the change) + $oObjSet = new CMDBObjectSet($oFilter); + $oObjSet->Load(); + + // Keep track of the previous values (will be overwritten when the objects are synchronized with the DB) + $aOriginalValues = array(); + $oObjSet->Rewind(); + while ($oItem = $oObjSet->Fetch()) + { + $aOriginalValues[$oItem->GetKey()] = $oItem->m_aOrigValues; + } + + // Update in one single efficient query + $ret = parent::BulkUpdate($oFilter, $aValues); + + // Record... in many queries !!! + $oObjSet->Rewind(); + while ($oItem = $oObjSet->Fetch()) + { + $aChangedValues = $oItem->ListChangedValues($aValues); + $oItem->RecordAttChanges($aChangedValues, $aOriginalValues[$oItem->GetKey()]); + } + return $ret; + } + + public function DBArchive() + { + // Note: do the job anyway, so as to repair any DB discrepancy + $bOriginal = $this->Get('archive_flag'); + parent::DBArchive(); + + if (!$bOriginal) + { + utils::PushArchiveMode(false); + $this->RecordAttChange('archive_flag', false, true); + utils::PopArchiveMode(); + } + } + + public function DBUnarchive() + { + // Note: do the job anyway, so as to repair any DB discrepancy + $bOriginal = $this->Get('archive_flag'); + parent::DBUnarchive(); + + if ($bOriginal) + { + utils::PushArchiveMode(false); + $this->RecordAttChange('archive_flag', true, false); + utils::PopArchiveMode(); + } + } +} + + + +/** + * TODO: investigate how to get rid of this class that was made to workaround some language limitation... or a poor design! + * + * @package iTopORM + */ +class CMDBObjectSet extends DBObjectSet +{ + // this is the public interface (?) + + // I have to define those constructors here... :-( + // just to get the right object class in return. + // I have to think again to those things: maybe it will work fine if a have a constructor define here (?) + + static public function FromScratch($sClass) + { + $oFilter = new DBObjectSearch($sClass); + $oFilter->AddConditionExpression(new FalseExpression()); + $oRetSet = new self($oFilter); + // NOTE: THIS DOES NOT WORK IF m_bLoaded is private in the base class (and you will not get any error message) + $oRetSet->m_bLoaded = true; // no DB load + return $oRetSet; + } + + // create an object set ex nihilo + // input = array of objects + static public function FromArray($sClass, $aObjects) + { + $oRetSet = self::FromScratch($sClass); + $oRetSet->AddObjectArray($aObjects, $sClass); + return $oRetSet; + } + + static public function FromArrayAssoc($aClasses, $aObjects) + { + // In a perfect world, we should create a complete tree of DBObjectSearch, + // but as we lack most of the information related to the objects, + // let's create one search definition + $sClass = reset($aClasses); + $sAlias = key($aClasses); + $oFilter = new DBObjectSearch($sClass, $sAlias); + + $oRetSet = new CMDBObjectSet($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + + foreach($aObjects as $rowIndex => $aObjectsByClassAlias) + { + $oRetSet->AddObjectExtended($aObjectsByClassAlias); + } + return $oRetSet; + } +} diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index f9a23bc33..e08f70660 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -1,1214 +1,1214 @@ - - - -/** - * DB Server abstraction - * - * @copyright Copyright (C) 2010-2018 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once('MyHelpers.class.inc.php'); -require_once(APPROOT.'core/kpi.class.inc.php'); - -class MySQLException extends CoreException -{ - /** - * MySQLException constructor. - * - * @param string $sIssue - * @param array $aContext - * @param \Exception $oException - * @param \mysqli $oMysqli to use when working with a custom mysqli instance - */ - public function __construct($sIssue, $aContext, $oException = null, $oMysqli = null) - { - if ($oException != null) - { - $aContext['mysql_errno'] = $oException->getCode(); - $this->code = $oException->getCode(); - $aContext['mysql_error'] = $oException->getMessage(); - } - else if ($oMysqli != null) - { - $aContext['mysql_errno'] = $oMysqli->errno; - $this->code = $oMysqli->errno; - $aContext['mysql_error'] = $oMysqli->error; - } - else - { - $aContext['mysql_errno'] = CMDBSource::GetErrNo(); - $this->code = CMDBSource::GetErrNo(); - $aContext['mysql_error'] = CMDBSource::GetError(); - } - parent::__construct($sIssue, $aContext); - } -} - -/** - * Class MySQLQueryHasNoResultException - * - * @since 2.5 - */ -class MySQLQueryHasNoResultException extends MySQLException -{ - -} - -/** - * Class MySQLHasGoneAwayException - * - * @since 2.5 - * @see itop bug 1195 - * @see https://dev.mysql.com/doc/refman/5.7/en/gone-away.html - */ -class MySQLHasGoneAwayException extends MySQLException -{ - /** - * can not be a constant before PHP 5.6 (http://php.net/manual/fr/language.oop5.constants.php) - * - * @return int[] - */ - public static function getErrorCodes() - { - return array( - 2006, - 2013 - ); - } - - public function __construct($sIssue, $aContext) - { - parent::__construct($sIssue, $aContext, null); - } -} - - -/** - * CMDBSource - * database access wrapper - * - * @package iTopORM - */ -class CMDBSource -{ - protected static $m_sDBHost; - protected static $m_sDBUser; - protected static $m_sDBPwd; - protected static $m_sDBName; - /** - * @var boolean - * @since 2.5 #1260 MySQL TLS first implementation - */ - protected static $m_bDBTlsEnabled; - /** - * @var string - * @since 2.5 #1260 MySQL TLS first implementation - */ - protected static $m_sDBTlsCA; - - /** @var mysqli $m_oMysqli */ - protected static $m_oMysqli; - - /** - * SQL charset & collation declaration for text columns - * - * Using a function instead of a constant or attribute to avoid crash in the setup for older PHP versions (cannot - * use expression as value) - * - * @see https://dev.mysql.com/doc/refman/5.7/en/charset-column.html - * @since 2.5 #1001 switch to utf8mb4 - */ - public static function GetSqlStringColumnDefinition() - { - return ' CHARACTER SET '.DEFAULT_CHARACTER_SET.' COLLATE '.DEFAULT_COLLATION; - } - - /** - * @param Config $oConfig - * - * @throws \MySQLException - * @uses \CMDBSource::Init() - * @uses \CMDBSource::SetCharacterSet() - */ - public static function InitFromConfig($oConfig) - { - $sServer = $oConfig->Get('db_host'); - $sUser = $oConfig->Get('db_user'); - $sPwd = $oConfig->Get('db_pwd'); - $sSource = $oConfig->Get('db_name'); - $bTlsEnabled = $oConfig->Get('db_tls.enabled'); - $sTlsCA = $oConfig->Get('db_tls.ca'); - - self::Init($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA); - - $sCharacterSet = DEFAULT_CHARACTER_SET; - $sCollation = DEFAULT_COLLATION; - self::SetCharacterSet($sCharacterSet, $sCollation); - } - - /** - * @param string $sServer - * @param string $sUser - * @param string $sPwd - * @param string $sSource database to use - * @param bool $bTlsEnabled - * @param string $sTlsCA - * - * @throws \MySQLException - */ - public static function Init( - $sServer, $sUser, $sPwd, $sSource = '', $bTlsEnabled = false, $sTlsCA = null - ) - { - self::$m_sDBHost = $sServer; - self::$m_sDBUser = $sUser; - self::$m_sDBPwd = $sPwd; - self::$m_sDBName = $sSource; - self::$m_bDBTlsEnabled = empty($bTlsEnabled) ? false : $bTlsEnabled; - self::$m_sDBTlsCA = empty($sTlsCA) ? null : $sTlsCA; - - self::$m_oMysqli = self::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, true); - } - - /** - * @param string $sDbHost - * @param string $sUser - * @param string $sPwd - * @param string $sSource database to use - * @param bool $bTlsEnabled - * @param string $sTlsCa - * @param bool $bCheckTlsAfterConnection If true then verify after connection if it is encrypted - * - * @return \mysqli - * @throws \MySQLException - */ - public static function GetMysqliInstance( - $sDbHost, $sUser, $sPwd, $sSource = '', $bTlsEnabled = false, $sTlsCa = null, $bCheckTlsAfterConnection = false - ) { - $oMysqli = null; - - $sServer = null; - $iPort = null; - self::InitServerAndPort($sDbHost, $sServer, $iPort); - - $iFlags = null; - - // *some* errors (like connection errors) will throw mysqli_sql_exception instead of generating warnings printed to the output - // but some other errors will still cause the query() method to return false !!! - mysqli_report(MYSQLI_REPORT_STRICT); - - try - { - $oMysqli = new mysqli(); - $oMysqli->init(); - - if ($bTlsEnabled) - { - $iFlags = (empty($sTlsCa)) - ? MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT - : MYSQLI_CLIENT_SSL; - $sTlsCert = null; // not implemented - $sTlsCaPath = null; // not implemented - $sTlsCipher = null; // not implemented - $oMysqli->ssl_set($bTlsEnabled, $sTlsCert, $sTlsCa, $sTlsCaPath, $sTlsCipher); - } - $oMysqli->real_connect($sServer, $sUser, $sPwd, '', $iPort, ini_get("mysqli.default_socket"), $iFlags); - } - catch(mysqli_sql_exception $e) - { - throw new MySQLException('Could not connect to the DB server', array('host' => $sServer, 'user' => $sUser), $e); - } - - if ($bTlsEnabled - && $bCheckTlsAfterConnection - && !self::IsOpenedDbConnectionUsingTls($oMysqli)) - { - throw new MySQLException("Connection to the database is not encrypted whereas it was opened using TLS parameters", - null, null, $oMysqli); - } - - if (!empty($sSource)) - { - try - { - mysqli_report(MYSQLI_REPORT_STRICT); // Errors, in the next query, will throw mysqli_sql_exception - $oMysqli->query("USE `$sSource`"); - } - catch(mysqli_sql_exception $e) - { - throw new MySQLException('Could not select DB', - array('host' => $sServer, 'user' => $sUser, 'db_name' => $sSource), $e); - } - } - - return $oMysqli; - } - - /** - * @param string $sDbHost initial value ("p:domain:port" syntax) - * @param string $sServer server variable to update - * @param int $iPort port variable to update - */ - public static function InitServerAndPort($sDbHost, &$sServer, &$iPort) - { - $aConnectInfo = explode(':', $sDbHost); - - $bUsePersistentConnection = false; - if (strcasecmp($aConnectInfo[0], 'p') == 0) - { - // we might have "p:" prefix to use persistent connections (see http://php.net/manual/en/mysqli.persistconns.php) - $bUsePersistentConnection = true; - $sServer = $aConnectInfo[0].':'.$aConnectInfo[1]; - } - else - { - $sServer = $aConnectInfo[0]; - } - - $iConnectInfoCount = count($aConnectInfo); - if ($bUsePersistentConnection && ($iConnectInfoCount == 3)) - { - $iPort = $aConnectInfo[2]; - } - else if (!$bUsePersistentConnection && ($iConnectInfoCount == 2)) - { - $iPort = $aConnectInfo[1]; - } - else - { - $iPort = 3306; - } - } - - /** - *

    A DB connection can be opened transparently (no errors thrown) without being encrypted, whereas the TLS - * parameters were used.
    - * This method can be called to ensure that the DB connection really uses TLS. - * - *

    We're using this object connection : {@link self::$m_oMysqli} - * - * @param \mysqli $oMysqli - * - * @return boolean true if the connection was really established using TLS - * @throws \MySQLException - * - * @uses IsMySqlVarNonEmpty - */ - private static function IsOpenedDbConnectionUsingTls($oMysqli) - { - if (self::$m_oMysqli == null) - { - self::$m_oMysqli = $oMysqli; - } - - $bNonEmptySslVersionVar = self::IsMySqlVarNonEmpty('ssl_version'); - $bNonEmptySslCipherVar = self::IsMySqlVarNonEmpty('ssl_cipher'); - - return ($bNonEmptySslVersionVar && $bNonEmptySslCipherVar); - } - - /** - * @param string $sVarName - * - * @return bool - * @throws \MySQLException - * - * @uses SHOW STATUS queries - */ - private static function IsMySqlVarNonEmpty($sVarName) - { - try - { - $sResult = self::QueryToScalar("SHOW SESSION STATUS LIKE '$sVarName'", 1); - } - catch (MySQLQueryHasNoResultException $e) - { - $sResult = null; - } - - return (!empty($sResult)); - } - - public static function SetCharacterSet($sCharset = DEFAULT_CHARACTER_SET, $sCollation = DEFAULT_COLLATION) - { - if (strlen($sCharset) > 0) - { - if (strlen($sCollation) > 0) - { - self::Query("SET NAMES '$sCharset' COLLATE '$sCollation'"); - } - else - { - self::Query("SET NAMES '$sCharset'"); - } - } - } - - public static function SetTimezone($sTimezone = null) - { - // Note: requires the installation of MySQL special tables, - // otherwise, only 'SYSTEM' or "+10:00' may be specified which is NOT sufficient because of day light saving times - if (!is_null($sTimezone)) - { - $sQuotedTimezone = self::Quote($sTimezone); - self::Query("SET time_zone = $sQuotedTimezone"); - } - } - - public static function ListDB() - { - $aDBs = self::QueryToCol('SHOW DATABASES', 'Database'); - // Show Database does return the DB names in lower case - return $aDBs; - } - - public static function IsDB($sSource) - { - try - { - $aDBs = self::ListDB(); - foreach($aDBs as $sDBName) - { - // perform a case insensitive test because on Windows the table names become lowercase :-( - if (strtolower($sDBName) == strtolower($sSource)) return true; - } - return false; - } - catch(Exception $e) - { - // In case we don't have rights to enumerate the databases - // Let's try to connect directly - return @((bool)self::$m_oMysqli->query("USE `$sSource`")); - } - - } - - public static function GetDBVersion() - { - $aVersions = self::QueryToCol('SELECT Version() as version', 'version'); - return $aVersions[0]; - } - - /** - * @param string $sSource - * - * @throws \MySQLException - */ - public static function SelectDB($sSource) - { - if (!((bool)self::$m_oMysqli->query("USE `$sSource`"))) - { - throw new MySQLException('Could not select DB', array('db_name'=>$sSource)); - } - self::$m_sDBName = $sSource; - } - - /** - * @param string $sSource - * - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public static function CreateDB($sSource) - { - self::Query("CREATE DATABASE `$sSource` CHARACTER SET ".DEFAULT_CHARACTER_SET." COLLATE ".DEFAULT_COLLATION); - self::SelectDB($sSource); - } - - public static function DropDB($sDBToDrop = '') - { - if (empty($sDBToDrop)) - { - $sDBToDrop = self::$m_sDBName; - } - self::Query("DROP DATABASE `$sDBToDrop`"); - if ($sDBToDrop == self::$m_sDBName) - { - self::$m_sDBName = ''; - } - } - - public static function CreateTable($sQuery) - { - $res = self::Query($sQuery); - self::_TablesInfoCacheReset(); // reset the table info cache! - return $res; - } - - public static function DropTable($sTable) - { - $res = self::Query("DROP TABLE `$sTable`"); - self::_TablesInfoCacheReset(); // reset the table info cache! - return $res; - } - - /** - * @return \mysqli - */ - public static function GetMysqli() - { - return self::$m_oMysqli; - } - - public static function GetErrNo() - { - if (self::$m_oMysqli->errno != 0) - { - return self::$m_oMysqli->errno; - } - else - { - return self::$m_oMysqli->connect_errno; - } - } - - public static function GetError() - { - if (self::$m_oMysqli->error != '') - { - return self::$m_oMysqli->error; - } - else - { - return self::$m_oMysqli->connect_error; - } - } - - public static function DBHost() {return self::$m_sDBHost;} - public static function DBUser() {return self::$m_sDBUser;} - public static function DBPwd() {return self::$m_sDBPwd;} - public static function DBName() {return self::$m_sDBName;} - - /** - * Quote variable and protect against SQL injection attacks - * Code found in the PHP documentation: quote_smart($value) - * - * @param mixed $value - * @param bool $bAlways should be set to true when the purpose is to create a IN clause, - * otherwise and if there is a mix of strings and numbers, the clause would always be false - * @param string $cQuoteStyle - * - * @return array|string - */ - public static function Quote($value, $bAlways = false, $cQuoteStyle = "'") - { - if (is_null($value)) - { - return 'NULL'; - } - - if (is_array($value)) - { - $aRes = array(); - foreach ($value as $key => $itemvalue) - { - $aRes[$key] = self::Quote($itemvalue, $bAlways, $cQuoteStyle); - } - return $aRes; - } - - // Stripslashes - if (get_magic_quotes_gpc()) - { - $value = stripslashes($value); - } - // Quote if not a number or a numeric string - if ($bAlways || is_string($value)) - { - $value = $cQuoteStyle . self::$m_oMysqli->real_escape_string($value) . $cQuoteStyle; - } - return $value; - } - - /** - * @param string $sSQLQuery - * - * @return \mysqli_result - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public static function Query($sSQLQuery) - { - $oKPI = new ExecutionKPI(); - try - { - $oResult = self::$m_oMysqli->query($sSQLQuery); - } - catch(mysqli_sql_exception $e) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSQLQuery, $e)); - } - $oKPI->ComputeStats('Query exec (mySQL)', $sSQLQuery); - if ($oResult === false) - { - $aContext = array('query' => $sSQLQuery); - - $iMySqlErrorNo = self::$m_oMysqli->errno; - $aMySqlHasGoneAwayErrorCodes = MySQLHasGoneAwayException::getErrorCodes(); - if (in_array($iMySqlErrorNo, $aMySqlHasGoneAwayErrorCodes)) - { - throw new MySQLHasGoneAwayException(self::GetError(), $aContext); - } - - throw new MySQLException('Failed to issue SQL query', $aContext); - } - - return $oResult; - } - - /** - * @param string $sTable - * - * @return int - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public static function GetNextInsertId($sTable) - { - $sSQL = "SHOW TABLE STATUS LIKE '$sTable'"; - $oResult = self::Query($sSQL); - $aRow = $oResult->fetch_assoc(); - - return $aRow['Auto_increment']; - } - - public static function GetInsertId() - { - $iRes = self::$m_oMysqli->insert_id; - if (is_null($iRes)) - { - return 0; - } - return $iRes; - } - - public static function InsertInto($sSQLQuery) - { - if (self::Query($sSQLQuery)) - { - return self::GetInsertId(); - } - return false; - } - - public static function DeleteFrom($sSQLQuery) - { - self::Query($sSQLQuery); - } - - /** - * @param string $sSql - * @param int $iCol beginning at 0 - * - * @return string corresponding cell content on the first line - * @throws \MySQLException - * @throws \MySQLQueryHasNoResultException - */ - public static function QueryToScalar($sSql, $iCol = 0) - { - $oKPI = new ExecutionKPI(); - try - { - $oResult = self::$m_oMysqli->query($sSql); - } - catch(mysqli_sql_exception $e) - { - $oKPI->ComputeStats('Query exec (mySQL)', $sSql); - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); - } - $oKPI->ComputeStats('Query exec (mySQL)', $sSql); - if ($oResult === false) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); - } - - if ($aRow = $oResult->fetch_array(MYSQLI_BOTH)) - { - $res = $aRow[$iCol]; - } - else - { - $oResult->free(); - throw new MySQLQueryHasNoResultException('Found no result for query', array('query' => $sSql)); - } - $oResult->free(); - - return $res; - } - - - /** - * @param string $sSql - * - * @return array - * @throws \MySQLException if query cannot be processed - */ - public static function QueryToArray($sSql) - { - $aData = array(); - $oKPI = new ExecutionKPI(); - try - { - $oResult = self::$m_oMysqli->query($sSql); - } - catch(mysqli_sql_exception $e) - { - $oKPI->ComputeStats('Query exec (mySQL)', $sSql); - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); - } - $oKPI->ComputeStats('Query exec (mySQL)', $sSql); - if ($oResult === false) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); - } - - while ($aRow = $oResult->fetch_array(MYSQLI_BOTH)) - { - $aData[] = $aRow; - } - $oResult->free(); - return $aData; - } - - /** - * @param string $sSql - * @param int $col - * - * @return array - * @throws \MySQLException - */ - public static function QueryToCol($sSql, $col) - { - $aColumn = array(); - $aData = self::QueryToArray($sSql); - foreach($aData as $aRow) - { - @$aColumn[] = $aRow[$col]; - } - return $aColumn; - } - - /** - * @param string $sSql - * - * @return array - * @throws \MySQLException if query cannot be processed - */ - public static function ExplainQuery($sSql) - { - $aData = array(); - try - { - $oResult = self::$m_oMysqli->query($sSql); - } - catch(mysqli_sql_exception $e) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); - } - if ($oResult === false) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); - } - - $aNames = self::GetColumns($oResult, $sSql); - - $aData[] = $aNames; - while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC)) - { - $aData[] = $aRow; - } - $oResult->free(); - return $aData; - } - - /** - * @param string $sSql - * - * @return string - * @throws \MySQLException if query cannot be processed - */ - public static function TestQuery($sSql) - { - try - { - $oResult = self::$m_oMysqli->query($sSql); - } - catch(mysqli_sql_exception $e) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); - } - if ($oResult === false) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); - } - - if (is_object($oResult)) - { - $oResult->free(); - } - return ''; - } - - public static function NbRows($oResult) - { - return $oResult->num_rows; - } - - public static function AffectedRows() - { - return self::$m_oMysqli->affected_rows; - } - - public static function FetchArray($oResult) - { - return $oResult->fetch_array(MYSQLI_ASSOC); - } - - /** - * @param mysqli_result $oResult - * @param string $sSql - * - * @return string[] - * @throws \MySQLException - */ - public static function GetColumns($oResult, $sSql) - { - $aNames = array(); - for ($i = 0; $i < (($___mysqli_tmp = $oResult->field_count) ? $___mysqli_tmp : 0) ; $i++) - { - $meta = $oResult->fetch_field_direct($i); - if (!$meta) - { - throw new MySQLException('mysql_fetch_field: No information available', array('query'=>$sSql, 'i'=>$i)); - } - else - { - $aNames[] = $meta->name; - } - } - return $aNames; - } - - public static function Seek($oResult, $iRow) - { - return $oResult->data_seek($iRow); - } - - public static function FreeResult($oResult) - { - $oResult->free(); /* returns void */ - return true; - } - - public static function IsTable($sTable) - { - $aTableInfo = self::GetTableInfo($sTable); - return (!empty($aTableInfo)); - } - - public static function IsKey($sTable, $iKey) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($iKey, $aTableInfo["Fields"])) return false; - $aFieldData = $aTableInfo["Fields"][$iKey]; - if (!array_key_exists("Key", $aFieldData)) return false; - return ($aFieldData["Key"] == "PRI"); - } - - public static function IsAutoIncrement($sTable, $sField) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; - $aFieldData = $aTableInfo["Fields"][$sField]; - if (!array_key_exists("Extra", $aFieldData)) return false; - //MyHelpers::debug_breakpoint($aFieldData); - return (strstr($aFieldData["Extra"], "auto_increment")); - } - - public static function IsField($sTable, $sField) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; - return true; - } - - public static function IsNullAllowed($sTable, $sField) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; - $aFieldData = $aTableInfo["Fields"][$sField]; - return (strtolower($aFieldData["Null"]) == "yes"); - } - - public static function GetFieldType($sTable, $sField) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; - $aFieldData = $aTableInfo["Fields"][$sField]; - return ($aFieldData["Type"]); - } - - /** - * @param string $sTable - * @param string $sField - * - * @return bool|string - * @see \AttributeDefinition::GetSQLColumns() - */ - public static function GetFieldSpec($sTable, $sField) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; - $aFieldData = $aTableInfo["Fields"][$sField]; - - $sRet = $aFieldData["Type"]; - - $sColumnCharset = $aFieldData["Charset"]; - $sColumnCollation = $aFieldData["Collation"]; - if (!empty($sColumnCharset)) - { - $sRet .= ' CHARACTER SET '.$sColumnCharset; - $sRet .= ' COLLATE '.$sColumnCollation; - } - - if ($aFieldData["Null"] == 'NO') - { - $sRet .= ' NOT NULL'; - } - - if (is_numeric($aFieldData["Default"])) - { - if (strtolower(substr($aFieldData["Type"], 0, 5)) == 'enum(') - { - // Force quotes to match the column declaration statement - $sRet .= ' DEFAULT '.self::Quote($aFieldData["Default"], true); - } - else - { - $default = $aFieldData["Default"] + 0; // Coerce to a numeric variable - $sRet .= ' DEFAULT '.self::Quote($default); - } - } - elseif (is_string($aFieldData["Default"]) == 'string') - { - $sRet .= ' DEFAULT '.self::Quote($aFieldData["Default"]); - } - - return $sRet; - } - - public static function HasIndex($sTable, $sIndexId, $aFields = null, $aLength = null) - { - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return false; - if (!array_key_exists($sIndexId, $aTableInfo['Indexes'])) return false; - - if ($aFields == null) - { - // Just searching for the name - return true; - } - - // Compare the columns - $sSearchedIndex = implode(',', $aFields); - $aColumnNames = array(); - $aSubParts = array(); - foreach($aTableInfo['Indexes'][$sIndexId] as $aIndexDef) - { - $aColumnNames[] = $aIndexDef['Column_name']; - $aSubParts[] = $aIndexDef['Sub_part']; - } - $sExistingIndex = implode(',', $aColumnNames); - - if (is_null($aLength)) - { - return ($sSearchedIndex == $sExistingIndex); - } - - $sSearchedLength = implode(',', $aLength); - $sExistingLength = implode(',', $aSubParts); - - return ($sSearchedIndex == $sExistingIndex) && ($sSearchedLength == $sExistingLength); - } - - // Returns an array of (fieldname => array of field info) - public static function GetTableFieldsList($sTable) - { - assert(!empty($sTable)); - - $aTableInfo = self::GetTableInfo($sTable); - if (empty($aTableInfo)) return array(); // #@# or an error ? - - return array_keys($aTableInfo["Fields"]); - } - - // Cache the information about existing tables, and their fields - private static $m_aTablesInfo = array(); - private static function _TablesInfoCacheReset() - { - self::$m_aTablesInfo = array(); - } - - /** - * @param $sTableName - * - * @throws \MySQLException - */ - private static function _TableInfoCacheInit($sTableName) - { - if (isset(self::$m_aTablesInfo[strtolower($sTableName)]) - && (self::$m_aTablesInfo[strtolower($sTableName)] != null)) - { - return; - } - - // Create array entry, if table does not exist / has no columns - self::$m_aTablesInfo[strtolower($sTableName)] = null; - - // Get table informations - // We were using SHOW COLUMNS FROM... but this don't return charset and collation info ! - // so since 2.5 and #1001 (switch to utf8mb4) we're using INFORMATION_SCHEMA ! - $aMapping = array( - "Name" => "COLUMN_NAME", - "Type" => "COLUMN_TYPE", - "Null" => "IS_NULLABLE", - "Key" => "COLUMN_KEY", - "Default" => "COLUMN_DEFAULT", - "Extra" => "EXTRA", - "Charset" => "CHARACTER_SET_NAME", - "Collation" => "COLLATION_NAME", - "CharMaxLength" => "CHARACTER_MAXIMUM_LENGTH", - ); - $sColumns = implode(', ', $aMapping); - $sDBName = self::$m_sDBName; - $aFields = self::QueryToArray("SELECT $sColumns FROM information_schema.`COLUMNS` WHERE table_schema = '$sDBName' AND table_name = '$sTableName';"); - foreach ($aFields as $aFieldData) - { - $aFields = array(); - foreach($aMapping as $sKey => $sColumn) - { - $aFields[$sKey] = $aFieldData[$sColumn]; - } - $sFieldName = $aFieldData["COLUMN_NAME"]; - self::$m_aTablesInfo[strtolower($sTableName)]["Fields"][$sFieldName] = $aFields; - } - - if (!is_null(self::$m_aTablesInfo[strtolower($sTableName)])) - { - $aIndexes = self::QueryToArray("SHOW INDEXES FROM `$sTableName`"); - $aMyIndexes = array(); - foreach ($aIndexes as $aIndexColumn) - { - $aMyIndexes[$aIndexColumn['Key_name']][$aIndexColumn['Seq_in_index']-1] = $aIndexColumn; - } - self::$m_aTablesInfo[strtolower($sTableName)]["Indexes"] = $aMyIndexes; - } - } - - public static function GetTableInfo($sTable) - { - self::_TableInfoCacheInit($sTable); - - // perform a case insensitive match because on Windows the table names become lowercase :-( - return self::$m_aTablesInfo[strtolower($sTable)]; - } - - /** - * @param string $sTableName - * - * @return string query to upgrade table charset and collation if needed, null if not - * @throws \MySQLException - * - * @since 2.5 #1001 switch to utf8mb4 - * @see https://dev.mysql.com/doc/refman/5.7/en/charset-table.html - */ - public static function DBCheckTableCharsetAndCollation($sTableName) - { - $sDBName = self::DBName(); - $sTableInfoQuery = "SELECT C.character_set_name, T.table_collation - FROM information_schema.`TABLES` T inner join information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` C - ON T.table_collation = C.collation_name - WHERE T.table_schema = '$sDBName' - AND T.table_name = '$sTableName';"; - $aTableInfo = self::QueryToArray($sTableInfoQuery); - $sTableCharset = $aTableInfo[0]['character_set_name']; - $sTableCollation = $aTableInfo[0]['table_collation']; - - if ((DEFAULT_CHARACTER_SET == $sTableCharset) && (DEFAULT_COLLATION == $sTableCollation)) - { - return null; - } - - - return 'ALTER TABLE `'.$sTableName.'` '.self::GetSqlStringColumnDefinition().';'; - - } - - /** - * @param string $sTable - * - * @return array - * @throws \MySQLException if query cannot be processed - */ - public static function DumpTable($sTable) - { - $sSql = "SELECT * FROM `$sTable`"; - try - { - $oResult = self::$m_oMysqli->query($sSql); - } - catch(mysqli_sql_exception $e) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql), $e); - } - if ($oResult === false) - { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); - } - - $aRows = array(); - while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC)) - { - $aRows[] = $aRow; - } - $oResult->free(); - return $aRows; - } - - /** - * Returns the value of the specified server variable - * @param string $sVarName Name of the server variable - * @return mixed Current value of the variable - */ - public static function GetServerVariable($sVarName) - { - $result = ''; - $sSql = "SELECT @@$sVarName as theVar"; - $aRows = self::QueryToArray($sSql); - if (count($aRows) > 0) - { - $result = $aRows[0]['theVar']; - } - return $result; - } - - - /** - * Returns the privileges of the current user - * @return string privileges in a raw format - */ - public static function GetRawPrivileges() - { - try - { - $oResult = self::Query('SHOW GRANTS'); // [ FOR CURRENT_USER()] - } - catch(MySQLException $e) - { - $iCode = self::GetErrNo(); - return "Current user not allowed to see his own privileges (could not access to the database 'mysql' - $iCode)"; - } - - $aRes = array(); - while ($aRow = $oResult->fetch_array(MYSQLI_NUM)) - { - // so far, only one column... - $aRes[] = implode('/', $aRow); - } - $oResult->free(); - // so far, only one line... - return implode(', ', $aRes); - } - - /** - * Determine the slave status of the server - * @return bool true if the server is slave - */ - public static function IsSlaveServer() - { - try - { - $oResult = self::Query('SHOW SLAVE STATUS'); - } - catch(MySQLException $e) - { - throw new CoreException("Current user not allowed to check the status", array('mysql_error' => $e->getMessage())); - } - - if ($oResult->num_rows == 0) - { - return false; - } - - // Returns one single row anytime - $aRow = $oResult->fetch_array(MYSQLI_ASSOC); - $oResult->free(); - - if (!isset($aRow['Slave_IO_Running'])) - { - return false; - } - if (!isset($aRow['Slave_SQL_Running'])) - { - return false; - } - - // If at least one slave thread is running, then we consider that the slave is enabled - if ($aRow['Slave_IO_Running'] == 'Yes') - { - return true; - } - if ($aRow['Slave_SQL_Running'] == 'Yes') - { - return true; - } - return false; - } - - /** - * @return string query to upgrade database charset and collation if needed, null if not - * @throws \MySQLException - * - * @since 2.5 #1001 switch to utf8mb4 - * @see https://dev.mysql.com/doc/refman/5.7/en/charset-database.html - */ - public static function DBCheckCharsetAndCollation() - { - $sDBName = CMDBSource::DBName(); - $sDBInfoQuery = "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME - FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$sDBName';"; - $aDBInfo = CMDBSource::QueryToArray($sDBInfoQuery); - $sDBCharset = $aDBInfo[0]['DEFAULT_CHARACTER_SET_NAME']; - $sDBCollation = $aDBInfo[0]['DEFAULT_COLLATION_NAME']; - - if ((DEFAULT_CHARACTER_SET == $sDBCharset) && (DEFAULT_COLLATION == $sDBCollation)) - { - return null; - } - - return 'ALTER DATABASE'.CMDBSource::GetSqlStringColumnDefinition().';'; - } -} + + + +/** + * DB Server abstraction + * + * @copyright Copyright (C) 2010-2018 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('MyHelpers.class.inc.php'); +require_once(APPROOT.'core/kpi.class.inc.php'); + +class MySQLException extends CoreException +{ + /** + * MySQLException constructor. + * + * @param string $sIssue + * @param array $aContext + * @param \Exception $oException + * @param \mysqli $oMysqli to use when working with a custom mysqli instance + */ + public function __construct($sIssue, $aContext, $oException = null, $oMysqli = null) + { + if ($oException != null) + { + $aContext['mysql_errno'] = $oException->getCode(); + $this->code = $oException->getCode(); + $aContext['mysql_error'] = $oException->getMessage(); + } + else if ($oMysqli != null) + { + $aContext['mysql_errno'] = $oMysqli->errno; + $this->code = $oMysqli->errno; + $aContext['mysql_error'] = $oMysqli->error; + } + else + { + $aContext['mysql_errno'] = CMDBSource::GetErrNo(); + $this->code = CMDBSource::GetErrNo(); + $aContext['mysql_error'] = CMDBSource::GetError(); + } + parent::__construct($sIssue, $aContext); + } +} + +/** + * Class MySQLQueryHasNoResultException + * + * @since 2.5 + */ +class MySQLQueryHasNoResultException extends MySQLException +{ + +} + +/** + * Class MySQLHasGoneAwayException + * + * @since 2.5 + * @see itop bug 1195 + * @see https://dev.mysql.com/doc/refman/5.7/en/gone-away.html + */ +class MySQLHasGoneAwayException extends MySQLException +{ + /** + * can not be a constant before PHP 5.6 (http://php.net/manual/fr/language.oop5.constants.php) + * + * @return int[] + */ + public static function getErrorCodes() + { + return array( + 2006, + 2013 + ); + } + + public function __construct($sIssue, $aContext) + { + parent::__construct($sIssue, $aContext, null); + } +} + + +/** + * CMDBSource + * database access wrapper + * + * @package iTopORM + */ +class CMDBSource +{ + protected static $m_sDBHost; + protected static $m_sDBUser; + protected static $m_sDBPwd; + protected static $m_sDBName; + /** + * @var boolean + * @since 2.5 #1260 MySQL TLS first implementation + */ + protected static $m_bDBTlsEnabled; + /** + * @var string + * @since 2.5 #1260 MySQL TLS first implementation + */ + protected static $m_sDBTlsCA; + + /** @var mysqli $m_oMysqli */ + protected static $m_oMysqli; + + /** + * SQL charset & collation declaration for text columns + * + * Using a function instead of a constant or attribute to avoid crash in the setup for older PHP versions (cannot + * use expression as value) + * + * @see https://dev.mysql.com/doc/refman/5.7/en/charset-column.html + * @since 2.5 #1001 switch to utf8mb4 + */ + public static function GetSqlStringColumnDefinition() + { + return ' CHARACTER SET '.DEFAULT_CHARACTER_SET.' COLLATE '.DEFAULT_COLLATION; + } + + /** + * @param Config $oConfig + * + * @throws \MySQLException + * @uses \CMDBSource::Init() + * @uses \CMDBSource::SetCharacterSet() + */ + public static function InitFromConfig($oConfig) + { + $sServer = $oConfig->Get('db_host'); + $sUser = $oConfig->Get('db_user'); + $sPwd = $oConfig->Get('db_pwd'); + $sSource = $oConfig->Get('db_name'); + $bTlsEnabled = $oConfig->Get('db_tls.enabled'); + $sTlsCA = $oConfig->Get('db_tls.ca'); + + self::Init($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA); + + $sCharacterSet = DEFAULT_CHARACTER_SET; + $sCollation = DEFAULT_COLLATION; + self::SetCharacterSet($sCharacterSet, $sCollation); + } + + /** + * @param string $sServer + * @param string $sUser + * @param string $sPwd + * @param string $sSource database to use + * @param bool $bTlsEnabled + * @param string $sTlsCA + * + * @throws \MySQLException + */ + public static function Init( + $sServer, $sUser, $sPwd, $sSource = '', $bTlsEnabled = false, $sTlsCA = null + ) + { + self::$m_sDBHost = $sServer; + self::$m_sDBUser = $sUser; + self::$m_sDBPwd = $sPwd; + self::$m_sDBName = $sSource; + self::$m_bDBTlsEnabled = empty($bTlsEnabled) ? false : $bTlsEnabled; + self::$m_sDBTlsCA = empty($sTlsCA) ? null : $sTlsCA; + + self::$m_oMysqli = self::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, true); + } + + /** + * @param string $sDbHost + * @param string $sUser + * @param string $sPwd + * @param string $sSource database to use + * @param bool $bTlsEnabled + * @param string $sTlsCa + * @param bool $bCheckTlsAfterConnection If true then verify after connection if it is encrypted + * + * @return \mysqli + * @throws \MySQLException + */ + public static function GetMysqliInstance( + $sDbHost, $sUser, $sPwd, $sSource = '', $bTlsEnabled = false, $sTlsCa = null, $bCheckTlsAfterConnection = false + ) { + $oMysqli = null; + + $sServer = null; + $iPort = null; + self::InitServerAndPort($sDbHost, $sServer, $iPort); + + $iFlags = null; + + // *some* errors (like connection errors) will throw mysqli_sql_exception instead of generating warnings printed to the output + // but some other errors will still cause the query() method to return false !!! + mysqli_report(MYSQLI_REPORT_STRICT); + + try + { + $oMysqli = new mysqli(); + $oMysqli->init(); + + if ($bTlsEnabled) + { + $iFlags = (empty($sTlsCa)) + ? MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT + : MYSQLI_CLIENT_SSL; + $sTlsCert = null; // not implemented + $sTlsCaPath = null; // not implemented + $sTlsCipher = null; // not implemented + $oMysqli->ssl_set($bTlsEnabled, $sTlsCert, $sTlsCa, $sTlsCaPath, $sTlsCipher); + } + $oMysqli->real_connect($sServer, $sUser, $sPwd, '', $iPort, ini_get("mysqli.default_socket"), $iFlags); + } + catch(mysqli_sql_exception $e) + { + throw new MySQLException('Could not connect to the DB server', array('host' => $sServer, 'user' => $sUser), $e); + } + + if ($bTlsEnabled + && $bCheckTlsAfterConnection + && !self::IsOpenedDbConnectionUsingTls($oMysqli)) + { + throw new MySQLException("Connection to the database is not encrypted whereas it was opened using TLS parameters", + null, null, $oMysqli); + } + + if (!empty($sSource)) + { + try + { + mysqli_report(MYSQLI_REPORT_STRICT); // Errors, in the next query, will throw mysqli_sql_exception + $oMysqli->query("USE `$sSource`"); + } + catch(mysqli_sql_exception $e) + { + throw new MySQLException('Could not select DB', + array('host' => $sServer, 'user' => $sUser, 'db_name' => $sSource), $e); + } + } + + return $oMysqli; + } + + /** + * @param string $sDbHost initial value ("p:domain:port" syntax) + * @param string $sServer server variable to update + * @param int $iPort port variable to update + */ + public static function InitServerAndPort($sDbHost, &$sServer, &$iPort) + { + $aConnectInfo = explode(':', $sDbHost); + + $bUsePersistentConnection = false; + if (strcasecmp($aConnectInfo[0], 'p') == 0) + { + // we might have "p:" prefix to use persistent connections (see http://php.net/manual/en/mysqli.persistconns.php) + $bUsePersistentConnection = true; + $sServer = $aConnectInfo[0].':'.$aConnectInfo[1]; + } + else + { + $sServer = $aConnectInfo[0]; + } + + $iConnectInfoCount = count($aConnectInfo); + if ($bUsePersistentConnection && ($iConnectInfoCount == 3)) + { + $iPort = $aConnectInfo[2]; + } + else if (!$bUsePersistentConnection && ($iConnectInfoCount == 2)) + { + $iPort = $aConnectInfo[1]; + } + else + { + $iPort = 3306; + } + } + + /** + *

    A DB connection can be opened transparently (no errors thrown) without being encrypted, whereas the TLS + * parameters were used.
    + * This method can be called to ensure that the DB connection really uses TLS. + * + *

    We're using this object connection : {@link self::$m_oMysqli} + * + * @param \mysqli $oMysqli + * + * @return boolean true if the connection was really established using TLS + * @throws \MySQLException + * + * @uses IsMySqlVarNonEmpty + */ + private static function IsOpenedDbConnectionUsingTls($oMysqli) + { + if (self::$m_oMysqli == null) + { + self::$m_oMysqli = $oMysqli; + } + + $bNonEmptySslVersionVar = self::IsMySqlVarNonEmpty('ssl_version'); + $bNonEmptySslCipherVar = self::IsMySqlVarNonEmpty('ssl_cipher'); + + return ($bNonEmptySslVersionVar && $bNonEmptySslCipherVar); + } + + /** + * @param string $sVarName + * + * @return bool + * @throws \MySQLException + * + * @uses SHOW STATUS queries + */ + private static function IsMySqlVarNonEmpty($sVarName) + { + try + { + $sResult = self::QueryToScalar("SHOW SESSION STATUS LIKE '$sVarName'", 1); + } + catch (MySQLQueryHasNoResultException $e) + { + $sResult = null; + } + + return (!empty($sResult)); + } + + public static function SetCharacterSet($sCharset = DEFAULT_CHARACTER_SET, $sCollation = DEFAULT_COLLATION) + { + if (strlen($sCharset) > 0) + { + if (strlen($sCollation) > 0) + { + self::Query("SET NAMES '$sCharset' COLLATE '$sCollation'"); + } + else + { + self::Query("SET NAMES '$sCharset'"); + } + } + } + + public static function SetTimezone($sTimezone = null) + { + // Note: requires the installation of MySQL special tables, + // otherwise, only 'SYSTEM' or "+10:00' may be specified which is NOT sufficient because of day light saving times + if (!is_null($sTimezone)) + { + $sQuotedTimezone = self::Quote($sTimezone); + self::Query("SET time_zone = $sQuotedTimezone"); + } + } + + public static function ListDB() + { + $aDBs = self::QueryToCol('SHOW DATABASES', 'Database'); + // Show Database does return the DB names in lower case + return $aDBs; + } + + public static function IsDB($sSource) + { + try + { + $aDBs = self::ListDB(); + foreach($aDBs as $sDBName) + { + // perform a case insensitive test because on Windows the table names become lowercase :-( + if (strtolower($sDBName) == strtolower($sSource)) return true; + } + return false; + } + catch(Exception $e) + { + // In case we don't have rights to enumerate the databases + // Let's try to connect directly + return @((bool)self::$m_oMysqli->query("USE `$sSource`")); + } + + } + + public static function GetDBVersion() + { + $aVersions = self::QueryToCol('SELECT Version() as version', 'version'); + return $aVersions[0]; + } + + /** + * @param string $sSource + * + * @throws \MySQLException + */ + public static function SelectDB($sSource) + { + if (!((bool)self::$m_oMysqli->query("USE `$sSource`"))) + { + throw new MySQLException('Could not select DB', array('db_name'=>$sSource)); + } + self::$m_sDBName = $sSource; + } + + /** + * @param string $sSource + * + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public static function CreateDB($sSource) + { + self::Query("CREATE DATABASE `$sSource` CHARACTER SET ".DEFAULT_CHARACTER_SET." COLLATE ".DEFAULT_COLLATION); + self::SelectDB($sSource); + } + + public static function DropDB($sDBToDrop = '') + { + if (empty($sDBToDrop)) + { + $sDBToDrop = self::$m_sDBName; + } + self::Query("DROP DATABASE `$sDBToDrop`"); + if ($sDBToDrop == self::$m_sDBName) + { + self::$m_sDBName = ''; + } + } + + public static function CreateTable($sQuery) + { + $res = self::Query($sQuery); + self::_TablesInfoCacheReset(); // reset the table info cache! + return $res; + } + + public static function DropTable($sTable) + { + $res = self::Query("DROP TABLE `$sTable`"); + self::_TablesInfoCacheReset(); // reset the table info cache! + return $res; + } + + /** + * @return \mysqli + */ + public static function GetMysqli() + { + return self::$m_oMysqli; + } + + public static function GetErrNo() + { + if (self::$m_oMysqli->errno != 0) + { + return self::$m_oMysqli->errno; + } + else + { + return self::$m_oMysqli->connect_errno; + } + } + + public static function GetError() + { + if (self::$m_oMysqli->error != '') + { + return self::$m_oMysqli->error; + } + else + { + return self::$m_oMysqli->connect_error; + } + } + + public static function DBHost() {return self::$m_sDBHost;} + public static function DBUser() {return self::$m_sDBUser;} + public static function DBPwd() {return self::$m_sDBPwd;} + public static function DBName() {return self::$m_sDBName;} + + /** + * Quote variable and protect against SQL injection attacks + * Code found in the PHP documentation: quote_smart($value) + * + * @param mixed $value + * @param bool $bAlways should be set to true when the purpose is to create a IN clause, + * otherwise and if there is a mix of strings and numbers, the clause would always be false + * @param string $cQuoteStyle + * + * @return array|string + */ + public static function Quote($value, $bAlways = false, $cQuoteStyle = "'") + { + if (is_null($value)) + { + return 'NULL'; + } + + if (is_array($value)) + { + $aRes = array(); + foreach ($value as $key => $itemvalue) + { + $aRes[$key] = self::Quote($itemvalue, $bAlways, $cQuoteStyle); + } + return $aRes; + } + + // Stripslashes + if (get_magic_quotes_gpc()) + { + $value = stripslashes($value); + } + // Quote if not a number or a numeric string + if ($bAlways || is_string($value)) + { + $value = $cQuoteStyle . self::$m_oMysqli->real_escape_string($value) . $cQuoteStyle; + } + return $value; + } + + /** + * @param string $sSQLQuery + * + * @return \mysqli_result + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public static function Query($sSQLQuery) + { + $oKPI = new ExecutionKPI(); + try + { + $oResult = self::$m_oMysqli->query($sSQLQuery); + } + catch(mysqli_sql_exception $e) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSQLQuery, $e)); + } + $oKPI->ComputeStats('Query exec (mySQL)', $sSQLQuery); + if ($oResult === false) + { + $aContext = array('query' => $sSQLQuery); + + $iMySqlErrorNo = self::$m_oMysqli->errno; + $aMySqlHasGoneAwayErrorCodes = MySQLHasGoneAwayException::getErrorCodes(); + if (in_array($iMySqlErrorNo, $aMySqlHasGoneAwayErrorCodes)) + { + throw new MySQLHasGoneAwayException(self::GetError(), $aContext); + } + + throw new MySQLException('Failed to issue SQL query', $aContext); + } + + return $oResult; + } + + /** + * @param string $sTable + * + * @return int + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public static function GetNextInsertId($sTable) + { + $sSQL = "SHOW TABLE STATUS LIKE '$sTable'"; + $oResult = self::Query($sSQL); + $aRow = $oResult->fetch_assoc(); + + return $aRow['Auto_increment']; + } + + public static function GetInsertId() + { + $iRes = self::$m_oMysqli->insert_id; + if (is_null($iRes)) + { + return 0; + } + return $iRes; + } + + public static function InsertInto($sSQLQuery) + { + if (self::Query($sSQLQuery)) + { + return self::GetInsertId(); + } + return false; + } + + public static function DeleteFrom($sSQLQuery) + { + self::Query($sSQLQuery); + } + + /** + * @param string $sSql + * @param int $iCol beginning at 0 + * + * @return string corresponding cell content on the first line + * @throws \MySQLException + * @throws \MySQLQueryHasNoResultException + */ + public static function QueryToScalar($sSql, $iCol = 0) + { + $oKPI = new ExecutionKPI(); + try + { + $oResult = self::$m_oMysqli->query($sSql); + } + catch(mysqli_sql_exception $e) + { + $oKPI->ComputeStats('Query exec (mySQL)', $sSql); + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); + } + $oKPI->ComputeStats('Query exec (mySQL)', $sSql); + if ($oResult === false) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + if ($aRow = $oResult->fetch_array(MYSQLI_BOTH)) + { + $res = $aRow[$iCol]; + } + else + { + $oResult->free(); + throw new MySQLQueryHasNoResultException('Found no result for query', array('query' => $sSql)); + } + $oResult->free(); + + return $res; + } + + + /** + * @param string $sSql + * + * @return array + * @throws \MySQLException if query cannot be processed + */ + public static function QueryToArray($sSql) + { + $aData = array(); + $oKPI = new ExecutionKPI(); + try + { + $oResult = self::$m_oMysqli->query($sSql); + } + catch(mysqli_sql_exception $e) + { + $oKPI->ComputeStats('Query exec (mySQL)', $sSql); + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); + } + $oKPI->ComputeStats('Query exec (mySQL)', $sSql); + if ($oResult === false) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + while ($aRow = $oResult->fetch_array(MYSQLI_BOTH)) + { + $aData[] = $aRow; + } + $oResult->free(); + return $aData; + } + + /** + * @param string $sSql + * @param int $col + * + * @return array + * @throws \MySQLException + */ + public static function QueryToCol($sSql, $col) + { + $aColumn = array(); + $aData = self::QueryToArray($sSql); + foreach($aData as $aRow) + { + @$aColumn[] = $aRow[$col]; + } + return $aColumn; + } + + /** + * @param string $sSql + * + * @return array + * @throws \MySQLException if query cannot be processed + */ + public static function ExplainQuery($sSql) + { + $aData = array(); + try + { + $oResult = self::$m_oMysqli->query($sSql); + } + catch(mysqli_sql_exception $e) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); + } + if ($oResult === false) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + $aNames = self::GetColumns($oResult, $sSql); + + $aData[] = $aNames; + while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC)) + { + $aData[] = $aRow; + } + $oResult->free(); + return $aData; + } + + /** + * @param string $sSql + * + * @return string + * @throws \MySQLException if query cannot be processed + */ + public static function TestQuery($sSql) + { + try + { + $oResult = self::$m_oMysqli->query($sSql); + } + catch(mysqli_sql_exception $e) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); + } + if ($oResult === false) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + if (is_object($oResult)) + { + $oResult->free(); + } + return ''; + } + + public static function NbRows($oResult) + { + return $oResult->num_rows; + } + + public static function AffectedRows() + { + return self::$m_oMysqli->affected_rows; + } + + public static function FetchArray($oResult) + { + return $oResult->fetch_array(MYSQLI_ASSOC); + } + + /** + * @param mysqli_result $oResult + * @param string $sSql + * + * @return string[] + * @throws \MySQLException + */ + public static function GetColumns($oResult, $sSql) + { + $aNames = array(); + for ($i = 0; $i < (($___mysqli_tmp = $oResult->field_count) ? $___mysqli_tmp : 0) ; $i++) + { + $meta = $oResult->fetch_field_direct($i); + if (!$meta) + { + throw new MySQLException('mysql_fetch_field: No information available', array('query'=>$sSql, 'i'=>$i)); + } + else + { + $aNames[] = $meta->name; + } + } + return $aNames; + } + + public static function Seek($oResult, $iRow) + { + return $oResult->data_seek($iRow); + } + + public static function FreeResult($oResult) + { + $oResult->free(); /* returns void */ + return true; + } + + public static function IsTable($sTable) + { + $aTableInfo = self::GetTableInfo($sTable); + return (!empty($aTableInfo)); + } + + public static function IsKey($sTable, $iKey) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($iKey, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$iKey]; + if (!array_key_exists("Key", $aFieldData)) return false; + return ($aFieldData["Key"] == "PRI"); + } + + public static function IsAutoIncrement($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + if (!array_key_exists("Extra", $aFieldData)) return false; + //MyHelpers::debug_breakpoint($aFieldData); + return (strstr($aFieldData["Extra"], "auto_increment")); + } + + public static function IsField($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + return true; + } + + public static function IsNullAllowed($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + return (strtolower($aFieldData["Null"]) == "yes"); + } + + public static function GetFieldType($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + return ($aFieldData["Type"]); + } + + /** + * @param string $sTable + * @param string $sField + * + * @return bool|string + * @see \AttributeDefinition::GetSQLColumns() + */ + public static function GetFieldSpec($sTable, $sField) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sField, $aTableInfo["Fields"])) return false; + $aFieldData = $aTableInfo["Fields"][$sField]; + + $sRet = $aFieldData["Type"]; + + $sColumnCharset = $aFieldData["Charset"]; + $sColumnCollation = $aFieldData["Collation"]; + if (!empty($sColumnCharset)) + { + $sRet .= ' CHARACTER SET '.$sColumnCharset; + $sRet .= ' COLLATE '.$sColumnCollation; + } + + if ($aFieldData["Null"] == 'NO') + { + $sRet .= ' NOT NULL'; + } + + if (is_numeric($aFieldData["Default"])) + { + if (strtolower(substr($aFieldData["Type"], 0, 5)) == 'enum(') + { + // Force quotes to match the column declaration statement + $sRet .= ' DEFAULT '.self::Quote($aFieldData["Default"], true); + } + else + { + $default = $aFieldData["Default"] + 0; // Coerce to a numeric variable + $sRet .= ' DEFAULT '.self::Quote($default); + } + } + elseif (is_string($aFieldData["Default"]) == 'string') + { + $sRet .= ' DEFAULT '.self::Quote($aFieldData["Default"]); + } + + return $sRet; + } + + public static function HasIndex($sTable, $sIndexId, $aFields = null, $aLength = null) + { + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return false; + if (!array_key_exists($sIndexId, $aTableInfo['Indexes'])) return false; + + if ($aFields == null) + { + // Just searching for the name + return true; + } + + // Compare the columns + $sSearchedIndex = implode(',', $aFields); + $aColumnNames = array(); + $aSubParts = array(); + foreach($aTableInfo['Indexes'][$sIndexId] as $aIndexDef) + { + $aColumnNames[] = $aIndexDef['Column_name']; + $aSubParts[] = $aIndexDef['Sub_part']; + } + $sExistingIndex = implode(',', $aColumnNames); + + if (is_null($aLength)) + { + return ($sSearchedIndex == $sExistingIndex); + } + + $sSearchedLength = implode(',', $aLength); + $sExistingLength = implode(',', $aSubParts); + + return ($sSearchedIndex == $sExistingIndex) && ($sSearchedLength == $sExistingLength); + } + + // Returns an array of (fieldname => array of field info) + public static function GetTableFieldsList($sTable) + { + assert(!empty($sTable)); + + $aTableInfo = self::GetTableInfo($sTable); + if (empty($aTableInfo)) return array(); // #@# or an error ? + + return array_keys($aTableInfo["Fields"]); + } + + // Cache the information about existing tables, and their fields + private static $m_aTablesInfo = array(); + private static function _TablesInfoCacheReset() + { + self::$m_aTablesInfo = array(); + } + + /** + * @param $sTableName + * + * @throws \MySQLException + */ + private static function _TableInfoCacheInit($sTableName) + { + if (isset(self::$m_aTablesInfo[strtolower($sTableName)]) + && (self::$m_aTablesInfo[strtolower($sTableName)] != null)) + { + return; + } + + // Create array entry, if table does not exist / has no columns + self::$m_aTablesInfo[strtolower($sTableName)] = null; + + // Get table informations + // We were using SHOW COLUMNS FROM... but this don't return charset and collation info ! + // so since 2.5 and #1001 (switch to utf8mb4) we're using INFORMATION_SCHEMA ! + $aMapping = array( + "Name" => "COLUMN_NAME", + "Type" => "COLUMN_TYPE", + "Null" => "IS_NULLABLE", + "Key" => "COLUMN_KEY", + "Default" => "COLUMN_DEFAULT", + "Extra" => "EXTRA", + "Charset" => "CHARACTER_SET_NAME", + "Collation" => "COLLATION_NAME", + "CharMaxLength" => "CHARACTER_MAXIMUM_LENGTH", + ); + $sColumns = implode(', ', $aMapping); + $sDBName = self::$m_sDBName; + $aFields = self::QueryToArray("SELECT $sColumns FROM information_schema.`COLUMNS` WHERE table_schema = '$sDBName' AND table_name = '$sTableName';"); + foreach ($aFields as $aFieldData) + { + $aFields = array(); + foreach($aMapping as $sKey => $sColumn) + { + $aFields[$sKey] = $aFieldData[$sColumn]; + } + $sFieldName = $aFieldData["COLUMN_NAME"]; + self::$m_aTablesInfo[strtolower($sTableName)]["Fields"][$sFieldName] = $aFields; + } + + if (!is_null(self::$m_aTablesInfo[strtolower($sTableName)])) + { + $aIndexes = self::QueryToArray("SHOW INDEXES FROM `$sTableName`"); + $aMyIndexes = array(); + foreach ($aIndexes as $aIndexColumn) + { + $aMyIndexes[$aIndexColumn['Key_name']][$aIndexColumn['Seq_in_index']-1] = $aIndexColumn; + } + self::$m_aTablesInfo[strtolower($sTableName)]["Indexes"] = $aMyIndexes; + } + } + + public static function GetTableInfo($sTable) + { + self::_TableInfoCacheInit($sTable); + + // perform a case insensitive match because on Windows the table names become lowercase :-( + return self::$m_aTablesInfo[strtolower($sTable)]; + } + + /** + * @param string $sTableName + * + * @return string query to upgrade table charset and collation if needed, null if not + * @throws \MySQLException + * + * @since 2.5 #1001 switch to utf8mb4 + * @see https://dev.mysql.com/doc/refman/5.7/en/charset-table.html + */ + public static function DBCheckTableCharsetAndCollation($sTableName) + { + $sDBName = self::DBName(); + $sTableInfoQuery = "SELECT C.character_set_name, T.table_collation + FROM information_schema.`TABLES` T inner join information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` C + ON T.table_collation = C.collation_name + WHERE T.table_schema = '$sDBName' + AND T.table_name = '$sTableName';"; + $aTableInfo = self::QueryToArray($sTableInfoQuery); + $sTableCharset = $aTableInfo[0]['character_set_name']; + $sTableCollation = $aTableInfo[0]['table_collation']; + + if ((DEFAULT_CHARACTER_SET == $sTableCharset) && (DEFAULT_COLLATION == $sTableCollation)) + { + return null; + } + + + return 'ALTER TABLE `'.$sTableName.'` '.self::GetSqlStringColumnDefinition().';'; + + } + + /** + * @param string $sTable + * + * @return array + * @throws \MySQLException if query cannot be processed + */ + public static function DumpTable($sTable) + { + $sSql = "SELECT * FROM `$sTable`"; + try + { + $oResult = self::$m_oMysqli->query($sSql); + } + catch(mysqli_sql_exception $e) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql), $e); + } + if ($oResult === false) + { + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql)); + } + + $aRows = array(); + while ($aRow = $oResult->fetch_array(MYSQLI_ASSOC)) + { + $aRows[] = $aRow; + } + $oResult->free(); + return $aRows; + } + + /** + * Returns the value of the specified server variable + * @param string $sVarName Name of the server variable + * @return mixed Current value of the variable + */ + public static function GetServerVariable($sVarName) + { + $result = ''; + $sSql = "SELECT @@$sVarName as theVar"; + $aRows = self::QueryToArray($sSql); + if (count($aRows) > 0) + { + $result = $aRows[0]['theVar']; + } + return $result; + } + + + /** + * Returns the privileges of the current user + * @return string privileges in a raw format + */ + public static function GetRawPrivileges() + { + try + { + $oResult = self::Query('SHOW GRANTS'); // [ FOR CURRENT_USER()] + } + catch(MySQLException $e) + { + $iCode = self::GetErrNo(); + return "Current user not allowed to see his own privileges (could not access to the database 'mysql' - $iCode)"; + } + + $aRes = array(); + while ($aRow = $oResult->fetch_array(MYSQLI_NUM)) + { + // so far, only one column... + $aRes[] = implode('/', $aRow); + } + $oResult->free(); + // so far, only one line... + return implode(', ', $aRes); + } + + /** + * Determine the slave status of the server + * @return bool true if the server is slave + */ + public static function IsSlaveServer() + { + try + { + $oResult = self::Query('SHOW SLAVE STATUS'); + } + catch(MySQLException $e) + { + throw new CoreException("Current user not allowed to check the status", array('mysql_error' => $e->getMessage())); + } + + if ($oResult->num_rows == 0) + { + return false; + } + + // Returns one single row anytime + $aRow = $oResult->fetch_array(MYSQLI_ASSOC); + $oResult->free(); + + if (!isset($aRow['Slave_IO_Running'])) + { + return false; + } + if (!isset($aRow['Slave_SQL_Running'])) + { + return false; + } + + // If at least one slave thread is running, then we consider that the slave is enabled + if ($aRow['Slave_IO_Running'] == 'Yes') + { + return true; + } + if ($aRow['Slave_SQL_Running'] == 'Yes') + { + return true; + } + return false; + } + + /** + * @return string query to upgrade database charset and collation if needed, null if not + * @throws \MySQLException + * + * @since 2.5 #1001 switch to utf8mb4 + * @see https://dev.mysql.com/doc/refman/5.7/en/charset-database.html + */ + public static function DBCheckCharsetAndCollation() + { + $sDBName = CMDBSource::DBName(); + $sDBInfoQuery = "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME + FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$sDBName';"; + $aDBInfo = CMDBSource::QueryToArray($sDBInfoQuery); + $sDBCharset = $aDBInfo[0]['DEFAULT_CHARACTER_SET_NAME']; + $sDBCollation = $aDBInfo[0]['DEFAULT_COLLATION_NAME']; + + if ((DEFAULT_CHARACTER_SET == $sDBCharset) && (DEFAULT_COLLATION == $sDBCollation)) + { + return null; + } + + return 'ALTER DATABASE'.CMDBSource::GetSqlStringColumnDefinition().';'; + } +} diff --git a/core/computing.inc.php b/core/computing.inc.php index 7bb37daf6..17d813f7d 100644 --- a/core/computing.inc.php +++ b/core/computing.inc.php @@ -1,139 +1,139 @@ - - - -/** - * Any extension to compute things like a stop watch deadline or working hours - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * Metric computing for stop watches - */ -interface iMetricComputer -{ - public static function GetDescription(); - public function ComputeMetric($oObject); -} - -/** - * Working time computing for stop watches - */ -interface iWorkingTimeComputer -{ - public static function GetDescription(); - - /** - * Get the date/time corresponding to a given delay in the future from the present - * considering only the valid (open) hours for a specified object - * @param $oObject DBObject The object for which to compute the deadline - * @param $iDuration integer The duration (in seconds) in the future - * @param $oStartDate DateTime The starting point for the computation - * @return DateTime The date/time for the deadline - */ - public function GetDeadline($oObject, $iDuration, DateTime $oStartDate); - - /** - * Get duration (considering only open hours) elapsed bewteen two given DateTimes - * @param $oObject DBObject The object for which to compute the duration - * @param $oStartDate DateTime The starting point for the computation (default = now) - * @param $oEndDate DateTime The ending point for the computation (default = now) - * @return integer The duration (number of seconds) of open hours elapsed between the two dates - */ - public function GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate); -} - -/** - * Default implementation oof deadline computing: NO deadline - */ -class DefaultMetricComputer implements iMetricComputer -{ - public static function GetDescription() - { - return "Null"; - } - - public function ComputeMetric($oObject) - { - return null; - } -} - -/** - * Default implementation of working time computing - */ -class DefaultWorkingTimeComputer implements iWorkingTimeComputer -{ - public static function GetDescription() - { - return "24x7, no holidays"; - } - - /** - * Get the date/time corresponding to a given delay in the future from the present - * considering only the valid (open) hours for a specified object - * @param $oObject DBObject The object for which to compute the deadline - * @param $iDuration integer The duration (in seconds) in the future - * @param $oStartDate DateTime The starting point for the computation - * @return DateTime The date/time for the deadline - */ - public function GetDeadline($oObject, $iDuration, DateTime $oStartDate) - { - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::Trace(WorkingTimeRecorder::TRACE_DEBUG, __class__.'::'.__function__); - } - //echo "GetDeadline - default: ".$oStartDate->format('Y-m-d H:i:s')." + $iDuration
    \n"; - // Default implementation: 24x7, no holidays: to compute the deadline, just add - // the specified duration to the given date/time - $oResult = clone $oStartDate; - $oResult->modify('+'.$iDuration.' seconds'); - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::SetValues($oStartDate->format('U'), $oResult->format('U'), $iDuration, WorkingTimeRecorder::COMPUTED_END); - } - return $oResult; - } - - /** - * Get duration (considering only open hours) elapsed bewteen two given DateTimes - * @param $oObject DBObject The object for which to compute the duration - * @param $oStartDate DateTime The starting point for the computation (default = now) - * @param $oEndDate DateTime The ending point for the computation (default = now) - * @return integer The duration (number of seconds) of open hours elapsed between the two dates - */ - public function GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate) - { - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::Trace(WorkingTimeRecorder::TRACE_DEBUG, __class__.'::'.__function__); - } - //echo "GetOpenDuration - default: ".$oStartDate->format('Y-m-d H:i:s')." to ".$oEndDate->format('Y-m-d H:i:s')."
    \n"; - $iDuration = abs($oEndDate->format('U') - $oStartDate->format('U')); - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::SetValues($oStartDate->format('U'), $oEndDate->format('U'), $iDuration, WorkingTimeRecorder::COMPUTED_DURATION); - } - return $iDuration; - } -} - - -?> + + + +/** + * Any extension to compute things like a stop watch deadline or working hours + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * Metric computing for stop watches + */ +interface iMetricComputer +{ + public static function GetDescription(); + public function ComputeMetric($oObject); +} + +/** + * Working time computing for stop watches + */ +interface iWorkingTimeComputer +{ + public static function GetDescription(); + + /** + * Get the date/time corresponding to a given delay in the future from the present + * considering only the valid (open) hours for a specified object + * @param $oObject DBObject The object for which to compute the deadline + * @param $iDuration integer The duration (in seconds) in the future + * @param $oStartDate DateTime The starting point for the computation + * @return DateTime The date/time for the deadline + */ + public function GetDeadline($oObject, $iDuration, DateTime $oStartDate); + + /** + * Get duration (considering only open hours) elapsed bewteen two given DateTimes + * @param $oObject DBObject The object for which to compute the duration + * @param $oStartDate DateTime The starting point for the computation (default = now) + * @param $oEndDate DateTime The ending point for the computation (default = now) + * @return integer The duration (number of seconds) of open hours elapsed between the two dates + */ + public function GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate); +} + +/** + * Default implementation oof deadline computing: NO deadline + */ +class DefaultMetricComputer implements iMetricComputer +{ + public static function GetDescription() + { + return "Null"; + } + + public function ComputeMetric($oObject) + { + return null; + } +} + +/** + * Default implementation of working time computing + */ +class DefaultWorkingTimeComputer implements iWorkingTimeComputer +{ + public static function GetDescription() + { + return "24x7, no holidays"; + } + + /** + * Get the date/time corresponding to a given delay in the future from the present + * considering only the valid (open) hours for a specified object + * @param $oObject DBObject The object for which to compute the deadline + * @param $iDuration integer The duration (in seconds) in the future + * @param $oStartDate DateTime The starting point for the computation + * @return DateTime The date/time for the deadline + */ + public function GetDeadline($oObject, $iDuration, DateTime $oStartDate) + { + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::Trace(WorkingTimeRecorder::TRACE_DEBUG, __class__.'::'.__function__); + } + //echo "GetDeadline - default: ".$oStartDate->format('Y-m-d H:i:s')." + $iDuration
    \n"; + // Default implementation: 24x7, no holidays: to compute the deadline, just add + // the specified duration to the given date/time + $oResult = clone $oStartDate; + $oResult->modify('+'.$iDuration.' seconds'); + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::SetValues($oStartDate->format('U'), $oResult->format('U'), $iDuration, WorkingTimeRecorder::COMPUTED_END); + } + return $oResult; + } + + /** + * Get duration (considering only open hours) elapsed bewteen two given DateTimes + * @param $oObject DBObject The object for which to compute the duration + * @param $oStartDate DateTime The starting point for the computation (default = now) + * @param $oEndDate DateTime The ending point for the computation (default = now) + * @return integer The duration (number of seconds) of open hours elapsed between the two dates + */ + public function GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate) + { + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::Trace(WorkingTimeRecorder::TRACE_DEBUG, __class__.'::'.__function__); + } + //echo "GetOpenDuration - default: ".$oStartDate->format('Y-m-d H:i:s')." to ".$oEndDate->format('Y-m-d H:i:s')."
    \n"; + $iDuration = abs($oEndDate->format('U') - $oStartDate->format('U')); + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::SetValues($oStartDate->format('U'), $oEndDate->format('U'), $iDuration, WorkingTimeRecorder::COMPUTED_DURATION); + } + return $iDuration; + } +} + + +?> diff --git a/core/config.class.inc.php b/core/config.class.inc.php index d0d4590c7..4cdb28667 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -1,2168 +1,2168 @@ - - - -define('ITOP_APPLICATION', 'iTop'); -define('ITOP_APPLICATION_SHORT', 'iTop'); -define('ITOP_VERSION', '2.6.0-dev'); -define('ITOP_REVISION', 'svn'); -define('ITOP_BUILD_DATE', '$WCNOW$'); - -define('ACCESS_USER_WRITE', 1); -define('ACCESS_ADMIN_WRITE', 2); -define('ACCESS_FULL', ACCESS_USER_WRITE | ACCESS_ADMIN_WRITE); -define('ACCESS_READONLY', 0); - -/** - * Configuration read/write - * - * @copyright Copyright (C) 2010-2018 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once('coreexception.class.inc.php'); -require_once('attributedef.class.inc.php'); // For the defines -require_once('simplecrypt.class.inc.php'); - -class ConfigException extends CoreException -{ -} - -// was utf8 but it only supports BMP chars (https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html) -// so we switched to utf8mb4 in iTop 2.5, adding dependency to MySQL 5.5.3 -// The config params db_character_set and db_collation were introduced as a temporary workaround and removed in iTop 2.5 -// now everything uses those fixed value ! -define('DEFAULT_CHARACTER_SET', 'utf8mb4'); -define('DEFAULT_COLLATION', 'utf8mb4_unicode_ci'); - -define('DEFAULT_LOG_GLOBAL', true); -define('DEFAULT_LOG_NOTIFICATION', true); -define('DEFAULT_LOG_ISSUE', true); -define('DEFAULT_LOG_WEB_SERVICE', true); - -define('DEFAULT_QUERY_CACHE_ENABLED', true); - - -define('DEFAULT_MIN_DISPLAY_LIMIT', 10); -define('DEFAULT_MAX_DISPLAY_LIMIT', 15); -define('DEFAULT_STANDARD_RELOAD_INTERVAL', 5 * 60); -define('DEFAULT_FAST_RELOAD_INTERVAL', 1 * 60); -define('DEFAULT_SECURE_CONNECTION_REQUIRED', false); -define('DEFAULT_ALLOWED_LOGIN_TYPES', 'form|basic|external'); -define('DEFAULT_EXT_AUTH_VARIABLE', '$_SERVER[\'REMOTE_USER\']'); -define('DEFAULT_ENCRYPTION_KEY', '@iT0pEncr1pti0n!'); // We'll use a random generated key later (if possible) -define('DEFAULT_ENCRYPTION_LIB', 'Mcrypt'); // We'll define the best encryption available later -/** - * Config - * configuration data (this class cannot not be localized, because it is responsible for loading the dictionaries) - * - * @package iTopORM - */ -class Config -{ - //protected $m_bIsLoaded = false; - protected $m_sFile = ''; - - protected $m_aAppModules; - protected $m_aDataModels; - protected $m_aWebServiceCategories; - protected $m_aAddons; - - protected $m_aModuleSettings; - - /** - * New way to store the settings ! - * - * @var array - * @since 2.5 db* variables - */ - protected $m_aSettings = array( - 'app_env_label' => array( - 'type' => 'string', - 'description' => 'Label displayed to describe the current application environnment, defaults to the environment name (e.g. "production")', - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'app_root_url' => array( - 'type' => 'string', - 'description' => 'Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server\'s name)', - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'app_icon_url' => array( - 'type' => 'string', - 'description' => 'Hyperlink to redirect the user when clicking on the application icon (in the main window, or login/logoff pages)', - 'default' => 'http://www.combodo.com/itop', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'db_host' => array( - 'type' => 'string', - 'default' => null, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'db_user' => array( - 'type' => 'string', - 'default' => null, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'db_pwd' => array( - 'type' => 'string', - 'default' => null, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'db_name' => array( - 'type' => 'string', - 'default' => null, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'db_subname' => array( - 'type' => 'string', - 'default' => null, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'db_tls.enabled' => array( - 'type' => 'bool', - 'description' => 'If true then the connection to the DB will be encrypted', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'db_tls.ca' => array( - 'type' => 'string', - 'description' => 'Path to certificate authority file for SSL', - 'default' => null, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'db_character_set' => array( // @deprecated to remove in 2.7 ? #1001 utf8mb4 switch - 'type' => 'string', - 'description' => 'Deprecated since iTop 2.5 : now using utf8mb4', - 'default' => 'DEPRECATED_2.5', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'db_collation' => array( // @deprecated to remove in 2.7 ? #1001 utf8mb4 switch - 'type' => 'string', - 'description' => 'Deprecated since iTop 2.5 : now using utf8mb4_unicode_ci', - 'default' => 'DEPRECATED_2.5', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'skip_check_to_write' => array( - 'type' => 'bool', - 'description' => 'Disable data format and integrity checks to boost up data load (insert or update)', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'skip_check_ext_keys' => array( - 'type' => 'bool', - 'description' => 'Disable external key check when checking the value of attributes', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'skip_strong_security' => array( - 'type' => 'bool', - 'description' => 'Disable strong security - TEMPORY: this flag should be removed when we are more confident in the recent change in security', - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'query_optimization_enabled' => array( - 'type' => 'bool', - 'description' => 'The queries are optimized based on the assumption that the DB integrity has been preserved. By disabling the optimization one can ensure that the fetched data is clean... but this can be really slower or not usable at all (some queries will exceed the allowed number of joins in MySQL: 61!)', - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'query_indentation_enabled' => array( - 'type' => 'bool', - 'description' => 'For developpers: format the SQL queries for human analysis', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'disable_mandatory_ext_keys' => array( - 'type' => 'bool', - 'description' => 'For developpers: allow every external keys to be undefined', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'graphviz_path' => array( - 'type' => 'string', - 'description' => 'Path to the Graphviz "dot" executable for graphing objects lifecycle', - 'default' => '/usr/bin/dot', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'php_path' => array( - 'type' => 'string', - 'description' => 'Path to the php executable in CLI mode', - 'default' => 'php', - 'value' => 'php', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'session_name' => array( - 'type' => 'string', - 'description' => 'The name of the cookie used to store the PHP session id', - 'default' => 'iTop', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'max_combo_length' => array( - 'type' => 'integer', - 'description' => 'The maximum number of elements in a drop-down list. If more then an autocomplete will be used', - 'default' => 50, - 'value' => 50, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'min_autocomplete_chars' => array( - 'type' => 'integer', - 'description' => 'The minimum number of characters to type in order to trigger the "autocomplete" behavior', - 'default' => 2, - 'value' => 2, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'allow_menu_on_linkset' => array( - 'type' => 'bool', - 'description' => 'Display Action menus in view mode on any LinkedSet with edit_mode != none', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'allow_target_creation' => array( - 'type' => 'bool', - 'description' => 'Displays the + button on external keys to create target objects', - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - // Levels that trigger a confirmation in the CSV import/synchro wizard - 'csv_import_min_object_confirmation' => array( - 'type' => 'integer', - 'description' => 'Minimum number of objects to check for the confirmation percentages', - 'default' => 3, - 'value' => 3, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'csv_import_errors_percentage' => array( - 'type' => 'integer', - 'description' => 'Percentage of errors that trigger a confirmation in the CSV import', - 'default' => 50, - 'value' => 50, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'csv_import_modifications_percentage' => array( - 'type' => 'integer', - 'description' => 'Percentage of modifications that trigger a confirmation in the CSV import', - 'default' => 50, - 'value' => 50, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'csv_import_creations_percentage' => array( - 'type' => 'integer', - 'description' => 'Percentage of creations that trigger a confirmation in the CSV import', - 'default' => 50, - 'value' => 50, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'csv_import_history_display' => array( - 'type' => 'bool', - 'description' => 'Display the history tab in the import wizard', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'access_mode' => array( - 'type' => 'integer', - 'description' => 'Access mode: ACCESS_READONLY = 0, ACCESS_ADMIN_WRITE = 2, ACCESS_FULL = 3', - 'default' => ACCESS_FULL, - 'value' => ACCESS_FULL, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'access_message' => array( - 'type' => 'string', - 'description' => 'Message displayed to the users when there is any access restriction', - 'default' => 'iTop is temporarily frozen, please wait... (the admin team)', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'online_help' => array( - 'type' => 'string', - 'description' => 'Hyperlink to the online-help web page', - 'default' => 'http://www.combodo.com/itop-help', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'log_usage' => array( - 'type' => 'bool', - 'description' => 'Log the usage of the application (i.e. the date/time and the user name of each login)', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'log_rest_service' => array( - 'type' => 'bool', - 'description' => 'Log the usage of the REST/JSON service', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'synchro_trace' => array( - 'type' => 'string', - 'description' => 'Synchronization details: none, display, save (includes \'display\')', - 'default' => 'none', - 'value' => 'none', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'link_set_item_separator' => array( - 'type' => 'string', - 'description' => 'Link set from string: line separator', - 'default' => '|', - 'value' => '|', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'link_set_attribute_separator' => array( - 'type' => 'string', - 'description' => 'Link set from string: attribute separator', - 'default' => ';', - 'value' => ';', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'link_set_value_separator' => array( - 'type' => 'string', - 'description' => 'Link set from string: value separator (between the attcode and the value itself', - 'default' => ':', - 'value' => ':', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'link_set_attribute_qualifier' => array( - 'type' => 'string', - 'description' => 'Link set from string: attribute qualifier (encloses both the attcode and the value)', - 'default' => "'", - 'value' => "'", - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'cron_max_execution_time' => array( - 'type' => 'integer', - 'description' => 'Duration (seconds) of the page cron.php, must be shorter than php setting max_execution_time and shorter than the web server response timeout', - 'default' => 600, - 'value' => 600, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'cron_sleep' => array( - 'type' => 'integer', - 'description' => 'Duration (seconds) before cron.php checks again if something must be done', - 'default' => 2, - 'value' => 2, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'async_task_retries' => array( - 'type' => 'array', - 'description' => 'Automatic retries of asynchronous tasks in case of failure (per class)', - 'default' => array('AsyncSendEmail' => array('max_retries' => 0, 'retry_delay' => 600)), - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_asynchronous' => array( - 'type' => 'bool', - 'description' => 'If set, the emails are sent off line, which requires cron.php to be activated. Exception: some features like the email test utility will force the serialized mode', - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'email_transport' => array( - 'type' => 'string', - 'description' => 'Mean to send emails: PHPMail (uses the function mail()) or SMTP (implements the client protocole)', - 'default' => "PHPMail", - 'value' => "PHPMail", - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'email_transport_smtp.host' => array( - 'type' => 'string', - 'description' => 'host name or IP address (optional)', - 'default' => "localhost", - 'value' => "localhost", - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_transport_smtp.port' => array( - 'type' => 'integer', - 'description' => 'port number (optional)', - 'default' => 25, - 'value' => 25, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_transport_smtp.encryption' => array( - 'type' => 'string', - 'description' => 'tls or ssl (optional)', - 'default' => "", - 'value' => "", - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_transport_smtp.username' => array( - 'type' => 'string', - 'description' => 'Authentication user (optional)', - 'default' => "", - 'value' => "", - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_transport_smtp.password' => array( - 'type' => 'string', - 'description' => 'Authentication password (optional)', - 'default' => "", - 'value' => "", - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_css' => array( - 'type' => 'string', - 'description' => 'CSS that will override the standard stylesheet used for the notifications', - 'default' => "", - 'value' => "", - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'email_default_sender_address' => array( - 'type' => 'string', - 'description' => 'Default address provided in the email from header field.', - 'default' => "", - 'value' => "", - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'email_default_sender_label' => array( - 'type' => 'string', - 'description' => 'Default label provided in the email from header field.', - 'default' => "", - 'value' => "", - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'apc_cache.enabled' => array( - 'type' => 'bool', - 'description' => 'If set, the APC cache is allowed (the PHP extension must also be active)', - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'apc_cache.query_ttl' => array( - 'type' => 'integer', - 'description' => 'Time to live set in APC for the prepared queries (seconds - 0 means no timeout)', - 'default' => 3600, - 'value' => 3600, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'apc_cache_emulation.max_entries' => array( - 'type' => 'integer', - 'description' => 'Maximum number of cache entries (0 means no limit)', - 'default' => 1000, - 'value' => 1000, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'timezone' => array( - 'type' => 'string', - 'description' => 'Timezone (reference: http://php.net/manual/en/timezones.php). If empty, it will be left unchanged and MUST be explicitely configured in PHP', - // examples... not used (nor 'description') - 'examples' => array( - 'America/Sao_Paulo', - 'America/New_York (standing for EDT)', - 'America/Los_Angeles (standing for PDT)', - 'Asia/Istanbul', - 'Asia/Singapore', - 'Africa/Casablanca', - 'Australia/Sydney' - ), - 'default' => 'Europe/Paris', - 'value' => 'Europe/Paris', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'cas_include_path' => array( - 'type' => 'string', - 'description' => 'The path where to find the phpCAS library', - // examples... not used (nor 'description') - 'default' => '/usr/share/php', - 'value' => '/usr/share/php', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'cas_version' => array( - 'type' => 'string', - 'description' => 'The CAS protocol version to use: "1.0" (CAS v1), "2.0" (CAS v2) or "S1" (SAML V1) )', - // examples... not used (nor 'description') - 'default' => '2.0', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_host' => array( - 'type' => 'string', - 'description' => 'The name of the CAS host', - // examples... not used (nor 'description') - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_port' => array( - 'type' => 'integer', - 'description' => 'The port used by the CAS server', - // examples... not used (nor 'description') - 'default' => 443, - 'value' => 443, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_context' => array( - 'type' => 'string', - 'description' => 'The CAS context', - // examples... not used (nor 'description') - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_server_ca_cert_path' => array( - 'type' => 'string', - 'description' => 'The path where to find the certificate of the CA for validating the certificate of the CAS server', - // examples... not used (nor 'description') - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_logout_redirect_service' => array( - 'type' => 'string', - 'description' => 'The redirect service (URL) to use when logging-out with CAS', - // examples... not used (nor 'description') - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_memberof' => array( - 'type' => 'string', - 'description' => 'A semicolon separated list of group names that the user must be member of (works only with SAML - e.g. cas_version=> "S1")', - // examples... not used (nor 'description') - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_user_synchro' => array( - 'type' => 'bool', - 'description' => 'Whether or not to synchronize users with CAS/LDAP', - // examples... not used (nor 'description') - 'default' => 0, - 'value' => 0, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_update_profiles' => array( - 'type' => 'bool', - 'description' => 'Whether or not to update the profiles of an existing user from the CAS information', - // examples... not used (nor 'description') - 'default' => 0, - 'value' => 0, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_profile_pattern' => array( - 'type' => 'string', - 'description' => 'A regular expression pattern to extract the name of the iTop profile from the name of an LDAP/CAS group', - // examples... not used (nor 'description') - 'default' => '/^cn=([^,]+),/', - 'value' => '/^cn=([^,]+),/', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_default_profiles' => array( - 'type' => 'string', - 'description' => 'A semi-colon separated list of iTop Profiles to use when creating a new user if no profile is retrieved from CAS', - // examples... not used (nor 'description') - 'default' => 'Portal user', - 'value' => 'Portal user', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'cas_debug' => array( - 'type' => 'bool', - 'description' => 'Activate the CAS debug', - // examples... not used (nor 'description') - 'default' => false, - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'forgot_password' => array( - 'type' => 'bool', - 'description' => 'Enable the "Forgot password" feature', - // examples... not used (nor 'description') - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'forgot_password_from' => array( - 'type' => 'string', - 'description' => 'Sender email address for the "forgot password" feature. If empty, defaults to the recipient\'s email address.', - // examples... not used (nor 'description') - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'deadline_format' => array( - 'type' => 'string', - 'description' => 'The format used for displaying "deadline" attributes: any string with the following placeholders: $date$, $difference$', - // examples... $date$ ($deadline$) - 'default' => '$difference$', - 'value' => '$difference$', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'buttons_position' => array( - 'type' => 'string', - 'description' => 'Position of the forms buttons: bottom | top | both', - // examples... not used - 'default' => 'both', - 'value' => 'both', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'shortcut_actions' => array( - 'type' => 'string', - 'description' => 'Actions that are available as direct buttons next to the "Actions" menu', - // examples... not used - 'default' => 'UI:Menu:Modify,UI:Menu:New', - 'value' => 'UI:Menu:Modify', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'complex_actions_limit' => array( - 'type' => 'integer', - 'description' => 'Display the "actions" menu items that require long computation only if the list of objects is contains less objects than this number (0 means no limit)', - // examples... not used - 'default' => 50, - 'value' => 50, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'synchro_prevent_delete_all' => array( - 'type' => 'bool', - 'description' => 'Stop the synchro if all the replicas of a data source become obsolete at the same time.', - // examples... not used - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'source_dir' => array( - 'type' => 'string', - 'description' => 'Source directory for the datamodel files. (which gets compiled to env-production).', - // examples... not used - 'default' => '', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'csv_file_default_charset' => array( - 'type' => 'string', - 'description' => 'Character set used by default for downloading and uploading data as a CSV file. Warning: it is case sensitive (uppercase is preferable).', - // examples... not used - 'default' => 'ISO-8859-1', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'debug_report_spurious_chars' => array( - 'type' => 'bool', - 'description' => 'Report, in the error log, the characters found in the output buffer, echoed by mistake in the loaded modules, and potentially corrupting the output', - // examples... not used - 'default' => false, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'impact_analysis_first_tab' => array( - 'type' => 'string', - 'description' => 'Which tab to display first in the impact analysis view: list or graphics. Graphics are nicer but slower to display when there are many objects', - // examples... not used - 'default' => 'graphics', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'url_validation_pattern' => array( - 'type' => 'string', - 'description' => 'Regular expression to validate/detect the format of an URL (URL attributes and Wiki formatting for Text attributes)', - 'default' => '(https?|ftp)\://([a-zA-Z0-9+!*(),;?&=\$_.-]+(\:[a-zA-Z0-9+!*(),;?&=\$_.-]+)?@)?([a-zA-Z0-9-.]{3,})(\:[0-9]{2,5})?(/([a-zA-Z0-9%+\$_-]\.?)+)*/?(\?[a-zA-Z+&\$_.-][a-zA-Z0-9;:[\]@&%=+/\$_.-]*)?(#[a-zA-Z_.-][a-zA-Z0-9+\$_.-]*)?', - // SHEME.......... USER....................... PASSWORD.......................... HOST/IP........... PORT.......... PATH........................ GET............................................ ANCHOR............................ - // Example: http://User:passWord@127.0.0.1:8888/patH/Page.php?arrayArgument[2]=something:blah20#myAnchor - // Origin of this regexp: http://www.php.net/manual/fr/function.preg-match.php#93824 - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'email_validation_pattern' => array( - 'type' => 'string', - 'description' => 'Regular expression to validate/detect the format of an eMail address', - 'default' => "[a-zA-Z0-9._&'-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]{2,}", - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'email_decoration_class' => array( - 'type' => 'string', - 'description' => 'CSS class(es) to use as decoration for the HTML rendering of the attribute. eg. "fa fa-envelope" will put a mail icon.', - 'default' => 'fa fa-envelope', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'phone_number_validation_pattern' => array( - 'type' => 'string', - 'description' => 'Regular expression to validate/detect the format of a phone number', - 'default' => "[0-9.\-\ \+\(\)]+", - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'phone_number_url_pattern' => array( - 'type' => 'string', - 'description' => 'Format for phone number url, use %1$s as a placeholder for the value. eg. "tel:%1$s" for regular phone applications or "callto:%1$s" for Skype. Default is "tel:%1$s".', - 'default' => 'tel:%1$s', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'phone_number_decoration_class' => array( - 'type' => 'string', - 'description' => 'CSS class(es) to use as decoration for the HTML rendering of the attribute. eg. "fa fa-phone" will put a phone icon.', - 'default' => 'fa fa-phone', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'log_kpi_duration' => array( - 'type' => 'integer', - 'description' => 'Level of logging for troubleshooting performance issues (1 to enable, 2 +blame callers)', - // examples... not used - 'default' => 0, - 'value' => 0, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'log_kpi_memory' => array( - 'type' => 'integer', - 'description' => 'Level of logging for troubleshooting memory limit issues', - // examples... not used - 'default' => 0, - 'value' => 0, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'log_kpi_user_id' => array( - 'type' => 'string', - 'description' => 'Limit the scope of users to the given user id (* means no limit)', - // examples... not used - 'default' => '*', - 'value' => '*', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'max_linkset_output' => array( - 'type' => 'integer', - 'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.', - 'default' => 100, - 'value' => 100, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'demo_mode' => array( - 'type' => 'bool', - 'description' => 'Set to true to prevent users from changing passwords/languages', - 'default' => false, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'portal_tickets' => array( - 'type' => 'string', - 'description' => 'CSV list of classes supported in the portal', - // examples... not used - 'default' => 'UserRequest', - 'value' => 'UserRequest', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'portal_dispatch_urls' => array( - 'type' => 'array', - 'description' => 'Associative array of sPortalId => Home page URL (relatively to the application root)', - // examples... not used - 'default' => array(), - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'max_execution_time_per_loop' => array( - 'type' => 'integer', - 'description' => 'Maximum execution time requested, per loop, during bulk operations. Zero means no limit.', - // examples... not used - 'default' => 30, - 'value' => 30, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'max_history_length' => array( - 'type' => 'integer', - 'description' => 'Maximum length of the history table (in the "History" tab on each object) before it gets truncated. Latest modifications are displayed first.', - // examples... not used - 'default' => 50, - 'value' => 50, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'max_history_case_log_entry_length' => array( - 'type' => 'integer', - 'description' => 'The length (in number of characters) at which to truncate the (expandable) display (in the history) of a case log entry. If zero, the display in the history is not truncated.', - // examples... not used - 'default' => 60, - 'value' => 60, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'full_text_chunk_duration' => array( - 'type' => 'integer', - 'description' => 'Delay after which the results are displayed.', - // examples... not used - 'default' => 2, - 'value' => 2, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'full_text_accelerators' => array( - 'type' => 'array', - 'description' => 'Specifies classes to be searched at first (and the subset of data) when running the full text search.', - 'default' => array(), - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'full_text_needle_min' => array( - 'type' => 'integer', - 'description' => 'Minimum size of the full text needle.', - 'default' => 3, - 'value' => 3, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'tracking_level_linked_set_default' => array( - 'type' => 'integer', - 'description' => 'Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSet (defaults to NONE in case of a fresh install, LIST otherwise - this to preserve backward compatibility while upgrading from a version older than 2.0.3 - see TRAC #936)', - 'default' => LINKSET_TRACKING_LIST, - 'value' => LINKSET_TRACKING_LIST, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'tracking_level_linked_set_indirect_default' => array( - 'type' => 'integer', - 'description' => 'Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSetIndirect', - 'default' => LINKSET_TRACKING_ALL, - 'value' => LINKSET_TRACKING_ALL, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'user_rights_legacy' => array( - 'type' => 'bool', - 'description' => 'Set to true to restore the buggy algorithm for the computation of user rights (within the same profile, ALLOW on the class itself has precedence on DENY of a parent class)', - 'default' => false, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'xlsx_exporter_memory_limit' => array( - 'type' => 'string', - 'description' => 'Memory limit to use when (interactively) exporting data to Excel', - 'default' => '2048M', // Huuuuuuge 2GB! - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'min_reload_interval' => array( - 'type' => 'integer', - 'description' => 'Minimum refresh interval (seconds) for dashboards, shortcuts, etc. Even if the interval is set programmatically, it is forced to that minimum', - 'default' => 5, // In iTop 2.0.3, this was the hardcoded value - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'relations_max_depth' => array( - 'type' => 'integer', - 'description' => 'Maximum number of successive levels (depth) to explore when displaying the impact/depends on relations.', - 'default' => 20, // In iTop 2.0.3, this was the hardcoded value - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'transaction_storage' => array( - 'type' => 'string', - 'description' => 'The type of mechanism to use for storing the unique identifiers for transactions (Session|File).', - 'default' => 'Session', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'transactions_enabled' => array( - 'type' => 'bool', - 'description' => 'Whether or not the whole mechanism to prevent multiple submissions of a page is enabled.', - 'default' => true, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'log_transactions' => array( - 'type' => 'bool', - 'description' => 'Whether or not to enable the debug log for the transactions.', - 'default' => false, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'concurrent_lock_enabled' => array( - 'type' => 'bool', - 'description' => 'Whether or not to activate the locking mechanism in order to prevent concurrent edition of the same object.', - 'default' => false, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'concurrent_lock_expiration_delay' => array( - 'type' => 'integer', - 'description' => 'Delay (in seconds) for a concurrent lock to expire', - 'default' => 120, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'concurrent_lock_override_profiles' => array( - 'type' => 'array', - 'description' => 'The list of profiles allowed to "kill" a lock', - 'default' => array('Administrator'), - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'html_sanitizer' => array( - 'type' => 'string', - 'description' => 'The class to use for HTML sanitization: HTMLDOMSanitizer, HTMLPurifierSanitizer or HTMLNullSanitizer', - 'default' => 'HTMLDOMSanitizer', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'inline_image_max_display_width' => array( - 'type' => 'integer', - 'description' => 'The maximum width (in pixels) when displaying images inside an HTML formatted attribute. Images will be displayed using this this maximum width.', - 'default' => '250', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'inline_image_max_storage_width' => array( - 'type' => 'integer', - 'description' => 'The maximum width (in pixels) when uploading images to be used inside an HTML formatted attribute. Images larger than the given size will be downsampled before storing them in the database.', - 'default' => '1600', - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'draft_attachments_lifetime' => array( - 'type' => 'integer', - 'description' => 'Lifetime (in seconds) of drafts\' attachments and inline images: after this duration, the garbage collector will delete them.', - 'default' => 3600, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'date_and_time_format' => array( - 'type' => 'array', - 'description' => 'Format for date and time display (per language)', - 'default' => array('default' => array('date' => 'Y-m-d', 'time' => 'H:i:s', 'date_time' => '$date $time')), - 'value' => false, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'breadcrumb.max_count' => array( - 'type' => 'integer', - 'description' => 'Maximum number of items kept in the history breadcrumb. Set it to 0 to entirely disable the breadcrumb.', - 'default' => 8, - 'value' => 8, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'obsolescence.show_obsolete_data' => array( - 'type' => 'bool', - 'description' => 'Default value for the user preference "show obsolete data"', - 'default' => false, - 'value' => '', - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'obsolescence.date_update_interval' => array( - 'type' => 'integer', - 'description' => 'Delay in seconds between two refreshes of the obsolescence dates.', - 'default' => 600, - 'value' => 600, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'disable_attachments_download_legacy_portal' => array( - 'type' => 'bool', - 'description' => 'Disable attachments download from legacy portal', - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'secure_rest_services' => array( - 'type' => 'bool', - 'description' => 'When set to true, only the users with the profile "REST Services User" are allowed to use the REST web services.', - 'default' => true, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), - 'search_manual_submit' => array( - 'type' => 'array', - 'description' => 'Force manual submit of search all requests', - 'default' => false, - 'value' => true, - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - 'high_cardinality_classes' => array( - 'type' => 'array', - 'description' => 'List of classes with high cardinality (Force manual submit of search)', - 'default' => array(), - 'value' => array(), - 'source_of_value' => '', - 'show_in_conf_sample' => true, - ), - ); - - public function IsProperty($sPropCode) - { - return (array_key_exists($sPropCode, $this->m_aSettings)); - } - - /** - * @return string identifier that can be used for example to name WebStorage/SessionStorage keys (they - * are related to a whole domain, and a domain can host multiple itop) - * Beware: do not expose server side information to the client ! - */ - public function GetItopInstanceid() - { - return md5(utils::GetAbsoluteUrlAppRoot() - .'==='.$this->Get('db_host') - .'/'.$this->Get('db_name') - .'/'.$this->Get('db_subname')); - } - - public function GetDescription($sPropCode) - { - return $this->m_aSettings[$sPropCode]; - } - - /** - * @param string $sPropCode - * @param mixed $value - * @param string $sSourceDesc mandatory for variables with show_in_conf_sample=false - * - * @throws \CoreException - */ - public function Set($sPropCode, $value, $sSourceDesc = 'unknown') - { - $sType = $this->m_aSettings[$sPropCode]['type']; - switch ($sType) - { - case 'bool': - $value = (bool)$value; - break; - case 'string': - $value = (string)$value; - break; - case 'integer': - $value = (integer)$value; - break; - case 'float': - $value = (float)$value; - break; - case 'array': - break; - default: - throw new CoreException('Unknown type for setting', array('property' => $sPropCode, 'type' => $sType)); - } - $this->m_aSettings[$sPropCode]['value'] = $value; - $this->m_aSettings[$sPropCode]['source_of_value'] = $sSourceDesc; - - } - - /** - * @param string $sPropCode - * - * @return mixed - */ - public function Get($sPropCode) - { - return $this->m_aSettings[$sPropCode]['value']; - } - - /** - * Event log options (see LOG_... definition) - */ - // Those variables will be deprecated later, when the transition to ...Get('my_setting') will be done - protected $m_bLogGlobal; - protected $m_bLogNotification; - protected $m_bLogIssue; - protected $m_bLogWebService; - protected $m_bQueryCacheEnabled; // private setting - - /** - * @var integer Number of elements to be displayed when there are more than m_iMaxDisplayLimit elements - */ - protected $m_iMinDisplayLimit; - /** - * @var integer Max number of elements before truncating the display - */ - protected $m_iMaxDisplayLimit; - - /** - * @var integer Number of seconds between two reloads of the display (standard) - */ - protected $m_iStandardReloadInterval; - /** - * @var integer Number of seconds between two reloads of the display (fast) - */ - protected $m_iFastReloadInterval; - - /** - * @var boolean Whether or not a secure connection is required for using the application. - * If set, any attempt to connect to an iTop page with http:// will be redirected - * to https:// - */ - protected $m_bSecureConnectionRequired; - - /** - * @var string Langage code, default if the user language is undefined - */ - protected $m_sDefaultLanguage; - - /** - * @var string Type of login process allowed: form|basic|url|external - */ - protected $m_sAllowedLoginTypes; - - /** - * @var string Name of the PHP variable in which external authentication information is passed by the web server - */ - protected $m_sExtAuthVariable; - - /** - * @var string Encryption key used for all attributes of type "encrypted string". Can be set to a random value - * unless you want to import a database from another iTop instance, in which case you must use - * the same encryption key in order to properly decode the encrypted fields - */ - protected $m_sEncryptionKey; - - /** - * @var string Encryption key used for all attributes of type "encrypted string". Can be set to a random value - * unless you want to import a database from another iTop instance, in which case you must use - * the same encryption key in order to properly decode the encrypted fields - */ - protected $m_sEncryptionLibrary; - - /** - * @var array Additional character sets to be supported by the interactive CSV import - * 'iconv_code' => 'display name' - */ - protected $m_aCharsets; - - /** - * Config constructor. - * - * @param string|null $sConfigFile - * @param bool $bLoadConfig - * - * @throws \ConfigException - * @throws \CoreException - */ - public function __construct($sConfigFile = null, $bLoadConfig = true) - { - $this->m_sFile = $sConfigFile; - if (is_null($sConfigFile)) - { - $bLoadConfig = false; - } - - $this->m_aAddons = array( - // Default AddOn, always present can be moved to an official iTop Module later if needed - 'user rights' => 'addons/userrights/userrightsprofile.class.inc.php', - ); - - foreach ($this->m_aSettings as $sPropCode => $aSettingInfo) - { - $this->m_aSettings[$sPropCode]['value'] = $aSettingInfo['default']; - } - - $this->m_bLogGlobal = DEFAULT_LOG_GLOBAL; - $this->m_bLogNotification = DEFAULT_LOG_NOTIFICATION; - $this->m_bLogIssue = DEFAULT_LOG_ISSUE; - $this->m_bLogWebService = DEFAULT_LOG_WEB_SERVICE; - $this->m_iMinDisplayLimit = DEFAULT_MIN_DISPLAY_LIMIT; - $this->m_iMaxDisplayLimit = DEFAULT_MAX_DISPLAY_LIMIT; - $this->m_iStandardReloadInterval = DEFAULT_STANDARD_RELOAD_INTERVAL; - $this->m_iFastReloadInterval = DEFAULT_FAST_RELOAD_INTERVAL; - $this->m_bSecureConnectionRequired = DEFAULT_SECURE_CONNECTION_REQUIRED; - $this->m_sDefaultLanguage = 'EN US'; - $this->m_sAllowedLoginTypes = DEFAULT_ALLOWED_LOGIN_TYPES; - $this->m_sExtAuthVariable = DEFAULT_EXT_AUTH_VARIABLE; - $this->m_aCharsets = array(); - $this->m_bQueryCacheEnabled = DEFAULT_QUERY_CACHE_ENABLED; - - //define default encryption params according to php install - $aEncryptParams = SimpleCrypt::GetNewDefaultParams(); - $this->m_sEncryptionLibrary = isset($aEncryptParams['lib']) ? $aEncryptParams['lib'] : DEFAULT_ENCRYPTION_LIB; - $this->m_sEncryptionKey= isset($aEncryptParams['key']) ? $aEncryptParams['key'] : DEFAULT_ENCRYPTION_KEY; - - $this->m_aModuleSettings = array(); - - if ($bLoadConfig) - { - $this->Load($sConfigFile); - $this->Verify(); - } - - // Application root url: set a default value, then normalize it - /* - * Does not work in CLI/unattended mode - $sAppRootUrl = trim($this->Get('app_root_url')); - if (strlen($sAppRootUrl) == 0) - { - $sAppRootUrl = utils::GetDefaultUrlAppRoot(); - } - if (substr($sAppRootUrl, -1, 1) != '/') - { - $sAppRootUrl .= '/'; - } - $this->Set('app_root_url', $sAppRootUrl); - */ - } - - /** - * @param string $sPurpose - * @param string $sFileName - * - * @throws \ConfigException - */ - protected function CheckFile($sPurpose, $sFileName) - { - if (!file_exists($sFileName)) - { - throw new ConfigException("Could not find $sPurpose file", array('file' => $sFileName)); - } - if (!is_readable($sFileName)) - { - throw new ConfigException("Could not read $sPurpose file (the file exists but cannot be read). Do you have the rights to access this file?", - array('file' => $sFileName)); - } - } - - /** - * @param string $sConfigFile - * - * @throws \ConfigException - * @throws \CoreException - */ - protected function Load($sConfigFile) - { - $this->CheckFile('configuration', $sConfigFile); - - $sConfigCode = trim(file_get_contents($sConfigFile)); - - // Variables created when doing an eval() on the config file - /** @var array $MySettings */ - $MySettings = null; - /** @var array $MyModuleSettings */ - $MyModuleSettings = null; - /** @var array $MyModules */ - $MyModules = null; - - // This does not work on several lines - // preg_match('/^<\\?php(.*)\\?'.'>$/', $sConfigCode, $aMatches)... - // So, I've implemented a solution suggested in the PHP doc (search for phpWrapper) - try - { - ob_start(); - eval('?'.'>'.trim($sConfigCode)); - $sNoise = trim(ob_get_contents()); - ob_end_clean(); - } - catch (Exception $e) - { - // well, never reach in case of parsing error :-( - // will be improved in PHP 6 ? - throw new ConfigException('Error in configuration file', - array('file' => $sConfigFile, 'error' => $e->getMessage())); - } - catch(Error $e) - { - // PHP 7 - throw new ConfigException('Error in configuration file', - array('file' => $sConfigFile, 'error' => $e->getMessage().' at line '.$e->getLine())); - } - if (strlen($sNoise) > 0) - { - // Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack) - throw new ConfigException('Syntax error in configuration file', - array('file' => $sConfigFile, 'error' => ''.htmlentities($sNoise, ENT_QUOTES, 'UTF-8').'')); - } - - if (!isset($MySettings) || !is_array($MySettings)) - { - throw new ConfigException('Missing array in configuration file', - array('file' => $sConfigFile, 'expected' => '$MySettings')); - } - - if (!array_key_exists('addons', $MyModules)) - { - throw new ConfigException('Missing item in configuration file', - array('file' => $sConfigFile, 'expected' => '$MyModules[\'addons\']')); - } - if (!array_key_exists('user rights', $MyModules['addons'])) - { - // Add one, by default - $MyModules['addons']['user rights'] = '/addons/userrights/userrightsnull.class.inc.php'; - } - - $this->m_aAddons = $MyModules['addons']; - - foreach ($MySettings as $sPropCode => $rawvalue) - { - if ($this->IsProperty($sPropCode)) - { - if (is_string($rawvalue)) - { - $value = trim($rawvalue); - } - else - { - $value = $rawvalue; - } - $this->Set($sPropCode, $value, $sConfigFile); - } - } - - $this->m_bLogGlobal = isset($MySettings['log_global']) ? (bool)trim($MySettings['log_global']) : DEFAULT_LOG_GLOBAL; - $this->m_bLogNotification = isset($MySettings['log_notification']) ? (bool)trim($MySettings['log_notification']) : DEFAULT_LOG_NOTIFICATION; - $this->m_bLogIssue = isset($MySettings['log_issue']) ? (bool)trim($MySettings['log_issue']) : DEFAULT_LOG_ISSUE; - $this->m_bLogWebService = isset($MySettings['log_web_service']) ? (bool)trim($MySettings['log_web_service']) : DEFAULT_LOG_WEB_SERVICE; - $this->m_bQueryCacheEnabled = isset($MySettings['query_cache_enabled']) ? (bool)trim($MySettings['query_cache_enabled']) : DEFAULT_QUERY_CACHE_ENABLED; - - $this->m_iMinDisplayLimit = isset($MySettings['min_display_limit']) ? trim($MySettings['min_display_limit']) : DEFAULT_MIN_DISPLAY_LIMIT; - $this->m_iMaxDisplayLimit = isset($MySettings['max_display_limit']) ? trim($MySettings['max_display_limit']) : DEFAULT_MAX_DISPLAY_LIMIT; - $this->m_iStandardReloadInterval = isset($MySettings['standard_reload_interval']) ? trim($MySettings['standard_reload_interval']) : DEFAULT_STANDARD_RELOAD_INTERVAL; - $this->m_iFastReloadInterval = isset($MySettings['fast_reload_interval']) ? trim($MySettings['fast_reload_interval']) : DEFAULT_FAST_RELOAD_INTERVAL; - $this->m_bSecureConnectionRequired = isset($MySettings['secure_connection_required']) ? (bool)trim($MySettings['secure_connection_required']) : DEFAULT_SECURE_CONNECTION_REQUIRED; - - $this->m_aModuleSettings = isset($MyModuleSettings) ? $MyModuleSettings : array(); - - $this->m_sDefaultLanguage = isset($MySettings['default_language']) ? trim($MySettings['default_language']) : 'EN US'; - $this->m_sAllowedLoginTypes = isset($MySettings['allowed_login_types']) ? trim($MySettings['allowed_login_types']) : DEFAULT_ALLOWED_LOGIN_TYPES; - $this->m_sExtAuthVariable = isset($MySettings['ext_auth_variable']) ? trim($MySettings['ext_auth_variable']) : DEFAULT_EXT_AUTH_VARIABLE; - $this->m_sEncryptionKey = isset($MySettings['encryption_key']) ? trim($MySettings['encryption_key']) : $this->m_sEncryptionKey; - $this->m_sEncryptionLibrary = isset($MySettings['encryption_library']) ? trim($MySettings['encryption_library']) : $this->m_sEncryptionLibrary; - $this->m_aCharsets = isset($MySettings['csv_import_charsets']) ? $MySettings['csv_import_charsets'] : array(); - } - - protected function Verify() - { - // Files are verified later on, just before using them -see MetaModel::Plugin() - // (we have their final path at that point) - } - - public function GetModuleSetting($sModule, $sProperty, $defaultvalue = null) - { - if (isset($this->m_aModuleSettings[$sModule][$sProperty])) - { - return $this->m_aModuleSettings[$sModule][$sProperty]; - } - - // Fall back to the predefined XML parameter, if any - return $this->GetModuleParameter($sModule, $sProperty, $defaultvalue); - } - - /** - * @param string $sModule - * @param string $sProperty - * @param mixed|null $defaultvalue - * - * @return mixed|null - */ - public function GetModuleParameter($sModule, $sProperty, $defaultvalue = null) - { - $ret = $defaultvalue; - if (class_exists('ModulesXMLParameters')) - { - $aAllParams = ModulesXMLParameters::GetData($sModule); - if (array_key_exists($sProperty, $aAllParams)) - { - $ret = $aAllParams[$sProperty]; - } - } - - return $ret; - } - - public function SetModuleSetting($sModule, $sProperty, $value) - { - $this->m_aModuleSettings[$sModule][$sProperty] = $value; - } - - public function GetAddons() - { - return $this->m_aAddons; - } - - public function SetAddons($aAddons) - { - $this->m_aAddons = $aAddons; - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 - * @see Config::Get() as a replacement - */ - public function GetDBHost() - { - return $this->Get('db_host'); - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 - * @see Config::Get() as a replacement - */ - public function GetDBName() - { - return $this->Get('db_name'); - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 - * @see Config::Get() as a replacement - */ - public function GetDBSubname() - { - return $this->Get('db_subname'); - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 #1001 utf8mb4 switch - * @see Config::DEFAULT_CHARACTER_SET - */ - public function GetDBCharacterSet() - { - return DEFAULT_CHARACTER_SET; - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 #1001 utf8mb4 switch - * @see Config::DEFAULT_COLLATION - */ - public function GetDBCollation() - { - return DEFAULT_COLLATION; - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 - * @see Config::Get() as a replacement - */ - public function GetDBUser() - { - return $this->Get('db_user'); - } - - /** - * @return string - * - * @deprecated 2.5 will be removed in 2.6 - * @see Config::Get() as a replacement - */ - public function GetDBPwd() - { - return $this->Get('db_pwd'); - } - - public function GetLogGlobal() - { - return $this->m_bLogGlobal; - } - - public function GetLogNotification() - { - return $this->m_bLogNotification; - } - - public function GetLogIssue() - { - return $this->m_bLogIssue; - } - - public function GetLogWebService() - { - return $this->m_bLogWebService; - } - - public function GetLogQueries() - { - return false; - } - - public function GetQueryCacheEnabled() - { - return $this->m_bQueryCacheEnabled; - } - - public function GetMinDisplayLimit() - { - return $this->m_iMinDisplayLimit; - } - - public function GetMaxDisplayLimit() - { - return $this->m_iMaxDisplayLimit; - } - - public function GetStandardReloadInterval() - { - return $this->m_iStandardReloadInterval; - } - - public function GetFastReloadInterval() - { - return $this->m_iFastReloadInterval; - } - - public function GetSecureConnectionRequired() - { - return $this->m_bSecureConnectionRequired; - } - - public function GetDefaultLanguage() - { - return $this->m_sDefaultLanguage; - } - - public function GetEncryptionKey() - { - return $this->m_sEncryptionKey; - } - - public function GetEncryptionLibrary() - { - return $this->m_sEncryptionLibrary; - } - - public function GetAllowedLoginTypes() - { - return explode('|', $this->m_sAllowedLoginTypes); - } - - public function GetExternalAuthenticationVariable() - { - return $this->m_sExtAuthVariable; - } - - public function GetCSVImportCharsets() - { - return $this->m_aCharsets; - } - - public function SetLogGlobal($iLogGlobal) - { - $this->m_iLogGlobal = $iLogGlobal; - } - - public function SetLogNotification($iLogNotification) - { - $this->m_iLogNotification = $iLogNotification; - } - - public function SetLogIssue($iLogIssue) - { - $this->m_iLogIssue = $iLogIssue; - } - - public function SetLogWebService($iLogWebService) - { - $this->m_iLogWebService = $iLogWebService; - } - - public function SetMinDisplayLimit($iMinDisplayLimit) - { - $this->m_iMinDisplayLimit = $iMinDisplayLimit; - } - - public function SetMaxDisplayLimit($iMaxDisplayLimit) - { - $this->m_iMaxDisplayLimit = $iMaxDisplayLimit; - } - - public function SetStandardReloadInterval($iStandardReloadInterval) - { - $this->m_iStandardReloadInterval = $iStandardReloadInterval; - } - - public function SetFastReloadInterval($iFastReloadInterval) - { - $this->m_iFastReloadInterval = $iFastReloadInterval; - } - - public function SetSecureConnectionRequired($bSecureConnectionRequired) - { - $this->m_bSecureConnectionRequired = $bSecureConnectionRequired; - } - - public function SetDefaultLanguage($sLanguageCode) - { - $this->m_sDefaultLanguage = $sLanguageCode; - } - - public function SetAllowedLoginTypes($aAllowedLoginTypes) - { - $this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes); - } - - public function SetExternalAuthenticationVariable($sExtAuthVariable) - { - $this->m_sExtAuthVariable = $sExtAuthVariable; - } - - public function SetEncryptionKey($sKey) - { - $this->m_sEncryptionKey = $sKey; - } - - public function SetCSVImportCharsets($aCharsets) - { - $this->m_aCharsets = $aCharsets; - } - - public function AddCSVImportCharset($sIconvCode, $sDisplayName) - { - $this->m_aCharsets[$sIconvCode] = $sDisplayName; - } - - public function GetLoadedFile() - { - if (is_null($this->m_sFile)) - { - return ''; - } - else - { - return $this->m_sFile; - } - } - - /** - * Render the configuration as an associative array - * - * @return array - */ - public function ToArray() - { - $aSettings = array(); - foreach ($this->m_aSettings as $sPropCode => $aSettingInfo) - { - $aSettings[$sPropCode] = $aSettingInfo['value']; - } - $aSettings['log_global'] = $this->m_bLogGlobal; - $aSettings['log_notification'] = $this->m_bLogNotification; - $aSettings['log_issue'] = $this->m_bLogIssue; - $aSettings['log_web_service'] = $this->m_bLogWebService; - $aSettings['query_cache_enabled'] = $this->m_bQueryCacheEnabled; - $aSettings['min_display_limit'] = $this->m_iMinDisplayLimit; - $aSettings['max_display_limit'] = $this->m_iMaxDisplayLimit; - $aSettings['standard_reload_interval'] = $this->m_iStandardReloadInterval; - $aSettings['fast_reload_interval'] = $this->m_iFastReloadInterval; - $aSettings['secure_connection_required'] = $this->m_bSecureConnectionRequired; - $aSettings['default_language'] = $this->m_sDefaultLanguage; - $aSettings['allowed_login_types'] = $this->m_sAllowedLoginTypes; - $aSettings['ext_auth_variable'] = $this->m_sExtAuthVariable; - $aSettings['encryption_key'] = $this->m_sEncryptionKey; - $aSettings['encryption_library'] = $this->m_sEncryptionLibrary; - $aSettings['csv_import_charsets'] = $this->m_aCharsets; - - foreach ($this->m_aModuleSettings as $sModule => $aProperties) - { - foreach ($aProperties as $sProperty => $value) - { - $aSettings['module_settings'][$sModule][$sProperty] = $value; - } - } - foreach ($this->m_aAddons as $sKey => $sFile) - { - $aSettings['addon_list'][] = $sFile; - } - - return $aSettings; - } - - /** - * Write the configuration to a file (php format) that can be reloaded later - * By default write to the same file that was specified when constructing the object - * - * @param string $sFileName string Name of the file to write to (emtpy to write to the same file) - * - * @return boolean True otherwise throws an Exception - * - * @throws \ConfigException - */ - public function WriteToFile($sFileName = '') - { - if (empty($sFileName)) - { - $sFileName = $this->m_sFile; - } - $hFile = @fopen($sFileName, 'w'); - if ($hFile !== false) - { - fwrite($hFile, "m_aSettings; - - // Old fashioned boolean settings - $aBoolValues = array( - 'log_global' => $this->m_bLogGlobal, - 'log_notification' => $this->m_bLogNotification, - 'log_issue' => $this->m_bLogIssue, - 'log_web_service' => $this->m_bLogWebService, - 'query_cache_enabled' => $this->m_bQueryCacheEnabled, - 'secure_connection_required' => $this->m_bSecureConnectionRequired, - ); - foreach ($aBoolValues as $sKey => $bValue) - { - $aConfigSettings[$sKey] = array( - 'show_in_conf_sample' => true, - 'type' => 'bool', - 'value' => $bValue, - ); - } - - // Old fashioned integer settings - $aIntValues = array( - 'fast_reload_interval' => $this->m_iFastReloadInterval, - 'max_display_limit' => $this->m_iMaxDisplayLimit, - 'min_display_limit' => $this->m_iMinDisplayLimit, - 'standard_reload_interval' => $this->m_iStandardReloadInterval, - ); - foreach ($aIntValues as $sKey => $iValue) - { - $aConfigSettings[$sKey] = array( - 'show_in_conf_sample' => true, - 'type' => 'integer', - 'value' => $iValue, - ); - } - - // Old fashioned remaining values - $aOtherValues = array( - 'default_language' => $this->m_sDefaultLanguage, - 'allowed_login_types' => $this->m_sAllowedLoginTypes, - 'ext_auth_variable' => $this->m_sExtAuthVariable, - 'encryption_key' => $this->m_sEncryptionKey, - 'encryption_library' => $this->m_sEncryptionLibrary, - 'csv_import_charsets' => $this->m_aCharsets, - ); - foreach ($aOtherValues as $sKey => $value) - { - $aConfigSettings[$sKey] = array( - 'show_in_conf_sample' => true, - 'type' => is_string($value) ? 'string' : 'mixed', - 'value' => $value, - ); - } - - ksort($aConfigSettings); - fwrite($hFile, "\$MySettings = array(\n"); - foreach ($aConfigSettings as $sPropCode => $aSettingInfo) - { - // Write all values that are either always visible or present in the cloned config file - if ($aSettingInfo['show_in_conf_sample'] || (!empty($aSettingInfo['source_of_value']) && ($aSettingInfo['source_of_value'] != 'unknown'))) - { - $sType = $aSettingInfo['type']; - switch ($sType) - { - case 'bool': - $sSeenAs = $aSettingInfo['value'] ? 'true' : 'false'; - break; - default: - $sSeenAs = self::PrettyVarExport($aSettingInfo['value'], "\t"); - } - fwrite($hFile, "\n"); - if (isset($aSettingInfo['description'])) - { - fwrite($hFile, "\t// $sPropCode: {$aSettingInfo['description']}\n"); - } - if (isset($aSettingInfo['default'])) - { - $default = $aSettingInfo['default']; - if ($aSettingInfo['type'] == 'bool') - { - $default = $default ? 'true' : 'false'; - } - fwrite($hFile, - "\t//\tdefault: ".self::PrettyVarExport($aSettingInfo['default'], "\t//\t\t", true)."\n"); - } - fwrite($hFile, "\t'$sPropCode' => $sSeenAs,\n"); - } - } - fwrite($hFile, ");\n"); - - fwrite($hFile, "\n"); - fwrite($hFile, "/**\n *\n * Modules specific settings\n *\n */\n"); - fwrite($hFile, "\$MyModuleSettings = array(\n"); - foreach ($this->m_aModuleSettings as $sModule => $aProperties) - { - fwrite($hFile, "\t'$sModule' => array (\n"); - foreach ($aProperties as $sProperty => $value) - { - $sNiceExport = self::PrettyVarExport($value, "\t\t"); - fwrite($hFile, "\t\t'$sProperty' => $sNiceExport,\n"); - } - fwrite($hFile, "\t),\n"); - } - fwrite($hFile, ");\n"); - - fwrite($hFile, "\n/**\n"); - fwrite($hFile, " *\n"); - fwrite($hFile, " * Data model modules to be loaded. Names are specified as relative paths\n"); - fwrite($hFile, " *\n"); - fwrite($hFile, " */\n"); - fwrite($hFile, "\$MyModules = array(\n"); - fwrite($hFile, "\t'addons' => array (\n"); - foreach ($this->m_aAddons as $sKey => $sFile) - { - fwrite($hFile, "\t\t'$sKey' => '$sFile',\n"); - } - fwrite($hFile, "\t),\n"); - fwrite($hFile, ");\n"); - fwrite($hFile, '?'.'>'); // Avoid perturbing the syntax highlighting ! - - return fclose($hFile); - } - else - { - throw new ConfigException("Could not write to configuration file", array('file' => $sFileName)); - } - } - - /** - * Helper function to initialize a configuration from the page arguments - * - * @param array $aParamValues - * @param string|null $sModulesDir - * @param bool $bPreserveModuleSettings - * - * @throws \Exception - * @throws \CoreException - */ - public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false) - { - if (isset($aParamValues['application_path'])) - { - $this->Set('app_root_url', $aParamValues['application_path']); - } - if (isset($aParamValues['graphviz_path'])) - { - $this->Set('graphviz_path', $aParamValues['graphviz_path']); - } - if (isset($aParamValues['mode']) && isset($aParamValues['language'])) - { - if (($aParamValues['mode'] == 'install') || $this->GetDefaultLanguage() == '') - { - $this->SetDefaultLanguage($aParamValues['language']); - } - } - if (isset($aParamValues['db_server'])) - { - $this->Set('db_host', $aParamValues['db_server']); - $this->Set('db_user', $aParamValues['db_user']); - $this->Set('db_pwd', $aParamValues['db_pwd']); - $sDBName = $aParamValues['db_name']; - if ($sDBName == '') - { - // Todo - obsolete after the transition to the new setup (2.0) is complete (WARNING: used by the designer) - if (isset($aParamValues['new_db_name'])) - { - $sDBName = $aParamValues['new_db_name']; - } - } - $this->Set('db_name', $sDBName); - $this->Set('db_subname', $aParamValues['db_prefix']); - - $bDbTlsEnabled = (bool) $aParamValues['db_tls_enabled']; - if ($bDbTlsEnabled) - { - $this->Set('db_tls.enabled', $bDbTlsEnabled, 'UpdateFromParams'); - } - else - { - // disabled : we don't want parameter in the file - $this->Set('db_tls.enabled', $bDbTlsEnabled, null); - } - $sDbTlsCa = $bDbTlsEnabled ? $aParamValues['db_tls_ca'] : null; - if (isset($sDbTlsCa) && !empty($sDbTlsCa)) { - $this->Set('db_tls.ca', $sDbTlsCa, 'UpdateFromParams'); - } else { - // empty parameter : we don't want it in the file - $this->Set('db_tls.ca', null, null); - } - } - - if (isset($aParamValues['selected_modules'])) - { - $aSelectedModules = explode(',', $aParamValues['selected_modules']); - } - else - { - $aSelectedModules = null; - } - $this->UpdateIncludes($sModulesDir, $aSelectedModules); - - if (isset($aParamValues['source_dir'])) - { - $this->Set('source_dir', $aParamValues['source_dir']); - } - } - - /** - * Helper function to rebuild the default configuration and the list of includes from a directory and a list of - * selected modules - * - * @param string $sModulesDir The relative path to the directory to scan for modules (typically the 'env-xxx' - * directory resulting from the compilation) - * @param array $aSelectedModules An array of selected modules' identifiers. If null all modules found will be - * considered as installed - * - * @throws Exception - */ - public function UpdateIncludes($sModulesDir, $aSelectedModules = null) - { - if (!is_null($sModulesDir)) - { - // Initialize the arrays below with default values for the application... - $oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values - $aAddOns = $oEmptyConfig->GetAddOns(); - - $aModules = ModuleDiscovery::GetAvailableModules(array(APPROOT.$sModulesDir)); - foreach ($aModules as $sModuleId => $aModuleInfo) - { - list ($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); - if (is_null($aSelectedModules) || in_array($sModuleName, $aSelectedModules)) - { - if (isset($aModuleInfo['settings'])) - { - list ($sName, $sVersion) = ModuleDiscovery::GetModuleName($sModuleId); - foreach ($aModuleInfo['settings'] as $sProperty => $value) - { - if (isset($this->m_aModuleSettings[$sName][$sProperty])) - { - // Do nothing keep the original value - } - else - { - $this->SetModuleSetting($sName, $sProperty, $value); - } - } - } - if (isset($aModuleInfo['installer'])) - { - $sModuleInstallerClass = $aModuleInfo['installer']; - if (!class_exists($sModuleInstallerClass)) - { - throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); - } - if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) - { - throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); - } - $aCallSpec = array($sModuleInstallerClass, 'BeforeWritingConfig'); - call_user_func_array($aCallSpec, array($this)); - } - } - } - $this->SetAddOns($aAddOns); - } - } - - /** - * Helper: for an array of string, change the prefix when found - * - * @param array $aStrings - * @param string $sSearchPrefix - * @param string $sNewPrefix - */ - protected static function ChangePrefix(&$aStrings, $sSearchPrefix, $sNewPrefix) - { - foreach ($aStrings as &$sFile) - { - if (substr($sFile, 0, strlen($sSearchPrefix)) == $sSearchPrefix) - { - $sFile = $sNewPrefix.substr($sFile, strlen($sSearchPrefix)); - } - } - } - - /** - * Obsolete: kept only for backward compatibility of the Toolkit - * Quick and dirty way to clone a config file into another environment - * - * @param string $sSourceEnv - * @param string $sTargetEnv - */ - public function ChangeModulesPath($sSourceEnv, $sTargetEnv) - { - // Now does nothing since the includes are built into the environment itself - } - - /** - * Pretty format a var_export'ed value so that (if possible) the identation is preserved on every line - * - * @param mixed $value The value to export - * @param string $sIndentation The string to use to indent the text - * @param bool $bForceIndentation Forces the identation (enven if it breaks/changes an eval, for example to ouput a - * value inside a comment) - * - * @return string The indented export string - */ - protected static function PrettyVarExport($value, $sIndentation, $bForceIndentation = false) - { - $sExport = var_export($value, true); - $sNiceExport = str_replace(array("\r\n", "\n", "\r"), "\n".$sIndentation, trim($sExport)); - if (!$bForceIndentation) - { - /** @var array $aImported */ - $aImported = null; - eval('$aImported='.$sNiceExport.';'); - // Check if adding the identations at the beginning of each line - // did not modify the values (in case of a string containing a line break) - if ($aImported != $value) - { - $sNiceExport = $sExport; - } - } - - return $sNiceExport; - } - -} + + + +define('ITOP_APPLICATION', 'iTop'); +define('ITOP_APPLICATION_SHORT', 'iTop'); +define('ITOP_VERSION', '2.6.0-dev'); +define('ITOP_REVISION', 'svn'); +define('ITOP_BUILD_DATE', '$WCNOW$'); + +define('ACCESS_USER_WRITE', 1); +define('ACCESS_ADMIN_WRITE', 2); +define('ACCESS_FULL', ACCESS_USER_WRITE | ACCESS_ADMIN_WRITE); +define('ACCESS_READONLY', 0); + +/** + * Configuration read/write + * + * @copyright Copyright (C) 2010-2018 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('coreexception.class.inc.php'); +require_once('attributedef.class.inc.php'); // For the defines +require_once('simplecrypt.class.inc.php'); + +class ConfigException extends CoreException +{ +} + +// was utf8 but it only supports BMP chars (https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html) +// so we switched to utf8mb4 in iTop 2.5, adding dependency to MySQL 5.5.3 +// The config params db_character_set and db_collation were introduced as a temporary workaround and removed in iTop 2.5 +// now everything uses those fixed value ! +define('DEFAULT_CHARACTER_SET', 'utf8mb4'); +define('DEFAULT_COLLATION', 'utf8mb4_unicode_ci'); + +define('DEFAULT_LOG_GLOBAL', true); +define('DEFAULT_LOG_NOTIFICATION', true); +define('DEFAULT_LOG_ISSUE', true); +define('DEFAULT_LOG_WEB_SERVICE', true); + +define('DEFAULT_QUERY_CACHE_ENABLED', true); + + +define('DEFAULT_MIN_DISPLAY_LIMIT', 10); +define('DEFAULT_MAX_DISPLAY_LIMIT', 15); +define('DEFAULT_STANDARD_RELOAD_INTERVAL', 5 * 60); +define('DEFAULT_FAST_RELOAD_INTERVAL', 1 * 60); +define('DEFAULT_SECURE_CONNECTION_REQUIRED', false); +define('DEFAULT_ALLOWED_LOGIN_TYPES', 'form|basic|external'); +define('DEFAULT_EXT_AUTH_VARIABLE', '$_SERVER[\'REMOTE_USER\']'); +define('DEFAULT_ENCRYPTION_KEY', '@iT0pEncr1pti0n!'); // We'll use a random generated key later (if possible) +define('DEFAULT_ENCRYPTION_LIB', 'Mcrypt'); // We'll define the best encryption available later +/** + * Config + * configuration data (this class cannot not be localized, because it is responsible for loading the dictionaries) + * + * @package iTopORM + */ +class Config +{ + //protected $m_bIsLoaded = false; + protected $m_sFile = ''; + + protected $m_aAppModules; + protected $m_aDataModels; + protected $m_aWebServiceCategories; + protected $m_aAddons; + + protected $m_aModuleSettings; + + /** + * New way to store the settings ! + * + * @var array + * @since 2.5 db* variables + */ + protected $m_aSettings = array( + 'app_env_label' => array( + 'type' => 'string', + 'description' => 'Label displayed to describe the current application environnment, defaults to the environment name (e.g. "production")', + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'app_root_url' => array( + 'type' => 'string', + 'description' => 'Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server\'s name)', + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'app_icon_url' => array( + 'type' => 'string', + 'description' => 'Hyperlink to redirect the user when clicking on the application icon (in the main window, or login/logoff pages)', + 'default' => 'http://www.combodo.com/itop', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'db_host' => array( + 'type' => 'string', + 'default' => null, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'db_user' => array( + 'type' => 'string', + 'default' => null, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'db_pwd' => array( + 'type' => 'string', + 'default' => null, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'db_name' => array( + 'type' => 'string', + 'default' => null, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'db_subname' => array( + 'type' => 'string', + 'default' => null, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'db_tls.enabled' => array( + 'type' => 'bool', + 'description' => 'If true then the connection to the DB will be encrypted', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'db_tls.ca' => array( + 'type' => 'string', + 'description' => 'Path to certificate authority file for SSL', + 'default' => null, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'db_character_set' => array( // @deprecated to remove in 2.7 ? #1001 utf8mb4 switch + 'type' => 'string', + 'description' => 'Deprecated since iTop 2.5 : now using utf8mb4', + 'default' => 'DEPRECATED_2.5', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'db_collation' => array( // @deprecated to remove in 2.7 ? #1001 utf8mb4 switch + 'type' => 'string', + 'description' => 'Deprecated since iTop 2.5 : now using utf8mb4_unicode_ci', + 'default' => 'DEPRECATED_2.5', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'skip_check_to_write' => array( + 'type' => 'bool', + 'description' => 'Disable data format and integrity checks to boost up data load (insert or update)', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'skip_check_ext_keys' => array( + 'type' => 'bool', + 'description' => 'Disable external key check when checking the value of attributes', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'skip_strong_security' => array( + 'type' => 'bool', + 'description' => 'Disable strong security - TEMPORY: this flag should be removed when we are more confident in the recent change in security', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'query_optimization_enabled' => array( + 'type' => 'bool', + 'description' => 'The queries are optimized based on the assumption that the DB integrity has been preserved. By disabling the optimization one can ensure that the fetched data is clean... but this can be really slower or not usable at all (some queries will exceed the allowed number of joins in MySQL: 61!)', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'query_indentation_enabled' => array( + 'type' => 'bool', + 'description' => 'For developpers: format the SQL queries for human analysis', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'disable_mandatory_ext_keys' => array( + 'type' => 'bool', + 'description' => 'For developpers: allow every external keys to be undefined', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'graphviz_path' => array( + 'type' => 'string', + 'description' => 'Path to the Graphviz "dot" executable for graphing objects lifecycle', + 'default' => '/usr/bin/dot', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'php_path' => array( + 'type' => 'string', + 'description' => 'Path to the php executable in CLI mode', + 'default' => 'php', + 'value' => 'php', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'session_name' => array( + 'type' => 'string', + 'description' => 'The name of the cookie used to store the PHP session id', + 'default' => 'iTop', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'max_combo_length' => array( + 'type' => 'integer', + 'description' => 'The maximum number of elements in a drop-down list. If more then an autocomplete will be used', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'min_autocomplete_chars' => array( + 'type' => 'integer', + 'description' => 'The minimum number of characters to type in order to trigger the "autocomplete" behavior', + 'default' => 2, + 'value' => 2, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'allow_menu_on_linkset' => array( + 'type' => 'bool', + 'description' => 'Display Action menus in view mode on any LinkedSet with edit_mode != none', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'allow_target_creation' => array( + 'type' => 'bool', + 'description' => 'Displays the + button on external keys to create target objects', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + // Levels that trigger a confirmation in the CSV import/synchro wizard + 'csv_import_min_object_confirmation' => array( + 'type' => 'integer', + 'description' => 'Minimum number of objects to check for the confirmation percentages', + 'default' => 3, + 'value' => 3, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_errors_percentage' => array( + 'type' => 'integer', + 'description' => 'Percentage of errors that trigger a confirmation in the CSV import', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_modifications_percentage' => array( + 'type' => 'integer', + 'description' => 'Percentage of modifications that trigger a confirmation in the CSV import', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_creations_percentage' => array( + 'type' => 'integer', + 'description' => 'Percentage of creations that trigger a confirmation in the CSV import', + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'csv_import_history_display' => array( + 'type' => 'bool', + 'description' => 'Display the history tab in the import wizard', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'access_mode' => array( + 'type' => 'integer', + 'description' => 'Access mode: ACCESS_READONLY = 0, ACCESS_ADMIN_WRITE = 2, ACCESS_FULL = 3', + 'default' => ACCESS_FULL, + 'value' => ACCESS_FULL, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'access_message' => array( + 'type' => 'string', + 'description' => 'Message displayed to the users when there is any access restriction', + 'default' => 'iTop is temporarily frozen, please wait... (the admin team)', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'online_help' => array( + 'type' => 'string', + 'description' => 'Hyperlink to the online-help web page', + 'default' => 'http://www.combodo.com/itop-help', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'log_usage' => array( + 'type' => 'bool', + 'description' => 'Log the usage of the application (i.e. the date/time and the user name of each login)', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'log_rest_service' => array( + 'type' => 'bool', + 'description' => 'Log the usage of the REST/JSON service', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'synchro_trace' => array( + 'type' => 'string', + 'description' => 'Synchronization details: none, display, save (includes \'display\')', + 'default' => 'none', + 'value' => 'none', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'link_set_item_separator' => array( + 'type' => 'string', + 'description' => 'Link set from string: line separator', + 'default' => '|', + 'value' => '|', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'link_set_attribute_separator' => array( + 'type' => 'string', + 'description' => 'Link set from string: attribute separator', + 'default' => ';', + 'value' => ';', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'link_set_value_separator' => array( + 'type' => 'string', + 'description' => 'Link set from string: value separator (between the attcode and the value itself', + 'default' => ':', + 'value' => ':', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'link_set_attribute_qualifier' => array( + 'type' => 'string', + 'description' => 'Link set from string: attribute qualifier (encloses both the attcode and the value)', + 'default' => "'", + 'value' => "'", + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'cron_max_execution_time' => array( + 'type' => 'integer', + 'description' => 'Duration (seconds) of the page cron.php, must be shorter than php setting max_execution_time and shorter than the web server response timeout', + 'default' => 600, + 'value' => 600, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'cron_sleep' => array( + 'type' => 'integer', + 'description' => 'Duration (seconds) before cron.php checks again if something must be done', + 'default' => 2, + 'value' => 2, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'async_task_retries' => array( + 'type' => 'array', + 'description' => 'Automatic retries of asynchronous tasks in case of failure (per class)', + 'default' => array('AsyncSendEmail' => array('max_retries' => 0, 'retry_delay' => 600)), + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_asynchronous' => array( + 'type' => 'bool', + 'description' => 'If set, the emails are sent off line, which requires cron.php to be activated. Exception: some features like the email test utility will force the serialized mode', + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'email_transport' => array( + 'type' => 'string', + 'description' => 'Mean to send emails: PHPMail (uses the function mail()) or SMTP (implements the client protocole)', + 'default' => "PHPMail", + 'value' => "PHPMail", + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'email_transport_smtp.host' => array( + 'type' => 'string', + 'description' => 'host name or IP address (optional)', + 'default' => "localhost", + 'value' => "localhost", + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_transport_smtp.port' => array( + 'type' => 'integer', + 'description' => 'port number (optional)', + 'default' => 25, + 'value' => 25, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_transport_smtp.encryption' => array( + 'type' => 'string', + 'description' => 'tls or ssl (optional)', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_transport_smtp.username' => array( + 'type' => 'string', + 'description' => 'Authentication user (optional)', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_transport_smtp.password' => array( + 'type' => 'string', + 'description' => 'Authentication password (optional)', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_css' => array( + 'type' => 'string', + 'description' => 'CSS that will override the standard stylesheet used for the notifications', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'email_default_sender_address' => array( + 'type' => 'string', + 'description' => 'Default address provided in the email from header field.', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'email_default_sender_label' => array( + 'type' => 'string', + 'description' => 'Default label provided in the email from header field.', + 'default' => "", + 'value' => "", + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'apc_cache.enabled' => array( + 'type' => 'bool', + 'description' => 'If set, the APC cache is allowed (the PHP extension must also be active)', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'apc_cache.query_ttl' => array( + 'type' => 'integer', + 'description' => 'Time to live set in APC for the prepared queries (seconds - 0 means no timeout)', + 'default' => 3600, + 'value' => 3600, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'apc_cache_emulation.max_entries' => array( + 'type' => 'integer', + 'description' => 'Maximum number of cache entries (0 means no limit)', + 'default' => 1000, + 'value' => 1000, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'timezone' => array( + 'type' => 'string', + 'description' => 'Timezone (reference: http://php.net/manual/en/timezones.php). If empty, it will be left unchanged and MUST be explicitely configured in PHP', + // examples... not used (nor 'description') + 'examples' => array( + 'America/Sao_Paulo', + 'America/New_York (standing for EDT)', + 'America/Los_Angeles (standing for PDT)', + 'Asia/Istanbul', + 'Asia/Singapore', + 'Africa/Casablanca', + 'Australia/Sydney' + ), + 'default' => 'Europe/Paris', + 'value' => 'Europe/Paris', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'cas_include_path' => array( + 'type' => 'string', + 'description' => 'The path where to find the phpCAS library', + // examples... not used (nor 'description') + 'default' => '/usr/share/php', + 'value' => '/usr/share/php', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'cas_version' => array( + 'type' => 'string', + 'description' => 'The CAS protocol version to use: "1.0" (CAS v1), "2.0" (CAS v2) or "S1" (SAML V1) )', + // examples... not used (nor 'description') + 'default' => '2.0', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_host' => array( + 'type' => 'string', + 'description' => 'The name of the CAS host', + // examples... not used (nor 'description') + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_port' => array( + 'type' => 'integer', + 'description' => 'The port used by the CAS server', + // examples... not used (nor 'description') + 'default' => 443, + 'value' => 443, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_context' => array( + 'type' => 'string', + 'description' => 'The CAS context', + // examples... not used (nor 'description') + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_server_ca_cert_path' => array( + 'type' => 'string', + 'description' => 'The path where to find the certificate of the CA for validating the certificate of the CAS server', + // examples... not used (nor 'description') + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_logout_redirect_service' => array( + 'type' => 'string', + 'description' => 'The redirect service (URL) to use when logging-out with CAS', + // examples... not used (nor 'description') + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_memberof' => array( + 'type' => 'string', + 'description' => 'A semicolon separated list of group names that the user must be member of (works only with SAML - e.g. cas_version=> "S1")', + // examples... not used (nor 'description') + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_user_synchro' => array( + 'type' => 'bool', + 'description' => 'Whether or not to synchronize users with CAS/LDAP', + // examples... not used (nor 'description') + 'default' => 0, + 'value' => 0, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_update_profiles' => array( + 'type' => 'bool', + 'description' => 'Whether or not to update the profiles of an existing user from the CAS information', + // examples... not used (nor 'description') + 'default' => 0, + 'value' => 0, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_profile_pattern' => array( + 'type' => 'string', + 'description' => 'A regular expression pattern to extract the name of the iTop profile from the name of an LDAP/CAS group', + // examples... not used (nor 'description') + 'default' => '/^cn=([^,]+),/', + 'value' => '/^cn=([^,]+),/', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_default_profiles' => array( + 'type' => 'string', + 'description' => 'A semi-colon separated list of iTop Profiles to use when creating a new user if no profile is retrieved from CAS', + // examples... not used (nor 'description') + 'default' => 'Portal user', + 'value' => 'Portal user', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'cas_debug' => array( + 'type' => 'bool', + 'description' => 'Activate the CAS debug', + // examples... not used (nor 'description') + 'default' => false, + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'forgot_password' => array( + 'type' => 'bool', + 'description' => 'Enable the "Forgot password" feature', + // examples... not used (nor 'description') + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'forgot_password_from' => array( + 'type' => 'string', + 'description' => 'Sender email address for the "forgot password" feature. If empty, defaults to the recipient\'s email address.', + // examples... not used (nor 'description') + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'deadline_format' => array( + 'type' => 'string', + 'description' => 'The format used for displaying "deadline" attributes: any string with the following placeholders: $date$, $difference$', + // examples... $date$ ($deadline$) + 'default' => '$difference$', + 'value' => '$difference$', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'buttons_position' => array( + 'type' => 'string', + 'description' => 'Position of the forms buttons: bottom | top | both', + // examples... not used + 'default' => 'both', + 'value' => 'both', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'shortcut_actions' => array( + 'type' => 'string', + 'description' => 'Actions that are available as direct buttons next to the "Actions" menu', + // examples... not used + 'default' => 'UI:Menu:Modify,UI:Menu:New', + 'value' => 'UI:Menu:Modify', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'complex_actions_limit' => array( + 'type' => 'integer', + 'description' => 'Display the "actions" menu items that require long computation only if the list of objects is contains less objects than this number (0 means no limit)', + // examples... not used + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'synchro_prevent_delete_all' => array( + 'type' => 'bool', + 'description' => 'Stop the synchro if all the replicas of a data source become obsolete at the same time.', + // examples... not used + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'source_dir' => array( + 'type' => 'string', + 'description' => 'Source directory for the datamodel files. (which gets compiled to env-production).', + // examples... not used + 'default' => '', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'csv_file_default_charset' => array( + 'type' => 'string', + 'description' => 'Character set used by default for downloading and uploading data as a CSV file. Warning: it is case sensitive (uppercase is preferable).', + // examples... not used + 'default' => 'ISO-8859-1', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'debug_report_spurious_chars' => array( + 'type' => 'bool', + 'description' => 'Report, in the error log, the characters found in the output buffer, echoed by mistake in the loaded modules, and potentially corrupting the output', + // examples... not used + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'impact_analysis_first_tab' => array( + 'type' => 'string', + 'description' => 'Which tab to display first in the impact analysis view: list or graphics. Graphics are nicer but slower to display when there are many objects', + // examples... not used + 'default' => 'graphics', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'url_validation_pattern' => array( + 'type' => 'string', + 'description' => 'Regular expression to validate/detect the format of an URL (URL attributes and Wiki formatting for Text attributes)', + 'default' => '(https?|ftp)\://([a-zA-Z0-9+!*(),;?&=\$_.-]+(\:[a-zA-Z0-9+!*(),;?&=\$_.-]+)?@)?([a-zA-Z0-9-.]{3,})(\:[0-9]{2,5})?(/([a-zA-Z0-9%+\$_-]\.?)+)*/?(\?[a-zA-Z+&\$_.-][a-zA-Z0-9;:[\]@&%=+/\$_.-]*)?(#[a-zA-Z_.-][a-zA-Z0-9+\$_.-]*)?', + // SHEME.......... USER....................... PASSWORD.......................... HOST/IP........... PORT.......... PATH........................ GET............................................ ANCHOR............................ + // Example: http://User:passWord@127.0.0.1:8888/patH/Page.php?arrayArgument[2]=something:blah20#myAnchor + // Origin of this regexp: http://www.php.net/manual/fr/function.preg-match.php#93824 + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'email_validation_pattern' => array( + 'type' => 'string', + 'description' => 'Regular expression to validate/detect the format of an eMail address', + 'default' => "[a-zA-Z0-9._&'-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]{2,}", + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'email_decoration_class' => array( + 'type' => 'string', + 'description' => 'CSS class(es) to use as decoration for the HTML rendering of the attribute. eg. "fa fa-envelope" will put a mail icon.', + 'default' => 'fa fa-envelope', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'phone_number_validation_pattern' => array( + 'type' => 'string', + 'description' => 'Regular expression to validate/detect the format of a phone number', + 'default' => "[0-9.\-\ \+\(\)]+", + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'phone_number_url_pattern' => array( + 'type' => 'string', + 'description' => 'Format for phone number url, use %1$s as a placeholder for the value. eg. "tel:%1$s" for regular phone applications or "callto:%1$s" for Skype. Default is "tel:%1$s".', + 'default' => 'tel:%1$s', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'phone_number_decoration_class' => array( + 'type' => 'string', + 'description' => 'CSS class(es) to use as decoration for the HTML rendering of the attribute. eg. "fa fa-phone" will put a phone icon.', + 'default' => 'fa fa-phone', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'log_kpi_duration' => array( + 'type' => 'integer', + 'description' => 'Level of logging for troubleshooting performance issues (1 to enable, 2 +blame callers)', + // examples... not used + 'default' => 0, + 'value' => 0, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'log_kpi_memory' => array( + 'type' => 'integer', + 'description' => 'Level of logging for troubleshooting memory limit issues', + // examples... not used + 'default' => 0, + 'value' => 0, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'log_kpi_user_id' => array( + 'type' => 'string', + 'description' => 'Limit the scope of users to the given user id (* means no limit)', + // examples... not used + 'default' => '*', + 'value' => '*', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'max_linkset_output' => array( + 'type' => 'integer', + 'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.', + 'default' => 100, + 'value' => 100, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'demo_mode' => array( + 'type' => 'bool', + 'description' => 'Set to true to prevent users from changing passwords/languages', + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'portal_tickets' => array( + 'type' => 'string', + 'description' => 'CSV list of classes supported in the portal', + // examples... not used + 'default' => 'UserRequest', + 'value' => 'UserRequest', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'portal_dispatch_urls' => array( + 'type' => 'array', + 'description' => 'Associative array of sPortalId => Home page URL (relatively to the application root)', + // examples... not used + 'default' => array(), + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'max_execution_time_per_loop' => array( + 'type' => 'integer', + 'description' => 'Maximum execution time requested, per loop, during bulk operations. Zero means no limit.', + // examples... not used + 'default' => 30, + 'value' => 30, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'max_history_length' => array( + 'type' => 'integer', + 'description' => 'Maximum length of the history table (in the "History" tab on each object) before it gets truncated. Latest modifications are displayed first.', + // examples... not used + 'default' => 50, + 'value' => 50, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'max_history_case_log_entry_length' => array( + 'type' => 'integer', + 'description' => 'The length (in number of characters) at which to truncate the (expandable) display (in the history) of a case log entry. If zero, the display in the history is not truncated.', + // examples... not used + 'default' => 60, + 'value' => 60, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'full_text_chunk_duration' => array( + 'type' => 'integer', + 'description' => 'Delay after which the results are displayed.', + // examples... not used + 'default' => 2, + 'value' => 2, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'full_text_accelerators' => array( + 'type' => 'array', + 'description' => 'Specifies classes to be searched at first (and the subset of data) when running the full text search.', + 'default' => array(), + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'full_text_needle_min' => array( + 'type' => 'integer', + 'description' => 'Minimum size of the full text needle.', + 'default' => 3, + 'value' => 3, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'tracking_level_linked_set_default' => array( + 'type' => 'integer', + 'description' => 'Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSet (defaults to NONE in case of a fresh install, LIST otherwise - this to preserve backward compatibility while upgrading from a version older than 2.0.3 - see TRAC #936)', + 'default' => LINKSET_TRACKING_LIST, + 'value' => LINKSET_TRACKING_LIST, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'tracking_level_linked_set_indirect_default' => array( + 'type' => 'integer', + 'description' => 'Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSetIndirect', + 'default' => LINKSET_TRACKING_ALL, + 'value' => LINKSET_TRACKING_ALL, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'user_rights_legacy' => array( + 'type' => 'bool', + 'description' => 'Set to true to restore the buggy algorithm for the computation of user rights (within the same profile, ALLOW on the class itself has precedence on DENY of a parent class)', + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'xlsx_exporter_memory_limit' => array( + 'type' => 'string', + 'description' => 'Memory limit to use when (interactively) exporting data to Excel', + 'default' => '2048M', // Huuuuuuge 2GB! + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'min_reload_interval' => array( + 'type' => 'integer', + 'description' => 'Minimum refresh interval (seconds) for dashboards, shortcuts, etc. Even if the interval is set programmatically, it is forced to that minimum', + 'default' => 5, // In iTop 2.0.3, this was the hardcoded value + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'relations_max_depth' => array( + 'type' => 'integer', + 'description' => 'Maximum number of successive levels (depth) to explore when displaying the impact/depends on relations.', + 'default' => 20, // In iTop 2.0.3, this was the hardcoded value + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'transaction_storage' => array( + 'type' => 'string', + 'description' => 'The type of mechanism to use for storing the unique identifiers for transactions (Session|File).', + 'default' => 'Session', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'transactions_enabled' => array( + 'type' => 'bool', + 'description' => 'Whether or not the whole mechanism to prevent multiple submissions of a page is enabled.', + 'default' => true, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'log_transactions' => array( + 'type' => 'bool', + 'description' => 'Whether or not to enable the debug log for the transactions.', + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'concurrent_lock_enabled' => array( + 'type' => 'bool', + 'description' => 'Whether or not to activate the locking mechanism in order to prevent concurrent edition of the same object.', + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'concurrent_lock_expiration_delay' => array( + 'type' => 'integer', + 'description' => 'Delay (in seconds) for a concurrent lock to expire', + 'default' => 120, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'concurrent_lock_override_profiles' => array( + 'type' => 'array', + 'description' => 'The list of profiles allowed to "kill" a lock', + 'default' => array('Administrator'), + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'html_sanitizer' => array( + 'type' => 'string', + 'description' => 'The class to use for HTML sanitization: HTMLDOMSanitizer, HTMLPurifierSanitizer or HTMLNullSanitizer', + 'default' => 'HTMLDOMSanitizer', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'inline_image_max_display_width' => array( + 'type' => 'integer', + 'description' => 'The maximum width (in pixels) when displaying images inside an HTML formatted attribute. Images will be displayed using this this maximum width.', + 'default' => '250', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'inline_image_max_storage_width' => array( + 'type' => 'integer', + 'description' => 'The maximum width (in pixels) when uploading images to be used inside an HTML formatted attribute. Images larger than the given size will be downsampled before storing them in the database.', + 'default' => '1600', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'draft_attachments_lifetime' => array( + 'type' => 'integer', + 'description' => 'Lifetime (in seconds) of drafts\' attachments and inline images: after this duration, the garbage collector will delete them.', + 'default' => 3600, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'date_and_time_format' => array( + 'type' => 'array', + 'description' => 'Format for date and time display (per language)', + 'default' => array('default' => array('date' => 'Y-m-d', 'time' => 'H:i:s', 'date_time' => '$date $time')), + 'value' => false, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'breadcrumb.max_count' => array( + 'type' => 'integer', + 'description' => 'Maximum number of items kept in the history breadcrumb. Set it to 0 to entirely disable the breadcrumb.', + 'default' => 8, + 'value' => 8, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'obsolescence.show_obsolete_data' => array( + 'type' => 'bool', + 'description' => 'Default value for the user preference "show obsolete data"', + 'default' => false, + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'obsolescence.date_update_interval' => array( + 'type' => 'integer', + 'description' => 'Delay in seconds between two refreshes of the obsolescence dates.', + 'default' => 600, + 'value' => 600, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'disable_attachments_download_legacy_portal' => array( + 'type' => 'bool', + 'description' => 'Disable attachments download from legacy portal', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'secure_rest_services' => array( + 'type' => 'bool', + 'description' => 'When set to true, only the users with the profile "REST Services User" are allowed to use the REST web services.', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), + 'search_manual_submit' => array( + 'type' => 'array', + 'description' => 'Force manual submit of search all requests', + 'default' => false, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'high_cardinality_classes' => array( + 'type' => 'array', + 'description' => 'List of classes with high cardinality (Force manual submit of search)', + 'default' => array(), + 'value' => array(), + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + ); + + public function IsProperty($sPropCode) + { + return (array_key_exists($sPropCode, $this->m_aSettings)); + } + + /** + * @return string identifier that can be used for example to name WebStorage/SessionStorage keys (they + * are related to a whole domain, and a domain can host multiple itop) + * Beware: do not expose server side information to the client ! + */ + public function GetItopInstanceid() + { + return md5(utils::GetAbsoluteUrlAppRoot() + .'==='.$this->Get('db_host') + .'/'.$this->Get('db_name') + .'/'.$this->Get('db_subname')); + } + + public function GetDescription($sPropCode) + { + return $this->m_aSettings[$sPropCode]; + } + + /** + * @param string $sPropCode + * @param mixed $value + * @param string $sSourceDesc mandatory for variables with show_in_conf_sample=false + * + * @throws \CoreException + */ + public function Set($sPropCode, $value, $sSourceDesc = 'unknown') + { + $sType = $this->m_aSettings[$sPropCode]['type']; + switch ($sType) + { + case 'bool': + $value = (bool)$value; + break; + case 'string': + $value = (string)$value; + break; + case 'integer': + $value = (integer)$value; + break; + case 'float': + $value = (float)$value; + break; + case 'array': + break; + default: + throw new CoreException('Unknown type for setting', array('property' => $sPropCode, 'type' => $sType)); + } + $this->m_aSettings[$sPropCode]['value'] = $value; + $this->m_aSettings[$sPropCode]['source_of_value'] = $sSourceDesc; + + } + + /** + * @param string $sPropCode + * + * @return mixed + */ + public function Get($sPropCode) + { + return $this->m_aSettings[$sPropCode]['value']; + } + + /** + * Event log options (see LOG_... definition) + */ + // Those variables will be deprecated later, when the transition to ...Get('my_setting') will be done + protected $m_bLogGlobal; + protected $m_bLogNotification; + protected $m_bLogIssue; + protected $m_bLogWebService; + protected $m_bQueryCacheEnabled; // private setting + + /** + * @var integer Number of elements to be displayed when there are more than m_iMaxDisplayLimit elements + */ + protected $m_iMinDisplayLimit; + /** + * @var integer Max number of elements before truncating the display + */ + protected $m_iMaxDisplayLimit; + + /** + * @var integer Number of seconds between two reloads of the display (standard) + */ + protected $m_iStandardReloadInterval; + /** + * @var integer Number of seconds between two reloads of the display (fast) + */ + protected $m_iFastReloadInterval; + + /** + * @var boolean Whether or not a secure connection is required for using the application. + * If set, any attempt to connect to an iTop page with http:// will be redirected + * to https:// + */ + protected $m_bSecureConnectionRequired; + + /** + * @var string Langage code, default if the user language is undefined + */ + protected $m_sDefaultLanguage; + + /** + * @var string Type of login process allowed: form|basic|url|external + */ + protected $m_sAllowedLoginTypes; + + /** + * @var string Name of the PHP variable in which external authentication information is passed by the web server + */ + protected $m_sExtAuthVariable; + + /** + * @var string Encryption key used for all attributes of type "encrypted string". Can be set to a random value + * unless you want to import a database from another iTop instance, in which case you must use + * the same encryption key in order to properly decode the encrypted fields + */ + protected $m_sEncryptionKey; + + /** + * @var string Encryption key used for all attributes of type "encrypted string". Can be set to a random value + * unless you want to import a database from another iTop instance, in which case you must use + * the same encryption key in order to properly decode the encrypted fields + */ + protected $m_sEncryptionLibrary; + + /** + * @var array Additional character sets to be supported by the interactive CSV import + * 'iconv_code' => 'display name' + */ + protected $m_aCharsets; + + /** + * Config constructor. + * + * @param string|null $sConfigFile + * @param bool $bLoadConfig + * + * @throws \ConfigException + * @throws \CoreException + */ + public function __construct($sConfigFile = null, $bLoadConfig = true) + { + $this->m_sFile = $sConfigFile; + if (is_null($sConfigFile)) + { + $bLoadConfig = false; + } + + $this->m_aAddons = array( + // Default AddOn, always present can be moved to an official iTop Module later if needed + 'user rights' => 'addons/userrights/userrightsprofile.class.inc.php', + ); + + foreach ($this->m_aSettings as $sPropCode => $aSettingInfo) + { + $this->m_aSettings[$sPropCode]['value'] = $aSettingInfo['default']; + } + + $this->m_bLogGlobal = DEFAULT_LOG_GLOBAL; + $this->m_bLogNotification = DEFAULT_LOG_NOTIFICATION; + $this->m_bLogIssue = DEFAULT_LOG_ISSUE; + $this->m_bLogWebService = DEFAULT_LOG_WEB_SERVICE; + $this->m_iMinDisplayLimit = DEFAULT_MIN_DISPLAY_LIMIT; + $this->m_iMaxDisplayLimit = DEFAULT_MAX_DISPLAY_LIMIT; + $this->m_iStandardReloadInterval = DEFAULT_STANDARD_RELOAD_INTERVAL; + $this->m_iFastReloadInterval = DEFAULT_FAST_RELOAD_INTERVAL; + $this->m_bSecureConnectionRequired = DEFAULT_SECURE_CONNECTION_REQUIRED; + $this->m_sDefaultLanguage = 'EN US'; + $this->m_sAllowedLoginTypes = DEFAULT_ALLOWED_LOGIN_TYPES; + $this->m_sExtAuthVariable = DEFAULT_EXT_AUTH_VARIABLE; + $this->m_aCharsets = array(); + $this->m_bQueryCacheEnabled = DEFAULT_QUERY_CACHE_ENABLED; + + //define default encryption params according to php install + $aEncryptParams = SimpleCrypt::GetNewDefaultParams(); + $this->m_sEncryptionLibrary = isset($aEncryptParams['lib']) ? $aEncryptParams['lib'] : DEFAULT_ENCRYPTION_LIB; + $this->m_sEncryptionKey= isset($aEncryptParams['key']) ? $aEncryptParams['key'] : DEFAULT_ENCRYPTION_KEY; + + $this->m_aModuleSettings = array(); + + if ($bLoadConfig) + { + $this->Load($sConfigFile); + $this->Verify(); + } + + // Application root url: set a default value, then normalize it + /* + * Does not work in CLI/unattended mode + $sAppRootUrl = trim($this->Get('app_root_url')); + if (strlen($sAppRootUrl) == 0) + { + $sAppRootUrl = utils::GetDefaultUrlAppRoot(); + } + if (substr($sAppRootUrl, -1, 1) != '/') + { + $sAppRootUrl .= '/'; + } + $this->Set('app_root_url', $sAppRootUrl); + */ + } + + /** + * @param string $sPurpose + * @param string $sFileName + * + * @throws \ConfigException + */ + protected function CheckFile($sPurpose, $sFileName) + { + if (!file_exists($sFileName)) + { + throw new ConfigException("Could not find $sPurpose file", array('file' => $sFileName)); + } + if (!is_readable($sFileName)) + { + throw new ConfigException("Could not read $sPurpose file (the file exists but cannot be read). Do you have the rights to access this file?", + array('file' => $sFileName)); + } + } + + /** + * @param string $sConfigFile + * + * @throws \ConfigException + * @throws \CoreException + */ + protected function Load($sConfigFile) + { + $this->CheckFile('configuration', $sConfigFile); + + $sConfigCode = trim(file_get_contents($sConfigFile)); + + // Variables created when doing an eval() on the config file + /** @var array $MySettings */ + $MySettings = null; + /** @var array $MyModuleSettings */ + $MyModuleSettings = null; + /** @var array $MyModules */ + $MyModules = null; + + // This does not work on several lines + // preg_match('/^<\\?php(.*)\\?'.'>$/', $sConfigCode, $aMatches)... + // So, I've implemented a solution suggested in the PHP doc (search for phpWrapper) + try + { + ob_start(); + eval('?'.'>'.trim($sConfigCode)); + $sNoise = trim(ob_get_contents()); + ob_end_clean(); + } + catch (Exception $e) + { + // well, never reach in case of parsing error :-( + // will be improved in PHP 6 ? + throw new ConfigException('Error in configuration file', + array('file' => $sConfigFile, 'error' => $e->getMessage())); + } + catch(Error $e) + { + // PHP 7 + throw new ConfigException('Error in configuration file', + array('file' => $sConfigFile, 'error' => $e->getMessage().' at line '.$e->getLine())); + } + if (strlen($sNoise) > 0) + { + // Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack) + throw new ConfigException('Syntax error in configuration file', + array('file' => $sConfigFile, 'error' => ''.htmlentities($sNoise, ENT_QUOTES, 'UTF-8').'')); + } + + if (!isset($MySettings) || !is_array($MySettings)) + { + throw new ConfigException('Missing array in configuration file', + array('file' => $sConfigFile, 'expected' => '$MySettings')); + } + + if (!array_key_exists('addons', $MyModules)) + { + throw new ConfigException('Missing item in configuration file', + array('file' => $sConfigFile, 'expected' => '$MyModules[\'addons\']')); + } + if (!array_key_exists('user rights', $MyModules['addons'])) + { + // Add one, by default + $MyModules['addons']['user rights'] = '/addons/userrights/userrightsnull.class.inc.php'; + } + + $this->m_aAddons = $MyModules['addons']; + + foreach ($MySettings as $sPropCode => $rawvalue) + { + if ($this->IsProperty($sPropCode)) + { + if (is_string($rawvalue)) + { + $value = trim($rawvalue); + } + else + { + $value = $rawvalue; + } + $this->Set($sPropCode, $value, $sConfigFile); + } + } + + $this->m_bLogGlobal = isset($MySettings['log_global']) ? (bool)trim($MySettings['log_global']) : DEFAULT_LOG_GLOBAL; + $this->m_bLogNotification = isset($MySettings['log_notification']) ? (bool)trim($MySettings['log_notification']) : DEFAULT_LOG_NOTIFICATION; + $this->m_bLogIssue = isset($MySettings['log_issue']) ? (bool)trim($MySettings['log_issue']) : DEFAULT_LOG_ISSUE; + $this->m_bLogWebService = isset($MySettings['log_web_service']) ? (bool)trim($MySettings['log_web_service']) : DEFAULT_LOG_WEB_SERVICE; + $this->m_bQueryCacheEnabled = isset($MySettings['query_cache_enabled']) ? (bool)trim($MySettings['query_cache_enabled']) : DEFAULT_QUERY_CACHE_ENABLED; + + $this->m_iMinDisplayLimit = isset($MySettings['min_display_limit']) ? trim($MySettings['min_display_limit']) : DEFAULT_MIN_DISPLAY_LIMIT; + $this->m_iMaxDisplayLimit = isset($MySettings['max_display_limit']) ? trim($MySettings['max_display_limit']) : DEFAULT_MAX_DISPLAY_LIMIT; + $this->m_iStandardReloadInterval = isset($MySettings['standard_reload_interval']) ? trim($MySettings['standard_reload_interval']) : DEFAULT_STANDARD_RELOAD_INTERVAL; + $this->m_iFastReloadInterval = isset($MySettings['fast_reload_interval']) ? trim($MySettings['fast_reload_interval']) : DEFAULT_FAST_RELOAD_INTERVAL; + $this->m_bSecureConnectionRequired = isset($MySettings['secure_connection_required']) ? (bool)trim($MySettings['secure_connection_required']) : DEFAULT_SECURE_CONNECTION_REQUIRED; + + $this->m_aModuleSettings = isset($MyModuleSettings) ? $MyModuleSettings : array(); + + $this->m_sDefaultLanguage = isset($MySettings['default_language']) ? trim($MySettings['default_language']) : 'EN US'; + $this->m_sAllowedLoginTypes = isset($MySettings['allowed_login_types']) ? trim($MySettings['allowed_login_types']) : DEFAULT_ALLOWED_LOGIN_TYPES; + $this->m_sExtAuthVariable = isset($MySettings['ext_auth_variable']) ? trim($MySettings['ext_auth_variable']) : DEFAULT_EXT_AUTH_VARIABLE; + $this->m_sEncryptionKey = isset($MySettings['encryption_key']) ? trim($MySettings['encryption_key']) : $this->m_sEncryptionKey; + $this->m_sEncryptionLibrary = isset($MySettings['encryption_library']) ? trim($MySettings['encryption_library']) : $this->m_sEncryptionLibrary; + $this->m_aCharsets = isset($MySettings['csv_import_charsets']) ? $MySettings['csv_import_charsets'] : array(); + } + + protected function Verify() + { + // Files are verified later on, just before using them -see MetaModel::Plugin() + // (we have their final path at that point) + } + + public function GetModuleSetting($sModule, $sProperty, $defaultvalue = null) + { + if (isset($this->m_aModuleSettings[$sModule][$sProperty])) + { + return $this->m_aModuleSettings[$sModule][$sProperty]; + } + + // Fall back to the predefined XML parameter, if any + return $this->GetModuleParameter($sModule, $sProperty, $defaultvalue); + } + + /** + * @param string $sModule + * @param string $sProperty + * @param mixed|null $defaultvalue + * + * @return mixed|null + */ + public function GetModuleParameter($sModule, $sProperty, $defaultvalue = null) + { + $ret = $defaultvalue; + if (class_exists('ModulesXMLParameters')) + { + $aAllParams = ModulesXMLParameters::GetData($sModule); + if (array_key_exists($sProperty, $aAllParams)) + { + $ret = $aAllParams[$sProperty]; + } + } + + return $ret; + } + + public function SetModuleSetting($sModule, $sProperty, $value) + { + $this->m_aModuleSettings[$sModule][$sProperty] = $value; + } + + public function GetAddons() + { + return $this->m_aAddons; + } + + public function SetAddons($aAddons) + { + $this->m_aAddons = $aAddons; + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 + * @see Config::Get() as a replacement + */ + public function GetDBHost() + { + return $this->Get('db_host'); + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 + * @see Config::Get() as a replacement + */ + public function GetDBName() + { + return $this->Get('db_name'); + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 + * @see Config::Get() as a replacement + */ + public function GetDBSubname() + { + return $this->Get('db_subname'); + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 #1001 utf8mb4 switch + * @see Config::DEFAULT_CHARACTER_SET + */ + public function GetDBCharacterSet() + { + return DEFAULT_CHARACTER_SET; + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 #1001 utf8mb4 switch + * @see Config::DEFAULT_COLLATION + */ + public function GetDBCollation() + { + return DEFAULT_COLLATION; + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 + * @see Config::Get() as a replacement + */ + public function GetDBUser() + { + return $this->Get('db_user'); + } + + /** + * @return string + * + * @deprecated 2.5 will be removed in 2.6 + * @see Config::Get() as a replacement + */ + public function GetDBPwd() + { + return $this->Get('db_pwd'); + } + + public function GetLogGlobal() + { + return $this->m_bLogGlobal; + } + + public function GetLogNotification() + { + return $this->m_bLogNotification; + } + + public function GetLogIssue() + { + return $this->m_bLogIssue; + } + + public function GetLogWebService() + { + return $this->m_bLogWebService; + } + + public function GetLogQueries() + { + return false; + } + + public function GetQueryCacheEnabled() + { + return $this->m_bQueryCacheEnabled; + } + + public function GetMinDisplayLimit() + { + return $this->m_iMinDisplayLimit; + } + + public function GetMaxDisplayLimit() + { + return $this->m_iMaxDisplayLimit; + } + + public function GetStandardReloadInterval() + { + return $this->m_iStandardReloadInterval; + } + + public function GetFastReloadInterval() + { + return $this->m_iFastReloadInterval; + } + + public function GetSecureConnectionRequired() + { + return $this->m_bSecureConnectionRequired; + } + + public function GetDefaultLanguage() + { + return $this->m_sDefaultLanguage; + } + + public function GetEncryptionKey() + { + return $this->m_sEncryptionKey; + } + + public function GetEncryptionLibrary() + { + return $this->m_sEncryptionLibrary; + } + + public function GetAllowedLoginTypes() + { + return explode('|', $this->m_sAllowedLoginTypes); + } + + public function GetExternalAuthenticationVariable() + { + return $this->m_sExtAuthVariable; + } + + public function GetCSVImportCharsets() + { + return $this->m_aCharsets; + } + + public function SetLogGlobal($iLogGlobal) + { + $this->m_iLogGlobal = $iLogGlobal; + } + + public function SetLogNotification($iLogNotification) + { + $this->m_iLogNotification = $iLogNotification; + } + + public function SetLogIssue($iLogIssue) + { + $this->m_iLogIssue = $iLogIssue; + } + + public function SetLogWebService($iLogWebService) + { + $this->m_iLogWebService = $iLogWebService; + } + + public function SetMinDisplayLimit($iMinDisplayLimit) + { + $this->m_iMinDisplayLimit = $iMinDisplayLimit; + } + + public function SetMaxDisplayLimit($iMaxDisplayLimit) + { + $this->m_iMaxDisplayLimit = $iMaxDisplayLimit; + } + + public function SetStandardReloadInterval($iStandardReloadInterval) + { + $this->m_iStandardReloadInterval = $iStandardReloadInterval; + } + + public function SetFastReloadInterval($iFastReloadInterval) + { + $this->m_iFastReloadInterval = $iFastReloadInterval; + } + + public function SetSecureConnectionRequired($bSecureConnectionRequired) + { + $this->m_bSecureConnectionRequired = $bSecureConnectionRequired; + } + + public function SetDefaultLanguage($sLanguageCode) + { + $this->m_sDefaultLanguage = $sLanguageCode; + } + + public function SetAllowedLoginTypes($aAllowedLoginTypes) + { + $this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes); + } + + public function SetExternalAuthenticationVariable($sExtAuthVariable) + { + $this->m_sExtAuthVariable = $sExtAuthVariable; + } + + public function SetEncryptionKey($sKey) + { + $this->m_sEncryptionKey = $sKey; + } + + public function SetCSVImportCharsets($aCharsets) + { + $this->m_aCharsets = $aCharsets; + } + + public function AddCSVImportCharset($sIconvCode, $sDisplayName) + { + $this->m_aCharsets[$sIconvCode] = $sDisplayName; + } + + public function GetLoadedFile() + { + if (is_null($this->m_sFile)) + { + return ''; + } + else + { + return $this->m_sFile; + } + } + + /** + * Render the configuration as an associative array + * + * @return array + */ + public function ToArray() + { + $aSettings = array(); + foreach ($this->m_aSettings as $sPropCode => $aSettingInfo) + { + $aSettings[$sPropCode] = $aSettingInfo['value']; + } + $aSettings['log_global'] = $this->m_bLogGlobal; + $aSettings['log_notification'] = $this->m_bLogNotification; + $aSettings['log_issue'] = $this->m_bLogIssue; + $aSettings['log_web_service'] = $this->m_bLogWebService; + $aSettings['query_cache_enabled'] = $this->m_bQueryCacheEnabled; + $aSettings['min_display_limit'] = $this->m_iMinDisplayLimit; + $aSettings['max_display_limit'] = $this->m_iMaxDisplayLimit; + $aSettings['standard_reload_interval'] = $this->m_iStandardReloadInterval; + $aSettings['fast_reload_interval'] = $this->m_iFastReloadInterval; + $aSettings['secure_connection_required'] = $this->m_bSecureConnectionRequired; + $aSettings['default_language'] = $this->m_sDefaultLanguage; + $aSettings['allowed_login_types'] = $this->m_sAllowedLoginTypes; + $aSettings['ext_auth_variable'] = $this->m_sExtAuthVariable; + $aSettings['encryption_key'] = $this->m_sEncryptionKey; + $aSettings['encryption_library'] = $this->m_sEncryptionLibrary; + $aSettings['csv_import_charsets'] = $this->m_aCharsets; + + foreach ($this->m_aModuleSettings as $sModule => $aProperties) + { + foreach ($aProperties as $sProperty => $value) + { + $aSettings['module_settings'][$sModule][$sProperty] = $value; + } + } + foreach ($this->m_aAddons as $sKey => $sFile) + { + $aSettings['addon_list'][] = $sFile; + } + + return $aSettings; + } + + /** + * Write the configuration to a file (php format) that can be reloaded later + * By default write to the same file that was specified when constructing the object + * + * @param string $sFileName string Name of the file to write to (emtpy to write to the same file) + * + * @return boolean True otherwise throws an Exception + * + * @throws \ConfigException + */ + public function WriteToFile($sFileName = '') + { + if (empty($sFileName)) + { + $sFileName = $this->m_sFile; + } + $hFile = @fopen($sFileName, 'w'); + if ($hFile !== false) + { + fwrite($hFile, "m_aSettings; + + // Old fashioned boolean settings + $aBoolValues = array( + 'log_global' => $this->m_bLogGlobal, + 'log_notification' => $this->m_bLogNotification, + 'log_issue' => $this->m_bLogIssue, + 'log_web_service' => $this->m_bLogWebService, + 'query_cache_enabled' => $this->m_bQueryCacheEnabled, + 'secure_connection_required' => $this->m_bSecureConnectionRequired, + ); + foreach ($aBoolValues as $sKey => $bValue) + { + $aConfigSettings[$sKey] = array( + 'show_in_conf_sample' => true, + 'type' => 'bool', + 'value' => $bValue, + ); + } + + // Old fashioned integer settings + $aIntValues = array( + 'fast_reload_interval' => $this->m_iFastReloadInterval, + 'max_display_limit' => $this->m_iMaxDisplayLimit, + 'min_display_limit' => $this->m_iMinDisplayLimit, + 'standard_reload_interval' => $this->m_iStandardReloadInterval, + ); + foreach ($aIntValues as $sKey => $iValue) + { + $aConfigSettings[$sKey] = array( + 'show_in_conf_sample' => true, + 'type' => 'integer', + 'value' => $iValue, + ); + } + + // Old fashioned remaining values + $aOtherValues = array( + 'default_language' => $this->m_sDefaultLanguage, + 'allowed_login_types' => $this->m_sAllowedLoginTypes, + 'ext_auth_variable' => $this->m_sExtAuthVariable, + 'encryption_key' => $this->m_sEncryptionKey, + 'encryption_library' => $this->m_sEncryptionLibrary, + 'csv_import_charsets' => $this->m_aCharsets, + ); + foreach ($aOtherValues as $sKey => $value) + { + $aConfigSettings[$sKey] = array( + 'show_in_conf_sample' => true, + 'type' => is_string($value) ? 'string' : 'mixed', + 'value' => $value, + ); + } + + ksort($aConfigSettings); + fwrite($hFile, "\$MySettings = array(\n"); + foreach ($aConfigSettings as $sPropCode => $aSettingInfo) + { + // Write all values that are either always visible or present in the cloned config file + if ($aSettingInfo['show_in_conf_sample'] || (!empty($aSettingInfo['source_of_value']) && ($aSettingInfo['source_of_value'] != 'unknown'))) + { + $sType = $aSettingInfo['type']; + switch ($sType) + { + case 'bool': + $sSeenAs = $aSettingInfo['value'] ? 'true' : 'false'; + break; + default: + $sSeenAs = self::PrettyVarExport($aSettingInfo['value'], "\t"); + } + fwrite($hFile, "\n"); + if (isset($aSettingInfo['description'])) + { + fwrite($hFile, "\t// $sPropCode: {$aSettingInfo['description']}\n"); + } + if (isset($aSettingInfo['default'])) + { + $default = $aSettingInfo['default']; + if ($aSettingInfo['type'] == 'bool') + { + $default = $default ? 'true' : 'false'; + } + fwrite($hFile, + "\t//\tdefault: ".self::PrettyVarExport($aSettingInfo['default'], "\t//\t\t", true)."\n"); + } + fwrite($hFile, "\t'$sPropCode' => $sSeenAs,\n"); + } + } + fwrite($hFile, ");\n"); + + fwrite($hFile, "\n"); + fwrite($hFile, "/**\n *\n * Modules specific settings\n *\n */\n"); + fwrite($hFile, "\$MyModuleSettings = array(\n"); + foreach ($this->m_aModuleSettings as $sModule => $aProperties) + { + fwrite($hFile, "\t'$sModule' => array (\n"); + foreach ($aProperties as $sProperty => $value) + { + $sNiceExport = self::PrettyVarExport($value, "\t\t"); + fwrite($hFile, "\t\t'$sProperty' => $sNiceExport,\n"); + } + fwrite($hFile, "\t),\n"); + } + fwrite($hFile, ");\n"); + + fwrite($hFile, "\n/**\n"); + fwrite($hFile, " *\n"); + fwrite($hFile, " * Data model modules to be loaded. Names are specified as relative paths\n"); + fwrite($hFile, " *\n"); + fwrite($hFile, " */\n"); + fwrite($hFile, "\$MyModules = array(\n"); + fwrite($hFile, "\t'addons' => array (\n"); + foreach ($this->m_aAddons as $sKey => $sFile) + { + fwrite($hFile, "\t\t'$sKey' => '$sFile',\n"); + } + fwrite($hFile, "\t),\n"); + fwrite($hFile, ");\n"); + fwrite($hFile, '?'.'>'); // Avoid perturbing the syntax highlighting ! + + return fclose($hFile); + } + else + { + throw new ConfigException("Could not write to configuration file", array('file' => $sFileName)); + } + } + + /** + * Helper function to initialize a configuration from the page arguments + * + * @param array $aParamValues + * @param string|null $sModulesDir + * @param bool $bPreserveModuleSettings + * + * @throws \Exception + * @throws \CoreException + */ + public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false) + { + if (isset($aParamValues['application_path'])) + { + $this->Set('app_root_url', $aParamValues['application_path']); + } + if (isset($aParamValues['graphviz_path'])) + { + $this->Set('graphviz_path', $aParamValues['graphviz_path']); + } + if (isset($aParamValues['mode']) && isset($aParamValues['language'])) + { + if (($aParamValues['mode'] == 'install') || $this->GetDefaultLanguage() == '') + { + $this->SetDefaultLanguage($aParamValues['language']); + } + } + if (isset($aParamValues['db_server'])) + { + $this->Set('db_host', $aParamValues['db_server']); + $this->Set('db_user', $aParamValues['db_user']); + $this->Set('db_pwd', $aParamValues['db_pwd']); + $sDBName = $aParamValues['db_name']; + if ($sDBName == '') + { + // Todo - obsolete after the transition to the new setup (2.0) is complete (WARNING: used by the designer) + if (isset($aParamValues['new_db_name'])) + { + $sDBName = $aParamValues['new_db_name']; + } + } + $this->Set('db_name', $sDBName); + $this->Set('db_subname', $aParamValues['db_prefix']); + + $bDbTlsEnabled = (bool) $aParamValues['db_tls_enabled']; + if ($bDbTlsEnabled) + { + $this->Set('db_tls.enabled', $bDbTlsEnabled, 'UpdateFromParams'); + } + else + { + // disabled : we don't want parameter in the file + $this->Set('db_tls.enabled', $bDbTlsEnabled, null); + } + $sDbTlsCa = $bDbTlsEnabled ? $aParamValues['db_tls_ca'] : null; + if (isset($sDbTlsCa) && !empty($sDbTlsCa)) { + $this->Set('db_tls.ca', $sDbTlsCa, 'UpdateFromParams'); + } else { + // empty parameter : we don't want it in the file + $this->Set('db_tls.ca', null, null); + } + } + + if (isset($aParamValues['selected_modules'])) + { + $aSelectedModules = explode(',', $aParamValues['selected_modules']); + } + else + { + $aSelectedModules = null; + } + $this->UpdateIncludes($sModulesDir, $aSelectedModules); + + if (isset($aParamValues['source_dir'])) + { + $this->Set('source_dir', $aParamValues['source_dir']); + } + } + + /** + * Helper function to rebuild the default configuration and the list of includes from a directory and a list of + * selected modules + * + * @param string $sModulesDir The relative path to the directory to scan for modules (typically the 'env-xxx' + * directory resulting from the compilation) + * @param array $aSelectedModules An array of selected modules' identifiers. If null all modules found will be + * considered as installed + * + * @throws Exception + */ + public function UpdateIncludes($sModulesDir, $aSelectedModules = null) + { + if (!is_null($sModulesDir)) + { + // Initialize the arrays below with default values for the application... + $oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values + $aAddOns = $oEmptyConfig->GetAddOns(); + + $aModules = ModuleDiscovery::GetAvailableModules(array(APPROOT.$sModulesDir)); + foreach ($aModules as $sModuleId => $aModuleInfo) + { + list ($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); + if (is_null($aSelectedModules) || in_array($sModuleName, $aSelectedModules)) + { + if (isset($aModuleInfo['settings'])) + { + list ($sName, $sVersion) = ModuleDiscovery::GetModuleName($sModuleId); + foreach ($aModuleInfo['settings'] as $sProperty => $value) + { + if (isset($this->m_aModuleSettings[$sName][$sProperty])) + { + // Do nothing keep the original value + } + else + { + $this->SetModuleSetting($sName, $sProperty, $value); + } + } + } + if (isset($aModuleInfo['installer'])) + { + $sModuleInstallerClass = $aModuleInfo['installer']; + if (!class_exists($sModuleInstallerClass)) + { + throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); + } + if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) + { + throw new Exception("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); + } + $aCallSpec = array($sModuleInstallerClass, 'BeforeWritingConfig'); + call_user_func_array($aCallSpec, array($this)); + } + } + } + $this->SetAddOns($aAddOns); + } + } + + /** + * Helper: for an array of string, change the prefix when found + * + * @param array $aStrings + * @param string $sSearchPrefix + * @param string $sNewPrefix + */ + protected static function ChangePrefix(&$aStrings, $sSearchPrefix, $sNewPrefix) + { + foreach ($aStrings as &$sFile) + { + if (substr($sFile, 0, strlen($sSearchPrefix)) == $sSearchPrefix) + { + $sFile = $sNewPrefix.substr($sFile, strlen($sSearchPrefix)); + } + } + } + + /** + * Obsolete: kept only for backward compatibility of the Toolkit + * Quick and dirty way to clone a config file into another environment + * + * @param string $sSourceEnv + * @param string $sTargetEnv + */ + public function ChangeModulesPath($sSourceEnv, $sTargetEnv) + { + // Now does nothing since the includes are built into the environment itself + } + + /** + * Pretty format a var_export'ed value so that (if possible) the identation is preserved on every line + * + * @param mixed $value The value to export + * @param string $sIndentation The string to use to indent the text + * @param bool $bForceIndentation Forces the identation (enven if it breaks/changes an eval, for example to ouput a + * value inside a comment) + * + * @return string The indented export string + */ + protected static function PrettyVarExport($value, $sIndentation, $bForceIndentation = false) + { + $sExport = var_export($value, true); + $sNiceExport = str_replace(array("\r\n", "\n", "\r"), "\n".$sIndentation, trim($sExport)); + if (!$bForceIndentation) + { + /** @var array $aImported */ + $aImported = null; + eval('$aImported='.$sNiceExport.';'); + // Check if adding the identations at the beginning of each line + // did not modify the values (in case of a string containing a line break) + if ($aImported != $value) + { + $sNiceExport = $sExport; + } + } + + return $sNiceExport; + } + +} diff --git a/core/coreexception.class.inc.php b/core/coreexception.class.inc.php index ae1a18af8..f0ffbd229 100644 --- a/core/coreexception.class.inc.php +++ b/core/coreexception.class.inc.php @@ -1,129 +1,129 @@ - - - -/** - * Exception management - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - - -class CoreException extends Exception -{ - public function __construct($sIssue, $aContextData = null, $sImpact = '') - { - $this->m_sIssue = $sIssue; - $this->m_sImpact = $sImpact; - $this->m_aContextData = $aContextData ? $aContextData : array(); - - $sMessage = $sIssue; - if (!empty($sImpact)) $sMessage .= "($sImpact)"; - if (count($this->m_aContextData) > 0) - { - $sMessage .= ": "; - $aContextItems = array(); - foreach($this->m_aContextData as $sKey => $value) - { - if (is_array($value)) - { - $aPairs = array(); - foreach($value as $key => $val) - { - if (is_array($val)) - { - $aPairs[] = $key.'=>('.implode(', ', $val).')'; - } - else - { - $aPairs[] = $key.'=>'.$val; - } - } - $sValue = '{'.implode('; ', $aPairs).'}'; - } - else - { - $sValue = $value; - } - $aContextItems[] = "$sKey = $sValue"; - } - $sMessage .= implode(', ', $aContextItems); - } - parent::__construct($sMessage, 0); - } - - /** - * @return string code and message for log purposes - */ - public function getInfoLog() - { - return 'error_code='.$this->getCode().', message="'.$this->getMessage().'"'; - } - public function getHtmlDesc($sHighlightHtmlBegin = '', $sHighlightHtmlEnd = '') - { - return $this->getMessage(); - } - - public function getTraceAsHtml() - { - $aBackTrace = $this->getTrace(); - return MyHelpers::get_callstack_html(0, $this->getTrace()); - // return "

    \n".$this->getTraceAsString()."
    \n"; - } - - public function addInfo($sKey, $value) - { - $this->m_aContextData[$sKey] = $value; - } - - public function getIssue() - { - return $this->m_sIssue; - } - public function getImpact() - { - return $this->m_sImpact; - } - public function getContextData() - { - return $this->m_aContextData; - } -} - -class CoreWarning extends CoreException -{ -} - -class CoreUnexpectedValue extends CoreException -{ -} - -class SecurityException extends CoreException -{ -} - -/** - * Throwned when querying on an object that exists in the database but is archived - * - * @see N.1108 - */ -class ArchivedObjectException extends CoreException -{ -} + + + +/** + * Exception management + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + + +class CoreException extends Exception +{ + public function __construct($sIssue, $aContextData = null, $sImpact = '') + { + $this->m_sIssue = $sIssue; + $this->m_sImpact = $sImpact; + $this->m_aContextData = $aContextData ? $aContextData : array(); + + $sMessage = $sIssue; + if (!empty($sImpact)) $sMessage .= "($sImpact)"; + if (count($this->m_aContextData) > 0) + { + $sMessage .= ": "; + $aContextItems = array(); + foreach($this->m_aContextData as $sKey => $value) + { + if (is_array($value)) + { + $aPairs = array(); + foreach($value as $key => $val) + { + if (is_array($val)) + { + $aPairs[] = $key.'=>('.implode(', ', $val).')'; + } + else + { + $aPairs[] = $key.'=>'.$val; + } + } + $sValue = '{'.implode('; ', $aPairs).'}'; + } + else + { + $sValue = $value; + } + $aContextItems[] = "$sKey = $sValue"; + } + $sMessage .= implode(', ', $aContextItems); + } + parent::__construct($sMessage, 0); + } + + /** + * @return string code and message for log purposes + */ + public function getInfoLog() + { + return 'error_code='.$this->getCode().', message="'.$this->getMessage().'"'; + } + public function getHtmlDesc($sHighlightHtmlBegin = '', $sHighlightHtmlEnd = '') + { + return $this->getMessage(); + } + + public function getTraceAsHtml() + { + $aBackTrace = $this->getTrace(); + return MyHelpers::get_callstack_html(0, $this->getTrace()); + // return "
    \n".$this->getTraceAsString()."
    \n"; + } + + public function addInfo($sKey, $value) + { + $this->m_aContextData[$sKey] = $value; + } + + public function getIssue() + { + return $this->m_sIssue; + } + public function getImpact() + { + return $this->m_sImpact; + } + public function getContextData() + { + return $this->m_aContextData; + } +} + +class CoreWarning extends CoreException +{ +} + +class CoreUnexpectedValue extends CoreException +{ +} + +class SecurityException extends CoreException +{ +} + +/** + * Throwned when querying on an object that exists in the database but is archived + * + * @see N.1108 + */ +class ArchivedObjectException extends CoreException +{ +} diff --git a/core/csvparser.class.inc.php b/core/csvparser.class.inc.php index 3912a37e7..fae9af6d1 100644 --- a/core/csvparser.class.inc.php +++ b/core/csvparser.class.inc.php @@ -1,271 +1,271 @@ - - - -/** - * CSV parser - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class CSVParserException extends CoreException -{ -} - -define('stSTARTING', 1); //grey zone: the type is undetermined -define('stRAW', 2); //building a non-qualified string -define('stQUALIFIED', 3); //building qualified string -define('stESCAPED', 4); //just encountered an escape char - -define('evBLANK', 0); -define('evSEPARATOR', 1); -define('evNEWLINE', 2); -define('evTEXTQUAL', 3); // used for escaping as well -define('evOTHERCHAR', 4); -define('evEND', 5); - -define('NULL_VALUE', ''); - - -/** - * CSVParser - * - * @package iTopORM - */ -class CSVParser -{ - private $m_sCSVData; - private $m_sSep; - private $m_sTextQualifier; - private $m_iTimeLimitPerRow; - - public function __construct($sTxt, $sSep = ',', $sTextQualifier = '"', $iTimeLimitPerRow = null) - { - $this->m_sCSVData = str_replace("\r\n", "\n", $sTxt); - $this->m_sSep = $sSep; - $this->m_sTextQualifier = $sTextQualifier; - $this->m_iTimeLimitPerRow = $iTimeLimitPerRow; - } - - protected $m_sCurrCell = ''; - protected $m_aCurrRow = array(); - protected $m_iToSkip = 0; - protected $m_aDataSet = array(); - - protected function __AddChar($c) - { - $this->m_sCurrCell .= $c; - } - protected function __ClearCell() - { - $this->m_sCurrCell = ''; - } - protected function __AddCell($c = null, $aFieldMap = null, $bTrimSpaces = false) - { - if ($bTrimSpaces) - { - $sCell = trim($this->m_sCurrCell); - } - else - { - $sCell = $this->m_sCurrCell; - } - if ($sCell == NULL_VALUE) - { - $sCell = null; - } - - if (!is_null($aFieldMap)) - { - $iNextCol = count($this->m_aCurrRow); - $iNextName = $aFieldMap[$iNextCol]; - $this->m_aCurrRow[$iNextName] = $sCell; - } - else - { - $this->m_aCurrRow[] = $sCell; - } - $this->m_sCurrCell = ''; - } - protected function __AddRow($c = null, $aFieldMap = null, $bTrimSpaces = false) - { - $this->__AddCell($c, $aFieldMap, $bTrimSpaces); - - if ($this->m_iToSkip > 0) - { - $this->m_iToSkip--; - } - elseif (count($this->m_aCurrRow) > 1) - { - $this->m_aDataSet[] = $this->m_aCurrRow; - } - elseif (count($this->m_aCurrRow) == 1) - { - // Get the unique value - $aValues = array_values($this->m_aCurrRow); - $sValue = $aValues[0]; - if (strlen($sValue) > 0) - { - $this->m_aDataSet[] = $this->m_aCurrRow; - } - } - else - { - // blank line, skip silently - } - $this->m_aCurrRow = array(); - - // More time for the next row - if ($this->m_iTimeLimitPerRow !== null) - { - set_time_limit($this->m_iTimeLimitPerRow); - } - } - protected function __AddCellTrimmed($c = null, $aFieldMap = null) - { - $this->__AddCell($c, $aFieldMap, true); - } - - protected function __AddRowTrimmed($c = null, $aFieldMap = null) - { - $this->__AddRow($c, $aFieldMap, true); - } - - function ToArray($iToSkip = 1, $aFieldMap = null, $iMax = 0) - { - $aTransitions = array(); - - $aTransitions[stSTARTING][evBLANK] = array('', stSTARTING); - $aTransitions[stSTARTING][evSEPARATOR] = array('__AddCell', stSTARTING); - $aTransitions[stSTARTING][evNEWLINE] = array('__AddRow', stSTARTING); - $aTransitions[stSTARTING][evTEXTQUAL] = array('', stQUALIFIED); - $aTransitions[stSTARTING][evOTHERCHAR] = array('__AddChar', stRAW); - $aTransitions[stSTARTING][evEND] = array('__AddRow', stSTARTING); - - $aTransitions[stRAW][evBLANK] = array('__AddChar', stRAW); - $aTransitions[stRAW][evSEPARATOR] = array('__AddCellTrimmed', stSTARTING); - $aTransitions[stRAW][evNEWLINE] = array('__AddRowTrimmed', stSTARTING); - $aTransitions[stRAW][evTEXTQUAL] = array('__AddChar', stRAW); - $aTransitions[stRAW][evOTHERCHAR] = array('__AddChar', stRAW); - $aTransitions[stRAW][evEND] = array('__AddRowTrimmed', stSTARTING); - - $aTransitions[stQUALIFIED][evBLANK] = array('__AddChar', stQUALIFIED); - $aTransitions[stQUALIFIED][evSEPARATOR] = array('__AddChar', stQUALIFIED); - $aTransitions[stQUALIFIED][evNEWLINE] = array('__AddChar', stQUALIFIED); - $aTransitions[stQUALIFIED][evTEXTQUAL] = array('', stESCAPED); - $aTransitions[stQUALIFIED][evOTHERCHAR] = array('__AddChar', stQUALIFIED); - $aTransitions[stQUALIFIED][evEND] = array('__AddRow', stSTARTING); - - $aTransitions[stESCAPED][evBLANK] = array('', stESCAPED); - $aTransitions[stESCAPED][evSEPARATOR] = array('__AddCell', stSTARTING); - $aTransitions[stESCAPED][evNEWLINE] = array('__AddRow', stSTARTING); - $aTransitions[stESCAPED][evTEXTQUAL] = array('__AddChar', stQUALIFIED); - $aTransitions[stESCAPED][evOTHERCHAR] = array('__AddChar', stSTARTING); - $aTransitions[stESCAPED][evEND] = array('__AddRow', stSTARTING); - - // Reset parser variables - $this->m_sCurrCell = ''; - $this->m_aCurrRow = array(); - $this->m_iToSkip = $iToSkip; - $this->m_aDataSet = array(); - - $iDataLength = strlen($this->m_sCSVData); - - $iState = stSTARTING; - $iTimeLimit = null; - if ($this->m_iTimeLimitPerRow !== null) - { - // Give some time for the first row - $iTimeLimit = ini_get('max_execution_time'); - set_time_limit($this->m_iTimeLimitPerRow); - } - for($i = 0; $i <= $iDataLength ; $i++) - { - if ($i == $iDataLength) - { - $c = null; - $iEvent = evEND; - } - else - { - $c = $this->m_sCSVData[$i]; - - if ($c == $this->m_sSep) - { - $iEvent = evSEPARATOR; - } - elseif ($c == ' ') - { - $iEvent = evBLANK; - } - elseif ($c == "\t") - { - $iEvent = evBLANK; - } - elseif ($c == "\n") - { - $iEvent = evNEWLINE; - } - elseif ($c == $this->m_sTextQualifier) - { - $iEvent = evTEXTQUAL; - } - else - { - $iEvent = evOTHERCHAR; - } - } - - $sAction = $aTransitions[$iState][$iEvent][0]; - $iState = $aTransitions[$iState][$iEvent][1]; - - if (!empty($sAction)) - { - $aCallSpec = array($this, $sAction); - if (is_callable($aCallSpec)) - { - call_user_func($aCallSpec, $c, $aFieldMap); - } - else - { - throw new CSVParserException("CSVParser: unknown verb '$sAction'"); - } - } - - $iLineCount = count($this->m_aDataSet); - if (($iMax > 0) && ($iLineCount >= $iMax)) break; - } - if ($iTimeLimit !== null) - { - // Restore the previous time limit - set_time_limit($iTimeLimit); - } - return $this->m_aDataSet; - } - - public function ListFields() - { - $aHeader = $this->ToArray(0, null, 1); - return $aHeader[0]; - } -} - - -?> + + + +/** + * CSV parser + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +class CSVParserException extends CoreException +{ +} + +define('stSTARTING', 1); //grey zone: the type is undetermined +define('stRAW', 2); //building a non-qualified string +define('stQUALIFIED', 3); //building qualified string +define('stESCAPED', 4); //just encountered an escape char + +define('evBLANK', 0); +define('evSEPARATOR', 1); +define('evNEWLINE', 2); +define('evTEXTQUAL', 3); // used for escaping as well +define('evOTHERCHAR', 4); +define('evEND', 5); + +define('NULL_VALUE', ''); + + +/** + * CSVParser + * + * @package iTopORM + */ +class CSVParser +{ + private $m_sCSVData; + private $m_sSep; + private $m_sTextQualifier; + private $m_iTimeLimitPerRow; + + public function __construct($sTxt, $sSep = ',', $sTextQualifier = '"', $iTimeLimitPerRow = null) + { + $this->m_sCSVData = str_replace("\r\n", "\n", $sTxt); + $this->m_sSep = $sSep; + $this->m_sTextQualifier = $sTextQualifier; + $this->m_iTimeLimitPerRow = $iTimeLimitPerRow; + } + + protected $m_sCurrCell = ''; + protected $m_aCurrRow = array(); + protected $m_iToSkip = 0; + protected $m_aDataSet = array(); + + protected function __AddChar($c) + { + $this->m_sCurrCell .= $c; + } + protected function __ClearCell() + { + $this->m_sCurrCell = ''; + } + protected function __AddCell($c = null, $aFieldMap = null, $bTrimSpaces = false) + { + if ($bTrimSpaces) + { + $sCell = trim($this->m_sCurrCell); + } + else + { + $sCell = $this->m_sCurrCell; + } + if ($sCell == NULL_VALUE) + { + $sCell = null; + } + + if (!is_null($aFieldMap)) + { + $iNextCol = count($this->m_aCurrRow); + $iNextName = $aFieldMap[$iNextCol]; + $this->m_aCurrRow[$iNextName] = $sCell; + } + else + { + $this->m_aCurrRow[] = $sCell; + } + $this->m_sCurrCell = ''; + } + protected function __AddRow($c = null, $aFieldMap = null, $bTrimSpaces = false) + { + $this->__AddCell($c, $aFieldMap, $bTrimSpaces); + + if ($this->m_iToSkip > 0) + { + $this->m_iToSkip--; + } + elseif (count($this->m_aCurrRow) > 1) + { + $this->m_aDataSet[] = $this->m_aCurrRow; + } + elseif (count($this->m_aCurrRow) == 1) + { + // Get the unique value + $aValues = array_values($this->m_aCurrRow); + $sValue = $aValues[0]; + if (strlen($sValue) > 0) + { + $this->m_aDataSet[] = $this->m_aCurrRow; + } + } + else + { + // blank line, skip silently + } + $this->m_aCurrRow = array(); + + // More time for the next row + if ($this->m_iTimeLimitPerRow !== null) + { + set_time_limit($this->m_iTimeLimitPerRow); + } + } + protected function __AddCellTrimmed($c = null, $aFieldMap = null) + { + $this->__AddCell($c, $aFieldMap, true); + } + + protected function __AddRowTrimmed($c = null, $aFieldMap = null) + { + $this->__AddRow($c, $aFieldMap, true); + } + + function ToArray($iToSkip = 1, $aFieldMap = null, $iMax = 0) + { + $aTransitions = array(); + + $aTransitions[stSTARTING][evBLANK] = array('', stSTARTING); + $aTransitions[stSTARTING][evSEPARATOR] = array('__AddCell', stSTARTING); + $aTransitions[stSTARTING][evNEWLINE] = array('__AddRow', stSTARTING); + $aTransitions[stSTARTING][evTEXTQUAL] = array('', stQUALIFIED); + $aTransitions[stSTARTING][evOTHERCHAR] = array('__AddChar', stRAW); + $aTransitions[stSTARTING][evEND] = array('__AddRow', stSTARTING); + + $aTransitions[stRAW][evBLANK] = array('__AddChar', stRAW); + $aTransitions[stRAW][evSEPARATOR] = array('__AddCellTrimmed', stSTARTING); + $aTransitions[stRAW][evNEWLINE] = array('__AddRowTrimmed', stSTARTING); + $aTransitions[stRAW][evTEXTQUAL] = array('__AddChar', stRAW); + $aTransitions[stRAW][evOTHERCHAR] = array('__AddChar', stRAW); + $aTransitions[stRAW][evEND] = array('__AddRowTrimmed', stSTARTING); + + $aTransitions[stQUALIFIED][evBLANK] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evSEPARATOR] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evNEWLINE] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evTEXTQUAL] = array('', stESCAPED); + $aTransitions[stQUALIFIED][evOTHERCHAR] = array('__AddChar', stQUALIFIED); + $aTransitions[stQUALIFIED][evEND] = array('__AddRow', stSTARTING); + + $aTransitions[stESCAPED][evBLANK] = array('', stESCAPED); + $aTransitions[stESCAPED][evSEPARATOR] = array('__AddCell', stSTARTING); + $aTransitions[stESCAPED][evNEWLINE] = array('__AddRow', stSTARTING); + $aTransitions[stESCAPED][evTEXTQUAL] = array('__AddChar', stQUALIFIED); + $aTransitions[stESCAPED][evOTHERCHAR] = array('__AddChar', stSTARTING); + $aTransitions[stESCAPED][evEND] = array('__AddRow', stSTARTING); + + // Reset parser variables + $this->m_sCurrCell = ''; + $this->m_aCurrRow = array(); + $this->m_iToSkip = $iToSkip; + $this->m_aDataSet = array(); + + $iDataLength = strlen($this->m_sCSVData); + + $iState = stSTARTING; + $iTimeLimit = null; + if ($this->m_iTimeLimitPerRow !== null) + { + // Give some time for the first row + $iTimeLimit = ini_get('max_execution_time'); + set_time_limit($this->m_iTimeLimitPerRow); + } + for($i = 0; $i <= $iDataLength ; $i++) + { + if ($i == $iDataLength) + { + $c = null; + $iEvent = evEND; + } + else + { + $c = $this->m_sCSVData[$i]; + + if ($c == $this->m_sSep) + { + $iEvent = evSEPARATOR; + } + elseif ($c == ' ') + { + $iEvent = evBLANK; + } + elseif ($c == "\t") + { + $iEvent = evBLANK; + } + elseif ($c == "\n") + { + $iEvent = evNEWLINE; + } + elseif ($c == $this->m_sTextQualifier) + { + $iEvent = evTEXTQUAL; + } + else + { + $iEvent = evOTHERCHAR; + } + } + + $sAction = $aTransitions[$iState][$iEvent][0]; + $iState = $aTransitions[$iState][$iEvent][1]; + + if (!empty($sAction)) + { + $aCallSpec = array($this, $sAction); + if (is_callable($aCallSpec)) + { + call_user_func($aCallSpec, $c, $aFieldMap); + } + else + { + throw new CSVParserException("CSVParser: unknown verb '$sAction'"); + } + } + + $iLineCount = count($this->m_aDataSet); + if (($iMax > 0) && ($iLineCount >= $iMax)) break; + } + if ($iTimeLimit !== null) + { + // Restore the previous time limit + set_time_limit($iTimeLimit); + } + return $this->m_aDataSet; + } + + public function ListFields() + { + $aHeader = $this->ToArray(0, null, 1); + return $aHeader[0]; + } +} + + +?> diff --git a/core/data.generator.class.inc.php b/core/data.generator.class.inc.php index ca5260363..1b095366d 100644 --- a/core/data.generator.class.inc.php +++ b/core/data.generator.class.inc.php @@ -1,374 +1,374 @@ - - - -/** - * data generator - * helps the consultants in creating dummy data sets, for various test purposes (validation, usability, scalability) - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * Data Generator helper class - * - * This class is useful to generate a lot of sample data that look consistent - * for a given organization in order to simulate a real CMDB - */ -class cmdbDataGenerator -{ - protected $m_sOrganizationKey; - protected $m_sOrganizationCode; - protected $m_sOrganizationName; - protected $m_OrganizationDomains; - - /** - * Constructor - */ - public function __construct($sOrganizationId = "") - { - global $aCompanies, $aCompaniesCode; - if ($sOrganizationId == '') - { - // No organization provided, pick a random and unused one from our predefined list - $retries = 5*count($aCompanies); - while ( ($retries > 0) && !isset($this->m_sOrganizationCode)) // Stupid algorithm, but I'm too lazy to do something bulletproof tonight - { - $index = rand(0, count($aCompanies) - 1); - if (!$this->OrganizationExists($aCompanies[$index]['code'])) - { - $this->m_sOrganizationCode = $aCompanies[$index]['code']; - $this->m_sOrganizationName = $aCompanies[$index]['name']; - $this->m_OrganizationDomains = $aCompanies[$index]['domain']; - } - $retries--; - } - } - else - { - // A code has been provided, let's take the information we need from the organization itself - $this->m_sOrganizationId = $sOrganizationId; - $oOrg = $this->GetOrganization($sOrganizationId); - if ($oOrg == null) - { - echo "Unable to find the organization '$sOrganisationCode' in the database... can not add objects into this organization.
    \n"; - exit(); - } - $this->m_sOrganizationCode = $oOrg->Get('code'); - $this->m_sOrganizationName = $oOrg->Get('name'); - if (!isset($aCompaniesCode[$this->m_sOrganizationCode]['domain'])) - { - // Generate some probable domain names for this organization - $this->m_OrganizationDomains = array(strtolower($this->m_sOrganizationCode).".com", strtolower($this->m_sOrganizationCode).".org", strtolower($this->m_sOrganizationCode)."corp.net",); - } - else - { - // Pick the domain names for this organization from the predefined list - $this->m_OrganizationDomains = $aCompaniesCode[$this->m_sOrganizationCode]['domain']; - } - } - - if (!isset($this->m_sOrganizationCode)) - { - echo "Unable to find an organization code which is not already used... can not create a new organization. Enhance the list of fake organizations (\$aCompanies in data_sample.inc.php).
    \n"; - exit(); - } - } - - /** - * Get the current organization id used by the generator - * - * @return string The organization id - */ - public function GetOrganizationId() - { - return $this->m_sOrganizationId; - } - - /** - * Get the current organization id used by the generator - * - * @param string The organization id - * @return none - */ - public function SetOrganizationId($sId) - { - $this->m_sOrganizationId = $sId; - } - - /** - * Get the current organization code used by the generator - * - * @return string The organization code - */ - public function GetOrganizationCode() - { - return $this->m_sOrganizationCode; - } - - /** - * Get the current organization name used by the generator - * - * @return string The organization name - */ - function GetOrganizationName() - { - return $this->m_sOrganizationName; - } - - /** - * Get a pseudo random first name taken from a (big) prefedined list - * - * @return string A random first name - */ - function GenerateFirstName() - { - global $aFirstNames; - return $aFirstNames[rand(0, count($aFirstNames) - 1)]; - } - - /** - * Get a pseudo random last name taken from a (big) prefedined list - * - * @return string A random last name - */ - function GenerateLastName() - { - global $aNames; - return $aNames[rand(0, count($aNames) - 1)]; - } - - /** - * Get a pseudo random country name taken from a prefedined list - * - * @return string A random city name - */ - function GenerateCountryName() - { - global $aCountries; - return $aCountries[rand(0, count($aCountries) - 1)]; - } - - /** - * Get a pseudo random city name taken from a (big) prefedined list - * - * @return string A random city name - */ - function GenerateCityName() - { - global $aCities; - return $aCities[rand(0, count($aCities) - 1)]; - } - - /** - * Get a pseudo random email address made of the first name, last name and organization's domain - * - * @return string A random email address - */ - function GenerateEmail($sFirstName, $sLastName) - { - if (rand(1, 20) > 18) - { - // some people (let's say 5~10%) have an irregular email address - $sEmail = strtolower($this->CleanForEmail($sLastName))."@".strtolower($this->GenerateDomain()); - } - else - { - $sEmail = strtolower($this->CleanForEmail($sFirstName)).".".strtolower($this->CleanForEmail($sLastName))."@".strtolower($this->GenerateDomain()); - } - return $sEmail; - } - - /** - * Generate (pseudo) random strings that follow a given pattern - * - * The template is made of any number of 'parts' separated by pipes '|' - * Each part is either: - * - domain() => returns a domain name for the current organization - * - enum(aaa,bb,c,dddd) => returns randomly one of aaa,bb,c or dddd with the same - * probability of occurence. If you want to change the probability you can repeat some values - * i.e enum(most probable,most probable,most probable,most probable,most probable,rare) - * - number(xxx-yyy) => a random number between xxx and yyy (bounds included) - * note that if the first number (xxx) begins with a zero, then the result will zero padded - * to the same number of digits as xxx. - * All other 'part' that does not follow one of the above mentioned pattern is returned as is - * - * Example: GenerateString("enum(sw,rtr,gw)|number(00-99)|.|domain()") - * will produce strings like "sw01.netcmdb.com" or "rtr45.itop.org" - * - * @param string $sTemplate The template used for generating the string - * @return string The generated pseudo random the string - */ - function GenerateString($sTemplate) - { - $sResult = ""; - $aParts = explode("\|", $sTemplate); - foreach($aParts as $sPart) - { - if (preg_match("/domain\(\)/", $sPart, $aMatches)) - { - $sResult .= strtolower($this->GenerateDomain()); - } - elseif (preg_match("/enum\((.+)\)/", $sPart, $aMatches)) - { - $sEnumValues = $aMatches[1]; - $aEnumValues = explode(",", $sEnumValues); - $sResult .= $aEnumValues[rand(0, count($aEnumValues) - 1)]; - } - elseif (preg_match("/number\((\d+)-(\d+)\)/", $sPart, $aMatches)) - { - $sStartNumber = $aMatches[1]; - if ($sStartNumber[0] == '0') - { - // number must be zero padded - $sFormat = "%0".strlen($sStartNumber)."d"; - } - else - { - $sFormat = "%d"; - } - $sEndNumber = $aMatches[2]; - $sResult .= sprintf($sFormat, rand($sStartNumber, $sEndNumber)); - } - else - { - $sResult .= $sPart; - } - } - return $sResult; - } - - /** - * Generate a foreign key by picking a random element of the given class in a set limited by the given search criteria - * - * Example: GenerateKey("bizLocation", array('org_id', $oGenerator->GetOrganizationId()); - * will produce the foreign key of a Location object picked at random in the same organization - * - * @param string $sClass The name of the class to search for - * @param string $aFilterCriteria A hash array of filterCOde => FilterValue (the strict operator '=' is used ) - * @return mixed The key to an object of the given class, or null if none are found - */ - function GenerateKey($sClass, $aFilterCriteria) - { - $retKey = null; - $oFilter = new DBObjectSearch($sClass); - foreach($aFilterCriteria as $sFilterCode => $filterValue) - { - $oFilter->AddCondition($sFilterCode, $filterValue, '='); - } - $oSet = new CMDBObjectSet($oFilter); - if ($oSet->Count() > 0) - { - $max_count = $index = rand(1, $oSet->Count()); - do - { - $oObj = $oSet->Fetch(); - $index--; - } - while($index > 0); - - if (!is_object($oObj)) - { - echo "
    ";
    -				echo "ERROR: non empty set, but invalid object picked! class='$sClass'\n";
    -				echo "Index chosen: $max_count\n";
    -				echo "The set is supposed to contain ".$oSet->Count()." object(s)\n";
    -				echo "Filter criteria:\n";
    -				print_r($aFilterCriteria);
    -				echo "
    "; - } - else - { - $retKey = $oObj->GetKey(); - } - } - return $retKey; - } - /////////////////////////////////////////////////////////////////////////////// - // - // Protected methods - // - /////////////////////////////////////////////////////////////////////////////// - - /** - * Generate a (random) domain name consistent with the organization name & code - * - * The values are pulled from a (limited) predefined list. Note that a given - * organization may have several domain names, so the result may be random - * - * @return string A domain name (like netcnmdb.com) - */ - protected function GenerateDomain() - { - if (is_array($this->m_OrganizationDomains)) - { - $sDomain = $this->m_OrganizationDomains[rand(0, count($this->m_OrganizationDomains)-1)]; - } - else - { - $sDomain = $this->m_OrganizationDomains; - } - return $sDomain; - } - - /** - * Strips accented characters from a string in order to produce a suitable email address - * - * @param string The text string to clean - * @return string The cleanified text string - */ - protected function CleanForEmail($sText) - { - return str_replace(array("'", "", "", "", "", "", "", "", "", ""), array("", "e", "e", "e", "c", "a", "a", "n", "oe", "ae"), $sText); - } - - /** - * Check if an organization with the given code already exists in the database - * - * @param string $sCode The code to look for - * @return boolean true if the given organization exists, false otherwise - */ - protected function OrganizationExists($sCode) - { - $oFilter = new DBObjectSearch('Organization'); - $oFilter->AddCondition('code', $sCode, '='); - $oSet = new CMDBObjectSet($oFilter); - return ($oSet->Count() > 0); - } - - /** - * Search for an organization with the given code in the database - * - * @param string $Id The organization Id to look for - * @return cmdbOrganization the organization if it exists, null otherwise - */ - protected function GetOrganization($sId) - { - $oOrg = null; - $oFilter = new DBObjectSearch('Organization'); - $oFilter->AddCondition('id', $sId, '='); - $oSet = new CMDBObjectSet($oFilter); - if ($oSet->Count() > 0) - { - $oOrg = $oSet->Fetch(); // Let's take the first one found - } - return $oOrg; - } -} -?> + + + +/** + * data generator + * helps the consultants in creating dummy data sets, for various test purposes (validation, usability, scalability) + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * Data Generator helper class + * + * This class is useful to generate a lot of sample data that look consistent + * for a given organization in order to simulate a real CMDB + */ +class cmdbDataGenerator +{ + protected $m_sOrganizationKey; + protected $m_sOrganizationCode; + protected $m_sOrganizationName; + protected $m_OrganizationDomains; + + /** + * Constructor + */ + public function __construct($sOrganizationId = "") + { + global $aCompanies, $aCompaniesCode; + if ($sOrganizationId == '') + { + // No organization provided, pick a random and unused one from our predefined list + $retries = 5*count($aCompanies); + while ( ($retries > 0) && !isset($this->m_sOrganizationCode)) // Stupid algorithm, but I'm too lazy to do something bulletproof tonight + { + $index = rand(0, count($aCompanies) - 1); + if (!$this->OrganizationExists($aCompanies[$index]['code'])) + { + $this->m_sOrganizationCode = $aCompanies[$index]['code']; + $this->m_sOrganizationName = $aCompanies[$index]['name']; + $this->m_OrganizationDomains = $aCompanies[$index]['domain']; + } + $retries--; + } + } + else + { + // A code has been provided, let's take the information we need from the organization itself + $this->m_sOrganizationId = $sOrganizationId; + $oOrg = $this->GetOrganization($sOrganizationId); + if ($oOrg == null) + { + echo "Unable to find the organization '$sOrganisationCode' in the database... can not add objects into this organization.
    \n"; + exit(); + } + $this->m_sOrganizationCode = $oOrg->Get('code'); + $this->m_sOrganizationName = $oOrg->Get('name'); + if (!isset($aCompaniesCode[$this->m_sOrganizationCode]['domain'])) + { + // Generate some probable domain names for this organization + $this->m_OrganizationDomains = array(strtolower($this->m_sOrganizationCode).".com", strtolower($this->m_sOrganizationCode).".org", strtolower($this->m_sOrganizationCode)."corp.net",); + } + else + { + // Pick the domain names for this organization from the predefined list + $this->m_OrganizationDomains = $aCompaniesCode[$this->m_sOrganizationCode]['domain']; + } + } + + if (!isset($this->m_sOrganizationCode)) + { + echo "Unable to find an organization code which is not already used... can not create a new organization. Enhance the list of fake organizations (\$aCompanies in data_sample.inc.php).
    \n"; + exit(); + } + } + + /** + * Get the current organization id used by the generator + * + * @return string The organization id + */ + public function GetOrganizationId() + { + return $this->m_sOrganizationId; + } + + /** + * Get the current organization id used by the generator + * + * @param string The organization id + * @return none + */ + public function SetOrganizationId($sId) + { + $this->m_sOrganizationId = $sId; + } + + /** + * Get the current organization code used by the generator + * + * @return string The organization code + */ + public function GetOrganizationCode() + { + return $this->m_sOrganizationCode; + } + + /** + * Get the current organization name used by the generator + * + * @return string The organization name + */ + function GetOrganizationName() + { + return $this->m_sOrganizationName; + } + + /** + * Get a pseudo random first name taken from a (big) prefedined list + * + * @return string A random first name + */ + function GenerateFirstName() + { + global $aFirstNames; + return $aFirstNames[rand(0, count($aFirstNames) - 1)]; + } + + /** + * Get a pseudo random last name taken from a (big) prefedined list + * + * @return string A random last name + */ + function GenerateLastName() + { + global $aNames; + return $aNames[rand(0, count($aNames) - 1)]; + } + + /** + * Get a pseudo random country name taken from a prefedined list + * + * @return string A random city name + */ + function GenerateCountryName() + { + global $aCountries; + return $aCountries[rand(0, count($aCountries) - 1)]; + } + + /** + * Get a pseudo random city name taken from a (big) prefedined list + * + * @return string A random city name + */ + function GenerateCityName() + { + global $aCities; + return $aCities[rand(0, count($aCities) - 1)]; + } + + /** + * Get a pseudo random email address made of the first name, last name and organization's domain + * + * @return string A random email address + */ + function GenerateEmail($sFirstName, $sLastName) + { + if (rand(1, 20) > 18) + { + // some people (let's say 5~10%) have an irregular email address + $sEmail = strtolower($this->CleanForEmail($sLastName))."@".strtolower($this->GenerateDomain()); + } + else + { + $sEmail = strtolower($this->CleanForEmail($sFirstName)).".".strtolower($this->CleanForEmail($sLastName))."@".strtolower($this->GenerateDomain()); + } + return $sEmail; + } + + /** + * Generate (pseudo) random strings that follow a given pattern + * + * The template is made of any number of 'parts' separated by pipes '|' + * Each part is either: + * - domain() => returns a domain name for the current organization + * - enum(aaa,bb,c,dddd) => returns randomly one of aaa,bb,c or dddd with the same + * probability of occurence. If you want to change the probability you can repeat some values + * i.e enum(most probable,most probable,most probable,most probable,most probable,rare) + * - number(xxx-yyy) => a random number between xxx and yyy (bounds included) + * note that if the first number (xxx) begins with a zero, then the result will zero padded + * to the same number of digits as xxx. + * All other 'part' that does not follow one of the above mentioned pattern is returned as is + * + * Example: GenerateString("enum(sw,rtr,gw)|number(00-99)|.|domain()") + * will produce strings like "sw01.netcmdb.com" or "rtr45.itop.org" + * + * @param string $sTemplate The template used for generating the string + * @return string The generated pseudo random the string + */ + function GenerateString($sTemplate) + { + $sResult = ""; + $aParts = explode("\|", $sTemplate); + foreach($aParts as $sPart) + { + if (preg_match("/domain\(\)/", $sPart, $aMatches)) + { + $sResult .= strtolower($this->GenerateDomain()); + } + elseif (preg_match("/enum\((.+)\)/", $sPart, $aMatches)) + { + $sEnumValues = $aMatches[1]; + $aEnumValues = explode(",", $sEnumValues); + $sResult .= $aEnumValues[rand(0, count($aEnumValues) - 1)]; + } + elseif (preg_match("/number\((\d+)-(\d+)\)/", $sPart, $aMatches)) + { + $sStartNumber = $aMatches[1]; + if ($sStartNumber[0] == '0') + { + // number must be zero padded + $sFormat = "%0".strlen($sStartNumber)."d"; + } + else + { + $sFormat = "%d"; + } + $sEndNumber = $aMatches[2]; + $sResult .= sprintf($sFormat, rand($sStartNumber, $sEndNumber)); + } + else + { + $sResult .= $sPart; + } + } + return $sResult; + } + + /** + * Generate a foreign key by picking a random element of the given class in a set limited by the given search criteria + * + * Example: GenerateKey("bizLocation", array('org_id', $oGenerator->GetOrganizationId()); + * will produce the foreign key of a Location object picked at random in the same organization + * + * @param string $sClass The name of the class to search for + * @param string $aFilterCriteria A hash array of filterCOde => FilterValue (the strict operator '=' is used ) + * @return mixed The key to an object of the given class, or null if none are found + */ + function GenerateKey($sClass, $aFilterCriteria) + { + $retKey = null; + $oFilter = new DBObjectSearch($sClass); + foreach($aFilterCriteria as $sFilterCode => $filterValue) + { + $oFilter->AddCondition($sFilterCode, $filterValue, '='); + } + $oSet = new CMDBObjectSet($oFilter); + if ($oSet->Count() > 0) + { + $max_count = $index = rand(1, $oSet->Count()); + do + { + $oObj = $oSet->Fetch(); + $index--; + } + while($index > 0); + + if (!is_object($oObj)) + { + echo "
    ";
    +				echo "ERROR: non empty set, but invalid object picked! class='$sClass'\n";
    +				echo "Index chosen: $max_count\n";
    +				echo "The set is supposed to contain ".$oSet->Count()." object(s)\n";
    +				echo "Filter criteria:\n";
    +				print_r($aFilterCriteria);
    +				echo "
    "; + } + else + { + $retKey = $oObj->GetKey(); + } + } + return $retKey; + } + /////////////////////////////////////////////////////////////////////////////// + // + // Protected methods + // + /////////////////////////////////////////////////////////////////////////////// + + /** + * Generate a (random) domain name consistent with the organization name & code + * + * The values are pulled from a (limited) predefined list. Note that a given + * organization may have several domain names, so the result may be random + * + * @return string A domain name (like netcnmdb.com) + */ + protected function GenerateDomain() + { + if (is_array($this->m_OrganizationDomains)) + { + $sDomain = $this->m_OrganizationDomains[rand(0, count($this->m_OrganizationDomains)-1)]; + } + else + { + $sDomain = $this->m_OrganizationDomains; + } + return $sDomain; + } + + /** + * Strips accented characters from a string in order to produce a suitable email address + * + * @param string The text string to clean + * @return string The cleanified text string + */ + protected function CleanForEmail($sText) + { + return str_replace(array("'", "�", "�", "�", "�", "�", "�", "�", "�", "�"), array("", "e", "e", "e", "c", "a", "a", "n", "oe", "ae"), $sText); + } + + /** + * Check if an organization with the given code already exists in the database + * + * @param string $sCode The code to look for + * @return boolean true if the given organization exists, false otherwise + */ + protected function OrganizationExists($sCode) + { + $oFilter = new DBObjectSearch('Organization'); + $oFilter->AddCondition('code', $sCode, '='); + $oSet = new CMDBObjectSet($oFilter); + return ($oSet->Count() > 0); + } + + /** + * Search for an organization with the given code in the database + * + * @param string $Id The organization Id to look for + * @return cmdbOrganization the organization if it exists, null otherwise + */ + protected function GetOrganization($sId) + { + $oOrg = null; + $oFilter = new DBObjectSearch('Organization'); + $oFilter->AddCondition('id', $sId, '='); + $oSet = new CMDBObjectSet($oFilter); + if ($oSet->Count() > 0) + { + $oOrg = $oSet->Fetch(); // Let's take the first one found + } + return $oOrg; + } +} +?> diff --git a/core/dbobject.class.php b/core/dbobject.class.php index ff5c5c55d..68c8c4332 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1,3733 +1,3733 @@ - - -/** - * All objects to be displayed in the application (either as a list or as details) - * must implement this interface. - */ -interface iDisplay -{ - - /** - * Maps the given context parameter name to the appropriate filter/search code for this class - * @param string $sContextParam Name of the context parameter, i.e. 'org_id' - * @return string Filter code, i.e. 'customer_id' - */ - public static function MapContextParam($sContextParam); - /** - * This function returns a 'hilight' CSS class, used to hilight a given row in a table - * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL, - * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE - * To Be overridden by derived classes - * @param void - * @return String The desired higlight class for the object/row - */ - public function GetHilightClass(); - /** - * Returns the relative path to the page that handles the display of the object - * @return string - */ - public static function GetUIPage(); - /** - * Displays the details of the object - */ - public function DisplayDetails(WebPage $oPage, $bEditMode = false); -} - -/** - * Class dbObject: the root of persistent classes - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once('metamodel.class.php'); -require_once('deletionplan.class.inc.php'); -require_once('mutex.class.inc.php'); - - -/** - * A persistent object, as defined by the metamodel - * - * @package iTopORM - */ -abstract class DBObject implements iDisplay -{ - private static $m_aMemoryObjectsByClass = array(); - - private static $m_aBulkInsertItems = array(); // class => array of ('table' => array of (array of )) - private static $m_aBulkInsertCols = array(); // class => array of ('table' => array of ) - private static $m_bBulkInsert = false; - - protected $m_bIsInDB = false; // true IIF the object is mapped to a DB record - protected $m_iKey = null; - private $m_aCurrValues = array(); - protected $m_aOrigValues = array(); - - protected $m_aExtendedData = null; - - private $m_bDirty = false; // Means: "a modification is ongoing" - // The object may have incorrect external keys, then any attempt of reload must be avoided - private $m_bCheckStatus = null; // Means: the object has been verified and is consistent with integrity rules - // if null, then the check has to be performed again to know the status - protected $m_bSecurityIssue = null; - protected $m_aCheckIssues = null; - protected $m_aDeleteIssues = null; - - private $m_bFullyLoaded = false; // Compound objects can be partially loaded - private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode - protected $m_aTouchedAtt = array(); // list of (potentially) modified sAttCodes - protected $m_aModifiedAtt = array(); // real modification status: for each attCode can be: unset => don't know, true => modified, false => not modified (the same value as the original value was set) - protected $m_aSynchroData = null; // Set of Synch data related to this object - protected $m_sHighlightCode = null; - protected $m_aCallbacks = array(); - - // Use the MetaModel::NewObject to build an object (do we have to force it?) - public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) - { - if (!empty($aRow)) - { - $this->FromRow($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec); - $this->m_bFullyLoaded = $this->IsFullyLoaded(); - $this->m_aTouchedAtt = array(); - $this->m_aModifiedAtt = array(); - return; - } - // Creation of a brand new object - // - - $this->m_iKey = self::GetNextTempId(get_class($this)); - - // set default values - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) - { - $this->m_aCurrValues[$sAttCode] = $this->GetDefaultValue($sAttCode); - $this->m_aOrigValues[$sAttCode] = null; - if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName)) - { - // This field has to be read from the DB - // Leave the flag unset (optimization) - } - else - { - // No need to trigger a reload for that attribute - // Let's consider it as being already fully loaded - $this->m_aLoadedAtt[$sAttCode] = true; - } - } - } - - // Read-only <=> Written once (archive) - public function RegisterAsDirty() - { - // While the object may be written to the DB, it is NOT possible to reload it - // or at least not possible to reload it the same way - $this->m_bDirty = true; - } - - public function IsNew() - { - return (!$this->m_bIsInDB); - } - - // Returns an Id for memory objects - static protected function GetNextTempId($sClass) - { - $sRootClass = MetaModel::GetRootClass($sClass); - if (!array_key_exists($sRootClass, self::$m_aMemoryObjectsByClass)) - { - self::$m_aMemoryObjectsByClass[$sRootClass] = 0; - } - self::$m_aMemoryObjectsByClass[$sRootClass]++; - return (- self::$m_aMemoryObjectsByClass[$sRootClass]); - } - - public function __toString() - { - $sRet = ''; - $sClass = get_class($this); - $sRootClass = MetaModel::GetRootClass($sClass); - $iPKey = $this->GetKey(); - $sFriendlyname = $this->Get('friendlyname'); - $sRet .= "$sClass::$iPKey ($sFriendlyname)
    \n"; - return $sRet; - } - - // Restore initial values... mmmm, to be discussed - public function DBRevert() - { - $this->Reload(); - } - - protected function IsFullyLoaded() - { - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) - { - if (!$oAttDef->LoadInObject()) continue; - if (!isset($this->m_aLoadedAtt[$sAttCode]) || !$this->m_aLoadedAtt[$sAttCode]) - { - return false; - } - } - return true; - } - - /** - * @param bool $bAllowAllData DEPRECATED: the reload must never fail! - * @throws CoreException - */ - public function Reload($bAllowAllData = false) - { - assert($this->m_bIsInDB); - $aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey, false /* must be found */, true /* AllowAllData */); - if (empty($aRow)) - { - throw new CoreException("Failed to reload object of class '".get_class($this)."', id = ".$this->m_iKey); - } - $this->FromRow($aRow); - - // Process linked set attributes - // - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) - { - if (!$oAttDef->IsLinkSet()) continue; - - $this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue($this); - $this->m_aOrigValues[$sAttCode] = clone $this->m_aCurrValues[$sAttCode]; - $this->m_aLoadedAtt[$sAttCode] = true; - } - - $this->m_bFullyLoaded = true; - $this->m_aTouchedAtt = array(); - $this->m_aModifiedAtt = array(); - } - - protected function FromRow($aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) - { - if (strlen($sClassAlias) == 0) - { - // Default to the current class - $sClassAlias = get_class($this); - } - - $this->m_iKey = null; - $this->m_bIsInDB = true; - $this->m_aCurrValues = array(); - $this->m_aOrigValues = array(); - $this->m_aLoadedAtt = array(); - $this->m_bCheckStatus = true; - - // Get the key - // - $sKeyField = $sClassAlias."id"; - if (!array_key_exists($sKeyField, $aRow)) - { - // #@# Bug ? - throw new CoreException("Missing key for class '".get_class($this)."'"); - } - - $iPKey = $aRow[$sKeyField]; - if (!self::IsValidPKey($iPKey)) - { - if (is_null($iPKey)) - { - throw new CoreException("Missing object id in query result (found null)"); - } - else - { - throw new CoreException("An object id must be an integer value ($iPKey)"); - } - } - $this->m_iKey = $iPKey; - - // Build the object from an array of "attCode"=>"value") - // - $bFullyLoaded = true; // ... set to false if any attribute is not found - if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad)) - { - $aAttList = MetaModel::ListAttributeDefs(get_class($this)); - } - else - { - $aAttList = $aAttToLoad[$sClassAlias]; - } - - foreach($aAttList as $sAttCode=>$oAttDef) - { - // Skip links (could not be loaded by the mean of this query) - if ($oAttDef->IsLinkSet()) continue; - - if (!$oAttDef->LoadInObject()) continue; - - unset($value); - $bIsDefined = false; - if ($oAttDef->LoadFromDB()) - { - // Note: we assume that, for a given attribute, if it can be loaded, - // then one column will be found with an empty suffix, the others have a suffix - // Take care: the function isset will return false in case the value is null, - // which is something that could happen on open joins - $sAttRef = $sClassAlias.$sAttCode; - - if (array_key_exists($sAttRef, $aRow)) - { - $value = $oAttDef->FromSQLToValue($aRow, $sAttRef); - $bIsDefined = true; - } - } - else - { - $value = $oAttDef->ReadValue($this); - $bIsDefined = true; - } - - if ($bIsDefined) - { - $this->m_aCurrValues[$sAttCode] = $value; - if (is_object($value)) - { - $this->m_aOrigValues[$sAttCode] = clone $value; - } - else - { - $this->m_aOrigValues[$sAttCode] = $value; - } - $this->m_aLoadedAtt[$sAttCode] = true; - } - else - { - // This attribute was expected and not found in the query columns - $bFullyLoaded = false; - } - } - - // Load extended data - if ($aExtendedDataSpec != null) - { - $aExtendedDataSpec['table']; - foreach($aExtendedDataSpec['fields'] as $sColumn) - { - $sColRef = $sClassAlias.'_extdata_'.$sColumn; - if (array_key_exists($sColRef, $aRow)) - { - $this->m_aExtendedData[$sColumn] = $aRow[$sColRef]; - } - } - } - return $bFullyLoaded; - } - - protected function _Set($sAttCode, $value) - { - $this->m_aCurrValues[$sAttCode] = $value; - $this->m_aTouchedAtt[$sAttCode] = true; - unset($this->m_aModifiedAtt[$sAttCode]); - } - - public function Set($sAttCode, $value) - { - if ($sAttCode == 'finalclass') - { - // Ignore it - this attribute is set upon object creation and that's it - return false; - } - - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - - if (!$oAttDef->IsWritable()) - { - $sClass = get_class($this); - throw new Exception("Attempting to set the value on the read-only attribute $sClass::$sAttCode"); - } - - if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty) - { - // First time Set is called... ensure that the object gets fully loaded - // Otherwise we would lose the values on a further Reload - // + consistency does not make sense ! - $this->Reload(); - } - - if ($oAttDef->IsExternalKey()) - { - if (is_object($value)) - { - // Setting an external key with a whole object (instead of just an ID) - // let's initialize also the external fields that depend on it - // (useful when building objects in memory and not from a query) - if ( (get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass()))) - { - throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored"); - } - - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) - { - if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) - { - $this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode()); - $this->m_aLoadedAtt[$sCode] = true; - } - } - } - else if ($this->m_aCurrValues[$sAttCode] != $value) - { - // Setting an external key, but no any other information is available... - // Invalidate the corresponding fields so that they get reloaded in case they are needed (See Get()) - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) - { - if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) - { - $this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode); - unset($this->m_aLoadedAtt[$sCode]); - } - } - } - } - if ($oAttDef->IsLinkSet() && ($value != null)) - { - $realvalue = clone $this->m_aCurrValues[$sAttCode]; - $realvalue->UpdateFromCompleteList($value); - } - else - { - $realvalue = $oAttDef->MakeRealValue($value, $this); - } - $this->_Set($sAttCode, $realvalue); - - foreach (MetaModel::ListMetaAttributes(get_class($this), $sAttCode) as $sMetaAttCode => $oMetaAttDef) - { - $this->_Set($sMetaAttCode, $oMetaAttDef->MapValue($this)); - } - - // The object has changed, reset caches - $this->m_bCheckStatus = null; - - // Make sure we do not reload it anymore... before saving it - $this->RegisterAsDirty(); - - // This function is eligible as a lifecycle action: returning true upon success is a must - return true; - } - - public function SetTrim($sAttCode, $sValue) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - $iMaxSize = $oAttDef->GetMaxSize(); - if ($iMaxSize && (strlen($sValue) > $iMaxSize)) - { - $sValue = substr($sValue, 0, $iMaxSize); - } - $this->Set($sAttCode, $sValue); - } - - public function GetLabel($sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAttDef->GetLabel(); - } - - public function Get($sAttCode) - { - if (($iPos = strpos($sAttCode, '->')) === false) - { - return $this->GetStrict($sAttCode); - } - else - { - $sExtKeyAttCode = substr($sAttCode, 0, $iPos); - $sRemoteAttCode = substr($sAttCode, $iPos + 2); - if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) - { - throw new CoreException("Unknown external key '$sExtKeyAttCode' for the class ".get_class($this)); - } - - $oExtFieldAtt = MetaModel::FindExternalField(get_class($this), $sExtKeyAttCode, $sRemoteAttCode); - if (!is_null($oExtFieldAtt)) - { - return $this->GetStrict($oExtFieldAtt->GetCode()); - } - else - { - $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); - $sRemoteClass = $oKeyAttDef->GetTargetClass(); - $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); - if (is_null($oRemoteObj)) - { - return ''; - } - else - { - return $oRemoteObj->Get($sRemoteAttCode); - } - } - } - } - - public function GetStrict($sAttCode) - { - if ($sAttCode == 'id') - { - return $this->m_iKey; - } - - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - - if (!$oAttDef->LoadInObject()) - { - $value = $oAttDef->GetValue($this); - } - else - { - if (isset($this->m_aLoadedAtt[$sAttCode])) - { - // Standard case... we have the information directly - } - elseif ($this->m_bIsInDB && !$this->m_bDirty) - { - // Lazy load (polymorphism): complete by reloading the entire object - // #@# non-scalar attributes.... handle that differently? - $oKPI = new ExecutionKPI(); - $this->Reload(); - $oKPI->ComputeStats('Reload', get_class($this).'/'.$sAttCode); - } - elseif ($sAttCode == 'friendlyname') - { - // The friendly name is not computed and the object is dirty - // Todo: implement the computation of the friendly name based on sprintf() - // - $this->m_aCurrValues[$sAttCode] = ''; - } - else - { - // Not loaded... is it related to an external key? - if ($oAttDef->IsExternalField()) - { - // Let's get the object and compute all of the corresponding attributes - // (i.e not only the requested attribute) - // - $sExtKeyAttCode = $oAttDef->GetKeyAttCode(); - - if (($iRemote = $this->Get($sExtKeyAttCode)) && ($iRemote > 0)) // Objects in memory have negative IDs - { - $oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); - // Note: "allow all data" must be enabled because the external fields are always visible - // to the current user even if this is not the case for the remote object - // This is consistent with the behavior of the lists - $oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote, true, true); - } - else - { - $oRemote = null; - } - - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) - { - if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sExtKeyAttCode)) - { - if ($oRemote) - { - $this->m_aCurrValues[$sCode] = $oRemote->Get($oDef->GetExtAttCode()); - } - else - { - $this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode); - } - $this->m_aLoadedAtt[$sCode] = true; - } - } - } - } - $value = $this->m_aCurrValues[$sAttCode]; - } - - if ($value instanceof ormLinkSet) - { - $value->Rewind(); - } - return $value; - } - - public function GetOriginal($sAttCode) - { - if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this)))) - { - throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this)); - } - $aOrigValues = $this->m_aOrigValues; - return isset($aOrigValues[$sAttCode]) ? $aOrigValues[$sAttCode] : null; - } - - /** - * Returns the default value of the $sAttCode. By default, returns the default value of the AttributeDefinition. - * Overridable. - * - * @param $sAttCode - * @return mixed - */ - public function GetDefaultValue($sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAttDef->GetDefaultValue($this); - } - - /** - * Returns data loaded by the mean of a dynamic and explicit JOIN - */ - public function GetExtendedData() - { - return $this->m_aExtendedData; - } - - /** - * Set the HighlightCode if the given code has a greater rank than the current HilightCode - * @param string $sCode - * @return void - */ - protected function SetHighlightCode($sCode) - { - $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); - $fCurrentRank = 0.0; - if (($this->m_sHighlightCode !== null) && array_key_exists($this->m_sHighlightCode, $aHighlightScale)) - { - $fCurrentRank = $aHighlightScale[$this->m_sHighlightCode]['rank']; - } - - if (array_key_exists($sCode, $aHighlightScale)) - { - $fRank = $aHighlightScale[$sCode]['rank']; - if ($fRank > $fCurrentRank) - { - $this->m_sHighlightCode = $sCode; - } - } - } - - /** - * Get the current HighlightCode - * @return string The Hightlight code (null if none set, meaning rank = 0) - */ - protected function GetHighlightCode() - { - return $this->m_sHighlightCode; - } - - protected function ComputeHighlightCode() - { - // First if the state defines a HiglightCode, apply it - $sState = $this->GetState(); - if ($sState != '') - { - $sCode = MetaModel::GetHighlightCode(get_class($this), $sState); - $this->SetHighlightCode($sCode); - } - // The check for each StopWatch if a HighlightCode is effective - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeStopWatch) - { - $oStopWatch = $this->Get($sAttCode); - $sCode = $oStopWatch->GetHighlightCode(); - if ($sCode !== '') - { - $this->SetHighlightCode($sCode); - } - } - } - return $this->GetHighlightCode(); - } - - /** - * Updates the value of an external field by (re)loading the object - * corresponding to the external key and getting the value from it - * - * UNUSED ? - * - * @param string $sAttCode Attribute code of the external field to update - * @return void - */ - protected function UpdateExternalField($sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if ($oAttDef->IsExternalField()) - { - $sTargetClass = $oAttDef->GetTargetClass(); - $objkey = $this->Get($oAttDef->GetKeyAttCode()); - // Note: "allow all data" must be enabled because the external fields are always visible - // to the current user even if this is not the case for the remote object - // This is consistent with the behavior of the lists - $oObj = MetaModel::GetObject($sTargetClass, $objkey, true, true); - if (is_object($oObj)) - { - $value = $oObj->Get($oAttDef->GetExtAttCode()); - $this->Set($sAttCode, $value); - } - } - } - - public function ComputeValues() - { - } - - // Compute scalar attributes that depend on any other type of attribute - final public function DoComputeValues() - { - // TODO - use a flag rather than checking the call stack -> this will certainly accelerate things - - // First check that we are not currently computing the fields - // (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...) - foreach (debug_backtrace() as $aCallInfo) - { - if (!array_key_exists("class", $aCallInfo)) continue; - if ($aCallInfo["class"] != get_class($this)) continue; - if ($aCallInfo["function"] != "ComputeValues") continue; - return; //skip! - } - - // Set the "null-not-allowed" datetimes (and dates) whose value is not initialized - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - // AttributeDate is derived from AttributeDateTime - if (($oAttDef instanceof AttributeDateTime) && (!$oAttDef->IsNullAllowed()) && ($this->Get($sAttCode) == $oAttDef->GetNullValue())) - { - $this->Set($sAttCode, date($oAttDef->GetInternalFormat())); - } - } - - $this->ComputeValues(); - } - - public function GetAsHTML($sAttCode, $bLocalize = true) - { - $sClass = get_class($this); - $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); - - if ($oAtt->IsExternalKey(EXTKEY_ABSOLUTE)) - { - //return $this->Get($sAttCode.'_friendlyname'); - $sTargetClass = $oAtt->GetTargetClass(EXTKEY_ABSOLUTE); - $iTargetKey = $this->Get($sAttCode); - if ($iTargetKey < 0) - { - // the key points to an object that exists only in memory... no hyperlink points to it yet - return ''; - } - else - { - $sHtmlLabel = htmlentities($this->Get($sAttCode.'_friendlyname'), ENT_QUOTES, 'UTF-8'); - $bArchived = $this->IsArchived($sAttCode); - $bObsolete = $this->IsObsolete($sAttCode); - return $this->MakeHyperLink($sTargetClass, $iTargetKey, $sHtmlLabel, null, true, $bArchived, $bObsolete); - } - } - - // That's a standard attribute (might be an ext field or a direct field, etc.) - return $oAtt->GetAsHTML($this->Get($sAttCode), $this, $bLocalize); - } - - public function GetEditValue($sAttCode) - { - $sClass = get_class($this); - $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); - - if ($oAtt->IsExternalKey()) - { - $sTargetClass = $oAtt->GetTargetClass(); - if ($this->IsNew()) - { - // The current object exists only in memory, don't try to query it in the DB ! - // instead let's query for the object pointed by the external key, and get its name - $targetObjId = $this->Get($sAttCode); - $oTargetObj = MetaModel::GetObject($sTargetClass, $targetObjId, false); // false => not sure it exists - if (is_object($oTargetObj)) - { - $sEditValue = $oTargetObj->GetName(); - } - else - { - $sEditValue = 0; - } - } - else - { - $sEditValue = $this->Get($sAttCode.'_friendlyname'); - } - } - else - { - $sEditValue = $oAtt->GetEditValue($this->Get($sAttCode), $this); - } - return $sEditValue; - } - - public function GetAsXML($sAttCode, $bLocalize = true) - { - $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsXML($this->Get($sAttCode), $this, $bLocalize); - } - - public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) - { - $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); - } - - public function GetOriginalAsHTML($sAttCode, $bLocalize = true) - { - $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this, $bLocalize); - } - - public function GetOriginalAsXML($sAttCode, $bLocalize = true) - { - $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this, $bLocalize); - } - - public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) - { - $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); - } - - /** - * @param $sObjClass - * @param $sObjKey - * @param string $sHtmlLabel Label with HTML entities escaped (< escaped as <) - * @param null $sUrlMakerClass - * @param bool|true $bWithNavigationContext - * @param bool|false $bArchived - * @param bool|false $bObsolete - * @return string - * @throws DictExceptionMissingString - */ - public static function MakeHyperLink($sObjClass, $sObjKey, $sHtmlLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true, $bArchived = false, $bObsolete = false) - { - if ($sObjKey <= 0) return ''.Dict::S('UI:UndefinedObject').''; // Objects built in memory have negative IDs - - // Safety net - // - if (empty($sHtmlLabel)) - { - // If the object if not issued from a query but constructed programmatically - // the label may be empty. In this case run a query to get the object's friendly name - $oTmpObj = MetaModel::GetObject($sObjClass, $sObjKey, false); - if (is_object($oTmpObj)) - { - $sHtmlLabel = $oTmpObj->GetName(); - } - else - { - // May happen in case the target object is not in the list of allowed values for this attribute - $sHtmlLabel = "$sObjClass::$sObjKey"; - } - } - $sHint = MetaModel::GetName($sObjClass)."::$sObjKey"; - $sUrl = ApplicationContext::MakeObjectUrl($sObjClass, $sObjKey, $sUrlMakerClass, $bWithNavigationContext); - - $bClickable = !$bArchived || utils::IsArchiveMode(); - if ($bArchived) - { - $sSpanClass = 'archived'; - $sFA = 'fa-archive object-archived'; - $sHint = Dict::S('ObjectRef:Archived'); - } - elseif ($bObsolete) - { - $sSpanClass = 'obsolete'; - $sFA = 'fa-eye-slash object-obsolete'; - $sHint = Dict::S('ObjectRef:Obsolete'); - } - else - { - $sSpanClass = ''; - $sFA = ''; - } - if ($sFA == '') - { - $sIcon = ''; - } - else - { - if ($bClickable) - { - $sIcon = ""; - } - else - { - $sIcon = ""; - } - } - - if ($bClickable && (strlen($sUrl) > 0)) - { - $sHLink = "
    $sIcon$sHtmlLabel"; - } - else - { - $sHLink = $sIcon.$sHtmlLabel; - } - $sRet = "$sHLink"; - return $sRet; - } - - public function GetHyperlink($sUrlMakerClass = null, $bWithNavigationContext = true) - { - $bArchived = $this->IsArchived(); - $bObsolete = $this->IsObsolete(); - return self::MakeHyperLink(get_class($this), $this->GetKey(), $this->GetName(), $sUrlMakerClass, $bWithNavigationContext, $bArchived, $bObsolete); - } - - public static function ComputeStandardUIPage($sClass) - { - static $aUIPagesCache = array(); // Cache to store the php page used to display each class of object - if (!isset($aUIPagesCache[$sClass])) - { - $UIPage = false; - if (is_callable("$sClass::GetUIPage")) - { - $UIPage = eval("return $sClass::GetUIPage();"); // May return false in case of error - } - $aUIPagesCache[$sClass] = $UIPage === false ? './UI.php' : $UIPage; - } - $sPage = $aUIPagesCache[$sClass]; - return $sPage; - } - - public static function GetUIPage() - { - return 'UI.php'; - } - - - // could be in the metamodel ? - public static function IsValidPKey($value) - { - return ((string)$value === (string)(int)$value); - } - - public function GetKey() - { - return $this->m_iKey; - } - public function SetKey($iNewKey) - { - if (!self::IsValidPKey($iNewKey)) - { - throw new CoreException("An object id must be an integer value ($iNewKey)"); - } - - if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey)) - { - throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database"); - } - $this->m_iKey = $iNewKey; - } - /** - * Get the icon representing this object - * @param boolean $bImgTag If true the result is a full IMG tag (or an emtpy string if no icon is defined) - * @return string Either the full IMG tag ($bImgTag == true) or just the URL to the icon file - */ - public function GetIcon($bImgTag = true) - { - $sCode = $this->ComputeHighlightCode(); - if($sCode != '') - { - $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); - if (array_key_exists($sCode, $aHighlightScale)) - { - $sIconUrl = $aHighlightScale[$sCode]['icon']; - if($bImgTag) - { - return ""; - } - else - { - return $sIconUrl; - } - } - } - return MetaModel::GetClassIcon(get_class($this), $bImgTag); - } - - /** - * Gets the name of an object in a safe manner for displaying inside a web page - * @return string - */ - public function GetName() - { - return htmlentities($this->GetRawName(), ENT_QUOTES, 'UTF-8'); - } - - /** - * Gets the raw name of an object, this is not safe for displaying inside a web page - * since the " < > characters are not escaped and the name may contain some XSS script - * instructions. - * Use this function only for internal computations or for an output to a non-HTML destination - * @return string - */ - public function GetRawName() - { - return $this->Get('friendlyname'); - } - - public function GetState() - { - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (empty($sStateAttCode)) - { - return ''; - } - else - { - return $this->Get($sStateAttCode); - } - } - - public function GetStateLabel() - { - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (empty($sStateAttCode)) - { - return ''; - } - else - { - $sStateValue = $this->Get($sStateAttCode); - return MetaModel::GetStateLabel(get_class($this), $sStateValue); - } - } - - public function GetStateDescription() - { - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (empty($sStateAttCode)) - { - return ''; - } - else - { - $sStateValue = $this->Get($sStateAttCode); - return MetaModel::GetStateDescription(get_class($this), $sStateValue); - } - } - - /** - * Overridable - Define attributes read-only from the end-user perspective - * - * @return array List of attcodes - */ - public static function GetReadOnlyAttributes() - { - return null; - } - - - /** - * Overridable - Get predefined objects (could be hardcoded) - * The predefined objects will be synchronized with the DB at each install/upgrade - * As soon as a class has predefined objects, then nobody can create nor delete objects - * @return array An array of id => array of attcode => php value(so-called "real value": integer, string, ormDocument, DBObjectSet, etc.) - */ - public static function GetPredefinedObjects() - { - return null; - } - - /** - * - * @param string $sAttCode $sAttCode The code of the attribute - * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas) - * @param string $sTargetState The target state in which to evalutate the flags, if empty the current state will be - * used - * - * @return integer the binary combination of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) for the - * given attribute in the given state of the object - * @throws \CoreException - */ - public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '') - { - $iFlags = 0; // By default (if no life cycle) no flag at all - - $aReadOnlyAtts = $this->GetReadOnlyAttributes(); - if (($aReadOnlyAtts != null) && (in_array($sAttCode, $aReadOnlyAtts))) - { - return OPT_ATT_READONLY; - } - - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (!empty($sStateAttCode)) - { - if ($sTargetState != '') - { - $iFlags = MetaModel::GetAttributeFlags(get_class($this), $sTargetState, $sAttCode); - } - else - { - $iFlags = MetaModel::GetAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); - } - } - $aReasons = array(); - $iSynchroFlags = 0; - if ($this->InSyncScope()) - { - $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); - } - return $iFlags | $iSynchroFlags; // Combine both sets of flags - } - - /** - * @param string $sAttCode - * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas) - * - * @throws \CoreException - */ - public function IsAttributeReadOnlyForCurrentState($sAttCode, &$aReasons = array()) - { - $iAttFlags = $this->GetAttributeFlags($sAttCode, $aReasons); - - return ($iAttFlags & OPT_ATT_READONLY); - } - - /** - * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) - * for the given attribute in a transition - * @param $sAttCode string $sAttCode The code of the attribute - * @param $sStimulus string The stimulus code to apply - * @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas) - * @param $sOriginState string The state from which to apply $sStimulus, if empty current state will be used - * @return integer Flags: the binary combination of the flags applicable to this attribute - */ - public function GetTransitionFlags($sAttCode, $sStimulus, &$aReasons = array(), $sOriginState = '') - { - $iFlags = 0; // By default (if no lifecycle) no flag at all - - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - // If no state attribute, there is no lifecycle - if (empty($sStateAttCode)) - { - return $iFlags; - } - - // Retrieving current state if necessary - if ($sOriginState === '') - { - $sOriginState = $this->Get($sStateAttCode); - } - - // Retrieving attribute flags - $iAttributeFlags = $this->GetAttributeFlags($sAttCode, $aReasons, $sOriginState); - - // Retrieving transition flags - $iTransitionFlags = MetaModel::GetTransitionFlags(get_class($this), $sOriginState, $sStimulus, $sAttCode); - - // Merging transition flags with attribute flags - $iFlags = $iTransitionFlags | $iAttributeFlags; - - return $iFlags; - } - - /** - * Returns an array of attribute codes (with their flags) when $sStimulus is applied on the object in the $sOriginState state. - * Note: Attributes (and flags) from the target state and the transition are combined. - * - * @param $sStimulus string - * @param $sOriginState string Default is current state - * @return array - */ - public function GetTransitionAttributes($sStimulus, $sOriginState = null) - { - $sObjClass = get_class($this); - - // Defining current state as origin state if not specified - if($sOriginState === null) - { - $sOriginState = $this->GetState(); - } - - $aAttributes = MetaModel::GetTransitionAttributes($sObjClass, $sStimulus, $sOriginState); - - return $aAttributes; - } - - /** - * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) - * for the given attribute for the current state of the object considered as an INITIAL state - * @param string $sAttCode The code of the attribute - * @return integer Flags: the binary combination of the flags applicable to this attribute - */ - public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array()) - { - $iFlags = 0; - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (!empty($sStateAttCode)) - { - $iFlags = MetaModel::GetInitialStateAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); - } - return $iFlags; // No need to care about the synchro flags since we'll be creating a new object anyway - } - - // check if the given (or current) value is suitable for the attribute - // return true if successfull - // return the error desciption otherwise - public function CheckValue($sAttCode, $value = null) - { - if (!is_null($value)) - { - $toCheck = $value; - } - else - { - $toCheck = $this->Get($sAttCode); - } - - $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if (!$oAtt->IsWritable()) - { - return true; - } - elseif ($oAtt->IsNull($toCheck)) - { - if ($oAtt->IsNullAllowed()) - { - return true; - } - else - { - return "Null not allowed"; - } - } - elseif ($oAtt->IsExternalKey()) - { - if (!MetaModel::SkipCheckExtKeys()) - { - $sTargetClass = $oAtt->GetTargetClass(); - $oTargetObj = MetaModel::GetObject($sTargetClass, $toCheck, false /*must be found*/, true /*allow all data*/); - if (is_null($oTargetObj)) - { - return "Target object not found ($sTargetClass::$toCheck)"; - } - } - if ($oAtt->IsHierarchicalKey()) - { - // This check cannot be deactivated since otherwise the user may break things by a CSV import of a bulk modify - $aValues = $oAtt->GetAllowedValues(array('this' => $this)); - if (!array_key_exists($toCheck, $aValues)) - { - return "Value not allowed [$toCheck]"; - } - } - } - elseif ($oAtt->IsScalar()) - { - $aValues = $oAtt->GetAllowedValues($this->ToArgsForQuery()); - if (is_array($aValues) && (count($aValues) > 0)) - { - if (!array_key_exists($toCheck, $aValues)) - { - return "Value not allowed [$toCheck]"; - } - } - if (!is_null($iMaxSize = $oAtt->GetMaxSize())) - { - $iLen = strlen($toCheck); - if ($iLen > $iMaxSize) - { - return "String too long (found $iLen, limited to $iMaxSize)"; - } - } - if (!$oAtt->CheckFormat($toCheck)) - { - return "Wrong format [$toCheck]"; - } - } - else - { - return $oAtt->CheckValue($this, $toCheck); - } - return true; - } - - // check attributes together - public function CheckConsistency() - { - return true; - } - - // check integrity rules (before inserting or updating the object) - // a displayable error is returned - public function DoCheckToWrite() - { - $this->DoComputeValues(); - - $aChanges = $this->ListChanges(); - - foreach($aChanges as $sAttCode => $value) - { - $res = $this->CheckValue($sAttCode); - if ($res !== true) - { - // $res contains the error description - $this->m_aCheckIssues[] = "Unexpected value for attribute '$sAttCode': $res"; - } - } - if (count($this->m_aCheckIssues) > 0) - { - // No need to check consistency between attributes if any of them has - // an unexpected value - return; - } - $res = $this->CheckConsistency(); - if ($res !== true) - { - // $res contains the error description - $this->m_aCheckIssues[] = "Consistency rules not followed: $res"; - } - - // Synchronization: are we attempting to modify an attribute for which an external source is master? - // - if ($this->m_bIsInDB && $this->InSyncScope() && (count($aChanges) > 0)) - { - foreach($aChanges as $sAttCode => $value) - { - $iFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); - if ($iFlags & OPT_ATT_SLAVE) - { - // Note: $aReasonInfo['name'] could be reported (the task owning the attribute) - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - $sAttLabel = $oAttDef->GetLabel(); - foreach($aReasons as $aReasonInfo) - { - // Todo: associate the attribute code with the error - $this->m_aCheckIssues[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $sAttLabel); - } - } - } - } - } - - final public function CheckToWrite() - { - if (MetaModel::SkipCheckToWrite()) - { - return array(true, array()); - } - if (is_null($this->m_bCheckStatus)) - { - $this->m_aCheckIssues = array(); - - $oKPI = new ExecutionKPI(); - $this->DoCheckToWrite(); - $oKPI->ComputeStats('CheckToWrite', get_class($this)); - if (count($this->m_aCheckIssues) == 0) - { - $this->m_bCheckStatus = true; - } - else - { - $this->m_bCheckStatus = false; - } - } - return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue); - } - - // check if it is allowed to delete the existing object from the database - // a displayable error is returned - protected function DoCheckToDelete(&$oDeletionPlan) - { - $this->m_aDeleteIssues = array(); // Ok - - if ($this->InSyncScope()) - { - - foreach ($this->GetSynchroData() as $iSourceId => $aSourceData) - { - foreach ($aSourceData['replica'] as $oReplica) - { - $oDeletionPlan->AddToDelete($oReplica, DEL_SILENT); - } - $oDataSource = $aSourceData['source']; - if ($oDataSource->GetKey() == SynchroExecution::GetCurrentTaskId()) - { - // The current task has the right to delete the object - continue; - } - $oReplica = reset($aSourceData['replica']); // Take the first one - if ($oReplica->Get('status_dest_creator') != 1) - { - // The object is not owned by the task - continue; - } - - $sLink = $oDataSource->GetName(); - $sUserDeletePolicy = $oDataSource->Get('user_delete_policy'); - switch($sUserDeletePolicy) - { - case 'nobody': - $this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink); - break; - - case 'administrators': - if (!UserRights::IsAdministrator()) - { - $this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink); - } - break; - - case 'everybody': - default: - // Ok - break; - } - } - } - } - - public function CheckToDelete(&$oDeletionPlan) - { - $this->MakeDeletionPlan($oDeletionPlan); - $oDeletionPlan->ComputeResults(); - return (!$oDeletionPlan->FoundStopper()); - } - - protected function ListChangedValues(array $aProposal) - { - $aDelta = array(); - foreach ($aProposal as $sAtt => $proposedValue) - { - if (!array_key_exists($sAtt, $this->m_aOrigValues)) - { - // The value was not set - $aDelta[$sAtt] = $proposedValue; - } - elseif(!array_key_exists($sAtt, $this->m_aTouchedAtt) || (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == false)) - { - // This attCode was never set, cannot be modified - // or the same value - as the original value - was set, and has been verified as equivalent to the original value - continue; - } - else if (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == true) - { - // We already know that the value is really modified - $aDelta[$sAtt] = $proposedValue; - } - elseif(is_object($proposedValue)) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt); - // The value is an object, the comparison is not strict - if (!$oAttDef->Equals($this->m_aOrigValues[$sAtt], $proposedValue)) - { - $aDelta[$sAtt] = $proposedValue; - $this->m_aModifiedAtt[$sAtt] = true; // Really modified - } - else - { - $this->m_aModifiedAtt[$sAtt] = false; // Not really modified - } - } - else - { - // The value is a scalar, the comparison must be 100% strict - if($this->m_aOrigValues[$sAtt] !== $proposedValue) - { - //echo "$sAtt:
    \n";
    -					//var_dump($this->m_aOrigValues[$sAtt]);
    -					//var_dump($proposedValue);
    -					//echo "
    \n"; - $aDelta[$sAtt] = $proposedValue; - $this->m_aModifiedAtt[$sAtt] = true; // Really modified - } - else - { - $this->m_aModifiedAtt[$sAtt] = false; // Not really modified - } - } - } - return $aDelta; - } - - // List the attributes that have been changed - // Returns an array of attname => currentvalue - public function ListChanges() - { - if ($this->m_bIsInDB) - { - return $this->ListChangedValues($this->m_aCurrValues); - } - else - { - return $this->m_aCurrValues; - } - } - - // Tells whether or not an object was modified since last read (ie: does it differ from the DB ?) - public function IsModified() - { - $aChanges = $this->ListChanges(); - return (count($aChanges) != 0); - } - - public function Equals($oSibling) - { - if (get_class($oSibling) != get_class($this)) - { - return false; - } - if ($this->GetKey() != $oSibling->GetKey()) - { - return false; - } - if ($this->m_bIsInDB) - { - // If one has changed, then consider them as being different - if ($this->IsModified() || $oSibling->IsModified()) - { - return false; - } - } - else - { - // Todo - implement this case (loop on every attribute) - //foreach(MetaModel::ListAttributeDefs(get_class($this) as $sAttCode => $oAttDef) - //{ - //if (!isset($this->m_CurrentValues[$sAttCode])) continue; - //if (!isset($this->m_CurrentValues[$sAttCode])) continue; - //if (!$oAttDef->Equals($this->m_CurrentValues[$sAttCode], $oSibling->m_CurrentValues[$sAttCode])) - //{ - //return false; - //} - //} - return false; - } - return true; - } - - // used only by insert - protected function OnObjectKeyReady() - { - // Meant to be overloaded - } - - // used both by insert/update - private function DBWriteLinks() - { - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if (!$oAttDef->IsLinkSet()) continue; - if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; - if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; - - $oLinkSet = $this->m_aCurrValues[$sAttCode]; - $oLinkSet->DBWrite($this); - } - } - - // used both by insert/update - private function WriteExternalAttributes() - { - foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if (!$oAttDef->LoadInObject()) continue; - if ($oAttDef->LoadFromDB()) continue; - if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; - if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; - $oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]); - } - } - - // Note: this is experimental - it was designed to speed up the setup of iTop - // Known limitations: - // - does not work with multi-table classes (issue with the unique id to maintain in several tables) - // - the id of the object is not updated - static public final function BulkInsertStart() - { - self::$m_bBulkInsert = true; - } - - static public final function BulkInsertFlush() - { - if (!self::$m_bBulkInsert) return; - - foreach(self::$m_aBulkInsertCols as $sClass => $aTables) - { - foreach ($aTables as $sTable => $sColumns) - { - $sValues = implode(', ', self::$m_aBulkInsertItems[$sClass][$sTable]); - $sInsertSQL = "INSERT INTO `$sTable` ($sColumns) VALUES $sValues"; - $iNewKey = CMDBSource::InsertInto($sInsertSQL); - } - } - - // Reset - self::$m_aBulkInsertItems = array(); - self::$m_aBulkInsertCols = array(); - self::$m_bBulkInsert = false; - } - - private function DBInsertSingleTable($sTableClass) - { - $sTable = MetaModel::DBGetTable($sTableClass); - // Abstract classes or classes having no specific attribute do not have an associated table - if ($sTable == '') return; - - $sClass = get_class($this); - - // fields in first array, values in the second - $aFieldsToWrite = array(); - $aValuesToWrite = array(); - - if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) - { - // Add it to the list of fields to write - $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; - $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); - } - - $aHierarchicalKeys = array(); - - foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) - { - // Skip this attribute if not defined in this table - if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; - $aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]); - foreach($aAttColumns as $sColumn => $sValue) - { - $aFieldsToWrite[] = "`$sColumn`"; - $aValuesToWrite[] = CMDBSource::Quote($sValue); - } - if ($oAttDef->IsHierarchicalKey()) - { - $aHierarchicalKeys[$sAttCode] = $oAttDef; - } - } - - if (count($aValuesToWrite) == 0) return false; - - if (MetaModel::DBIsReadOnly()) - { - $iNewKey = -1; - } - else - { - if (self::$m_bBulkInsert) - { - if (!isset(self::$m_aBulkInsertCols[$sClass][$sTable])) - { - self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite); - } - self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')'; - - $iNewKey = 999999; // TODO - compute next id.... - } - else - { - if (count($aHierarchicalKeys) > 0) - { - foreach($aHierarchicalKeys as $sAttCode => $oAttDef) - { - $aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable); - $aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`'; - $aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()]; - $aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`'; - $aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()]; - } - } - $sInsertSQL = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).")"; - $iNewKey = CMDBSource::InsertInto($sInsertSQL); - } - } - // Note that it is possible to have a key defined here, and the autoincrement expected, this is acceptable in a non root class - if (empty($this->m_iKey)) - { - // Take the autonumber - $this->m_iKey = $iNewKey; - } - return $this->m_iKey; - } - - // Insert of record for the new object into the database - // Returns the key of the newly created object - public function DBInsertNoReload() - { - if ($this->m_bIsInDB) - { - throw new CoreException("The object already exists into the Database, you may want to use the clone function"); - } - - $sClass = get_class($this); - $sRootClass = MetaModel::GetRootClass($sClass); - - // Ensure the update of the values (we are accessing the data directly) - $this->DoComputeValues(); - $this->OnInsert(); - - if ($this->m_iKey < 0) - { - // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it! - $this->m_iKey = null; - } - - // If not automatically computed, then check that the key is given by the caller - if (!MetaModel::IsAutoIncrementKey($sRootClass)) - { - if (empty($this->m_iKey)) - { - throw new CoreWarning("Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber", array('class' => $sRootClass)); - } - } - - // Ultimate check - ensure DB integrity - list($bRes, $aIssues) = $this->CheckToWrite(); - if (!$bRes) - { - $sIssues = implode(', ', $aIssues); - throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); - } - - // Stop watches - $sState = $this->GetState(); - if ($sState != '') - { - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeStopWatch) - { - if (in_array($sState, $oAttDef->GetStates())) - { - // Start the stop watch and compute the deadlines - $oSW = $this->Get($sAttCode); - $oSW->Start($this, $oAttDef); - $oSW->ComputeDeadlines($this, $oAttDef); - $this->Set($sAttCode, $oSW); - } - } - } - } - - // First query built upon on the root class, because the ID must be created first - $this->m_iKey = $this->DBInsertSingleTable($sRootClass); - - // Then do the leaf class, if different from the root class - if ($sClass != $sRootClass) - { - $this->DBInsertSingleTable($sClass); - } - - // Then do the other classes - foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) - { - if ($sParentClass == $sRootClass) continue; - $this->DBInsertSingleTable($sParentClass); - } - - $this->OnObjectKeyReady(); - - $this->DBWriteLinks(); - $this->WriteExternalAttributes(); - - $this->m_bIsInDB = true; - $this->m_bDirty = false; - foreach ($this->m_aCurrValues as $sAttCode => $value) - { - if (is_object($value)) - { - $value = clone $value; - } - $this->m_aOrigValues[$sAttCode] = $value; - } - - $this->AfterInsert(); - - // Activate any existing trigger - $sClass = get_class($this); - $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectCreate AS t WHERE t.target_class IN ('$sClassList')")); - while ($oTrigger = $oSet->Fetch()) - { - $oTrigger->DoActivate($this->ToArgs('this')); - } - - // Callbacks registered with RegisterCallback - if (isset($this->m_aCallbacks[self::CALLBACK_AFTERINSERT])) - { - foreach ($this->m_aCallbacks[self::CALLBACK_AFTERINSERT] as $aCallBackData) - { - call_user_func_array($aCallBackData['callback'], $aCallBackData['params']); - } - } - - $this->RecordObjCreation(); - - return $this->m_iKey; - } - - protected function MakeInsertStatementSingleTable($aAuthorizedExtKeys, &$aStatements, $sTableClass) - { - $sTable = MetaModel::DBGetTable($sTableClass); - // Abstract classes or classes having no specific attribute do not have an associated table - if ($sTable == '') return; - - $sClass = get_class($this); - - // fields in first array, values in the second - $aFieldsToWrite = array(); - $aValuesToWrite = array(); - - if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) - { - // Add it to the list of fields to write - $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; - $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); - } - - $aHierarchicalKeys = array(); - foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) - { - // Skip this attribute if not defined in this table - if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; - // Skip link set that can still be undefined though the object is 100% loaded - if ($oAttDef->IsLinkSet()) continue; - - $value = $this->m_aCurrValues[$sAttCode]; - if ($oAttDef->IsExternalKey()) - { - $sTargetClass = $oAttDef->GetTargetClass(); - if (is_array($aAuthorizedExtKeys)) - { - if (!array_key_exists($sTargetClass, $aAuthorizedExtKeys) || !array_key_exists($value, $aAuthorizedExtKeys[$sTargetClass])) - { - $value = 0; - } - } - } - $aAttColumns = $oAttDef->GetSQLValues($value); - foreach($aAttColumns as $sColumn => $sValue) - { - $aFieldsToWrite[] = "`$sColumn`"; - $aValuesToWrite[] = CMDBSource::Quote($sValue); - } - if ($oAttDef->IsHierarchicalKey()) - { - $aHierarchicalKeys[$sAttCode] = $oAttDef; - } - } - - if (count($aValuesToWrite) == 0) return false; - - if (count($aHierarchicalKeys) > 0) - { - foreach($aHierarchicalKeys as $sAttCode => $oAttDef) - { - $aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable); - $aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`'; - $aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()]; - $aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`'; - $aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()]; - } - } - $aStatements[] = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).");"; - } - - public function MakeInsertStatements($aAuthorizedExtKeys, &$aStatements) - { - $sClass = get_class($this); - $sRootClass = MetaModel::GetRootClass($sClass); - - // First query built upon on the root class, because the ID must be created first - $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sRootClass); - - // Then do the leaf class, if different from the root class - if ($sClass != $sRootClass) - { - $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sClass); - } - - // Then do the other classes - foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) - { - if ($sParentClass == $sRootClass) continue; - $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sParentClass); - } - } - - public function DBInsert() - { - $this->DBInsertNoReload(); - $this->Reload(); - return $this->m_iKey; - } - - public function DBInsertTracked(CMDBChange $oChange) - { - CMDBObject::SetCurrentChange($oChange); - return $this->DBInsert(); - } - - public function DBInsertTrackedNoReload(CMDBChange $oChange) - { - CMDBObject::SetCurrentChange($oChange); - return $this->DBInsertNoReload(); - } - - // Creates a copy of the current object into the database - // Returns the id of the newly created object - public function DBClone($iNewKey = null) - { - $this->m_bIsInDB = false; - $this->m_iKey = $iNewKey; - $ret = $this->DBInsert(); - $this->RecordObjCreation(); - return $ret; - } - - /** - * This function is automatically called after cloning an object with the "clone" PHP language construct - * The purpose of this method is to reset the appropriate attributes of the object in - * order to make sure that the newly cloned object is really distinct from its clone - */ - public function __clone() - { - $this->m_bIsInDB = false; - $this->m_bDirty = true; - $this->m_iKey = self::GetNextTempId(get_class($this)); - } - - // Update a record - public function DBUpdate() - { - if (!$this->m_bIsInDB) - { - throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead"); - } - - // Protect against reentrance (e.g. cascading the update of ticket logs) - static $aUpdateReentrance = array(); - $sKey = get_class($this).'::'.$this->GetKey(); - if (array_key_exists($sKey, $aUpdateReentrance)) - { - return; - } - $aUpdateReentrance[$sKey] = true; - - try - { - // Stop watches - $sState = $this->GetState(); - if ($sState != '') - { - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeStopWatch) - { - if (in_array($sState, $oAttDef->GetStates())) - { - // Compute or recompute the deadlines - $oSW = $this->Get($sAttCode); - $oSW->ComputeDeadlines($this, $oAttDef); - $this->Set($sAttCode, $oSW); - } - } - } - } - - $this->DoComputeValues(); - $this->OnUpdate(); - - $aChanges = $this->ListChanges(); - if (count($aChanges) == 0) - { - // Attempting to update an unchanged object - unset($aUpdateReentrance[$sKey]); - return $this->m_iKey; - } - - // Ultimate check - ensure DB integrity - list($bRes, $aIssues) = $this->CheckToWrite(); - if (!$bRes) - { - $sIssues = implode(', ', $aIssues); - throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); - } - - // Save the original values (will be reset to the new values when the object get written to the DB) - $aOriginalValues = $this->m_aOrigValues; - - $bHasANewExternalKeyValue = false; - $aHierarchicalKeys = array(); - $aDBChanges = array(); - foreach($aChanges as $sAttCode => $valuecurr) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true; - if ($oAttDef->IsBasedOnDBColumns()) - { - $aDBChanges[$sAttCode] = $aChanges[$sAttCode]; - } - if ($oAttDef->IsHierarchicalKey()) - { - $aHierarchicalKeys[$sAttCode] = $oAttDef; - } - } - - if (!MetaModel::DBIsReadOnly()) - { - // Update the left & right indexes for each hierarchical key - foreach($aHierarchicalKeys as $sAttCode => $oAttDef) - { - $sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); - $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey(); - $aRes = CMDBSource::QueryToArray($sSQL); - $iMyLeft = $aRes[0]['left']; - $iMyRight = $aRes[0]['right']; - $iDelta =$iMyRight - $iMyLeft + 1; - MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); - - if ($aDBChanges[$sAttCode] == 0) - { - // No new parent, insert completely at the right of the tree - $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; - $aRes = CMDBSource::QueryToArray($sSQL); - if (count($aRes) == 0) - { - $iNewLeft = 1; - } - else - { - $iNewLeft = $aRes[0]['max']+1; - } - } - else - { - // Insert at the right of the specified parent - $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aDBChanges[$sAttCode]); - $iNewLeft = CMDBSource::QueryToScalar($sSQL); - } - - MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); - - $aHKChanges = array(); - $aHKChanges[$sAttCode] = $aDBChanges[$sAttCode]; - $aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft; - $aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1; - $aDBChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below - } - - // Update scalar attributes - if (count($aDBChanges) != 0) - { - $oFilter = new DBObjectSearch(get_class($this)); - $oFilter->AddCondition('id', $this->m_iKey, '='); - $oFilter->AllowAllData(); - - $sSQL = $oFilter->MakeUpdateQuery($aDBChanges); - CMDBSource::Query($sSQL); - } - } - - $this->DBWriteLinks(); - $this->WriteExternalAttributes(); - - $this->m_bDirty = false; - $this->m_aTouchedAtt = array(); - $this->m_aModifiedAtt = array(); - - $this->AfterUpdate(); - - // Reload to get the external attributes - if ($bHasANewExternalKeyValue) - { - $this->Reload(true /* AllowAllData */); - } - else - { - // Reset original values although the object has not been reloaded - foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded) - { - if ($bLoaded) - { - $value = $this->m_aCurrValues[$sAttCode]; - $this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value; - } - } - } - - if (count($aChanges) != 0) - { - $this->RecordAttChanges($aChanges, $aOriginalValues); - } - } - catch (Exception $e) - { - unset($aUpdateReentrance[$sKey]); - throw $e; - } - - unset($aUpdateReentrance[$sKey]); - return $this->m_iKey; - } - - public function DBUpdateTracked(CMDBChange $oChange) - { - CMDBObject::SetCurrentChange($oChange); - return $this->DBUpdate(); - } - - // Make the current changes persistent - clever wrapper for Insert or Update - public function DBWrite() - { - if ($this->m_bIsInDB) - { - return $this->DBUpdate(); - } - else - { - return $this->DBInsert(); - } - } - - private function DBDeleteSingleTable($sTableClass) - { - $sTable = MetaModel::DBGetTable($sTableClass); - // Abstract classes or classes having no specific attribute do not have an associated table - if ($sTable == '') return; - - $sPKField = '`'.MetaModel::DBGetKey($sTableClass).'`'; - $sKey = CMDBSource::Quote($this->m_iKey); - - $sDeleteSQL = "DELETE FROM `$sTable` WHERE $sPKField = $sKey"; - CMDBSource::DeleteFrom($sDeleteSQL); - } - - protected function DBDeleteSingleObject() - { - if (!MetaModel::DBIsReadOnly()) - { - $this->OnDelete(); - $this->RecordObjDeletion($this->m_iKey); // May cause a reload for storing history information - - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsHierarchicalKey()) - { - // Update the left & right indexes for each hierarchical key - $sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); - $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".CMDBSource::Quote($this->m_iKey); - $aRes = CMDBSource::QueryToArray($sSQL); - $iMyLeft = $aRes[0]['left']; - $iMyRight = $aRes[0]['right']; - $iDelta =$iMyRight - $iMyLeft + 1; - MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); - - // No new parent for now, insert completely at the right of the tree - $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; - $aRes = CMDBSource::QueryToArray($sSQL); - if (count($aRes) == 0) - { - $iNewLeft = 1; - } - else - { - $iNewLeft = $aRes[0]['max']+1; - } - MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); - } - elseif (!$oAttDef->LoadFromDB()) - { - $oAttDef->DeleteValue($this); - } - } - - foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass) - { - $this->DBDeleteSingleTable($sParentClass); - } - - $this->AfterDelete(); - - $this->m_bIsInDB = false; - // Fix for #926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete objects, reported as bug #926) - // Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected - } - } - - // Delete an object... and guarantee data integrity - // - public function DBDelete(&$oDeletionPlan = null) - { - static $iLoopTimeLimit = null; - if ($iLoopTimeLimit == null) - { - $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); - } - if (is_null($oDeletionPlan)) - { - $oDeletionPlan = new DeletionPlan(); - } - $this->MakeDeletionPlan($oDeletionPlan); - $oDeletionPlan->ComputeResults(); - - if ($oDeletionPlan->FoundStopper()) - { - $aIssues = $oDeletionPlan->GetIssues(); - throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues))); - } - else - { - // Getting and setting time limit are not symetric: - // www.php.net/manual/fr/function.set-time-limit.php#72305 - $iPreviousTimeLimit = ini_get('max_execution_time'); - - foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete) - { - foreach ($aToDelete as $iId => $aData) - { - $oToDelete = $aData['to_delete']; - // The deletion based on a deletion plan should not be done for each oject if the deletion plan is common (Trac #457) - // because for each object we would try to update all the preceding ones... that are already deleted - // A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once - // As a temporary fix: delete only the objects that are still to be deleted... - if ($oToDelete->m_bIsInDB) - { - set_time_limit($iLoopTimeLimit); - $oToDelete->DBDeleteSingleObject(); - } - } - } - - foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) - { - foreach ($aToUpdate as $iId => $aData) - { - $oToUpdate = $aData['to_reset']; - foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) - { - $oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]); - set_time_limit($iLoopTimeLimit); - $oToUpdate->DBUpdate(); - } - } - } - - set_time_limit($iPreviousTimeLimit); - } - - return $oDeletionPlan; - } - - public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) - { - CMDBObject::SetCurrentChange($oChange); - $this->DBDelete($oDeletionPlan); - } - - public function EnumTransitions() - { - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (empty($sStateAttCode)) return array(); - - $sState = $this->Get(MetaModel::GetStateAttributeCode(get_class($this))); - return MetaModel::EnumTransitions(get_class($this), $sState); - } - - /** - * Designed as an action to be called when a stop watch threshold times out - * or from within the framework - * @param $sStimulusCode - * @param bool|false $bDoNotWrite - * @return bool - * @throws CoreException - * @throws CoreUnexpectedValue - */ - public function ApplyStimulus($sStimulusCode, $bDoNotWrite = false) - { - $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); - if (empty($sStateAttCode)) - { - throw new CoreException('No lifecycle for the class '.get_class($this)); - } - - MyHelpers::CheckKeyInArray('object lifecycle stimulus', $sStimulusCode, MetaModel::EnumStimuli(get_class($this))); - - $aStateTransitions = $this->EnumTransitions(); - if (!array_key_exists($sStimulusCode, $aStateTransitions)) - { - // This simulus has no effect in the current state... do nothing - return true; - } - $aTransitionDef = $aStateTransitions[$sStimulusCode]; - - // Change the state before proceeding to the actions, this is necessary because an action might - // trigger another stimuli (alternative: push the stimuli into a queue) - $sPreviousState = $this->Get($sStateAttCode); - $sNewState = $aTransitionDef['target_state']; - $this->Set($sStateAttCode, $sNewState); - - // $aTransitionDef is an - // array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD - - $bSuccess = true; - foreach ($aTransitionDef['actions'] as $actionHandler) - { - if (is_string($actionHandler)) - { - // Old (pre-2.1.0 modules) action definition without any parameter - $aActionCallSpec = array($this, $actionHandler); - $sActionDesc = get_class($this).'::'.$actionHandler; - - if (!is_callable($aActionCallSpec)) - { - throw new CoreException("Unable to call action: ".get_class($this)."::$actionHandler"); - } - $bRet = call_user_func($aActionCallSpec, $sStimulusCode); - } - else // if (is_array($actionHandler)) - { - // New syntax: 'verb' and typed parameters - $sAction = $actionHandler['verb']; - $sActionDesc = get_class($this).'::'.$sAction; - $aParams = array(); - foreach($actionHandler['params'] as $aDefinition) - { - $sParamType = array_key_exists('type', $aDefinition) ? $aDefinition['type'] : 'string'; - switch($sParamType) - { - case 'int': - $value = (int)$aDefinition['value']; - break; - - case 'float': - $value = (float)$aDefinition['value']; - break; - - case 'bool': - $value = (bool)$aDefinition['value']; - break; - - case 'reference': - $value = ${$aDefinition['value']}; - break; - - case 'string': - default: - $value = (string)$aDefinition['value']; - } - $aParams[] = $value; - } - $aCallSpec = array($this, $sAction); - $bRet = call_user_func_array($aCallSpec, $aParams); - } - // if one call fails, the whole is considered as failed - // (in case there is no returned value, null is obtained and means "ok") - if ($bRet === false) - { - IssueLog::Info("Lifecycle action $sActionDesc returned false on object #".$this->GetKey()); - $bSuccess = false; - } - } - if ($bSuccess) - { - $sClass = get_class($this); - - // Stop watches - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeStopWatch) - { - $oSW = $this->Get($sAttCode); - if (in_array($sNewState, $oAttDef->GetStates())) - { - $oSW->Start($this, $oAttDef); - } - else - { - $oSW->Stop($this, $oAttDef); - } - $this->Set($sAttCode, $oSW); - } - } - - if (!$bDoNotWrite) - { - $this->DBWrite(); - } - - // Change state triggers... - $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sPreviousState'")); - while ($oTrigger = $oSet->Fetch()) - { - $oTrigger->DoActivate($this->ToArgs('this')); - } - - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sNewState'")); - while ($oTrigger = $oSet->Fetch()) - { - $oTrigger->DoActivate($this->ToArgs('this')); - } - } - - return $bSuccess; - } - - /** - * Designed as an action to be called when a stop watch threshold times out - */ - public function ResetStopWatch($sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if (!$oAttDef instanceof AttributeStopWatch) - { - throw new CoreException("Invalid stop watch id: '$sAttCode'"); - } - $oSW = $this->Get($sAttCode); - $oSW->Reset($this, $oAttDef); - $this->Set($sAttCode, $oSW); - return true; - } - - /** - * Lifecycle action: Recover the default value (aka when an object is being created) - */ - public function Reset($sAttCode) - { - $this->Set($sAttCode, $this->GetDefaultValue($sAttCode)); - return true; - } - - /** - * Lifecycle action: Copy an attribute to another - */ - public function Copy($sDestAttCode, $sSourceAttCode) - { - $this->Set($sDestAttCode, $this->Get($sSourceAttCode)); - return true; - } - - /** - * Lifecycle action: Set the current date/time for the given attribute - */ - public function SetCurrentDate($sAttCode) - { - $this->Set($sAttCode, time()); - return true; - } - - /** - * Lifecycle action: Set the current logged in user for the given attribute - */ - public function SetCurrentUser($sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if ($oAttDef instanceof AttributeString) - { - // Note: the user friendly name is the contact friendly name if a contact is attached to the logged in user - $this->Set($sAttCode, UserRights::GetUserFriendlyName()); - } - else - { - if ($oAttDef->IsExternalKey()) - { - if ($oAttDef->GetTargetClass() != 'User') - { - throw new Exception("SetCurrentUser: the attribute $sAttCode must be an external key to 'User', found '".$oAttDef->GetTargetClass()."'"); - } - } - $this->Set($sAttCode, UserRights::GetUserId()); - } - return true; - } - - /** - * Lifecycle action: Set the current logged in CONTACT for the given attribute - */ - public function SetCurrentPerson($sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - if ($oAttDef instanceof AttributeString) - { - $iPerson = UserRights::GetContactId(); - if ($iPerson == 0) - { - $this->Set($sAttCode, ''); - } - else - { - $oPerson = MetaModel::GetObject('Person', $iPerson); - $this->Set($sAttCode, $oPerson->Get('friendlyname')); - } - } - else - { - if ($oAttDef->IsExternalKey()) - { - if (!MetaModel::IsParentClass($oAttDef->GetTargetClass(), 'Person')) - { - throw new Exception("SetCurrentContact: the attribute $sAttCode must be an external key to 'Person' or any other class above 'Person', found '".$oAttDef->GetTargetClass()."'"); - } - } - $this->Set($sAttCode, UserRights::GetContactId()); - } - return true; - } - - /** - * Lifecycle action: Set the time elapsed since a reference point - */ - public function SetElapsedTime($sAttCode, $sRefAttCode, $sWorkingTimeComputer = null) - { - if (is_null($sWorkingTimeComputer)) - { - $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; - } - $oComputer = new $sWorkingTimeComputer(); - $aCallSpec = array($oComputer, 'GetOpenDuration'); - if (!is_callable($aCallSpec)) - { - throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'"); - } - - $iStartTime = AttributeDateTime::GetAsUnixSeconds($this->Get($sRefAttCode)); - $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 - $oEndDate = new DateTime(); // now - - if (class_exists('WorkingTimeRecorder')) - { - $sClass = get_class($this); - WorkingTimeRecorder::Start($this, time(), "DBObject-SetElapsedTime-$sAttCode-$sRefAttCode", 'Core:ExplainWTC:ElapsedTime', array("Class:$sClass/Attribute:$sAttCode")); - } - $iElapsed = call_user_func($aCallSpec, $this, $oStartDate, $oEndDate); - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::End(); - } - - $this->Set($sAttCode, $iElapsed); - return true; - } - - - - /** - * Create query parameters (SELECT ... WHERE service = :this->service_id) - * to be used with the APIs DBObjectSearch/DBObjectSet - * - * Starting 2.0.2 the parameters are computed on demand, at the lowest level, - * in VariableExpression::Render() - */ - public function ToArgsForQuery($sArgName = 'this') - { - return array($sArgName.'->object()' => $this); - } - - /** - * Create template placeholders: now equivalent to ToArgsForQuery since the actual - * template placeholders are computed on demand. - */ - public function ToArgs($sArgName = 'this') - { - return $this->ToArgsForQuery($sArgName); - } - - public function GetForTemplate($sPlaceholderAttCode) - { - $ret = null; - if (preg_match('/^([^-]+)-(>|>)(.+)$/', $sPlaceholderAttCode, $aMatches)) // Support both syntaxes: this->xxx or this->xxx for HTML compatibility - { - $sExtKeyAttCode = $aMatches[1]; - $sRemoteAttCode = $aMatches[3]; - if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) - { - throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this)); - } - - $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); - if (!$oKeyAttDef instanceof AttributeExternalKey) - { - throw new CoreException("'$sExtKeyAttCode' is not an external key of the class ".get_class($this)); - } - $sRemoteClass = $oKeyAttDef->GetTargetClass(); - $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); - if (is_null($oRemoteObj)) - { - $ret = Dict::S('UI:UndefinedObject'); - } - else - { - // Recurse - $ret = $oRemoteObj->GetForTemplate($sRemoteAttCode); - } - } - else - { - switch($sPlaceholderAttCode) - { - case 'id': - $ret = $this->GetKey(); - break; - - case 'name()': - $ret = $this->GetName(); - break; - - default: - if (preg_match('/^([^(]+)\\((.*)\\)$/', $sPlaceholderAttCode, $aMatches)) - { - $sVerb = $aMatches[1]; - $sAttCode = $aMatches[2]; - } - else - { - $sVerb = ''; - $sAttCode = $sPlaceholderAttCode; - } - - if ($sVerb == 'hyperlink') - { - $sPortalId = ($sAttCode === '') ? 'console' : $sAttCode; - if (!array_key_exists($sPortalId, self::$aPortalToURLMaker)) - { - throw new Exception("Unknown portal id '$sPortalId' in placeholder '$sPlaceholderAttCode''"); - } - $ret = $this->GetHyperlink(self::$aPortalToURLMaker[$sPortalId], false); - } - else - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - $ret = $oAttDef->GetForTemplate($this->Get($sAttCode), $sVerb, $this); - } - } - if ($ret === null) - { - $ret = ''; - } - } - return $ret; - } - - static protected $aPortalToURLMaker = array('console' => 'iTopStandardURLMaker', 'portal' => 'PortalURLMaker'); - - /** - * Associate a portal to a class that implements iDBObjectURLMaker, - * and which will be invoked with placeholders like $this->org_id->hyperlink(portal)$ - * - * @param string $sPortalId Identifies the portal. Conventions: the main portal is 'console', The user requests portal is 'portal'. - * @param string $sUrlMakerClass - */ - static public function RegisterURLMakerClass($sPortalId, $sUrlMakerClass) - { - self::$aPortalToURLMaker[$sPortalId] = $sUrlMakerClass; - } - - // To be optionaly overloaded - protected function OnInsert() - { - } - - // To be optionaly overloaded - protected function AfterInsert() - { - } - - // To be optionaly overloaded - protected function OnUpdate() - { - } - - // To be optionaly overloaded - protected function AfterUpdate() - { - } - - // To be optionaly overloaded - protected function OnDelete() - { - } - - // To be optionaly overloaded - protected function AfterDelete() - { - } - - - /** - * Common to the recording of link set changes (add/remove/modify) - */ - private function PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, $sChangeOpClass, $aOriginalValues = null) - { - if ($iLinkSetOwnerId <= 0) - { - return null; - } - - if (!is_subclass_of($oLinkSet->GetHostClass(), 'CMDBObject')) - { - // The link set owner class does not keep track of its history - return null; - } - - // Determine the linked item class and id - // - if ($oLinkSet->IsIndirect()) - { - // The "item" is on the other end (N-N links) - $sExtKeyToRemote = $oLinkSet->GetExtKeyToRemote(); - $oExtKeyToRemote = MetaModel::GetAttributeDef(get_class($this), $sExtKeyToRemote); - $sItemClass = $oExtKeyToRemote->GetTargetClass(); - if ($aOriginalValues) - { - // Get the value from the original values - $iItemId = $aOriginalValues[$sExtKeyToRemote]; - } - else - { - $iItemId = $this->Get($sExtKeyToRemote); - } - } - else - { - // I am the "item" (1-N links) - $sItemClass = get_class($this); - $iItemId = $this->GetKey(); - } - - // Get the remote object, to determine its exact class - // Possible optimization: implement a tool in MetaModel, to get the final class of an object (not always querying + query reduced to a select on the root table! - $oOwner = MetaModel::GetObject($oLinkSet->GetHostClass(), $iLinkSetOwnerId, false); - if ($oOwner) - { - $sLinkSetOwnerClass = get_class($oOwner); - - $oMyChangeOp = MetaModel::NewObject($sChangeOpClass); - $oMyChangeOp->Set("objclass", $sLinkSetOwnerClass); - $oMyChangeOp->Set("objkey", $iLinkSetOwnerId); - $oMyChangeOp->Set("attcode", $oLinkSet->GetCode()); - $oMyChangeOp->Set("item_class", $sItemClass); - $oMyChangeOp->Set("item_id", $iItemId); - return $oMyChangeOp; - } - else - { - // Depending on the deletion order, it may happen that the id is already invalid... ignore - return null; - } - } - - /** - * This object has been created/deleted, record that as a change in link sets pointing to this (if any) - */ - private function RecordLinkSetListChange($bAdd = true) - { - $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys(get_class($this)); - foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) - { - if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; - - $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); - $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); - if ($oMyChangeOp) - { - if ($bAdd) - { - $oMyChangeOp->Set("type", "added"); - } - else - { - $oMyChangeOp->Set("type", "removed"); - } - $iId = $oMyChangeOp->DBInsertNoReload(); - } - } - } - - protected function RecordObjCreation() - { - $this->RecordLinkSetListChange(true); - } - - protected function RecordObjDeletion($objkey) - { - $this->RecordLinkSetListChange(false); - } - - protected function RecordAttChanges(array $aValues, array $aOrigValues) - { - $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys(get_class($this)); - foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) - { - - if (array_key_exists($sExtKeyAttCode, $aValues)) - { - if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; - - // Keep track of link added/removed - // - $iLinkSetOwnerNext = $aValues[$sExtKeyAttCode]; - $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerNext, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); - if ($oMyChangeOp) - { - $oMyChangeOp->Set("type", "added"); - $oMyChangeOp->DBInsertNoReload(); - } - - $iLinkSetOwnerPrevious = $aOrigValues[$sExtKeyAttCode]; - $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerPrevious, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove', $aOrigValues); - if ($oMyChangeOp) - { - $oMyChangeOp->Set("type", "removed"); - $oMyChangeOp->DBInsertNoReload(); - } - } - else - { - // Keep track of link changes - // - if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue; - - $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); - $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune'); - if ($oMyChangeOp) - { - $oMyChangeOp->Set("link_id", $this->GetKey()); - $iId = $oMyChangeOp->DBInsertNoReload(); - } - } - } - } - - // Return an empty set for the parent of all - // May be overloaded. - // Anyhow, this way of implementing the relations suffers limitations (not handling the redundancy) - // and you should consider defining those things in XML. - public static function GetRelationQueries($sRelCode) - { - return array(); - } - - // Reserved: do not overload - public static function GetRelationQueriesEx($sRelCode) - { - return array(); - } - - /** - * Will be deprecated soon - use GetRelatedObjectsDown/Up instead to take redundancy into account - */ - public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array()) - { - // Temporary patch: until the impact analysis GUI gets rewritten, - // let's consider that "depends on" is equivalent to "impacts/up" - // The current patch has been implemented in DBObject and MetaModel - $sHackedRelCode = $sRelCode; - $bDown = true; - if ($sRelCode == 'depends on') - { - $sHackedRelCode = 'impacts'; - $bDown = false; - } - foreach (MetaModel::EnumRelationQueries(get_class($this), $sHackedRelCode, $bDown) as $sDummy => $aQueryInfo) - { - $sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp']; - //$bPropagate = $aQueryInfo["bPropagate"]; - //$iDepth = $bPropagate ? $iMaxDepth - 1 : 0; - $iDepth = $iMaxDepth - 1; - - // Note: the loop over the result set has been written in an unusual way for error reporting purposes - // In the case of a wrong query parameter name, the error occurs on the first call to Fetch, - // thus we need to have this first call into the try/catch, but - // we do NOT want to nest the try/catch for the error message to be clear - try - { - $oFlt = DBObjectSearch::FromOQL($sQuery); - $oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgsForQuery()); - $oObj = $oObjSet->Fetch(); - } - catch (Exception $e) - { - $sClassOfDefinition = $aQueryInfo['_legacy_'] ? get_class($this).'(or a parent)::GetRelationQueries()' : $aQueryInfo['sDefinedInClass']; - throw new Exception("Wrong query for the relation $sRelCode/$sClassOfDefinition/{$aQueryInfo['sNeighbour']}: ".$e->getMessage()); - } - if ($oObj) - { - do - { - $sRootClass = MetaModel::GetRootClass(get_class($oObj)); - $sObjKey = $oObj->GetKey(); - if (array_key_exists($sRootClass, $aResults)) - { - if (array_key_exists($sObjKey, $aResults[$sRootClass])) - { - continue; // already visited, skip - } - } - - $aResults[$sRootClass][$sObjKey] = $oObj; - if ($iDepth > 0) - { - $oObj->GetRelatedObjects($sRelCode, $iDepth, $aResults); - } - } - while ($oObj = $oObjSet->Fetch()); - } - } - return $aResults; - } - - /** - * Compute the "RelatedObjects" (forward or "down" direction) for the object - * for the specified relation - * - * @param string $sRelCode The code of the relation to use for the computation - * @param int $iMaxDepth Maximum recursion depth - * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy - * - * @return RelationGraph The graph of all the related objects - */ - public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) - { - $oGraph = new RelationGraph(); - $oGraph->AddSourceObject($this); - $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy); - return $oGraph; - } - - /** - * Compute the "RelatedObjects" (reverse or "up" direction) for the object - * for the specified relation - * - * @param string $sRelCode The code of the relation to use for the computation - * @param int $iMaxDepth Maximum recursion depth - * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy - * - * @return RelationGraph The graph of all the related objects - */ - public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) - { - $oGraph = new RelationGraph(); - $oGraph->AddSourceObject($this); - $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); - return $oGraph; - } - - public function GetReferencingObjects($bAllowAllData = false) - { - $aDependentObjects = array(); - $aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this)); - foreach($aRererencingMe as $sRemoteClass => $aExtKeys) - { - foreach($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) - { - // skip if this external key is behind an external field - if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) continue; - - $oSearch = new DBObjectSearch($sRemoteClass); - $oSearch->AddCondition($sExtKeyAttCode, $this->GetKey(), '='); - if ($bAllowAllData) - { - $oSearch->AllowAllData(); - } - $oSet = new CMDBObjectSet($oSearch); - if ($oSet->CountExceeds(0)) - { - $aDependentObjects[$sRemoteClass][$sExtKeyAttCode] = array( - 'attribute' => $oExtKeyAttDef, - 'objects' => $oSet, - ); - } - } - } - return $aDependentObjects; - } - - private function MakeDeletionPlan(&$oDeletionPlan, $aVisited = array(), $iDeleteOption = null) - { - static $iLoopTimeLimit = null; - if ($iLoopTimeLimit == null) - { - $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); - } - $sClass = get_class($this); - $iThisId = $this->GetKey(); - - $iDeleteOption = $oDeletionPlan->AddToDelete($this, $iDeleteOption); - - if (array_key_exists($sClass, $aVisited)) - { - if (in_array($iThisId, $aVisited[$sClass])) - { - return; - } - } - $aVisited[$sClass] = $iThisId; - - if ($iDeleteOption == DEL_MANUAL) - { - // Stop the recursion here - return; - } - // Check the node itself - $this->DoCheckToDelete($oDeletionPlan); - $oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue); - - $aDependentObjects = $this->GetReferencingObjects(true /* allow all data */); - - // Getting and setting time limit are not symetric: - // www.php.net/manual/fr/function.set-time-limit.php#72305 - $iPreviousTimeLimit = ini_get('max_execution_time'); - - foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes) - { - foreach ($aPotentialDeletes as $sRemoteExtKey => $aData) - { - set_time_limit($iLoopTimeLimit); - - $oAttDef = $aData['attribute']; - $iDeletePropagationOption = $oAttDef->GetDeletionPropagationOption(); - $oDepSet = $aData['objects']; - $oDepSet->Rewind(); - while ($oDependentObj = $oDepSet->fetch()) - { - $iId = $oDependentObj->GetKey(); - if ($oAttDef->IsNullAllowed()) - { - // Optional external key, list to reset - if (($iDeletePropagationOption == DEL_MOVEUP) && ($oAttDef->IsHierarchicalKey())) - { - // Move the child up one level i.e. set the same parent as the current object - $iParentId = $this->Get($oAttDef->GetCode()); - $oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef, $iParentId); - } - else - { - $oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef); - } - } - else - { - // Mandatory external key, list to delete - $oDependentObj->MakeDeletionPlan($oDeletionPlan, $aVisited, $iDeletePropagationOption); - } - } - } - } - set_time_limit($iPreviousTimeLimit); - } - - /** - * WILL DEPRECATED SOON - * Caching relying on an object set is not efficient since 2.0.3 - * Use GetSynchroData instead - * - * Get all the synchro replica related to this object - * @param none - * @return DBObjectSet Set with two columns: R=SynchroReplica S=SynchroDataSource - */ - public function GetMasterReplica() - { - $sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id"; - $oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey())); - return $oReplicaSet; - } - - /** - * Get all the synchro data related to this object - * @param none - * @return array of data_source_id => array - * 'source' => $oSource, - * 'attributes' => array of $oSynchroAttribute - * 'replica' => array of $oReplica (though only one should exist, misuse of the data sync can have this consequence) - */ - public function GetSynchroData() - { - if (is_null($this->m_aSynchroData)) - { - $sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id"; - $oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey())); - $this->m_aSynchroData = array(); - while($aData = $oReplicaSet->FetchAssoc()) - { - $iSourceId = $aData['datasource']->GetKey(); - if (!array_key_exists($iSourceId, $this->m_aSynchroData)) - { - $aAttributes = array(); - $oAttrSet = $aData['datasource']->Get('attribute_list'); - while($oSyncAttr = $oAttrSet->Fetch()) - { - $aAttributes[$oSyncAttr->Get('attcode')] = $oSyncAttr; - } - $this->m_aSynchroData[$iSourceId] = array( - 'source' => $aData['datasource'], - 'attributes' => $aAttributes, - 'replica' => array() - ); - } - // Assumption: $aData['datasource'] will not be null because the data source id is always set... - $this->m_aSynchroData[$iSourceId]['replica'][] = $aData['replica']; - } - } - return $this->m_aSynchroData; - } - - public function GetSynchroReplicaFlags($sAttCode, &$aReason) - { - $iFlags = OPT_ATT_NORMAL; - foreach ($this->GetSynchroData() as $iSourceId => $aSourceData) - { - if ($iSourceId == SynchroExecution::GetCurrentTaskId()) - { - // Ignore the current task (check to write => ok) - continue; - } - // Assumption: one replica - take the first one! - $oReplica = reset($aSourceData['replica']); - $oSource = $aSourceData['source']; - if (array_key_exists($sAttCode, $aSourceData['attributes'])) - { - $oSyncAttr = $aSourceData['attributes'][$sAttCode]; - if (($oSyncAttr->Get('update') == 1) && ($oSyncAttr->Get('update_policy') == 'master_locked')) - { - $iFlags |= OPT_ATT_SLAVE; - $sUrl = $oSource->GetApplicationUrl($this, $oReplica); - $aReason[] = array('name' => $oSource->GetName(), 'description' => $oSource->Get('description'), 'url_application' => $sUrl); - } - } - } - return $iFlags; - } - - public function InSyncScope() - { - // - // Optimization: cache the list of Data Sources and classes candidates for synchro - // - static $aSynchroClasses = null; - if (is_null($aSynchroClasses)) - { - $aSynchroClasses = array(); - $sOQL = "SELECT SynchroDataSource AS datasource"; - $oSourceSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array()); - while($oSource = $oSourceSet->Fetch()) - { - $sTarget = $oSource->Get('scope_class'); - $aSynchroClasses[] = $sTarget; - } - } - - foreach($aSynchroClasses as $sClass) - { - if ($this instanceof $sClass) - { - return true; - } - } - return false; - } - ///////////////////////////////////////////////////////////////////////// - // - // Experimental iDisplay implementation - // - ///////////////////////////////////////////////////////////////////////// - - public static function MapContextParam($sContextParam) - { - return null; - } - - public function GetHilightClass() - { - $sCode = $this->ComputeHighlightCode(); - if($sCode != '') - { - $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); - if (array_key_exists($sCode, $aHighlightScale)) - { - return $aHighlightScale[$sCode]['color']; - } - } - return HILIGHT_CLASS_NONE; - } - - public function DisplayDetails(WebPage $oPage, $bEditMode = false) - { - $oPage->add('

    '.MetaModel::GetName(get_class($this)).': '.$this->GetName().'

    '); - $aValues = array(); - $aList = MetaModel::FlattenZList(MetaModel::GetZListItems(get_class($this), 'details')); - if (empty($aList)) - { - $aList = array_keys(MetaModel::ListAttributeDefs(get_class($this))); - } - foreach($aList as $sAttCode) - { - $aValues[$sAttCode] = array('label' => MetaModel::GetLabel(get_class($this), $sAttCode), 'value' => $this->GetAsHTML($sAttCode)); - } - $oPage->details($aValues); - } - - - const CALLBACK_AFTERINSERT = 0; - - /** - * Register a call back that will be called when some internal event happens - * - * @param $iType string Any of the CALLBACK_x constants - * @param $callback callable Call specification like a function name, or array('', '') or array($object, '') - * @param $aParameters Array Values that will be passed to the callback, after $this - */ - public function RegisterCallback($iType, $callback, $aParameters = array()) - { - $sCallBackName = ''; - if (!is_callable($callback, false, $sCallBackName)) - { - throw new Exception('Registering an unknown/protected function or wrong syntax for the call spec: '.$sCallBackName); - } - $this->m_aCallbacks[$iType][] = array( - 'callback' => $callback, - 'params' => $aParameters - ); - } - - /** - * Computes a text-like fingerprint identifying the content of the object - * but excluding the specified columns - * @param $aExcludedColumns array The list of columns to exclude - * @return string - */ - public function Fingerprint($aExcludedColumns = array()) - { - $sFingerprint = ''; - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if (!in_array($sAttCode, $aExcludedColumns)) - { - if ($oAttDef->IsPartOfFingerprint()) - { - $sFingerprint .= chr(0).$oAttDef->Fingerprint($this->Get($sAttCode)); - } - } - } - return $sFingerprint; - } - - /** - * Execute a set of scripted actions onto the current object - * See ExecAction for the syntax and features of the scripted actions - * - * @param $aActions array of statements (e.g. "set(name, Made after $source->name$)") - * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element - * @throws Exception - */ - public function ExecActions($aActions, $aSourceObjects) - { - foreach($aActions as $sAction) - { - try - { - if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot - { - $sVerb = trim($aMatches[1]); - $sParams = $aMatches[2]; - - // the coma is the separator for the parameters - // comas can be escaped: \, - $sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams); - $sParams = trim($sParams); - - if (strlen($sParams) == 0) - { - $aParams = array(); - } - else - { - $aParams = explode(',', $sParams); - foreach ($aParams as &$sParam) - { - $sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam); - $sParam = trim($sParam); - } - } - $this->ExecAction($sVerb, $aParams, $aSourceObjects); - } - else - { - throw new Exception("Invalid syntax"); - } - } - catch(Exception $e) - { - throw new Exception('Action: '.$sAction.' - '.$e->getMessage()); - } - } - } - - /** - * Helper to copy an attribute between two objects (in memory) - * Originally designed for ExecAction() - */ - public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode) - { - if ($sSourceAttCode == 'id') - { - $oSourceAttDef = null; - } - else - { - if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode)) - { - throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode); - } - if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode)) - { - throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode); - } - - $oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode); - } - if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet()) - { - // The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets) - $oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass()); - $oSourceSet = $oSourceObject->Get($sSourceAttCode); - $oSourceSet->Rewind(); - while ($oSourceLink = $oSourceSet->Fetch()) - { - // Clone the link - $sLinkClass = get_class($oSourceLink); - $oLinkClone = MetaModel::NewObject($sLinkClass); - foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef) - { - // As of now, ignore other attribute (do not attempt to recurse!) - if ($oAttDef->IsScalar()) - { - $oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode)); - } - } - - // Not necessary - this will be handled by DBObject - // $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0); - $oDestSet->AddObject($oLinkClone); - } - $this->Set($sDestAttCode, $oDestSet); - } - else - { - $this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode)); - } - } - - /** - * Execute a scripted action onto the current object - * - clone (att1, att2, att3, ...) - * - clone_scalars () - * - copy (source_att, dest_att) - * - reset (att) - * - nullify (att) - * - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) - * - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) - * - add_to_list (source_key_att, dest_att) - * - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value) - * - apply_stimulus (stimulus) - * - call_method (method_name) - * - * @param $sVerb string Any of the verb listed above (e.g. "set") - * @param $aParams array of strings (e.g. array('name', 'copied from $source->name$') - * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element - * @throws CoreException - * @throws CoreUnexpectedValue - * @throws Exception - */ - public function ExecAction($sVerb, $aParams, $aSourceObjects) - { - switch($sVerb) - { - case 'clone': - if (!array_key_exists('source', $aSourceObjects)) - { - throw new Exception('Missing conventional "source" object'); - } - $oObjectToRead = $aSourceObjects['source']; - foreach($aParams as $sAttCode) - { - $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); - } - break; - - case 'clone_scalars': - if (!array_key_exists('source', $aSourceObjects)) - { - throw new Exception('Missing conventional "source" object'); - } - $oObjectToRead = $aSourceObjects['source']; - foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsScalar()) - { - $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); - } - } - break; - - case 'copy': - if (!array_key_exists('source', $aSourceObjects)) - { - throw new Exception('Missing conventional "source" object'); - } - $oObjectToRead = $aSourceObjects['source']; - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: source attribute'); - } - $sSourceAttCode = $aParams[0]; - if (!array_key_exists(1, $aParams)) - { - throw new Exception('Missing argument #2: target attribute'); - } - $sDestAttCode = $aParams[1]; - $this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode); - break; - - case 'reset': - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: target attribute'); - } - $sAttCode = $aParams[0]; - if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) - { - throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); - } - $this->Set($sAttCode, $this->GetDefaultValue($sAttCode)); - break; - - case 'nullify': - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: target attribute'); - } - $sAttCode = $aParams[0]; - if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) - { - throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); - } - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - $this->Set($sAttCode, $oAttDef->GetNullValue()); - break; - - case 'set': - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: target attribute'); - } - $sAttCode = $aParams[0]; - if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) - { - throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); - } - if (!array_key_exists(1, $aParams)) - { - throw new Exception('Missing argument #2: value to set'); - } - $sRawValue = $aParams[1]; - $aContext = array(); - foreach ($aSourceObjects as $sAlias => $oObject) - { - $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); - } - $aContext['current_contact_id'] = UserRights::GetContactId(); - $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); - $aContext['current_date'] = date(AttributeDate::GetSQLFormat()); - $aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat()); - $sValue = MetaModel::ApplyParams($sRawValue, $aContext); - $this->Set($sAttCode, $sValue); - break; - - case 'append': - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: target attribute'); - } - $sAttCode = $aParams[0]; - if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) - { - throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); - } - if (!array_key_exists(1, $aParams)) - { - throw new Exception('Missing argument #2: value to append'); - } - $sRawAddendum = $aParams[1]; - $aContext = array(); - foreach ($aSourceObjects as $sAlias => $oObject) - { - $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); - } - $aContext['current_contact_id'] = UserRights::GetContactId(); - $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); - $aContext['current_date'] = date(AttributeDate::GetSQLFormat()); - $aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat()); - $sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext); - $this->Set($sAttCode, $this->Get($sAttCode).$sAddendum); - break; - - case 'add_to_list': - if (!array_key_exists('source', $aSourceObjects)) - { - throw new Exception('Missing conventional "source" object'); - } - $oObjectToRead = $aSourceObjects['source']; - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: source attribute'); - } - $sSourceKeyAttCode = $aParams[0]; - if (($sSourceKeyAttCode != 'id') && !MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode)) - { - throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode); - } - if (!array_key_exists(1, $aParams)) - { - throw new Exception('Missing argument #2: target attribute (link set)'); - } - $sTargetListAttCode = $aParams[1]; // indirect !!! - if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode)) - { - throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode); - } - if (isset($aParams[2]) && isset($aParams[3])) - { - $sRoleAttCode = $aParams[2]; - $sRoleValue = $aParams[3]; - } - - $iObjKey = $oObjectToRead->Get($sSourceKeyAttCode); - if ($iObjKey > 0) - { - $oLinkSet = $this->Get($sTargetListAttCode); - - $oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode); - $oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass()); - $oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey); - if (isset($sRoleAttCode)) - { - if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode)) - { - throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode); - } - $oLnk->Set($sRoleAttCode, $sRoleValue); - } - $oLinkSet->AddObject($oLnk); - $this->Set($sTargetListAttCode, $oLinkSet); - } - break; - - case 'apply_stimulus': - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: stimulus'); - } - $sStimulus = $aParams[0]; - if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this)))) - { - throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus); - } - $this->ApplyStimulus($sStimulus); - break; - - case 'call_method': - if (!array_key_exists('source', $aSourceObjects)) - { - throw new Exception('Missing conventional "source" object'); - } - $oObjectToRead = $aSourceObjects['source']; - if (!array_key_exists(0, $aParams)) - { - throw new Exception('Missing argument #1: method name'); - } - $sMethod = $aParams[0]; - $aCallSpec = array($this, $sMethod); - if (!is_callable($aCallSpec)) - { - throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()'); - } - // Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToRead� - call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects); - break; - - default: - throw new Exception("Invalid verb"); - } - } - - public function IsArchived($sKeyAttCode = null) - { - $bRet = false; - $sFlagAttCode = is_null($sKeyAttCode) ? 'archive_flag' : $sKeyAttCode.'_archive_flag'; - if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode)) - { - $bRet = true; - } - return $bRet; - } - - public function IsObsolete($sKeyAttCode = null) - { - $bRet = false; - $sFlagAttCode = is_null($sKeyAttCode) ? 'obsolescence_flag' : $sKeyAttCode.'_obsolescence_flag'; - if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode)) - { - $bRet = true; - } - return $bRet; - } - - /** - * @param $bArchive - * @throws Exception - */ - protected function DBWriteArchiveFlag($bArchive) - { - if (!MetaModel::IsArchivable(get_class($this))) - { - throw new Exception(get_class($this).' is not an archivable class'); - } - - $iFlag = $bArchive ? 1 : 0; - $sDate = $bArchive ? '"'.date(AttributeDate::GetSQLFormat()).'"' : 'null'; - - $sClass = get_class($this); - $sArchiveRoot = MetaModel::GetAttributeOrigin($sClass, 'archive_flag'); - $sRootTable = MetaModel::DBGetTable($sArchiveRoot); - $sRootKey = MetaModel::DBGetKey($sArchiveRoot); - $aJoins = array("`$sRootTable`"); - $aUpdates = array(); - foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass) - { - if (!MetaModel::IsValidAttCode($sParentClass, 'archive_flag')) continue; - - $sTable = MetaModel::DBGetTable($sParentClass); - $aUpdates[] = "`$sTable`.`archive_flag` = $iFlag"; - if ($sParentClass == $sArchiveRoot) - { - if (!$bArchive || $this->Get('archive_date') == '') - { - // Erase or set the date (do not change it) - $aUpdates[] = "`$sTable`.`archive_date` = $sDate"; - } - } - else - { - $sKey = MetaModel::DBGetKey($sParentClass); - $aJoins[] = "`$sTable` ON `$sTable`.`$sKey` = `$sRootTable`.`$sRootKey`"; - } - } - $sJoins = implode(' INNER JOIN ', $aJoins); - $sValues = implode(', ', $aUpdates); - $sUpdateQuery = "UPDATE $sJoins SET $sValues WHERE `$sRootTable`.`$sRootKey` = ".$this->GetKey(); - CMDBSource::Query($sUpdateQuery); - } - - /** - * Can be called to repair the database (tables consistency) - * The archive_date will be preserved - * @throws Exception - */ - public function DBArchive() - { - $this->DBWriteArchiveFlag(true); - $this->m_aCurrValues['archive_flag'] = true; - $this->m_aOrigValues['archive_flag'] = true; - } - - public function DBUnarchive() - { - $this->DBWriteArchiveFlag(false); - $this->m_aCurrValues['archive_flag'] = false; - $this->m_aOrigValues['archive_flag'] = false; - $this->m_aCurrValues['archive_date'] = null; - $this->m_aOrigValues['archive_date'] = null; - } - - - - /** - * @param string $sClass Needs to be an instanciable class - * @returns $oObj - **/ - public static function MakeDefaultInstance($sClass) - { - $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); - $oObj = MetaModel::NewObject($sClass); - if (!empty($sStateAttCode)) - { - $sTargetState = MetaModel::GetDefaultState($sClass); - $oObj->Set($sStateAttCode, $sTargetState); - } - return $oObj; - } - - /** - * Complete a new object with data from context - * @param array $aContextParam Context used for creation form prefilling - * - */ - public function PrefillCreationForm(&$aContextParam) - { - } - - /** - * Complete an object after a state transition with data from context - * @param array $aContextParam Context used for creation form prefilling - * - */ - public function PrefillTransitionForm(&$aContextParam) - { - } - - /** - * Complete a filter ($aContextParam['filter']) data from context - * (Called on source object) - * @param array $aContextParam Context used for creation form prefilling - * - */ - public function PrefillSearchForm(&$aContextParam) - { - } - - /** - * Prefill a creation / stimulus change / search form according to context, current state of an object, stimulus.. $sOperation - * @param string $sOperation Operation identifier - * @param array $aContextParam Context used for creation form prefilling - * - */ - public function PrefillForm($sOperation, &$aContextParam) - { - switch($sOperation){ - case 'creation_from_0': - case 'creation_from_extkey': - case 'creation_from_editinplace': - $this->PrefillCreationForm($aContextParam); - break; - case 'state_change': - $this->PrefillTransitionForm($aContextParam); - break; - case 'search': - $this->PrefillSearchForm($aContextParam); - break; - default: - break; - } - } -} - + + +/** + * All objects to be displayed in the application (either as a list or as details) + * must implement this interface. + */ +interface iDisplay +{ + + /** + * Maps the given context parameter name to the appropriate filter/search code for this class + * @param string $sContextParam Name of the context parameter, i.e. 'org_id' + * @return string Filter code, i.e. 'customer_id' + */ + public static function MapContextParam($sContextParam); + /** + * This function returns a 'hilight' CSS class, used to hilight a given row in a table + * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL, + * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE + * To Be overridden by derived classes + * @param void + * @return String The desired higlight class for the object/row + */ + public function GetHilightClass(); + /** + * Returns the relative path to the page that handles the display of the object + * @return string + */ + public static function GetUIPage(); + /** + * Displays the details of the object + */ + public function DisplayDetails(WebPage $oPage, $bEditMode = false); +} + +/** + * Class dbObject: the root of persistent classes + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('metamodel.class.php'); +require_once('deletionplan.class.inc.php'); +require_once('mutex.class.inc.php'); + + +/** + * A persistent object, as defined by the metamodel + * + * @package iTopORM + */ +abstract class DBObject implements iDisplay +{ + private static $m_aMemoryObjectsByClass = array(); + + private static $m_aBulkInsertItems = array(); // class => array of ('table' => array of (array of )) + private static $m_aBulkInsertCols = array(); // class => array of ('table' => array of ) + private static $m_bBulkInsert = false; + + protected $m_bIsInDB = false; // true IIF the object is mapped to a DB record + protected $m_iKey = null; + private $m_aCurrValues = array(); + protected $m_aOrigValues = array(); + + protected $m_aExtendedData = null; + + private $m_bDirty = false; // Means: "a modification is ongoing" + // The object may have incorrect external keys, then any attempt of reload must be avoided + private $m_bCheckStatus = null; // Means: the object has been verified and is consistent with integrity rules + // if null, then the check has to be performed again to know the status + protected $m_bSecurityIssue = null; + protected $m_aCheckIssues = null; + protected $m_aDeleteIssues = null; + + private $m_bFullyLoaded = false; // Compound objects can be partially loaded + private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode + protected $m_aTouchedAtt = array(); // list of (potentially) modified sAttCodes + protected $m_aModifiedAtt = array(); // real modification status: for each attCode can be: unset => don't know, true => modified, false => not modified (the same value as the original value was set) + protected $m_aSynchroData = null; // Set of Synch data related to this object + protected $m_sHighlightCode = null; + protected $m_aCallbacks = array(); + + // Use the MetaModel::NewObject to build an object (do we have to force it?) + public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) + { + if (!empty($aRow)) + { + $this->FromRow($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec); + $this->m_bFullyLoaded = $this->IsFullyLoaded(); + $this->m_aTouchedAtt = array(); + $this->m_aModifiedAtt = array(); + return; + } + // Creation of a brand new object + // + + $this->m_iKey = self::GetNextTempId(get_class($this)); + + // set default values + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + $this->m_aCurrValues[$sAttCode] = $this->GetDefaultValue($sAttCode); + $this->m_aOrigValues[$sAttCode] = null; + if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName)) + { + // This field has to be read from the DB + // Leave the flag unset (optimization) + } + else + { + // No need to trigger a reload for that attribute + // Let's consider it as being already fully loaded + $this->m_aLoadedAtt[$sAttCode] = true; + } + } + } + + // Read-only <=> Written once (archive) + public function RegisterAsDirty() + { + // While the object may be written to the DB, it is NOT possible to reload it + // or at least not possible to reload it the same way + $this->m_bDirty = true; + } + + public function IsNew() + { + return (!$this->m_bIsInDB); + } + + // Returns an Id for memory objects + static protected function GetNextTempId($sClass) + { + $sRootClass = MetaModel::GetRootClass($sClass); + if (!array_key_exists($sRootClass, self::$m_aMemoryObjectsByClass)) + { + self::$m_aMemoryObjectsByClass[$sRootClass] = 0; + } + self::$m_aMemoryObjectsByClass[$sRootClass]++; + return (- self::$m_aMemoryObjectsByClass[$sRootClass]); + } + + public function __toString() + { + $sRet = ''; + $sClass = get_class($this); + $sRootClass = MetaModel::GetRootClass($sClass); + $iPKey = $this->GetKey(); + $sFriendlyname = $this->Get('friendlyname'); + $sRet .= "$sClass::$iPKey ($sFriendlyname)
    \n"; + return $sRet; + } + + // Restore initial values... mmmm, to be discussed + public function DBRevert() + { + $this->Reload(); + } + + protected function IsFullyLoaded() + { + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + if (!$oAttDef->LoadInObject()) continue; + if (!isset($this->m_aLoadedAtt[$sAttCode]) || !$this->m_aLoadedAtt[$sAttCode]) + { + return false; + } + } + return true; + } + + /** + * @param bool $bAllowAllData DEPRECATED: the reload must never fail! + * @throws CoreException + */ + public function Reload($bAllowAllData = false) + { + assert($this->m_bIsInDB); + $aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey, false /* must be found */, true /* AllowAllData */); + if (empty($aRow)) + { + throw new CoreException("Failed to reload object of class '".get_class($this)."', id = ".$this->m_iKey); + } + $this->FromRow($aRow); + + // Process linked set attributes + // + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) + { + if (!$oAttDef->IsLinkSet()) continue; + + $this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue($this); + $this->m_aOrigValues[$sAttCode] = clone $this->m_aCurrValues[$sAttCode]; + $this->m_aLoadedAtt[$sAttCode] = true; + } + + $this->m_bFullyLoaded = true; + $this->m_aTouchedAtt = array(); + $this->m_aModifiedAtt = array(); + } + + protected function FromRow($aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) + { + if (strlen($sClassAlias) == 0) + { + // Default to the current class + $sClassAlias = get_class($this); + } + + $this->m_iKey = null; + $this->m_bIsInDB = true; + $this->m_aCurrValues = array(); + $this->m_aOrigValues = array(); + $this->m_aLoadedAtt = array(); + $this->m_bCheckStatus = true; + + // Get the key + // + $sKeyField = $sClassAlias."id"; + if (!array_key_exists($sKeyField, $aRow)) + { + // #@# Bug ? + throw new CoreException("Missing key for class '".get_class($this)."'"); + } + + $iPKey = $aRow[$sKeyField]; + if (!self::IsValidPKey($iPKey)) + { + if (is_null($iPKey)) + { + throw new CoreException("Missing object id in query result (found null)"); + } + else + { + throw new CoreException("An object id must be an integer value ($iPKey)"); + } + } + $this->m_iKey = $iPKey; + + // Build the object from an array of "attCode"=>"value") + // + $bFullyLoaded = true; // ... set to false if any attribute is not found + if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad)) + { + $aAttList = MetaModel::ListAttributeDefs(get_class($this)); + } + else + { + $aAttList = $aAttToLoad[$sClassAlias]; + } + + foreach($aAttList as $sAttCode=>$oAttDef) + { + // Skip links (could not be loaded by the mean of this query) + if ($oAttDef->IsLinkSet()) continue; + + if (!$oAttDef->LoadInObject()) continue; + + unset($value); + $bIsDefined = false; + if ($oAttDef->LoadFromDB()) + { + // Note: we assume that, for a given attribute, if it can be loaded, + // then one column will be found with an empty suffix, the others have a suffix + // Take care: the function isset will return false in case the value is null, + // which is something that could happen on open joins + $sAttRef = $sClassAlias.$sAttCode; + + if (array_key_exists($sAttRef, $aRow)) + { + $value = $oAttDef->FromSQLToValue($aRow, $sAttRef); + $bIsDefined = true; + } + } + else + { + $value = $oAttDef->ReadValue($this); + $bIsDefined = true; + } + + if ($bIsDefined) + { + $this->m_aCurrValues[$sAttCode] = $value; + if (is_object($value)) + { + $this->m_aOrigValues[$sAttCode] = clone $value; + } + else + { + $this->m_aOrigValues[$sAttCode] = $value; + } + $this->m_aLoadedAtt[$sAttCode] = true; + } + else + { + // This attribute was expected and not found in the query columns + $bFullyLoaded = false; + } + } + + // Load extended data + if ($aExtendedDataSpec != null) + { + $aExtendedDataSpec['table']; + foreach($aExtendedDataSpec['fields'] as $sColumn) + { + $sColRef = $sClassAlias.'_extdata_'.$sColumn; + if (array_key_exists($sColRef, $aRow)) + { + $this->m_aExtendedData[$sColumn] = $aRow[$sColRef]; + } + } + } + return $bFullyLoaded; + } + + protected function _Set($sAttCode, $value) + { + $this->m_aCurrValues[$sAttCode] = $value; + $this->m_aTouchedAtt[$sAttCode] = true; + unset($this->m_aModifiedAtt[$sAttCode]); + } + + public function Set($sAttCode, $value) + { + if ($sAttCode == 'finalclass') + { + // Ignore it - this attribute is set upon object creation and that's it + return false; + } + + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + + if (!$oAttDef->IsWritable()) + { + $sClass = get_class($this); + throw new Exception("Attempting to set the value on the read-only attribute $sClass::$sAttCode"); + } + + if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty) + { + // First time Set is called... ensure that the object gets fully loaded + // Otherwise we would lose the values on a further Reload + // + consistency does not make sense ! + $this->Reload(); + } + + if ($oAttDef->IsExternalKey()) + { + if (is_object($value)) + { + // Setting an external key with a whole object (instead of just an ID) + // let's initialize also the external fields that depend on it + // (useful when building objects in memory and not from a query) + if ( (get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass()))) + { + throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored"); + } + + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) + { + if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) + { + $this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode()); + $this->m_aLoadedAtt[$sCode] = true; + } + } + } + else if ($this->m_aCurrValues[$sAttCode] != $value) + { + // Setting an external key, but no any other information is available... + // Invalidate the corresponding fields so that they get reloaded in case they are needed (See Get()) + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) + { + if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) + { + $this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode); + unset($this->m_aLoadedAtt[$sCode]); + } + } + } + } + if ($oAttDef->IsLinkSet() && ($value != null)) + { + $realvalue = clone $this->m_aCurrValues[$sAttCode]; + $realvalue->UpdateFromCompleteList($value); + } + else + { + $realvalue = $oAttDef->MakeRealValue($value, $this); + } + $this->_Set($sAttCode, $realvalue); + + foreach (MetaModel::ListMetaAttributes(get_class($this), $sAttCode) as $sMetaAttCode => $oMetaAttDef) + { + $this->_Set($sMetaAttCode, $oMetaAttDef->MapValue($this)); + } + + // The object has changed, reset caches + $this->m_bCheckStatus = null; + + // Make sure we do not reload it anymore... before saving it + $this->RegisterAsDirty(); + + // This function is eligible as a lifecycle action: returning true upon success is a must + return true; + } + + public function SetTrim($sAttCode, $sValue) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $iMaxSize = $oAttDef->GetMaxSize(); + if ($iMaxSize && (strlen($sValue) > $iMaxSize)) + { + $sValue = substr($sValue, 0, $iMaxSize); + } + $this->Set($sAttCode, $sValue); + } + + public function GetLabel($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAttDef->GetLabel(); + } + + public function Get($sAttCode) + { + if (($iPos = strpos($sAttCode, '->')) === false) + { + return $this->GetStrict($sAttCode); + } + else + { + $sExtKeyAttCode = substr($sAttCode, 0, $iPos); + $sRemoteAttCode = substr($sAttCode, $iPos + 2); + if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) + { + throw new CoreException("Unknown external key '$sExtKeyAttCode' for the class ".get_class($this)); + } + + $oExtFieldAtt = MetaModel::FindExternalField(get_class($this), $sExtKeyAttCode, $sRemoteAttCode); + if (!is_null($oExtFieldAtt)) + { + return $this->GetStrict($oExtFieldAtt->GetCode()); + } + else + { + $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); + $sRemoteClass = $oKeyAttDef->GetTargetClass(); + $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); + if (is_null($oRemoteObj)) + { + return ''; + } + else + { + return $oRemoteObj->Get($sRemoteAttCode); + } + } + } + } + + public function GetStrict($sAttCode) + { + if ($sAttCode == 'id') + { + return $this->m_iKey; + } + + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + + if (!$oAttDef->LoadInObject()) + { + $value = $oAttDef->GetValue($this); + } + else + { + if (isset($this->m_aLoadedAtt[$sAttCode])) + { + // Standard case... we have the information directly + } + elseif ($this->m_bIsInDB && !$this->m_bDirty) + { + // Lazy load (polymorphism): complete by reloading the entire object + // #@# non-scalar attributes.... handle that differently? + $oKPI = new ExecutionKPI(); + $this->Reload(); + $oKPI->ComputeStats('Reload', get_class($this).'/'.$sAttCode); + } + elseif ($sAttCode == 'friendlyname') + { + // The friendly name is not computed and the object is dirty + // Todo: implement the computation of the friendly name based on sprintf() + // + $this->m_aCurrValues[$sAttCode] = ''; + } + else + { + // Not loaded... is it related to an external key? + if ($oAttDef->IsExternalField()) + { + // Let's get the object and compute all of the corresponding attributes + // (i.e not only the requested attribute) + // + $sExtKeyAttCode = $oAttDef->GetKeyAttCode(); + + if (($iRemote = $this->Get($sExtKeyAttCode)) && ($iRemote > 0)) // Objects in memory have negative IDs + { + $oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); + // Note: "allow all data" must be enabled because the external fields are always visible + // to the current user even if this is not the case for the remote object + // This is consistent with the behavior of the lists + $oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote, true, true); + } + else + { + $oRemote = null; + } + + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) + { + if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sExtKeyAttCode)) + { + if ($oRemote) + { + $this->m_aCurrValues[$sCode] = $oRemote->Get($oDef->GetExtAttCode()); + } + else + { + $this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode); + } + $this->m_aLoadedAtt[$sCode] = true; + } + } + } + } + $value = $this->m_aCurrValues[$sAttCode]; + } + + if ($value instanceof ormLinkSet) + { + $value->Rewind(); + } + return $value; + } + + public function GetOriginal($sAttCode) + { + if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this)))) + { + throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this)); + } + $aOrigValues = $this->m_aOrigValues; + return isset($aOrigValues[$sAttCode]) ? $aOrigValues[$sAttCode] : null; + } + + /** + * Returns the default value of the $sAttCode. By default, returns the default value of the AttributeDefinition. + * Overridable. + * + * @param $sAttCode + * @return mixed + */ + public function GetDefaultValue($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAttDef->GetDefaultValue($this); + } + + /** + * Returns data loaded by the mean of a dynamic and explicit JOIN + */ + public function GetExtendedData() + { + return $this->m_aExtendedData; + } + + /** + * Set the HighlightCode if the given code has a greater rank than the current HilightCode + * @param string $sCode + * @return void + */ + protected function SetHighlightCode($sCode) + { + $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); + $fCurrentRank = 0.0; + if (($this->m_sHighlightCode !== null) && array_key_exists($this->m_sHighlightCode, $aHighlightScale)) + { + $fCurrentRank = $aHighlightScale[$this->m_sHighlightCode]['rank']; + } + + if (array_key_exists($sCode, $aHighlightScale)) + { + $fRank = $aHighlightScale[$sCode]['rank']; + if ($fRank > $fCurrentRank) + { + $this->m_sHighlightCode = $sCode; + } + } + } + + /** + * Get the current HighlightCode + * @return string The Hightlight code (null if none set, meaning rank = 0) + */ + protected function GetHighlightCode() + { + return $this->m_sHighlightCode; + } + + protected function ComputeHighlightCode() + { + // First if the state defines a HiglightCode, apply it + $sState = $this->GetState(); + if ($sState != '') + { + $sCode = MetaModel::GetHighlightCode(get_class($this), $sState); + $this->SetHighlightCode($sCode); + } + // The check for each StopWatch if a HighlightCode is effective + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeStopWatch) + { + $oStopWatch = $this->Get($sAttCode); + $sCode = $oStopWatch->GetHighlightCode(); + if ($sCode !== '') + { + $this->SetHighlightCode($sCode); + } + } + } + return $this->GetHighlightCode(); + } + + /** + * Updates the value of an external field by (re)loading the object + * corresponding to the external key and getting the value from it + * + * UNUSED ? + * + * @param string $sAttCode Attribute code of the external field to update + * @return void + */ + protected function UpdateExternalField($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef->IsExternalField()) + { + $sTargetClass = $oAttDef->GetTargetClass(); + $objkey = $this->Get($oAttDef->GetKeyAttCode()); + // Note: "allow all data" must be enabled because the external fields are always visible + // to the current user even if this is not the case for the remote object + // This is consistent with the behavior of the lists + $oObj = MetaModel::GetObject($sTargetClass, $objkey, true, true); + if (is_object($oObj)) + { + $value = $oObj->Get($oAttDef->GetExtAttCode()); + $this->Set($sAttCode, $value); + } + } + } + + public function ComputeValues() + { + } + + // Compute scalar attributes that depend on any other type of attribute + final public function DoComputeValues() + { + // TODO - use a flag rather than checking the call stack -> this will certainly accelerate things + + // First check that we are not currently computing the fields + // (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...) + foreach (debug_backtrace() as $aCallInfo) + { + if (!array_key_exists("class", $aCallInfo)) continue; + if ($aCallInfo["class"] != get_class($this)) continue; + if ($aCallInfo["function"] != "ComputeValues") continue; + return; //skip! + } + + // Set the "null-not-allowed" datetimes (and dates) whose value is not initialized + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + // AttributeDate is derived from AttributeDateTime + if (($oAttDef instanceof AttributeDateTime) && (!$oAttDef->IsNullAllowed()) && ($this->Get($sAttCode) == $oAttDef->GetNullValue())) + { + $this->Set($sAttCode, date($oAttDef->GetInternalFormat())); + } + } + + $this->ComputeValues(); + } + + public function GetAsHTML($sAttCode, $bLocalize = true) + { + $sClass = get_class($this); + $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); + + if ($oAtt->IsExternalKey(EXTKEY_ABSOLUTE)) + { + //return $this->Get($sAttCode.'_friendlyname'); + $sTargetClass = $oAtt->GetTargetClass(EXTKEY_ABSOLUTE); + $iTargetKey = $this->Get($sAttCode); + if ($iTargetKey < 0) + { + // the key points to an object that exists only in memory... no hyperlink points to it yet + return ''; + } + else + { + $sHtmlLabel = htmlentities($this->Get($sAttCode.'_friendlyname'), ENT_QUOTES, 'UTF-8'); + $bArchived = $this->IsArchived($sAttCode); + $bObsolete = $this->IsObsolete($sAttCode); + return $this->MakeHyperLink($sTargetClass, $iTargetKey, $sHtmlLabel, null, true, $bArchived, $bObsolete); + } + } + + // That's a standard attribute (might be an ext field or a direct field, etc.) + return $oAtt->GetAsHTML($this->Get($sAttCode), $this, $bLocalize); + } + + public function GetEditValue($sAttCode) + { + $sClass = get_class($this); + $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); + + if ($oAtt->IsExternalKey()) + { + $sTargetClass = $oAtt->GetTargetClass(); + if ($this->IsNew()) + { + // The current object exists only in memory, don't try to query it in the DB ! + // instead let's query for the object pointed by the external key, and get its name + $targetObjId = $this->Get($sAttCode); + $oTargetObj = MetaModel::GetObject($sTargetClass, $targetObjId, false); // false => not sure it exists + if (is_object($oTargetObj)) + { + $sEditValue = $oTargetObj->GetName(); + } + else + { + $sEditValue = 0; + } + } + else + { + $sEditValue = $this->Get($sAttCode.'_friendlyname'); + } + } + else + { + $sEditValue = $oAtt->GetEditValue($this->Get($sAttCode), $this); + } + return $sEditValue; + } + + public function GetAsXML($sAttCode, $bLocalize = true) + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsXML($this->Get($sAttCode), $this, $bLocalize); + } + + public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); + } + + public function GetOriginalAsHTML($sAttCode, $bLocalize = true) + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this, $bLocalize); + } + + public function GetOriginalAsXML($sAttCode, $bLocalize = true) + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this, $bLocalize); + } + + public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) + { + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); + } + + /** + * @param $sObjClass + * @param $sObjKey + * @param string $sHtmlLabel Label with HTML entities escaped (< escaped as <) + * @param null $sUrlMakerClass + * @param bool|true $bWithNavigationContext + * @param bool|false $bArchived + * @param bool|false $bObsolete + * @return string + * @throws DictExceptionMissingString + */ + public static function MakeHyperLink($sObjClass, $sObjKey, $sHtmlLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true, $bArchived = false, $bObsolete = false) + { + if ($sObjKey <= 0) return ''.Dict::S('UI:UndefinedObject').''; // Objects built in memory have negative IDs + + // Safety net + // + if (empty($sHtmlLabel)) + { + // If the object if not issued from a query but constructed programmatically + // the label may be empty. In this case run a query to get the object's friendly name + $oTmpObj = MetaModel::GetObject($sObjClass, $sObjKey, false); + if (is_object($oTmpObj)) + { + $sHtmlLabel = $oTmpObj->GetName(); + } + else + { + // May happen in case the target object is not in the list of allowed values for this attribute + $sHtmlLabel = "$sObjClass::$sObjKey"; + } + } + $sHint = MetaModel::GetName($sObjClass)."::$sObjKey"; + $sUrl = ApplicationContext::MakeObjectUrl($sObjClass, $sObjKey, $sUrlMakerClass, $bWithNavigationContext); + + $bClickable = !$bArchived || utils::IsArchiveMode(); + if ($bArchived) + { + $sSpanClass = 'archived'; + $sFA = 'fa-archive object-archived'; + $sHint = Dict::S('ObjectRef:Archived'); + } + elseif ($bObsolete) + { + $sSpanClass = 'obsolete'; + $sFA = 'fa-eye-slash object-obsolete'; + $sHint = Dict::S('ObjectRef:Obsolete'); + } + else + { + $sSpanClass = ''; + $sFA = ''; + } + if ($sFA == '') + { + $sIcon = ''; + } + else + { + if ($bClickable) + { + $sIcon = ""; + } + else + { + $sIcon = ""; + } + } + + if ($bClickable && (strlen($sUrl) > 0)) + { + $sHLink = "$sIcon$sHtmlLabel"; + } + else + { + $sHLink = $sIcon.$sHtmlLabel; + } + $sRet = "$sHLink"; + return $sRet; + } + + public function GetHyperlink($sUrlMakerClass = null, $bWithNavigationContext = true) + { + $bArchived = $this->IsArchived(); + $bObsolete = $this->IsObsolete(); + return self::MakeHyperLink(get_class($this), $this->GetKey(), $this->GetName(), $sUrlMakerClass, $bWithNavigationContext, $bArchived, $bObsolete); + } + + public static function ComputeStandardUIPage($sClass) + { + static $aUIPagesCache = array(); // Cache to store the php page used to display each class of object + if (!isset($aUIPagesCache[$sClass])) + { + $UIPage = false; + if (is_callable("$sClass::GetUIPage")) + { + $UIPage = eval("return $sClass::GetUIPage();"); // May return false in case of error + } + $aUIPagesCache[$sClass] = $UIPage === false ? './UI.php' : $UIPage; + } + $sPage = $aUIPagesCache[$sClass]; + return $sPage; + } + + public static function GetUIPage() + { + return 'UI.php'; + } + + + // could be in the metamodel ? + public static function IsValidPKey($value) + { + return ((string)$value === (string)(int)$value); + } + + public function GetKey() + { + return $this->m_iKey; + } + public function SetKey($iNewKey) + { + if (!self::IsValidPKey($iNewKey)) + { + throw new CoreException("An object id must be an integer value ($iNewKey)"); + } + + if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey)) + { + throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database"); + } + $this->m_iKey = $iNewKey; + } + /** + * Get the icon representing this object + * @param boolean $bImgTag If true the result is a full IMG tag (or an emtpy string if no icon is defined) + * @return string Either the full IMG tag ($bImgTag == true) or just the URL to the icon file + */ + public function GetIcon($bImgTag = true) + { + $sCode = $this->ComputeHighlightCode(); + if($sCode != '') + { + $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); + if (array_key_exists($sCode, $aHighlightScale)) + { + $sIconUrl = $aHighlightScale[$sCode]['icon']; + if($bImgTag) + { + return ""; + } + else + { + return $sIconUrl; + } + } + } + return MetaModel::GetClassIcon(get_class($this), $bImgTag); + } + + /** + * Gets the name of an object in a safe manner for displaying inside a web page + * @return string + */ + public function GetName() + { + return htmlentities($this->GetRawName(), ENT_QUOTES, 'UTF-8'); + } + + /** + * Gets the raw name of an object, this is not safe for displaying inside a web page + * since the " < > characters are not escaped and the name may contain some XSS script + * instructions. + * Use this function only for internal computations or for an output to a non-HTML destination + * @return string + */ + public function GetRawName() + { + return $this->Get('friendlyname'); + } + + public function GetState() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + return ''; + } + else + { + return $this->Get($sStateAttCode); + } + } + + public function GetStateLabel() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + return ''; + } + else + { + $sStateValue = $this->Get($sStateAttCode); + return MetaModel::GetStateLabel(get_class($this), $sStateValue); + } + } + + public function GetStateDescription() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + return ''; + } + else + { + $sStateValue = $this->Get($sStateAttCode); + return MetaModel::GetStateDescription(get_class($this), $sStateValue); + } + } + + /** + * Overridable - Define attributes read-only from the end-user perspective + * + * @return array List of attcodes + */ + public static function GetReadOnlyAttributes() + { + return null; + } + + + /** + * Overridable - Get predefined objects (could be hardcoded) + * The predefined objects will be synchronized with the DB at each install/upgrade + * As soon as a class has predefined objects, then nobody can create nor delete objects + * @return array An array of id => array of attcode => php value(so-called "real value": integer, string, ormDocument, DBObjectSet, etc.) + */ + public static function GetPredefinedObjects() + { + return null; + } + + /** + * + * @param string $sAttCode $sAttCode The code of the attribute + * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas) + * @param string $sTargetState The target state in which to evalutate the flags, if empty the current state will be + * used + * + * @return integer the binary combination of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) for the + * given attribute in the given state of the object + * @throws \CoreException + */ + public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '') + { + $iFlags = 0; // By default (if no life cycle) no flag at all + + $aReadOnlyAtts = $this->GetReadOnlyAttributes(); + if (($aReadOnlyAtts != null) && (in_array($sAttCode, $aReadOnlyAtts))) + { + return OPT_ATT_READONLY; + } + + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (!empty($sStateAttCode)) + { + if ($sTargetState != '') + { + $iFlags = MetaModel::GetAttributeFlags(get_class($this), $sTargetState, $sAttCode); + } + else + { + $iFlags = MetaModel::GetAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); + } + } + $aReasons = array(); + $iSynchroFlags = 0; + if ($this->InSyncScope()) + { + $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); + } + return $iFlags | $iSynchroFlags; // Combine both sets of flags + } + + /** + * @param string $sAttCode + * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas) + * + * @throws \CoreException + */ + public function IsAttributeReadOnlyForCurrentState($sAttCode, &$aReasons = array()) + { + $iAttFlags = $this->GetAttributeFlags($sAttCode, $aReasons); + + return ($iAttFlags & OPT_ATT_READONLY); + } + + /** + * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) + * for the given attribute in a transition + * @param $sAttCode string $sAttCode The code of the attribute + * @param $sStimulus string The stimulus code to apply + * @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas) + * @param $sOriginState string The state from which to apply $sStimulus, if empty current state will be used + * @return integer Flags: the binary combination of the flags applicable to this attribute + */ + public function GetTransitionFlags($sAttCode, $sStimulus, &$aReasons = array(), $sOriginState = '') + { + $iFlags = 0; // By default (if no lifecycle) no flag at all + + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + // If no state attribute, there is no lifecycle + if (empty($sStateAttCode)) + { + return $iFlags; + } + + // Retrieving current state if necessary + if ($sOriginState === '') + { + $sOriginState = $this->Get($sStateAttCode); + } + + // Retrieving attribute flags + $iAttributeFlags = $this->GetAttributeFlags($sAttCode, $aReasons, $sOriginState); + + // Retrieving transition flags + $iTransitionFlags = MetaModel::GetTransitionFlags(get_class($this), $sOriginState, $sStimulus, $sAttCode); + + // Merging transition flags with attribute flags + $iFlags = $iTransitionFlags | $iAttributeFlags; + + return $iFlags; + } + + /** + * Returns an array of attribute codes (with their flags) when $sStimulus is applied on the object in the $sOriginState state. + * Note: Attributes (and flags) from the target state and the transition are combined. + * + * @param $sStimulus string + * @param $sOriginState string Default is current state + * @return array + */ + public function GetTransitionAttributes($sStimulus, $sOriginState = null) + { + $sObjClass = get_class($this); + + // Defining current state as origin state if not specified + if($sOriginState === null) + { + $sOriginState = $this->GetState(); + } + + $aAttributes = MetaModel::GetTransitionAttributes($sObjClass, $sStimulus, $sOriginState); + + return $aAttributes; + } + + /** + * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) + * for the given attribute for the current state of the object considered as an INITIAL state + * @param string $sAttCode The code of the attribute + * @return integer Flags: the binary combination of the flags applicable to this attribute + */ + public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array()) + { + $iFlags = 0; + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (!empty($sStateAttCode)) + { + $iFlags = MetaModel::GetInitialStateAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); + } + return $iFlags; // No need to care about the synchro flags since we'll be creating a new object anyway + } + + // check if the given (or current) value is suitable for the attribute + // return true if successfull + // return the error desciption otherwise + public function CheckValue($sAttCode, $value = null) + { + if (!is_null($value)) + { + $toCheck = $value; + } + else + { + $toCheck = $this->Get($sAttCode); + } + + $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if (!$oAtt->IsWritable()) + { + return true; + } + elseif ($oAtt->IsNull($toCheck)) + { + if ($oAtt->IsNullAllowed()) + { + return true; + } + else + { + return "Null not allowed"; + } + } + elseif ($oAtt->IsExternalKey()) + { + if (!MetaModel::SkipCheckExtKeys()) + { + $sTargetClass = $oAtt->GetTargetClass(); + $oTargetObj = MetaModel::GetObject($sTargetClass, $toCheck, false /*must be found*/, true /*allow all data*/); + if (is_null($oTargetObj)) + { + return "Target object not found ($sTargetClass::$toCheck)"; + } + } + if ($oAtt->IsHierarchicalKey()) + { + // This check cannot be deactivated since otherwise the user may break things by a CSV import of a bulk modify + $aValues = $oAtt->GetAllowedValues(array('this' => $this)); + if (!array_key_exists($toCheck, $aValues)) + { + return "Value not allowed [$toCheck]"; + } + } + } + elseif ($oAtt->IsScalar()) + { + $aValues = $oAtt->GetAllowedValues($this->ToArgsForQuery()); + if (is_array($aValues) && (count($aValues) > 0)) + { + if (!array_key_exists($toCheck, $aValues)) + { + return "Value not allowed [$toCheck]"; + } + } + if (!is_null($iMaxSize = $oAtt->GetMaxSize())) + { + $iLen = strlen($toCheck); + if ($iLen > $iMaxSize) + { + return "String too long (found $iLen, limited to $iMaxSize)"; + } + } + if (!$oAtt->CheckFormat($toCheck)) + { + return "Wrong format [$toCheck]"; + } + } + else + { + return $oAtt->CheckValue($this, $toCheck); + } + return true; + } + + // check attributes together + public function CheckConsistency() + { + return true; + } + + // check integrity rules (before inserting or updating the object) + // a displayable error is returned + public function DoCheckToWrite() + { + $this->DoComputeValues(); + + $aChanges = $this->ListChanges(); + + foreach($aChanges as $sAttCode => $value) + { + $res = $this->CheckValue($sAttCode); + if ($res !== true) + { + // $res contains the error description + $this->m_aCheckIssues[] = "Unexpected value for attribute '$sAttCode': $res"; + } + } + if (count($this->m_aCheckIssues) > 0) + { + // No need to check consistency between attributes if any of them has + // an unexpected value + return; + } + $res = $this->CheckConsistency(); + if ($res !== true) + { + // $res contains the error description + $this->m_aCheckIssues[] = "Consistency rules not followed: $res"; + } + + // Synchronization: are we attempting to modify an attribute for which an external source is master? + // + if ($this->m_bIsInDB && $this->InSyncScope() && (count($aChanges) > 0)) + { + foreach($aChanges as $sAttCode => $value) + { + $iFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); + if ($iFlags & OPT_ATT_SLAVE) + { + // Note: $aReasonInfo['name'] could be reported (the task owning the attribute) + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $sAttLabel = $oAttDef->GetLabel(); + foreach($aReasons as $aReasonInfo) + { + // Todo: associate the attribute code with the error + $this->m_aCheckIssues[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $sAttLabel); + } + } + } + } + } + + final public function CheckToWrite() + { + if (MetaModel::SkipCheckToWrite()) + { + return array(true, array()); + } + if (is_null($this->m_bCheckStatus)) + { + $this->m_aCheckIssues = array(); + + $oKPI = new ExecutionKPI(); + $this->DoCheckToWrite(); + $oKPI->ComputeStats('CheckToWrite', get_class($this)); + if (count($this->m_aCheckIssues) == 0) + { + $this->m_bCheckStatus = true; + } + else + { + $this->m_bCheckStatus = false; + } + } + return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue); + } + + // check if it is allowed to delete the existing object from the database + // a displayable error is returned + protected function DoCheckToDelete(&$oDeletionPlan) + { + $this->m_aDeleteIssues = array(); // Ok + + if ($this->InSyncScope()) + { + + foreach ($this->GetSynchroData() as $iSourceId => $aSourceData) + { + foreach ($aSourceData['replica'] as $oReplica) + { + $oDeletionPlan->AddToDelete($oReplica, DEL_SILENT); + } + $oDataSource = $aSourceData['source']; + if ($oDataSource->GetKey() == SynchroExecution::GetCurrentTaskId()) + { + // The current task has the right to delete the object + continue; + } + $oReplica = reset($aSourceData['replica']); // Take the first one + if ($oReplica->Get('status_dest_creator') != 1) + { + // The object is not owned by the task + continue; + } + + $sLink = $oDataSource->GetName(); + $sUserDeletePolicy = $oDataSource->Get('user_delete_policy'); + switch($sUserDeletePolicy) + { + case 'nobody': + $this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink); + break; + + case 'administrators': + if (!UserRights::IsAdministrator()) + { + $this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink); + } + break; + + case 'everybody': + default: + // Ok + break; + } + } + } + } + + public function CheckToDelete(&$oDeletionPlan) + { + $this->MakeDeletionPlan($oDeletionPlan); + $oDeletionPlan->ComputeResults(); + return (!$oDeletionPlan->FoundStopper()); + } + + protected function ListChangedValues(array $aProposal) + { + $aDelta = array(); + foreach ($aProposal as $sAtt => $proposedValue) + { + if (!array_key_exists($sAtt, $this->m_aOrigValues)) + { + // The value was not set + $aDelta[$sAtt] = $proposedValue; + } + elseif(!array_key_exists($sAtt, $this->m_aTouchedAtt) || (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == false)) + { + // This attCode was never set, cannot be modified + // or the same value - as the original value - was set, and has been verified as equivalent to the original value + continue; + } + else if (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == true) + { + // We already know that the value is really modified + $aDelta[$sAtt] = $proposedValue; + } + elseif(is_object($proposedValue)) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt); + // The value is an object, the comparison is not strict + if (!$oAttDef->Equals($this->m_aOrigValues[$sAtt], $proposedValue)) + { + $aDelta[$sAtt] = $proposedValue; + $this->m_aModifiedAtt[$sAtt] = true; // Really modified + } + else + { + $this->m_aModifiedAtt[$sAtt] = false; // Not really modified + } + } + else + { + // The value is a scalar, the comparison must be 100% strict + if($this->m_aOrigValues[$sAtt] !== $proposedValue) + { + //echo "$sAtt:
    \n";
    +					//var_dump($this->m_aOrigValues[$sAtt]);
    +					//var_dump($proposedValue);
    +					//echo "
    \n"; + $aDelta[$sAtt] = $proposedValue; + $this->m_aModifiedAtt[$sAtt] = true; // Really modified + } + else + { + $this->m_aModifiedAtt[$sAtt] = false; // Not really modified + } + } + } + return $aDelta; + } + + // List the attributes that have been changed + // Returns an array of attname => currentvalue + public function ListChanges() + { + if ($this->m_bIsInDB) + { + return $this->ListChangedValues($this->m_aCurrValues); + } + else + { + return $this->m_aCurrValues; + } + } + + // Tells whether or not an object was modified since last read (ie: does it differ from the DB ?) + public function IsModified() + { + $aChanges = $this->ListChanges(); + return (count($aChanges) != 0); + } + + public function Equals($oSibling) + { + if (get_class($oSibling) != get_class($this)) + { + return false; + } + if ($this->GetKey() != $oSibling->GetKey()) + { + return false; + } + if ($this->m_bIsInDB) + { + // If one has changed, then consider them as being different + if ($this->IsModified() || $oSibling->IsModified()) + { + return false; + } + } + else + { + // Todo - implement this case (loop on every attribute) + //foreach(MetaModel::ListAttributeDefs(get_class($this) as $sAttCode => $oAttDef) + //{ + //if (!isset($this->m_CurrentValues[$sAttCode])) continue; + //if (!isset($this->m_CurrentValues[$sAttCode])) continue; + //if (!$oAttDef->Equals($this->m_CurrentValues[$sAttCode], $oSibling->m_CurrentValues[$sAttCode])) + //{ + //return false; + //} + //} + return false; + } + return true; + } + + // used only by insert + protected function OnObjectKeyReady() + { + // Meant to be overloaded + } + + // used both by insert/update + private function DBWriteLinks() + { + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if (!$oAttDef->IsLinkSet()) continue; + if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; + if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; + + $oLinkSet = $this->m_aCurrValues[$sAttCode]; + $oLinkSet->DBWrite($this); + } + } + + // used both by insert/update + private function WriteExternalAttributes() + { + foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if (!$oAttDef->LoadInObject()) continue; + if ($oAttDef->LoadFromDB()) continue; + if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; + if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; + $oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]); + } + } + + // Note: this is experimental - it was designed to speed up the setup of iTop + // Known limitations: + // - does not work with multi-table classes (issue with the unique id to maintain in several tables) + // - the id of the object is not updated + static public final function BulkInsertStart() + { + self::$m_bBulkInsert = true; + } + + static public final function BulkInsertFlush() + { + if (!self::$m_bBulkInsert) return; + + foreach(self::$m_aBulkInsertCols as $sClass => $aTables) + { + foreach ($aTables as $sTable => $sColumns) + { + $sValues = implode(', ', self::$m_aBulkInsertItems[$sClass][$sTable]); + $sInsertSQL = "INSERT INTO `$sTable` ($sColumns) VALUES $sValues"; + $iNewKey = CMDBSource::InsertInto($sInsertSQL); + } + } + + // Reset + self::$m_aBulkInsertItems = array(); + self::$m_aBulkInsertCols = array(); + self::$m_bBulkInsert = false; + } + + private function DBInsertSingleTable($sTableClass) + { + $sTable = MetaModel::DBGetTable($sTableClass); + // Abstract classes or classes having no specific attribute do not have an associated table + if ($sTable == '') return; + + $sClass = get_class($this); + + // fields in first array, values in the second + $aFieldsToWrite = array(); + $aValuesToWrite = array(); + + if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) + { + // Add it to the list of fields to write + $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; + $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); + } + + $aHierarchicalKeys = array(); + + foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; + $aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]); + foreach($aAttColumns as $sColumn => $sValue) + { + $aFieldsToWrite[] = "`$sColumn`"; + $aValuesToWrite[] = CMDBSource::Quote($sValue); + } + if ($oAttDef->IsHierarchicalKey()) + { + $aHierarchicalKeys[$sAttCode] = $oAttDef; + } + } + + if (count($aValuesToWrite) == 0) return false; + + if (MetaModel::DBIsReadOnly()) + { + $iNewKey = -1; + } + else + { + if (self::$m_bBulkInsert) + { + if (!isset(self::$m_aBulkInsertCols[$sClass][$sTable])) + { + self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite); + } + self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')'; + + $iNewKey = 999999; // TODO - compute next id.... + } + else + { + if (count($aHierarchicalKeys) > 0) + { + foreach($aHierarchicalKeys as $sAttCode => $oAttDef) + { + $aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable); + $aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`'; + $aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()]; + $aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`'; + $aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()]; + } + } + $sInsertSQL = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).")"; + $iNewKey = CMDBSource::InsertInto($sInsertSQL); + } + } + // Note that it is possible to have a key defined here, and the autoincrement expected, this is acceptable in a non root class + if (empty($this->m_iKey)) + { + // Take the autonumber + $this->m_iKey = $iNewKey; + } + return $this->m_iKey; + } + + // Insert of record for the new object into the database + // Returns the key of the newly created object + public function DBInsertNoReload() + { + if ($this->m_bIsInDB) + { + throw new CoreException("The object already exists into the Database, you may want to use the clone function"); + } + + $sClass = get_class($this); + $sRootClass = MetaModel::GetRootClass($sClass); + + // Ensure the update of the values (we are accessing the data directly) + $this->DoComputeValues(); + $this->OnInsert(); + + if ($this->m_iKey < 0) + { + // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it! + $this->m_iKey = null; + } + + // If not automatically computed, then check that the key is given by the caller + if (!MetaModel::IsAutoIncrementKey($sRootClass)) + { + if (empty($this->m_iKey)) + { + throw new CoreWarning("Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber", array('class' => $sRootClass)); + } + } + + // Ultimate check - ensure DB integrity + list($bRes, $aIssues) = $this->CheckToWrite(); + if (!$bRes) + { + $sIssues = implode(', ', $aIssues); + throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); + } + + // Stop watches + $sState = $this->GetState(); + if ($sState != '') + { + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeStopWatch) + { + if (in_array($sState, $oAttDef->GetStates())) + { + // Start the stop watch and compute the deadlines + $oSW = $this->Get($sAttCode); + $oSW->Start($this, $oAttDef); + $oSW->ComputeDeadlines($this, $oAttDef); + $this->Set($sAttCode, $oSW); + } + } + } + } + + // First query built upon on the root class, because the ID must be created first + $this->m_iKey = $this->DBInsertSingleTable($sRootClass); + + // Then do the leaf class, if different from the root class + if ($sClass != $sRootClass) + { + $this->DBInsertSingleTable($sClass); + } + + // Then do the other classes + foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) + { + if ($sParentClass == $sRootClass) continue; + $this->DBInsertSingleTable($sParentClass); + } + + $this->OnObjectKeyReady(); + + $this->DBWriteLinks(); + $this->WriteExternalAttributes(); + + $this->m_bIsInDB = true; + $this->m_bDirty = false; + foreach ($this->m_aCurrValues as $sAttCode => $value) + { + if (is_object($value)) + { + $value = clone $value; + } + $this->m_aOrigValues[$sAttCode] = $value; + } + + $this->AfterInsert(); + + // Activate any existing trigger + $sClass = get_class($this); + $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectCreate AS t WHERE t.target_class IN ('$sClassList')")); + while ($oTrigger = $oSet->Fetch()) + { + $oTrigger->DoActivate($this->ToArgs('this')); + } + + // Callbacks registered with RegisterCallback + if (isset($this->m_aCallbacks[self::CALLBACK_AFTERINSERT])) + { + foreach ($this->m_aCallbacks[self::CALLBACK_AFTERINSERT] as $aCallBackData) + { + call_user_func_array($aCallBackData['callback'], $aCallBackData['params']); + } + } + + $this->RecordObjCreation(); + + return $this->m_iKey; + } + + protected function MakeInsertStatementSingleTable($aAuthorizedExtKeys, &$aStatements, $sTableClass) + { + $sTable = MetaModel::DBGetTable($sTableClass); + // Abstract classes or classes having no specific attribute do not have an associated table + if ($sTable == '') return; + + $sClass = get_class($this); + + // fields in first array, values in the second + $aFieldsToWrite = array(); + $aValuesToWrite = array(); + + if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) + { + // Add it to the list of fields to write + $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; + $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); + } + + $aHierarchicalKeys = array(); + foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; + // Skip link set that can still be undefined though the object is 100% loaded + if ($oAttDef->IsLinkSet()) continue; + + $value = $this->m_aCurrValues[$sAttCode]; + if ($oAttDef->IsExternalKey()) + { + $sTargetClass = $oAttDef->GetTargetClass(); + if (is_array($aAuthorizedExtKeys)) + { + if (!array_key_exists($sTargetClass, $aAuthorizedExtKeys) || !array_key_exists($value, $aAuthorizedExtKeys[$sTargetClass])) + { + $value = 0; + } + } + } + $aAttColumns = $oAttDef->GetSQLValues($value); + foreach($aAttColumns as $sColumn => $sValue) + { + $aFieldsToWrite[] = "`$sColumn`"; + $aValuesToWrite[] = CMDBSource::Quote($sValue); + } + if ($oAttDef->IsHierarchicalKey()) + { + $aHierarchicalKeys[$sAttCode] = $oAttDef; + } + } + + if (count($aValuesToWrite) == 0) return false; + + if (count($aHierarchicalKeys) > 0) + { + foreach($aHierarchicalKeys as $sAttCode => $oAttDef) + { + $aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable); + $aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`'; + $aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()]; + $aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`'; + $aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()]; + } + } + $aStatements[] = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).");"; + } + + public function MakeInsertStatements($aAuthorizedExtKeys, &$aStatements) + { + $sClass = get_class($this); + $sRootClass = MetaModel::GetRootClass($sClass); + + // First query built upon on the root class, because the ID must be created first + $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sRootClass); + + // Then do the leaf class, if different from the root class + if ($sClass != $sRootClass) + { + $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sClass); + } + + // Then do the other classes + foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) + { + if ($sParentClass == $sRootClass) continue; + $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sParentClass); + } + } + + public function DBInsert() + { + $this->DBInsertNoReload(); + $this->Reload(); + return $this->m_iKey; + } + + public function DBInsertTracked(CMDBChange $oChange) + { + CMDBObject::SetCurrentChange($oChange); + return $this->DBInsert(); + } + + public function DBInsertTrackedNoReload(CMDBChange $oChange) + { + CMDBObject::SetCurrentChange($oChange); + return $this->DBInsertNoReload(); + } + + // Creates a copy of the current object into the database + // Returns the id of the newly created object + public function DBClone($iNewKey = null) + { + $this->m_bIsInDB = false; + $this->m_iKey = $iNewKey; + $ret = $this->DBInsert(); + $this->RecordObjCreation(); + return $ret; + } + + /** + * This function is automatically called after cloning an object with the "clone" PHP language construct + * The purpose of this method is to reset the appropriate attributes of the object in + * order to make sure that the newly cloned object is really distinct from its clone + */ + public function __clone() + { + $this->m_bIsInDB = false; + $this->m_bDirty = true; + $this->m_iKey = self::GetNextTempId(get_class($this)); + } + + // Update a record + public function DBUpdate() + { + if (!$this->m_bIsInDB) + { + throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead"); + } + + // Protect against reentrance (e.g. cascading the update of ticket logs) + static $aUpdateReentrance = array(); + $sKey = get_class($this).'::'.$this->GetKey(); + if (array_key_exists($sKey, $aUpdateReentrance)) + { + return; + } + $aUpdateReentrance[$sKey] = true; + + try + { + // Stop watches + $sState = $this->GetState(); + if ($sState != '') + { + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeStopWatch) + { + if (in_array($sState, $oAttDef->GetStates())) + { + // Compute or recompute the deadlines + $oSW = $this->Get($sAttCode); + $oSW->ComputeDeadlines($this, $oAttDef); + $this->Set($sAttCode, $oSW); + } + } + } + } + + $this->DoComputeValues(); + $this->OnUpdate(); + + $aChanges = $this->ListChanges(); + if (count($aChanges) == 0) + { + // Attempting to update an unchanged object + unset($aUpdateReentrance[$sKey]); + return $this->m_iKey; + } + + // Ultimate check - ensure DB integrity + list($bRes, $aIssues) = $this->CheckToWrite(); + if (!$bRes) + { + $sIssues = implode(', ', $aIssues); + throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); + } + + // Save the original values (will be reset to the new values when the object get written to the DB) + $aOriginalValues = $this->m_aOrigValues; + + $bHasANewExternalKeyValue = false; + $aHierarchicalKeys = array(); + $aDBChanges = array(); + foreach($aChanges as $sAttCode => $valuecurr) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true; + if ($oAttDef->IsBasedOnDBColumns()) + { + $aDBChanges[$sAttCode] = $aChanges[$sAttCode]; + } + if ($oAttDef->IsHierarchicalKey()) + { + $aHierarchicalKeys[$sAttCode] = $oAttDef; + } + } + + if (!MetaModel::DBIsReadOnly()) + { + // Update the left & right indexes for each hierarchical key + foreach($aHierarchicalKeys as $sAttCode => $oAttDef) + { + $sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); + $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey(); + $aRes = CMDBSource::QueryToArray($sSQL); + $iMyLeft = $aRes[0]['left']; + $iMyRight = $aRes[0]['right']; + $iDelta =$iMyRight - $iMyLeft + 1; + MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); + + if ($aDBChanges[$sAttCode] == 0) + { + // No new parent, insert completely at the right of the tree + $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; + $aRes = CMDBSource::QueryToArray($sSQL); + if (count($aRes) == 0) + { + $iNewLeft = 1; + } + else + { + $iNewLeft = $aRes[0]['max']+1; + } + } + else + { + // Insert at the right of the specified parent + $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aDBChanges[$sAttCode]); + $iNewLeft = CMDBSource::QueryToScalar($sSQL); + } + + MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); + + $aHKChanges = array(); + $aHKChanges[$sAttCode] = $aDBChanges[$sAttCode]; + $aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft; + $aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1; + $aDBChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below + } + + // Update scalar attributes + if (count($aDBChanges) != 0) + { + $oFilter = new DBObjectSearch(get_class($this)); + $oFilter->AddCondition('id', $this->m_iKey, '='); + $oFilter->AllowAllData(); + + $sSQL = $oFilter->MakeUpdateQuery($aDBChanges); + CMDBSource::Query($sSQL); + } + } + + $this->DBWriteLinks(); + $this->WriteExternalAttributes(); + + $this->m_bDirty = false; + $this->m_aTouchedAtt = array(); + $this->m_aModifiedAtt = array(); + + $this->AfterUpdate(); + + // Reload to get the external attributes + if ($bHasANewExternalKeyValue) + { + $this->Reload(true /* AllowAllData */); + } + else + { + // Reset original values although the object has not been reloaded + foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded) + { + if ($bLoaded) + { + $value = $this->m_aCurrValues[$sAttCode]; + $this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value; + } + } + } + + if (count($aChanges) != 0) + { + $this->RecordAttChanges($aChanges, $aOriginalValues); + } + } + catch (Exception $e) + { + unset($aUpdateReentrance[$sKey]); + throw $e; + } + + unset($aUpdateReentrance[$sKey]); + return $this->m_iKey; + } + + public function DBUpdateTracked(CMDBChange $oChange) + { + CMDBObject::SetCurrentChange($oChange); + return $this->DBUpdate(); + } + + // Make the current changes persistent - clever wrapper for Insert or Update + public function DBWrite() + { + if ($this->m_bIsInDB) + { + return $this->DBUpdate(); + } + else + { + return $this->DBInsert(); + } + } + + private function DBDeleteSingleTable($sTableClass) + { + $sTable = MetaModel::DBGetTable($sTableClass); + // Abstract classes or classes having no specific attribute do not have an associated table + if ($sTable == '') return; + + $sPKField = '`'.MetaModel::DBGetKey($sTableClass).'`'; + $sKey = CMDBSource::Quote($this->m_iKey); + + $sDeleteSQL = "DELETE FROM `$sTable` WHERE $sPKField = $sKey"; + CMDBSource::DeleteFrom($sDeleteSQL); + } + + protected function DBDeleteSingleObject() + { + if (!MetaModel::DBIsReadOnly()) + { + $this->OnDelete(); + $this->RecordObjDeletion($this->m_iKey); // May cause a reload for storing history information + + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsHierarchicalKey()) + { + // Update the left & right indexes for each hierarchical key + $sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); + $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".CMDBSource::Quote($this->m_iKey); + $aRes = CMDBSource::QueryToArray($sSQL); + $iMyLeft = $aRes[0]['left']; + $iMyRight = $aRes[0]['right']; + $iDelta =$iMyRight - $iMyLeft + 1; + MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); + + // No new parent for now, insert completely at the right of the tree + $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; + $aRes = CMDBSource::QueryToArray($sSQL); + if (count($aRes) == 0) + { + $iNewLeft = 1; + } + else + { + $iNewLeft = $aRes[0]['max']+1; + } + MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); + } + elseif (!$oAttDef->LoadFromDB()) + { + $oAttDef->DeleteValue($this); + } + } + + foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass) + { + $this->DBDeleteSingleTable($sParentClass); + } + + $this->AfterDelete(); + + $this->m_bIsInDB = false; + // Fix for #926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete objects, reported as bug #926) + // Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected + } + } + + // Delete an object... and guarantee data integrity + // + public function DBDelete(&$oDeletionPlan = null) + { + static $iLoopTimeLimit = null; + if ($iLoopTimeLimit == null) + { + $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); + } + if (is_null($oDeletionPlan)) + { + $oDeletionPlan = new DeletionPlan(); + } + $this->MakeDeletionPlan($oDeletionPlan); + $oDeletionPlan->ComputeResults(); + + if ($oDeletionPlan->FoundStopper()) + { + $aIssues = $oDeletionPlan->GetIssues(); + throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues))); + } + else + { + // Getting and setting time limit are not symetric: + // www.php.net/manual/fr/function.set-time-limit.php#72305 + $iPreviousTimeLimit = ini_get('max_execution_time'); + + foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete) + { + foreach ($aToDelete as $iId => $aData) + { + $oToDelete = $aData['to_delete']; + // The deletion based on a deletion plan should not be done for each oject if the deletion plan is common (Trac #457) + // because for each object we would try to update all the preceding ones... that are already deleted + // A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once + // As a temporary fix: delete only the objects that are still to be deleted... + if ($oToDelete->m_bIsInDB) + { + set_time_limit($iLoopTimeLimit); + $oToDelete->DBDeleteSingleObject(); + } + } + } + + foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) + { + foreach ($aToUpdate as $iId => $aData) + { + $oToUpdate = $aData['to_reset']; + foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) + { + $oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]); + set_time_limit($iLoopTimeLimit); + $oToUpdate->DBUpdate(); + } + } + } + + set_time_limit($iPreviousTimeLimit); + } + + return $oDeletionPlan; + } + + public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) + { + CMDBObject::SetCurrentChange($oChange); + $this->DBDelete($oDeletionPlan); + } + + public function EnumTransitions() + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) return array(); + + $sState = $this->Get(MetaModel::GetStateAttributeCode(get_class($this))); + return MetaModel::EnumTransitions(get_class($this), $sState); + } + + /** + * Designed as an action to be called when a stop watch threshold times out + * or from within the framework + * @param $sStimulusCode + * @param bool|false $bDoNotWrite + * @return bool + * @throws CoreException + * @throws CoreUnexpectedValue + */ + public function ApplyStimulus($sStimulusCode, $bDoNotWrite = false) + { + $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); + if (empty($sStateAttCode)) + { + throw new CoreException('No lifecycle for the class '.get_class($this)); + } + + MyHelpers::CheckKeyInArray('object lifecycle stimulus', $sStimulusCode, MetaModel::EnumStimuli(get_class($this))); + + $aStateTransitions = $this->EnumTransitions(); + if (!array_key_exists($sStimulusCode, $aStateTransitions)) + { + // This simulus has no effect in the current state... do nothing + return true; + } + $aTransitionDef = $aStateTransitions[$sStimulusCode]; + + // Change the state before proceeding to the actions, this is necessary because an action might + // trigger another stimuli (alternative: push the stimuli into a queue) + $sPreviousState = $this->Get($sStateAttCode); + $sNewState = $aTransitionDef['target_state']; + $this->Set($sStateAttCode, $sNewState); + + // $aTransitionDef is an + // array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD + + $bSuccess = true; + foreach ($aTransitionDef['actions'] as $actionHandler) + { + if (is_string($actionHandler)) + { + // Old (pre-2.1.0 modules) action definition without any parameter + $aActionCallSpec = array($this, $actionHandler); + $sActionDesc = get_class($this).'::'.$actionHandler; + + if (!is_callable($aActionCallSpec)) + { + throw new CoreException("Unable to call action: ".get_class($this)."::$actionHandler"); + } + $bRet = call_user_func($aActionCallSpec, $sStimulusCode); + } + else // if (is_array($actionHandler)) + { + // New syntax: 'verb' and typed parameters + $sAction = $actionHandler['verb']; + $sActionDesc = get_class($this).'::'.$sAction; + $aParams = array(); + foreach($actionHandler['params'] as $aDefinition) + { + $sParamType = array_key_exists('type', $aDefinition) ? $aDefinition['type'] : 'string'; + switch($sParamType) + { + case 'int': + $value = (int)$aDefinition['value']; + break; + + case 'float': + $value = (float)$aDefinition['value']; + break; + + case 'bool': + $value = (bool)$aDefinition['value']; + break; + + case 'reference': + $value = ${$aDefinition['value']}; + break; + + case 'string': + default: + $value = (string)$aDefinition['value']; + } + $aParams[] = $value; + } + $aCallSpec = array($this, $sAction); + $bRet = call_user_func_array($aCallSpec, $aParams); + } + // if one call fails, the whole is considered as failed + // (in case there is no returned value, null is obtained and means "ok") + if ($bRet === false) + { + IssueLog::Info("Lifecycle action $sActionDesc returned false on object #".$this->GetKey()); + $bSuccess = false; + } + } + if ($bSuccess) + { + $sClass = get_class($this); + + // Stop watches + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeStopWatch) + { + $oSW = $this->Get($sAttCode); + if (in_array($sNewState, $oAttDef->GetStates())) + { + $oSW->Start($this, $oAttDef); + } + else + { + $oSW->Stop($this, $oAttDef); + } + $this->Set($sAttCode, $oSW); + } + } + + if (!$bDoNotWrite) + { + $this->DBWrite(); + } + + // Change state triggers... + $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sPreviousState'")); + while ($oTrigger = $oSet->Fetch()) + { + $oTrigger->DoActivate($this->ToArgs('this')); + } + + $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sNewState'")); + while ($oTrigger = $oSet->Fetch()) + { + $oTrigger->DoActivate($this->ToArgs('this')); + } + } + + return $bSuccess; + } + + /** + * Designed as an action to be called when a stop watch threshold times out + */ + public function ResetStopWatch($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if (!$oAttDef instanceof AttributeStopWatch) + { + throw new CoreException("Invalid stop watch id: '$sAttCode'"); + } + $oSW = $this->Get($sAttCode); + $oSW->Reset($this, $oAttDef); + $this->Set($sAttCode, $oSW); + return true; + } + + /** + * Lifecycle action: Recover the default value (aka when an object is being created) + */ + public function Reset($sAttCode) + { + $this->Set($sAttCode, $this->GetDefaultValue($sAttCode)); + return true; + } + + /** + * Lifecycle action: Copy an attribute to another + */ + public function Copy($sDestAttCode, $sSourceAttCode) + { + $this->Set($sDestAttCode, $this->Get($sSourceAttCode)); + return true; + } + + /** + * Lifecycle action: Set the current date/time for the given attribute + */ + public function SetCurrentDate($sAttCode) + { + $this->Set($sAttCode, time()); + return true; + } + + /** + * Lifecycle action: Set the current logged in user for the given attribute + */ + public function SetCurrentUser($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef instanceof AttributeString) + { + // Note: the user friendly name is the contact friendly name if a contact is attached to the logged in user + $this->Set($sAttCode, UserRights::GetUserFriendlyName()); + } + else + { + if ($oAttDef->IsExternalKey()) + { + if ($oAttDef->GetTargetClass() != 'User') + { + throw new Exception("SetCurrentUser: the attribute $sAttCode must be an external key to 'User', found '".$oAttDef->GetTargetClass()."'"); + } + } + $this->Set($sAttCode, UserRights::GetUserId()); + } + return true; + } + + /** + * Lifecycle action: Set the current logged in CONTACT for the given attribute + */ + public function SetCurrentPerson($sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + if ($oAttDef instanceof AttributeString) + { + $iPerson = UserRights::GetContactId(); + if ($iPerson == 0) + { + $this->Set($sAttCode, ''); + } + else + { + $oPerson = MetaModel::GetObject('Person', $iPerson); + $this->Set($sAttCode, $oPerson->Get('friendlyname')); + } + } + else + { + if ($oAttDef->IsExternalKey()) + { + if (!MetaModel::IsParentClass($oAttDef->GetTargetClass(), 'Person')) + { + throw new Exception("SetCurrentContact: the attribute $sAttCode must be an external key to 'Person' or any other class above 'Person', found '".$oAttDef->GetTargetClass()."'"); + } + } + $this->Set($sAttCode, UserRights::GetContactId()); + } + return true; + } + + /** + * Lifecycle action: Set the time elapsed since a reference point + */ + public function SetElapsedTime($sAttCode, $sRefAttCode, $sWorkingTimeComputer = null) + { + if (is_null($sWorkingTimeComputer)) + { + $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; + } + $oComputer = new $sWorkingTimeComputer(); + $aCallSpec = array($oComputer, 'GetOpenDuration'); + if (!is_callable($aCallSpec)) + { + throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'"); + } + + $iStartTime = AttributeDateTime::GetAsUnixSeconds($this->Get($sRefAttCode)); + $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 + $oEndDate = new DateTime(); // now + + if (class_exists('WorkingTimeRecorder')) + { + $sClass = get_class($this); + WorkingTimeRecorder::Start($this, time(), "DBObject-SetElapsedTime-$sAttCode-$sRefAttCode", 'Core:ExplainWTC:ElapsedTime', array("Class:$sClass/Attribute:$sAttCode")); + } + $iElapsed = call_user_func($aCallSpec, $this, $oStartDate, $oEndDate); + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::End(); + } + + $this->Set($sAttCode, $iElapsed); + return true; + } + + + + /** + * Create query parameters (SELECT ... WHERE service = :this->service_id) + * to be used with the APIs DBObjectSearch/DBObjectSet + * + * Starting 2.0.2 the parameters are computed on demand, at the lowest level, + * in VariableExpression::Render() + */ + public function ToArgsForQuery($sArgName = 'this') + { + return array($sArgName.'->object()' => $this); + } + + /** + * Create template placeholders: now equivalent to ToArgsForQuery since the actual + * template placeholders are computed on demand. + */ + public function ToArgs($sArgName = 'this') + { + return $this->ToArgsForQuery($sArgName); + } + + public function GetForTemplate($sPlaceholderAttCode) + { + $ret = null; + if (preg_match('/^([^-]+)-(>|>)(.+)$/', $sPlaceholderAttCode, $aMatches)) // Support both syntaxes: this->xxx or this->xxx for HTML compatibility + { + $sExtKeyAttCode = $aMatches[1]; + $sRemoteAttCode = $aMatches[3]; + if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) + { + throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this)); + } + + $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); + if (!$oKeyAttDef instanceof AttributeExternalKey) + { + throw new CoreException("'$sExtKeyAttCode' is not an external key of the class ".get_class($this)); + } + $sRemoteClass = $oKeyAttDef->GetTargetClass(); + $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); + if (is_null($oRemoteObj)) + { + $ret = Dict::S('UI:UndefinedObject'); + } + else + { + // Recurse + $ret = $oRemoteObj->GetForTemplate($sRemoteAttCode); + } + } + else + { + switch($sPlaceholderAttCode) + { + case 'id': + $ret = $this->GetKey(); + break; + + case 'name()': + $ret = $this->GetName(); + break; + + default: + if (preg_match('/^([^(]+)\\((.*)\\)$/', $sPlaceholderAttCode, $aMatches)) + { + $sVerb = $aMatches[1]; + $sAttCode = $aMatches[2]; + } + else + { + $sVerb = ''; + $sAttCode = $sPlaceholderAttCode; + } + + if ($sVerb == 'hyperlink') + { + $sPortalId = ($sAttCode === '') ? 'console' : $sAttCode; + if (!array_key_exists($sPortalId, self::$aPortalToURLMaker)) + { + throw new Exception("Unknown portal id '$sPortalId' in placeholder '$sPlaceholderAttCode''"); + } + $ret = $this->GetHyperlink(self::$aPortalToURLMaker[$sPortalId], false); + } + else + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $ret = $oAttDef->GetForTemplate($this->Get($sAttCode), $sVerb, $this); + } + } + if ($ret === null) + { + $ret = ''; + } + } + return $ret; + } + + static protected $aPortalToURLMaker = array('console' => 'iTopStandardURLMaker', 'portal' => 'PortalURLMaker'); + + /** + * Associate a portal to a class that implements iDBObjectURLMaker, + * and which will be invoked with placeholders like $this->org_id->hyperlink(portal)$ + * + * @param string $sPortalId Identifies the portal. Conventions: the main portal is 'console', The user requests portal is 'portal'. + * @param string $sUrlMakerClass + */ + static public function RegisterURLMakerClass($sPortalId, $sUrlMakerClass) + { + self::$aPortalToURLMaker[$sPortalId] = $sUrlMakerClass; + } + + // To be optionaly overloaded + protected function OnInsert() + { + } + + // To be optionaly overloaded + protected function AfterInsert() + { + } + + // To be optionaly overloaded + protected function OnUpdate() + { + } + + // To be optionaly overloaded + protected function AfterUpdate() + { + } + + // To be optionaly overloaded + protected function OnDelete() + { + } + + // To be optionaly overloaded + protected function AfterDelete() + { + } + + + /** + * Common to the recording of link set changes (add/remove/modify) + */ + private function PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, $sChangeOpClass, $aOriginalValues = null) + { + if ($iLinkSetOwnerId <= 0) + { + return null; + } + + if (!is_subclass_of($oLinkSet->GetHostClass(), 'CMDBObject')) + { + // The link set owner class does not keep track of its history + return null; + } + + // Determine the linked item class and id + // + if ($oLinkSet->IsIndirect()) + { + // The "item" is on the other end (N-N links) + $sExtKeyToRemote = $oLinkSet->GetExtKeyToRemote(); + $oExtKeyToRemote = MetaModel::GetAttributeDef(get_class($this), $sExtKeyToRemote); + $sItemClass = $oExtKeyToRemote->GetTargetClass(); + if ($aOriginalValues) + { + // Get the value from the original values + $iItemId = $aOriginalValues[$sExtKeyToRemote]; + } + else + { + $iItemId = $this->Get($sExtKeyToRemote); + } + } + else + { + // I am the "item" (1-N links) + $sItemClass = get_class($this); + $iItemId = $this->GetKey(); + } + + // Get the remote object, to determine its exact class + // Possible optimization: implement a tool in MetaModel, to get the final class of an object (not always querying + query reduced to a select on the root table! + $oOwner = MetaModel::GetObject($oLinkSet->GetHostClass(), $iLinkSetOwnerId, false); + if ($oOwner) + { + $sLinkSetOwnerClass = get_class($oOwner); + + $oMyChangeOp = MetaModel::NewObject($sChangeOpClass); + $oMyChangeOp->Set("objclass", $sLinkSetOwnerClass); + $oMyChangeOp->Set("objkey", $iLinkSetOwnerId); + $oMyChangeOp->Set("attcode", $oLinkSet->GetCode()); + $oMyChangeOp->Set("item_class", $sItemClass); + $oMyChangeOp->Set("item_id", $iItemId); + return $oMyChangeOp; + } + else + { + // Depending on the deletion order, it may happen that the id is already invalid... ignore + return null; + } + } + + /** + * This object has been created/deleted, record that as a change in link sets pointing to this (if any) + */ + private function RecordLinkSetListChange($bAdd = true) + { + $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys(get_class($this)); + foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) + { + if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; + + $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); + if ($oMyChangeOp) + { + if ($bAdd) + { + $oMyChangeOp->Set("type", "added"); + } + else + { + $oMyChangeOp->Set("type", "removed"); + } + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + } + + protected function RecordObjCreation() + { + $this->RecordLinkSetListChange(true); + } + + protected function RecordObjDeletion($objkey) + { + $this->RecordLinkSetListChange(false); + } + + protected function RecordAttChanges(array $aValues, array $aOrigValues) + { + $aForwardChangeTracking = MetaModel::GetTrackForwardExternalKeys(get_class($this)); + foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) + { + + if (array_key_exists($sExtKeyAttCode, $aValues)) + { + if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; + + // Keep track of link added/removed + // + $iLinkSetOwnerNext = $aValues[$sExtKeyAttCode]; + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerNext, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); + if ($oMyChangeOp) + { + $oMyChangeOp->Set("type", "added"); + $oMyChangeOp->DBInsertNoReload(); + } + + $iLinkSetOwnerPrevious = $aOrigValues[$sExtKeyAttCode]; + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerPrevious, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove', $aOrigValues); + if ($oMyChangeOp) + { + $oMyChangeOp->Set("type", "removed"); + $oMyChangeOp->DBInsertNoReload(); + } + } + else + { + // Keep track of link changes + // + if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue; + + $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); + $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune'); + if ($oMyChangeOp) + { + $oMyChangeOp->Set("link_id", $this->GetKey()); + $iId = $oMyChangeOp->DBInsertNoReload(); + } + } + } + } + + // Return an empty set for the parent of all + // May be overloaded. + // Anyhow, this way of implementing the relations suffers limitations (not handling the redundancy) + // and you should consider defining those things in XML. + public static function GetRelationQueries($sRelCode) + { + return array(); + } + + // Reserved: do not overload + public static function GetRelationQueriesEx($sRelCode) + { + return array(); + } + + /** + * Will be deprecated soon - use GetRelatedObjectsDown/Up instead to take redundancy into account + */ + public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array()) + { + // Temporary patch: until the impact analysis GUI gets rewritten, + // let's consider that "depends on" is equivalent to "impacts/up" + // The current patch has been implemented in DBObject and MetaModel + $sHackedRelCode = $sRelCode; + $bDown = true; + if ($sRelCode == 'depends on') + { + $sHackedRelCode = 'impacts'; + $bDown = false; + } + foreach (MetaModel::EnumRelationQueries(get_class($this), $sHackedRelCode, $bDown) as $sDummy => $aQueryInfo) + { + $sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp']; + //$bPropagate = $aQueryInfo["bPropagate"]; + //$iDepth = $bPropagate ? $iMaxDepth - 1 : 0; + $iDepth = $iMaxDepth - 1; + + // Note: the loop over the result set has been written in an unusual way for error reporting purposes + // In the case of a wrong query parameter name, the error occurs on the first call to Fetch, + // thus we need to have this first call into the try/catch, but + // we do NOT want to nest the try/catch for the error message to be clear + try + { + $oFlt = DBObjectSearch::FromOQL($sQuery); + $oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgsForQuery()); + $oObj = $oObjSet->Fetch(); + } + catch (Exception $e) + { + $sClassOfDefinition = $aQueryInfo['_legacy_'] ? get_class($this).'(or a parent)::GetRelationQueries()' : $aQueryInfo['sDefinedInClass']; + throw new Exception("Wrong query for the relation $sRelCode/$sClassOfDefinition/{$aQueryInfo['sNeighbour']}: ".$e->getMessage()); + } + if ($oObj) + { + do + { + $sRootClass = MetaModel::GetRootClass(get_class($oObj)); + $sObjKey = $oObj->GetKey(); + if (array_key_exists($sRootClass, $aResults)) + { + if (array_key_exists($sObjKey, $aResults[$sRootClass])) + { + continue; // already visited, skip + } + } + + $aResults[$sRootClass][$sObjKey] = $oObj; + if ($iDepth > 0) + { + $oObj->GetRelatedObjects($sRelCode, $iDepth, $aResults); + } + } + while ($oObj = $oObjSet->Fetch()); + } + } + return $aResults; + } + + /** + * Compute the "RelatedObjects" (forward or "down" direction) for the object + * for the specified relation + * + * @param string $sRelCode The code of the relation to use for the computation + * @param int $iMaxDepth Maximum recursion depth + * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy + * + * @return RelationGraph The graph of all the related objects + */ + public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) + { + $oGraph = new RelationGraph(); + $oGraph->AddSourceObject($this); + $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy); + return $oGraph; + } + + /** + * Compute the "RelatedObjects" (reverse or "up" direction) for the object + * for the specified relation + * + * @param string $sRelCode The code of the relation to use for the computation + * @param int $iMaxDepth Maximum recursion depth + * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy + * + * @return RelationGraph The graph of all the related objects + */ + public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) + { + $oGraph = new RelationGraph(); + $oGraph->AddSourceObject($this); + $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); + return $oGraph; + } + + public function GetReferencingObjects($bAllowAllData = false) + { + $aDependentObjects = array(); + $aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this)); + foreach($aRererencingMe as $sRemoteClass => $aExtKeys) + { + foreach($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) + { + // skip if this external key is behind an external field + if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) continue; + + $oSearch = new DBObjectSearch($sRemoteClass); + $oSearch->AddCondition($sExtKeyAttCode, $this->GetKey(), '='); + if ($bAllowAllData) + { + $oSearch->AllowAllData(); + } + $oSet = new CMDBObjectSet($oSearch); + if ($oSet->CountExceeds(0)) + { + $aDependentObjects[$sRemoteClass][$sExtKeyAttCode] = array( + 'attribute' => $oExtKeyAttDef, + 'objects' => $oSet, + ); + } + } + } + return $aDependentObjects; + } + + private function MakeDeletionPlan(&$oDeletionPlan, $aVisited = array(), $iDeleteOption = null) + { + static $iLoopTimeLimit = null; + if ($iLoopTimeLimit == null) + { + $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); + } + $sClass = get_class($this); + $iThisId = $this->GetKey(); + + $iDeleteOption = $oDeletionPlan->AddToDelete($this, $iDeleteOption); + + if (array_key_exists($sClass, $aVisited)) + { + if (in_array($iThisId, $aVisited[$sClass])) + { + return; + } + } + $aVisited[$sClass] = $iThisId; + + if ($iDeleteOption == DEL_MANUAL) + { + // Stop the recursion here + return; + } + // Check the node itself + $this->DoCheckToDelete($oDeletionPlan); + $oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue); + + $aDependentObjects = $this->GetReferencingObjects(true /* allow all data */); + + // Getting and setting time limit are not symetric: + // www.php.net/manual/fr/function.set-time-limit.php#72305 + $iPreviousTimeLimit = ini_get('max_execution_time'); + + foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes) + { + foreach ($aPotentialDeletes as $sRemoteExtKey => $aData) + { + set_time_limit($iLoopTimeLimit); + + $oAttDef = $aData['attribute']; + $iDeletePropagationOption = $oAttDef->GetDeletionPropagationOption(); + $oDepSet = $aData['objects']; + $oDepSet->Rewind(); + while ($oDependentObj = $oDepSet->fetch()) + { + $iId = $oDependentObj->GetKey(); + if ($oAttDef->IsNullAllowed()) + { + // Optional external key, list to reset + if (($iDeletePropagationOption == DEL_MOVEUP) && ($oAttDef->IsHierarchicalKey())) + { + // Move the child up one level i.e. set the same parent as the current object + $iParentId = $this->Get($oAttDef->GetCode()); + $oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef, $iParentId); + } + else + { + $oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef); + } + } + else + { + // Mandatory external key, list to delete + $oDependentObj->MakeDeletionPlan($oDeletionPlan, $aVisited, $iDeletePropagationOption); + } + } + } + } + set_time_limit($iPreviousTimeLimit); + } + + /** + * WILL DEPRECATED SOON + * Caching relying on an object set is not efficient since 2.0.3 + * Use GetSynchroData instead + * + * Get all the synchro replica related to this object + * @param none + * @return DBObjectSet Set with two columns: R=SynchroReplica S=SynchroDataSource + */ + public function GetMasterReplica() + { + $sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id"; + $oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey())); + return $oReplicaSet; + } + + /** + * Get all the synchro data related to this object + * @param none + * @return array of data_source_id => array + * 'source' => $oSource, + * 'attributes' => array of $oSynchroAttribute + * 'replica' => array of $oReplica (though only one should exist, misuse of the data sync can have this consequence) + */ + public function GetSynchroData() + { + if (is_null($this->m_aSynchroData)) + { + $sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id"; + $oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey())); + $this->m_aSynchroData = array(); + while($aData = $oReplicaSet->FetchAssoc()) + { + $iSourceId = $aData['datasource']->GetKey(); + if (!array_key_exists($iSourceId, $this->m_aSynchroData)) + { + $aAttributes = array(); + $oAttrSet = $aData['datasource']->Get('attribute_list'); + while($oSyncAttr = $oAttrSet->Fetch()) + { + $aAttributes[$oSyncAttr->Get('attcode')] = $oSyncAttr; + } + $this->m_aSynchroData[$iSourceId] = array( + 'source' => $aData['datasource'], + 'attributes' => $aAttributes, + 'replica' => array() + ); + } + // Assumption: $aData['datasource'] will not be null because the data source id is always set... + $this->m_aSynchroData[$iSourceId]['replica'][] = $aData['replica']; + } + } + return $this->m_aSynchroData; + } + + public function GetSynchroReplicaFlags($sAttCode, &$aReason) + { + $iFlags = OPT_ATT_NORMAL; + foreach ($this->GetSynchroData() as $iSourceId => $aSourceData) + { + if ($iSourceId == SynchroExecution::GetCurrentTaskId()) + { + // Ignore the current task (check to write => ok) + continue; + } + // Assumption: one replica - take the first one! + $oReplica = reset($aSourceData['replica']); + $oSource = $aSourceData['source']; + if (array_key_exists($sAttCode, $aSourceData['attributes'])) + { + $oSyncAttr = $aSourceData['attributes'][$sAttCode]; + if (($oSyncAttr->Get('update') == 1) && ($oSyncAttr->Get('update_policy') == 'master_locked')) + { + $iFlags |= OPT_ATT_SLAVE; + $sUrl = $oSource->GetApplicationUrl($this, $oReplica); + $aReason[] = array('name' => $oSource->GetName(), 'description' => $oSource->Get('description'), 'url_application' => $sUrl); + } + } + } + return $iFlags; + } + + public function InSyncScope() + { + // + // Optimization: cache the list of Data Sources and classes candidates for synchro + // + static $aSynchroClasses = null; + if (is_null($aSynchroClasses)) + { + $aSynchroClasses = array(); + $sOQL = "SELECT SynchroDataSource AS datasource"; + $oSourceSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array()); + while($oSource = $oSourceSet->Fetch()) + { + $sTarget = $oSource->Get('scope_class'); + $aSynchroClasses[] = $sTarget; + } + } + + foreach($aSynchroClasses as $sClass) + { + if ($this instanceof $sClass) + { + return true; + } + } + return false; + } + ///////////////////////////////////////////////////////////////////////// + // + // Experimental iDisplay implementation + // + ///////////////////////////////////////////////////////////////////////// + + public static function MapContextParam($sContextParam) + { + return null; + } + + public function GetHilightClass() + { + $sCode = $this->ComputeHighlightCode(); + if($sCode != '') + { + $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); + if (array_key_exists($sCode, $aHighlightScale)) + { + return $aHighlightScale[$sCode]['color']; + } + } + return HILIGHT_CLASS_NONE; + } + + public function DisplayDetails(WebPage $oPage, $bEditMode = false) + { + $oPage->add('

    '.MetaModel::GetName(get_class($this)).': '.$this->GetName().'

    '); + $aValues = array(); + $aList = MetaModel::FlattenZList(MetaModel::GetZListItems(get_class($this), 'details')); + if (empty($aList)) + { + $aList = array_keys(MetaModel::ListAttributeDefs(get_class($this))); + } + foreach($aList as $sAttCode) + { + $aValues[$sAttCode] = array('label' => MetaModel::GetLabel(get_class($this), $sAttCode), 'value' => $this->GetAsHTML($sAttCode)); + } + $oPage->details($aValues); + } + + + const CALLBACK_AFTERINSERT = 0; + + /** + * Register a call back that will be called when some internal event happens + * + * @param $iType string Any of the CALLBACK_x constants + * @param $callback callable Call specification like a function name, or array('', '') or array($object, '') + * @param $aParameters Array Values that will be passed to the callback, after $this + */ + public function RegisterCallback($iType, $callback, $aParameters = array()) + { + $sCallBackName = ''; + if (!is_callable($callback, false, $sCallBackName)) + { + throw new Exception('Registering an unknown/protected function or wrong syntax for the call spec: '.$sCallBackName); + } + $this->m_aCallbacks[$iType][] = array( + 'callback' => $callback, + 'params' => $aParameters + ); + } + + /** + * Computes a text-like fingerprint identifying the content of the object + * but excluding the specified columns + * @param $aExcludedColumns array The list of columns to exclude + * @return string + */ + public function Fingerprint($aExcludedColumns = array()) + { + $sFingerprint = ''; + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if (!in_array($sAttCode, $aExcludedColumns)) + { + if ($oAttDef->IsPartOfFingerprint()) + { + $sFingerprint .= chr(0).$oAttDef->Fingerprint($this->Get($sAttCode)); + } + } + } + return $sFingerprint; + } + + /** + * Execute a set of scripted actions onto the current object + * See ExecAction for the syntax and features of the scripted actions + * + * @param $aActions array of statements (e.g. "set(name, Made after $source->name$)") + * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element + * @throws Exception + */ + public function ExecActions($aActions, $aSourceObjects) + { + foreach($aActions as $sAction) + { + try + { + if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot + { + $sVerb = trim($aMatches[1]); + $sParams = $aMatches[2]; + + // the coma is the separator for the parameters + // comas can be escaped: \, + $sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams); + $sParams = trim($sParams); + + if (strlen($sParams) == 0) + { + $aParams = array(); + } + else + { + $aParams = explode(',', $sParams); + foreach ($aParams as &$sParam) + { + $sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam); + $sParam = trim($sParam); + } + } + $this->ExecAction($sVerb, $aParams, $aSourceObjects); + } + else + { + throw new Exception("Invalid syntax"); + } + } + catch(Exception $e) + { + throw new Exception('Action: '.$sAction.' - '.$e->getMessage()); + } + } + } + + /** + * Helper to copy an attribute between two objects (in memory) + * Originally designed for ExecAction() + */ + public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode) + { + if ($sSourceAttCode == 'id') + { + $oSourceAttDef = null; + } + else + { + if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode); + } + if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode)) + { + throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode); + } + + $oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode); + } + if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet()) + { + // The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets) + $oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass()); + $oSourceSet = $oSourceObject->Get($sSourceAttCode); + $oSourceSet->Rewind(); + while ($oSourceLink = $oSourceSet->Fetch()) + { + // Clone the link + $sLinkClass = get_class($oSourceLink); + $oLinkClone = MetaModel::NewObject($sLinkClass); + foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef) + { + // As of now, ignore other attribute (do not attempt to recurse!) + if ($oAttDef->IsScalar()) + { + $oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode)); + } + } + + // Not necessary - this will be handled by DBObject + // $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0); + $oDestSet->AddObject($oLinkClone); + } + $this->Set($sDestAttCode, $oDestSet); + } + else + { + $this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode)); + } + } + + /** + * Execute a scripted action onto the current object + * - clone (att1, att2, att3, ...) + * - clone_scalars () + * - copy (source_att, dest_att) + * - reset (att) + * - nullify (att) + * - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) + * - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) + * - add_to_list (source_key_att, dest_att) + * - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value) + * - apply_stimulus (stimulus) + * - call_method (method_name) + * + * @param $sVerb string Any of the verb listed above (e.g. "set") + * @param $aParams array of strings (e.g. array('name', 'copied from $source->name$') + * @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element + * @throws CoreException + * @throws CoreUnexpectedValue + * @throws Exception + */ + public function ExecAction($sVerb, $aParams, $aSourceObjects) + { + switch($sVerb) + { + case 'clone': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + foreach($aParams as $sAttCode) + { + $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); + } + break; + + case 'clone_scalars': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar()) + { + $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); + } + } + break; + + case 'copy': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: source attribute'); + } + $sSourceAttCode = $aParams[0]; + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: target attribute'); + } + $sDestAttCode = $aParams[1]; + $this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode); + break; + + case 'reset': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + $this->Set($sAttCode, $this->GetDefaultValue($sAttCode)); + break; + + case 'nullify': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $this->Set($sAttCode, $oAttDef->GetNullValue()); + break; + + case 'set': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: value to set'); + } + $sRawValue = $aParams[1]; + $aContext = array(); + foreach ($aSourceObjects as $sAlias => $oObject) + { + $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); + } + $aContext['current_contact_id'] = UserRights::GetContactId(); + $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); + $aContext['current_date'] = date(AttributeDate::GetSQLFormat()); + $aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat()); + $sValue = MetaModel::ApplyParams($sRawValue, $aContext); + $this->Set($sAttCode, $sValue); + break; + + case 'append': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: target attribute'); + } + $sAttCode = $aParams[0]; + if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); + } + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: value to append'); + } + $sRawAddendum = $aParams[1]; + $aContext = array(); + foreach ($aSourceObjects as $sAlias => $oObject) + { + $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); + } + $aContext['current_contact_id'] = UserRights::GetContactId(); + $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); + $aContext['current_date'] = date(AttributeDate::GetSQLFormat()); + $aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat()); + $sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext); + $this->Set($sAttCode, $this->Get($sAttCode).$sAddendum); + break; + + case 'add_to_list': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: source attribute'); + } + $sSourceKeyAttCode = $aParams[0]; + if (($sSourceKeyAttCode != 'id') && !MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode)) + { + throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode); + } + if (!array_key_exists(1, $aParams)) + { + throw new Exception('Missing argument #2: target attribute (link set)'); + } + $sTargetListAttCode = $aParams[1]; // indirect !!! + if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode)) + { + throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode); + } + if (isset($aParams[2]) && isset($aParams[3])) + { + $sRoleAttCode = $aParams[2]; + $sRoleValue = $aParams[3]; + } + + $iObjKey = $oObjectToRead->Get($sSourceKeyAttCode); + if ($iObjKey > 0) + { + $oLinkSet = $this->Get($sTargetListAttCode); + + $oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode); + $oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass()); + $oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey); + if (isset($sRoleAttCode)) + { + if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode)) + { + throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode); + } + $oLnk->Set($sRoleAttCode, $sRoleValue); + } + $oLinkSet->AddObject($oLnk); + $this->Set($sTargetListAttCode, $oLinkSet); + } + break; + + case 'apply_stimulus': + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: stimulus'); + } + $sStimulus = $aParams[0]; + if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this)))) + { + throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus); + } + $this->ApplyStimulus($sStimulus); + break; + + case 'call_method': + if (!array_key_exists('source', $aSourceObjects)) + { + throw new Exception('Missing conventional "source" object'); + } + $oObjectToRead = $aSourceObjects['source']; + if (!array_key_exists(0, $aParams)) + { + throw new Exception('Missing argument #1: method name'); + } + $sMethod = $aParams[0]; + $aCallSpec = array($this, $sMethod); + if (!is_callable($aCallSpec)) + { + throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()'); + } + // Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToRead� + call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects); + break; + + default: + throw new Exception("Invalid verb"); + } + } + + public function IsArchived($sKeyAttCode = null) + { + $bRet = false; + $sFlagAttCode = is_null($sKeyAttCode) ? 'archive_flag' : $sKeyAttCode.'_archive_flag'; + if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode)) + { + $bRet = true; + } + return $bRet; + } + + public function IsObsolete($sKeyAttCode = null) + { + $bRet = false; + $sFlagAttCode = is_null($sKeyAttCode) ? 'obsolescence_flag' : $sKeyAttCode.'_obsolescence_flag'; + if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode)) + { + $bRet = true; + } + return $bRet; + } + + /** + * @param $bArchive + * @throws Exception + */ + protected function DBWriteArchiveFlag($bArchive) + { + if (!MetaModel::IsArchivable(get_class($this))) + { + throw new Exception(get_class($this).' is not an archivable class'); + } + + $iFlag = $bArchive ? 1 : 0; + $sDate = $bArchive ? '"'.date(AttributeDate::GetSQLFormat()).'"' : 'null'; + + $sClass = get_class($this); + $sArchiveRoot = MetaModel::GetAttributeOrigin($sClass, 'archive_flag'); + $sRootTable = MetaModel::DBGetTable($sArchiveRoot); + $sRootKey = MetaModel::DBGetKey($sArchiveRoot); + $aJoins = array("`$sRootTable`"); + $aUpdates = array(); + foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass) + { + if (!MetaModel::IsValidAttCode($sParentClass, 'archive_flag')) continue; + + $sTable = MetaModel::DBGetTable($sParentClass); + $aUpdates[] = "`$sTable`.`archive_flag` = $iFlag"; + if ($sParentClass == $sArchiveRoot) + { + if (!$bArchive || $this->Get('archive_date') == '') + { + // Erase or set the date (do not change it) + $aUpdates[] = "`$sTable`.`archive_date` = $sDate"; + } + } + else + { + $sKey = MetaModel::DBGetKey($sParentClass); + $aJoins[] = "`$sTable` ON `$sTable`.`$sKey` = `$sRootTable`.`$sRootKey`"; + } + } + $sJoins = implode(' INNER JOIN ', $aJoins); + $sValues = implode(', ', $aUpdates); + $sUpdateQuery = "UPDATE $sJoins SET $sValues WHERE `$sRootTable`.`$sRootKey` = ".$this->GetKey(); + CMDBSource::Query($sUpdateQuery); + } + + /** + * Can be called to repair the database (tables consistency) + * The archive_date will be preserved + * @throws Exception + */ + public function DBArchive() + { + $this->DBWriteArchiveFlag(true); + $this->m_aCurrValues['archive_flag'] = true; + $this->m_aOrigValues['archive_flag'] = true; + } + + public function DBUnarchive() + { + $this->DBWriteArchiveFlag(false); + $this->m_aCurrValues['archive_flag'] = false; + $this->m_aOrigValues['archive_flag'] = false; + $this->m_aCurrValues['archive_date'] = null; + $this->m_aOrigValues['archive_date'] = null; + } + + + + /** + * @param string $sClass Needs to be an instanciable class + * @returns $oObj + **/ + public static function MakeDefaultInstance($sClass) + { + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + $oObj = MetaModel::NewObject($sClass); + if (!empty($sStateAttCode)) + { + $sTargetState = MetaModel::GetDefaultState($sClass); + $oObj->Set($sStateAttCode, $sTargetState); + } + return $oObj; + } + + /** + * Complete a new object with data from context + * @param array $aContextParam Context used for creation form prefilling + * + */ + public function PrefillCreationForm(&$aContextParam) + { + } + + /** + * Complete an object after a state transition with data from context + * @param array $aContextParam Context used for creation form prefilling + * + */ + public function PrefillTransitionForm(&$aContextParam) + { + } + + /** + * Complete a filter ($aContextParam['filter']) data from context + * (Called on source object) + * @param array $aContextParam Context used for creation form prefilling + * + */ + public function PrefillSearchForm(&$aContextParam) + { + } + + /** + * Prefill a creation / stimulus change / search form according to context, current state of an object, stimulus.. $sOperation + * @param string $sOperation Operation identifier + * @param array $aContextParam Context used for creation form prefilling + * + */ + public function PrefillForm($sOperation, &$aContextParam) + { + switch($sOperation){ + case 'creation_from_0': + case 'creation_from_extkey': + case 'creation_from_editinplace': + $this->PrefillCreationForm($aContextParam); + break; + case 'state_change': + $this->PrefillTransitionForm($aContextParam); + break; + case 'search': + $this->PrefillSearchForm($aContextParam); + break; + default: + break; + } + } +} + diff --git a/core/dbobjectiterator.php b/core/dbobjectiterator.php index 786c6c888..d1433a0b1 100644 --- a/core/dbobjectiterator.php +++ b/core/dbobjectiterator.php @@ -1,63 +1,63 @@ - - - -/** - * A set of persistent objects, could be heterogeneous as long as the objects in the set have a common ancestor class - * - * @package iTopORM - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ -interface iDBObjectSetIterator extends Countable -{ - /** - * The class of the objects of the collection (at least a common ancestor) - * - * @return string - */ - public function GetClass(); - - /** - * The total number of objects in the collection - * - * @return int - */ - public function Count(); - - /** - * Reset the cursor to the first item in the collection. Equivalent to Seek(0) - * - * @return DBObject The fetched object or null when at the end - */ - public function Rewind(); - - /** - * Position the cursor to the given 0-based position - * - * @param int $iRow - */ - public function Seek($iPosition); - - /** - * Fetch the object at the current position in the collection and move the cursor to the next position. - * - * @return DBObject The fetched object or null when at the end - */ - public function Fetch(); -} + + + +/** + * A set of persistent objects, could be heterogeneous as long as the objects in the set have a common ancestor class + * + * @package iTopORM + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ +interface iDBObjectSetIterator extends Countable +{ + /** + * The class of the objects of the collection (at least a common ancestor) + * + * @return string + */ + public function GetClass(); + + /** + * The total number of objects in the collection + * + * @return int + */ + public function Count(); + + /** + * Reset the cursor to the first item in the collection. Equivalent to Seek(0) + * + * @return DBObject The fetched object or null when at the end + */ + public function Rewind(); + + /** + * Position the cursor to the given 0-based position + * + * @param int $iRow + */ + public function Seek($iPosition); + + /** + * Fetch the object at the current position in the collection and move the cursor to the next position. + * + * @return DBObject The fetched object or null when at the end + */ + public function Fetch(); +} diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index ce6153aa4..39acb773c 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -1,2396 +1,2396 @@ - -// - -// Dev hack for disabling the some query build optimizations (Folding/Merging) -define('ENABLE_OPT', true); - -class DBObjectSearch extends DBSearch -{ - private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters) - private $m_aSelectedClasses; // selected for the output (alias => class name) - private $m_oSearchCondition; - private $m_aParams; - private $m_aPointingTo; - private $m_aReferencedBy; - - // By default, some information may be hidden to the current user - // But it may happen that we need to disable that feature - protected $m_bAllowAllData = false; - protected $m_bDataFiltered = false; - - public function __construct($sClass, $sClassAlias = null) - { - parent::__construct(); - - if (is_null($sClassAlias)) $sClassAlias = $sClass; - if(!is_string($sClass)) throw new Exception('DBObjectSearch::__construct called with a non-string parameter: $sClass = '.print_r($sClass, true)); - if(!MetaModel::IsValidClass($sClass)) throw new Exception('DBObjectSearch::__construct called for an invalid class: "'.$sClass.'"'); - - $this->m_aSelectedClasses = array($sClassAlias => $sClass); - $this->m_aClasses = array($sClassAlias => $sClass); - $this->m_oSearchCondition = new TrueExpression; - $this->m_aParams = array(); - $this->m_aPointingTo = array(); - $this->m_aReferencedBy = array(); - } - - public function AllowAllData($bAllowAllData = true) {$this->m_bAllowAllData = $bAllowAllData;} - public function IsAllDataAllowed() {return $this->m_bAllowAllData;} - protected function IsDataFiltered() {return $this->m_bDataFiltered; } - protected function SetDataFiltered() {$this->m_bDataFiltered = true;} - - // Create a search definition that leads to 0 result, still a valid search object - static public function FromEmptySet($sClass) - { - $oResultFilter = new DBObjectSearch($sClass); - $oResultFilter->m_oSearchCondition = new FalseExpression; - return $oResultFilter; - } - - - public function GetJoinedClasses() {return $this->m_aClasses;} - - public function GetClassName($sAlias) - { - if (array_key_exists($sAlias, $this->m_aSelectedClasses)) - { - return $this->m_aSelectedClasses[$sAlias]; - } - else - { - throw new CoreException("Invalid class alias '$sAlias'"); - } - } - - public function GetClass() - { - return reset($this->m_aSelectedClasses); - } - public function GetClassAlias() - { - reset($this->m_aSelectedClasses); - return key($this->m_aSelectedClasses); - } - - public function GetFirstJoinedClass() - { - return reset($this->m_aClasses); - } - public function GetFirstJoinedClassAlias() - { - reset($this->m_aClasses); - return key($this->m_aClasses); - } - - /** - * Change the class (only subclasses are supported as of now, because the conditions must fit the new class) - * Defaults to the first selected class (most of the time it is also the first joined class - * - * @param $sNewClass - * @param null $sAlias - * - * @throws \CoreException - */ - public function ChangeClass($sNewClass, $sAlias = null) - { - if (is_null($sAlias)) - { - $sAlias = $this->GetClassAlias(); - } - else - { - if (!array_key_exists($sAlias, $this->m_aSelectedClasses)) - { - // discard silently - necessary when recursing on the related nodes (see code below) - return; - } - } - $sCurrClass = $this->GetClassName($sAlias); - if ($sNewClass == $sCurrClass) - { - // Skip silently - return; - } - if (!MetaModel::IsParentClass($sCurrClass, $sNewClass)) - { - throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted."); - } - - // Change for this node - // - $this->m_aSelectedClasses[$sAlias] = $sNewClass; - $this->m_aClasses[$sAlias] = $sNewClass; - - // Change for all the related node (yes, this was necessary with some queries - strange effects otherwise) - // - foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oExtFilter) - { - $oExtFilter->ChangeClass($sNewClass, $sAlias); - } - } - } - foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $oForeignFilter->ChangeClass($sNewClass, $sAlias); - } - } - } - } - } - - public function GetSelectedClasses() - { - return $this->m_aSelectedClasses; - } - - /** - * @param array $aSelectedClasses array of aliases - * @throws CoreException - */ - public function SetSelectedClasses($aSelectedClasses) - { - $this->m_aSelectedClasses = array(); - foreach ($aSelectedClasses as $sAlias) - { - if (!array_key_exists($sAlias, $this->m_aClasses)) - { - throw new CoreException("SetSelectedClasses: Invalid class alias $sAlias"); - } - $this->m_aSelectedClasses[$sAlias] = $this->m_aClasses[$sAlias]; - } - } - - /** - * Change any alias of the query tree - * - * @param $sOldName - * @param $sNewName - * - * @return bool True if the alias has been found and changed - * @throws \Exception - */ - public function RenameAlias($sOldName, $sNewName) - { - $bFound = false; - if (array_key_exists($sOldName, $this->m_aClasses)) - { - $bFound = true; - } - if (array_key_exists($sNewName, $this->m_aClasses)) - { - throw new Exception("RenameAlias: alias '$sNewName' already used"); - } - - $aClasses = array(); - foreach ($this->m_aClasses as $sAlias => $sClass) - { - if ($sAlias === $sOldName) - { - $aClasses[$sNewName] = $sClass; - } - else - { - $aClasses[$sAlias] = $sClass; - } - } - $this->m_aClasses = $aClasses; - - $aSelectedClasses = array(); - foreach ($this->m_aSelectedClasses as $sAlias => $sClass) - { - if ($sAlias === $sOldName) - { - $aSelectedClasses[$sNewName] = $sClass; - } - else - { - $aSelectedClasses[$sAlias] = $sClass; - } - } - $this->m_aSelectedClasses = $aSelectedClasses; - - $this->m_oSearchCondition->RenameAlias($sOldName, $sNewName); - - foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oExtFilter) - { - $bFound = $oExtFilter->RenameAlias($sOldName, $sNewName) || $bFound; - } - } - } - foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $bFound = $oForeignFilter->RenameAlias($sOldName, $sNewName) || $bFound; - } - } - } - } - return $bFound; - } - - public function SetModifierProperty($sPluginClass, $sProperty, $value) - { - $this->m_aModifierProperties[$sPluginClass][$sProperty] = $value; - } - - public function GetModifierProperties($sPluginClass) - { - if (array_key_exists($sPluginClass, $this->m_aModifierProperties)) - { - return $this->m_aModifierProperties[$sPluginClass]; - } - else - { - return array(); - } - } - - public function IsAny() - { - if (!$this->m_oSearchCondition->IsTrue()) return false; - if (count($this->m_aPointingTo) > 0) return false; - if (count($this->m_aReferencedBy) > 0) return false; - return true; - } - - protected function TransferConditionExpression($oFilter, $aTranslation) - { - // Prevent collisions in the parameter names by renaming them if needed - foreach($this->m_aParams as $sParam => $value) - { - if (array_key_exists($sParam, $oFilter->m_aParams) && ($value != $oFilter->m_aParams[$sParam])) - { - // Generate a new and unique name for the collinding parameter - $index = 1; - while(array_key_exists($sParam.$index, $oFilter->m_aParams)) - { - $index++; - } - $secondValue = $oFilter->m_aParams[$sParam]; - $oFilter->RenameParam($sParam, $sParam.$index); - unset($oFilter->m_aParams[$sParam]); - $oFilter->m_aParams[$sParam.$index] = $secondValue; - } - } - $oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */); - $this->AddConditionExpression($oTranslated); - $this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams); - } - - protected function RenameParam($sOldName, $sNewName) - { - $this->m_oSearchCondition->RenameParam($sOldName, $sNewName); - foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oExtFilter) - { - $oExtFilter->RenameParam($sOldName, $sNewName); - } - } - } - foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $oForeignFilter->RenameParam($sOldName, $sNewName); - } - } - } - } - } - - public function ResetCondition() - { - $this->m_oSearchCondition = new TrueExpression(); - // ? is that usefull/enough, do I need to rebuild the list after the subqueries ? - } - - public function MergeConditionExpression($oExpression) - { - $this->m_oSearchCondition = $this->m_oSearchCondition->LogOr($oExpression); - } - - public function AddConditionExpression($oExpression) - { - $this->m_oSearchCondition = $this->m_oSearchCondition->LogAnd($oExpression); - } - - public function AddNameCondition($sName) - { - $oValueExpr = new ScalarExpression($sName); - $oNameExpr = new FieldExpression('friendlyname', $this->GetClassAlias()); - $oNewCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr); - $this->AddConditionExpression($oNewCondition); - } - - /** - * @param string $sFilterCode - * @param mixed $value - * @param string $sOpCode operator to use : 'IN', 'NOT IN', 'Contains',' Begins with', 'Finishes with', ... - * @param bool $bParseSearchString - * - * @throws \CoreException - * - * @see AddConditionForInOperatorUsingParam for IN/NOT IN queries with lots of params - */ - public function AddCondition($sFilterCode, $value, $sOpCode = null, $bParseSearchString = false) - { - MyHelpers::CheckKeyInArray('filter code in class: '.$this->GetClass(), $sFilterCode, MetaModel::GetClassFilterDefs($this->GetClass())); - - $oField = new FieldExpression($sFilterCode, $this->GetClassAlias()); - if (empty($sOpCode)) - { - if ($sFilterCode == 'id') - { - $sOpCode = '='; - } - else - { - $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); - $oNewCondition = $oAttDef->GetSmartConditionExpression($value, $oField, $this->m_aParams); - $this->AddConditionExpression($oNewCondition); - return; - } - } - // Parse search strings if needed and if the filter code corresponds to a valid attcode - if($bParseSearchString && MetaModel::IsValidAttCode($this->GetClass(), $sFilterCode)) - { - $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); - $value = $oAttDef->ParseSearchString($value); - } - - // Preserve backward compatibility - quick n'dirty way to change that API semantic - // - switch($sOpCode) - { - case 'SameDay': - case 'SameMonth': - case 'SameYear': - case 'Today': - case '>|': - case '<|': - case '=|': - throw new CoreException('Deprecated operator, please consider using OQL (SQL) expressions like "(TO_DAYS(NOW()) - TO_DAYS(x)) AS AgeDays"', array('operator' => $sOpCode)); - break; - - case 'IN': - if (!is_array($value)) $value = array($value); - if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.'); - $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; - $sOQLCondition = $oField->Render()." IN $sListExpr"; - break; - - case 'NOTIN': - if (!is_array($value)) $value = array($value); - if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.'); - $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; - $sOQLCondition = $oField->Render()." NOT IN $sListExpr"; - break; - - case 'Contains': - $this->m_aParams[$sFilterCode] = "%$value%"; - $sOperator = 'LIKE'; - break; - - case 'Begins with': - $this->m_aParams[$sFilterCode] = "$value%"; - $sOperator = 'LIKE'; - break; - - case 'Finishes with': - $this->m_aParams[$sFilterCode] = "%$value"; - $sOperator = 'LIKE'; - break; - - default: - if ($value === null) - { - switch ($sOpCode) - { - case '=': - $sOpCode = '*Expression*'; - $oExpression = new FunctionExpression('ISNULL', array($oField)); - break; - case '!=': - $sOpCode = '*Expression*'; - $oExpression = new FunctionExpression('ISNULL', array($oField)); - $oExpression = new BinaryExpression($oExpression, '=', new ScalarExpression(0)); - break; - default: - throw new Exception("AddCondition on null value: unsupported operator '$sOpCode''"); - } - } - else - { - $this->m_aParams[$sFilterCode] = $value; - $sOperator = $sOpCode; - } - } - - switch($sOpCode) - { - case '*Expression*': - $oNewCondition = $oExpression; - break; - case "IN": - case "NOTIN": - // this will parse all of the values... Can take forever if there are lots of them ! - // In this case using a parameter is far better : WHERE ... IN (:my_param) - $oNewCondition = Expression::FromOQL($sOQLCondition); - break; - - case 'Contains': - case 'Begins with': - case 'Finishes with': - default: - $oRightExpr = new VariableExpression($sFilterCode); - $oNewCondition = new BinaryExpression($oField, $sOperator, $oRightExpr); - } - - $this->AddConditionExpression($oNewCondition); - } - - /** - * @param string $sFilterCode attribute code to use - * @param array $aValues - * @param bool $bPositiveMatch if true will add a IN filter, else a NOT IN - * - * @throws \CoreException - * - * @since 2.5 N°1418 - */ - public function AddConditionForInOperatorUsingParam($sFilterCode, $aValues, $bPositiveMatch = true) - { - $oFieldExpression = new FieldExpression($sFilterCode, $this->GetClassAlias()); - - $sOperator = $bPositiveMatch ? 'IN' : 'NOT IN'; - - $sInParamName = $this->GenerateUniqueParamName(); - $oParamExpression = new VariableExpression($sInParamName); - $this->GetInternalParamsByRef()[$sInParamName] = $aValues; - - $oListExpression = new ListExpression(array($oParamExpression)); - - $oInCondition = new BinaryExpression($oFieldExpression, $sOperator, $oListExpression); - $this->AddConditionExpression($oInCondition); - } - - /** - * @return string a unique param name - */ - private function GenerateUniqueParamName() { - $iExistingParamsNb = count($this->m_aParams); - $iCurrentArrayParamNb = $iExistingParamsNb + 1; - $sParamName = 'param'.$iCurrentArrayParamNb; - - if (isset($this->m_aParams[$sParamName])) { - $sParamName .= '_'.microtime(true) . '_' .rand(0,100); - } - - return $sParamName; - } - - /** - * Specify a condition on external keys or link sets - * @param string $sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively - * Example: infra_list->ci_id->location_id->country - * @param $value - * @return void - * @throws \CoreException - * @throws \CoreWarning - */ - public function AddConditionAdvanced($sAttSpec, $value) - { - $sClass = $this->GetClass(); - - $iPos = strpos($sAttSpec, '->'); - if ($iPos !== false) - { - $sAttCode = substr($sAttSpec, 0, $iPos); - $sSubSpec = substr($sAttSpec, $iPos + 2); - - if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) - { - throw new Exception("Invalid attribute code '$sClass/$sAttCode' in condition specification '$sAttSpec'"); - } - - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef->IsLinkSet()) - { - $sTargetClass = $oAttDef->GetLinkedClass(); - $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); - - $oNewFilter = new DBObjectSearch($sTargetClass); - $oNewFilter->AddConditionAdvanced($sSubSpec, $value); - - $this->AddCondition_ReferencedBy($oNewFilter, $sExtKeyToMe); - } - elseif ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) - { - $sTargetClass = $oAttDef->GetTargetClass(EXTKEY_ABSOLUTE); - - $oNewFilter = new DBObjectSearch($sTargetClass); - $oNewFilter->AddConditionAdvanced($sSubSpec, $value); - - $this->AddCondition_PointingTo($oNewFilter, $sAttCode); - } - else - { - throw new Exception("Attribute specification '$sAttSpec', '$sAttCode' should be either a link set or an external key"); - } - } - else - { - // $sAttSpec is an attribute code - // - if (is_array($value)) - { - $oField = new FieldExpression($sAttSpec, $this->GetClass()); - $oListExpr = ListExpression::FromScalars($value); - $oInValues = new BinaryExpression($oField, 'IN', $oListExpr); - - $this->AddConditionExpression($oInValues); - } - else - { - $this->AddCondition($sAttSpec, $value); - } - } - } - - public function AddCondition_FullText($sNeedle) - { - // Transform the full text condition into additional condition expression - $aFullTextFields = array(); - foreach (MetaModel::ListAttributeDefs($this->GetClass()) as $sAttCode => $oAttDef) - { - if (!$oAttDef->IsScalar()) continue; - if ($oAttDef->IsExternalKey()) continue; - $aFullTextFields[] = new FieldExpression($sAttCode, $this->GetClassAlias()); - } - $oTextFields = new CharConcatWSExpression(' ', $aFullTextFields); - - $sQueryParam = 'needle'; - $oFlexNeedle = new CharConcatExpression(array(new ScalarExpression('%'), new VariableExpression($sQueryParam), new ScalarExpression('%'))); - - $oNewCond = new BinaryExpression($oTextFields, 'LIKE', $oFlexNeedle); - $this->AddConditionExpression($oNewCond); - $this->m_aParams[$sQueryParam] = $sNeedle; - } - - protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true) - { - if ($bTranslateMainAlias) - { - $sOrigAlias = $this->GetFirstJoinedClassAlias(); - if (array_key_exists($sOrigAlias, $aClassAliases)) - { - $sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetFirstJoinedClass()); - if (isset($this->m_aSelectedClasses[$sOrigAlias])) - { - $this->m_aSelectedClasses[$sNewAlias] = $this->GetFirstJoinedClass(); - unset($this->m_aSelectedClasses[$sOrigAlias]); - } - - // TEMPORARY ALGORITHM (m_aClasses is not correctly updated, it is not possible to add a subtree onto a subnode) - // Replace the element at the same position (unset + set is not enough because the hash array is ordered) - $aPrevList = $this->m_aClasses; - $this->m_aClasses = array(); - foreach ($aPrevList as $sSomeAlias => $sSomeClass) - { - if ($sSomeAlias == $sOrigAlias) - { - $this->m_aClasses[$sNewAlias] = $sSomeClass; // note: GetFirstJoinedClass now returns '' !!! - } - else - { - $this->m_aClasses[$sSomeAlias] = $sSomeClass; - } - } - - // Translate the condition expression with the new alias - $aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias; - } - - // add the alias into the filter aliases list - $aClassAliases[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass(); - } - - foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oFilter) - { - $oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); - } - } - } - - foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); - } - } - } - } - } - - - // Browse the tree nodes recursively - // - protected function GetNode($sAlias) - { - if ($this->GetFirstJoinedClassAlias() == $sAlias) - { - return $this; - } - else - { - foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oFilter) - { - $ret = $oFilter->GetNode($sAlias); - if (is_object($ret)) - { - return $ret; - } - } - } - } - foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $ret = $oForeignFilter->GetNode($sAlias); - if (is_object($ret)) - { - return $ret; - } - } - } - } - } - } - // Not found - return null; - } - - /** - * Helper to - * - convert a translation table (format optimized for the translation in an expression tree) into simple hash - * - compile over an eventually existing map - * - * @param array $aRealiasingMap Map to update - * @param array $aAliasTranslation Translation table resulting from calls to MergeWith_InNamespace - * @return void of => - */ - protected function UpdateRealiasingMap(&$aRealiasingMap, $aAliasTranslation) - { - if ($aRealiasingMap !== null) - { - foreach ($aAliasTranslation as $sPrevAlias => $aRules) - { - if (isset($aRules['*'])) - { - $sNewAlias = $aRules['*']; - $sOriginalAlias = array_search($sPrevAlias, $aRealiasingMap); - if ($sOriginalAlias !== false) - { - $aRealiasingMap[$sOriginalAlias] = $sNewAlias; - } - else - { - $aRealiasingMap[$sPrevAlias] = $sNewAlias; - } - } - } - } - } - - /** - * Completes the list of alias=>class by browsing the whole structure recursively - * This a workaround to handle some cases in which the list of classes is not correctly updated. - * This code should disappear as soon as DBObjectSearch get split between a container search class and a Node class - * - * @param array $aClasses List to be completed - */ - protected function RecomputeClassList(&$aClasses) - { - $aClasses[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass(); - - // Recurse in the query tree - foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oFilter) - { - $oFilter->RecomputeClassList($aClasses); - } - } - } - - foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $oForeignFilter->RecomputeClassList($aClasses); - } - } - } - } - } - - /** - * @param DBObjectSearch $oFilter - * @param $sExtKeyAttCode - * @param int $iOperatorCode - * @param null $aRealiasingMap array of => , for each alias that has changed - * @throws CoreException - * @throws CoreWarning - */ - public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) - { - if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode)) - { - throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}'"); - } - $oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode); - if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass())) - { - throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); - } - if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey)) - { - throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey"); - } - // Note: though it seems to be a good practice to clone the given source filter - // (as it was done and fixed an issue in Intersect()) - // this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge) - // root cause: FromOQL relies on the fact that the passed filter can be modified later - // NO: $oFilter = $oFilter->DeepClone(); - // See also: Trac #639, and self::AddCondition_ReferencedBy() - $aAliasTranslation = array(); - $res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode); - $this->TransferConditionExpression($oFilter, $aAliasTranslation); - $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); - - if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass())) - { - if (isset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode])) - { - foreach ($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode] as $oRemoteFilter) - { - if ($this->GetClass() == $oRemoteFilter->GetClass()) - { - // Optimization - fold sibling query - $aAliasTranslation = array(); - $this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation); - unset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode]); - $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false); - $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); - break; - } - } - } - } - $this->RecomputeClassList($this->m_aClasses); - return $res; - } - - protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode) - { - // Find the node on which the new tree must be attached (most of the time it is "this") - $oReceivingFilter = $this->GetNode($this->GetClassAlias()); - - $bMerged = false; - if (ENABLE_OPT && isset($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode])) - { - foreach ($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] as $oExisting) - { - if ($oExisting->GetClass() == $oFilter->GetClass()) - { - $oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation); - $bMerged = true; - break; - } - } - } - if (!$bMerged) - { - $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); - $oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][] = $oFilter; - } - } - - /** - * @param DBObjectSearch $oFilter - * @param $sForeignExtKeyAttCode - * @param int $iOperatorCode - * @param null $aRealiasingMap array of => , for each alias that has changed - * @return void - * @throws \CoreException - */ - public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) - { - $sForeignClass = $oFilter->GetClass(); - if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode)) - { - throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}'"); - } - $oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); - if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass())) - { - // à refaire en spécifique dans FromOQL - throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); - } - - // Note: though it seems to be a good practice to clone the given source filter - // (as it was done and fixed an issue in Intersect()) - // this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge) - // root cause: FromOQL relies on the fact that the passed filter can be modified later - // NO: $oFilter = $oFilter->DeepClone(); - // See also: Trac #639, and self::AddCondition_PointingTo() - $aAliasTranslation = array(); - $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode); - $this->TransferConditionExpression($oFilter, $aAliasTranslation); - $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); - - if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass())) - { - if (isset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode])) - { - foreach ($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode] as $oRemoteFilter) - { - if ($this->GetClass() == $oRemoteFilter->GetClass()) - { - // Optimization - fold sibling query - $aAliasTranslation = array(); - $this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation); - unset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode]); - $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false); - $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); - break; - } - } - } - } - $this->RecomputeClassList($this->m_aClasses); - } - - protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode) - { - $sForeignClass = $oFilter->GetClass(); - - // Find the node on which the new tree must be attached (most of the time it is "this") - $oReceivingFilter = $this->GetNode($this->GetClassAlias()); - - $bMerged = false; - if (ENABLE_OPT && isset($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode])) - { - foreach ($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode] as $oExisting) - { - if ($oExisting->GetClass() == $oFilter->GetClass()) - { - $oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation); - $bMerged = true; - break; - } - } - } - if (!$bMerged) - { - $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); - $oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode][] = $oFilter; - } - } - - public function Intersect(DBSearch $oFilter) - { - if ($oFilter instanceof DBUnionSearch) - { - // Develop! - $aFilters = $oFilter->GetSearches(); - } - else - { - $aFilters = array($oFilter); - } - - $aSearches = array(); - foreach ($aFilters as $oRightFilter) - { - // Limitation: the queried class must be the first declared class - if ($this->GetFirstJoinedClassAlias() != $this->GetClassAlias()) - { - throw new CoreException("Limitation: cannot merge two queries if the queried class ({$this->GetClass()} AS {$this->GetClassAlias()}) is not the first joined class ({$this->GetFirstJoinedClass()} AS {$this->GetFirstJoinedClassAlias()})"); - } - if ($oRightFilter->GetFirstJoinedClassAlias() != $oRightFilter->GetClassAlias()) - { - throw new CoreException("Limitation: cannot merge two queries if the queried class ({$oRightFilter->GetClass()} AS {$oRightFilter->GetClassAlias()}) is not the first joined class ({$oRightFilter->GetFirstJoinedClass()} AS {$oRightFilter->GetFirstJoinedClassAlias()})"); - } - - $oLeftFilter = $this->DeepClone(); - $oRightFilter = $oRightFilter->DeepClone(); - - $bAllowAllData = ($oLeftFilter->IsAllDataAllowed() && $oRightFilter->IsAllDataAllowed()); - if ($bAllowAllData) - { - $oLeftFilter->AllowAllData(); - } - - if ($oLeftFilter->GetClass() != $oRightFilter->GetClass()) - { - if (MetaModel::IsParentClass($oLeftFilter->GetClass(), $oRightFilter->GetClass())) - { - // Specialize $oLeftFilter - $oLeftFilter->ChangeClass($oRightFilter->GetClass()); - } - elseif (MetaModel::IsParentClass($oRightFilter->GetClass(), $oLeftFilter->GetClass())) - { - // Specialize $oRightFilter - $oRightFilter->ChangeClass($oLeftFilter->GetClass()); - } - else - { - throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'"); - } - } - - $aAliasTranslation = array(); - $oLeftFilter->MergeWith_InNamespace($oRightFilter, $oLeftFilter->m_aClasses, $aAliasTranslation); - $oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation); - $aSearches[] = $oLeftFilter; - } - if (count($aSearches) == 1) - { - // return a DBObjectSearch - return $aSearches[0]; - } - else - { - return new DBUnionSearch($aSearches); - } - } - - protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation) - { - if ($this->GetClass() != $oFilter->GetClass()) - { - throw new CoreException("Attempting to merge a filter of class '{$this->GetClass()}' with a filter of class '{$oFilter->GetClass()}'"); - } - - // Translate search condition into our aliasing scheme - $aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias(); - - foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oExtFilter) - { - $this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); - } - } - } - foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); - } - } - } - } - } - - public function GetCriteria() {return $this->m_oSearchCondition;} - public function GetCriteria_FullText() {throw new Exception("Removed GetCriteria_FullText");} - public function GetCriteria_PointingTo($sKeyAttCode = "") - { - if (empty($sKeyAttCode)) - { - return $this->m_aPointingTo; - } - if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array(); - return $this->m_aPointingTo[$sKeyAttCode]; - } - protected function GetCriteria_ReferencedBy() - { - return $this->m_aReferencedBy; - } - - public function SetInternalParams($aParams) - { - return $this->m_aParams = $aParams; - } - - /** - * @return array warning : array returned by value - * @see self::GetInternalParamsByRef to get the attribute by reference - */ - public function GetInternalParams() - { - return $this->m_aParams; - } - - /** - * @return array - * @see http://php.net/manual/en/language.references.return.php - * @since 2.5.1 N°1582 - */ - public function &GetInternalParamsByRef() - { - return $this->m_aParams; - } - - /** - * @param string $sKey - * @param mixed $value - * @param bool $bDoNotOverride - * - * @throws \CoreUnexpectedValue if $bDoNotOverride and $sKey already exists - */ - public function AddInternalParam($sKey, $value, $bDoNotOverride = false) - { - if ($bDoNotOverride) - { - if (array_key_exists($sKey, $this->m_aParams)) - { - throw new CoreUnexpectedValue("The key $sKey already exists with value : ".$this->m_aParams[$sKey]); - } - } - - $this->m_aParams[$sKey] = $value; - } - - public function GetQueryParams($bExcludeMagicParams = true) - { - $aParams = array(); - $this->m_oSearchCondition->Render($aParams, true); - - if ($bExcludeMagicParams) - { - $aRet = array(); - - // Make the list of acceptable arguments... could be factorized with run_query, into oSearch->GetQueryParams($bExclude magic params) - $aNakedMagicArguments = array(); - foreach (MetaModel::PrepareQueryArguments(array()) as $sArgName => $value) - { - $iPos = strpos($sArgName, '->object()'); - if ($iPos === false) - { - $aNakedMagicArguments[$sArgName] = $value; - } - else - { - $aNakedMagicArguments[substr($sArgName, 0, $iPos)] = true; - } - } - foreach ($aParams as $sParam => $foo) - { - $iPos = strpos($sParam, '->'); - if ($iPos === false) - { - $sRefName = $sParam; - } - else - { - $sRefName = substr($sParam, 0, $iPos); - } - if (!array_key_exists($sRefName, $aNakedMagicArguments)) - { - $aRet[$sParam] = $foo; - } - } - } - - return $aRet; - } - - public function ListConstantFields() - { - return $this->m_oSearchCondition->ListConstantFields(); - } - - /** - * Turn the parameters (:xxx) into scalar values in order to easily - * serialize a search - * @param $aArgs -*/ - public function ApplyParameters($aArgs) - { - $this->m_oSearchCondition->ApplyParameters(array_merge($this->m_aParams, $aArgs)); - } - - public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false) - { - // Currently unused, but could be useful later - $bRetrofitParams = false; - - if ($bDevelopParams) - { - if (is_null($aContextParams)) - { - $aParams = array_merge($this->m_aParams); - } - else - { - $aParams = array_merge($aContextParams, $this->m_aParams); - } - $aParams = MetaModel::PrepareQueryArguments($aParams); - } - else - { - // Leave it as is, the rendering will be made with parameters in clear - $aParams = null; - } - - $aSelectedAliases = array(); - foreach ($this->m_aSelectedClasses as $sAlias => $sClass) - { - $aSelectedAliases[] = '`' . $sAlias . '`'; - } - $sSelectedClasses = implode(', ', $aSelectedAliases); - $sRes = 'SELECT '.$sSelectedClasses.' FROM'; - - $sRes .= ' ' . $this->GetFirstJoinedClass() . ' AS `' . $this->GetFirstJoinedClassAlias() . '`'; - $sRes .= $this->ToOQL_Joins(); - $sRes .= " WHERE ".$this->m_oSearchCondition->Render($aParams, $bRetrofitParams); - - if ($bWithAllowAllFlag && $this->m_bAllowAllData) - { - $sRes .= " ALLOW ALL DATA"; - } - return $sRes; - } - - protected function OperatorCodeToOQL($iOperatorCode) - { - switch($iOperatorCode) - { - case TREE_OPERATOR_EQUALS: - $sOperator = ' = '; - break; - - case TREE_OPERATOR_BELOW: - $sOperator = ' BELOW '; - break; - - case TREE_OPERATOR_BELOW_STRICT: - $sOperator = ' BELOW STRICT '; - break; - - case TREE_OPERATOR_NOT_BELOW: - $sOperator = ' NOT BELOW '; - break; - - case TREE_OPERATOR_NOT_BELOW_STRICT: - $sOperator = ' NOT BELOW STRICT '; - break; - - case TREE_OPERATOR_ABOVE: - $sOperator = ' ABOVE '; - break; - - case TREE_OPERATOR_ABOVE_STRICT: - $sOperator = ' ABOVE STRICT '; - break; - - case TREE_OPERATOR_NOT_ABOVE: - $sOperator = ' NOT ABOVE '; - break; - - case TREE_OPERATOR_NOT_ABOVE_STRICT: - $sOperator = ' NOT ABOVE STRICT '; - break; - - } - return $sOperator; - } - - protected function ToOQL_Joins() - { - $sRes = ''; - foreach($this->m_aPointingTo as $sExtKey => $aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - $sOperator = $this->OperatorCodeToOQL($iOperatorCode); - foreach($aFilter as $oFilter) - { - $sRes .= ' JOIN ' . $oFilter->GetFirstJoinedClass() . ' AS `' . $oFilter->GetFirstJoinedClassAlias() . '` ON `' . $this->GetFirstJoinedClassAlias() . '`.' . $sExtKey . $sOperator . '`' . $oFilter->GetFirstJoinedClassAlias() . '`.id'; - $sRes .= $oFilter->ToOQL_Joins(); - } - } - } - foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - $sOperator = $this->OperatorCodeToOQL($iOperatorCode); - foreach ($aFilters as $oForeignFilter) - { - $sRes .= ' JOIN ' . $oForeignFilter->GetFirstJoinedClass() . ' AS `' . $oForeignFilter->GetFirstJoinedClassAlias() . '` ON `' . $oForeignFilter->GetFirstJoinedClassAlias() . '`.' . $sForeignExtKeyAttCode . $sOperator . '`' . $this->GetFirstJoinedClassAlias() . '`.id'; - $sRes .= $oForeignFilter->ToOQL_Joins(); - } - } - } - } - return $sRes; - } - - protected function OQLExpressionToCondition($sQuery, $oExpression, $aClassAliases) - { - if ($oExpression instanceof BinaryOqlExpression) - { - $sOperator = $oExpression->GetOperator(); - $oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases); - $oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases); - return new BinaryExpression($oLeft, $sOperator, $oRight); - } - elseif ($oExpression instanceof FieldOqlExpression) - { - $sClassAlias = $oExpression->GetParent(); - $sFltCode = $oExpression->GetName(); - if (empty($sClassAlias)) - { - // Need to find the right alias - // Build an array of field => array of aliases - $aFieldClasses = array(); - foreach($aClassAliases as $sAlias => $sReal) - { - foreach(MetaModel::GetFiltersList($sReal) as $sAnFltCode) - { - $aFieldClasses[$sAnFltCode][] = $sAlias; - } - } - $sClassAlias = $aFieldClasses[$sFltCode][0]; - } - return new FieldExpression($sFltCode, $sClassAlias); - } - elseif ($oExpression instanceof VariableOqlExpression) - { - return new VariableExpression($oExpression->GetName()); - } - elseif ($oExpression instanceof TrueOqlExpression) - { - return new TrueExpression; - } - elseif ($oExpression instanceof ScalarOqlExpression) - { - return new ScalarExpression($oExpression->GetValue()); - } - elseif ($oExpression instanceof ListOqlExpression) - { - $aItems = array(); - foreach ($oExpression->GetItems() as $oItemExpression) - { - $aItems[] = $this->OQLExpressionToCondition($sQuery, $oItemExpression, $aClassAliases); - } - return new ListExpression($aItems); - } - elseif ($oExpression instanceof FunctionOqlExpression) - { - $aArgs = array(); - foreach ($oExpression->GetArgs() as $oArgExpression) - { - $aArgs[] = $this->OQLExpressionToCondition($sQuery, $oArgExpression, $aClassAliases); - } - return new FunctionExpression($oExpression->GetVerb(), $aArgs); - } - elseif ($oExpression instanceof IntervalOqlExpression) - { - return new IntervalExpression($oExpression->GetValue(), $oExpression->GetUnit()); - } - else - { - throw new CoreException('Unknown expression type', array('class'=>get_class($oExpression), 'query'=>$sQuery)); - } - } - - public function InitFromOqlQuery(OqlQuery $oOqlQuery, $sQuery) - { - $oModelReflection = new ModelReflectionRuntime(); - $sClass = $oOqlQuery->GetClass($oModelReflection); - $sClassAlias = $oOqlQuery->GetClassAlias(); - - $aAliases = array($sClassAlias => $sClass); - - // Note: the condition must be built here, it may be altered later on when optimizing some joins - $oConditionTree = $oOqlQuery->GetCondition(); - if ($oConditionTree instanceof Expression) - { - $aRawAliases = array($sClassAlias => $sClass); - $aJoinSpecs = $oOqlQuery->GetJoins(); - if (is_array($aJoinSpecs)) - { - foreach ($aJoinSpecs as $oJoinSpec) - { - $aRawAliases[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass(); - } - } - $this->m_oSearchCondition = $this->OQLExpressionToCondition($sQuery, $oConditionTree, $aRawAliases); - } - - // Maintain an array of filters, because the flat list is in fact referring to a tree - // And this will be an easy way to dispatch the conditions - // $this will be referenced by the other filters, or the other way around... - $aJoinItems = array($sClassAlias => $this); - - $aJoinSpecs = $oOqlQuery->GetJoins(); - if (is_array($aJoinSpecs)) - { - $aAliasTranslation = array(); - foreach ($aJoinSpecs as $oJoinSpec) - { - $sJoinClass = $oJoinSpec->GetClass(); - $sJoinClassAlias = $oJoinSpec->GetClassAlias(); - if (isset($aAliasTranslation[$sJoinClassAlias]['*'])) - { - $sJoinClassAlias = $aAliasTranslation[$sJoinClassAlias]['*']; - } - - // Assumption: ext key on the left only !!! - // normalization should take care of this - $oLeftField = $oJoinSpec->GetLeftField(); - $sFromClass = $oLeftField->GetParent(); - if (isset($aAliasTranslation[$sFromClass]['*'])) - { - $sFromClass = $aAliasTranslation[$sFromClass]['*']; - } - $sExtKeyAttCode = $oLeftField->GetName(); - - $oRightField = $oJoinSpec->GetRightField(); - $sToClass = $oRightField->GetParent(); - if (isset($aAliasTranslation[$sToClass]['*'])) - { - $sToClass = $aAliasTranslation[$sToClass]['*']; - } - - $aAliases[$sJoinClassAlias] = $sJoinClass; - $aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias); - - $sOperator = $oJoinSpec->GetOperator(); - switch($sOperator) - { - case '=': - default: - $iOperatorCode = TREE_OPERATOR_EQUALS; - break; - case 'BELOW': - $iOperatorCode = TREE_OPERATOR_BELOW; - break; - case 'BELOW_STRICT': - $iOperatorCode = TREE_OPERATOR_BELOW_STRICT; - break; - case 'NOT_BELOW': - $iOperatorCode = TREE_OPERATOR_NOT_BELOW; - break; - case 'NOT_BELOW_STRICT': - $iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT; - break; - case 'ABOVE': - $iOperatorCode = TREE_OPERATOR_ABOVE; - break; - case 'ABOVE_STRICT': - $iOperatorCode = TREE_OPERATOR_ABOVE_STRICT; - break; - case 'NOT_ABOVE': - $iOperatorCode = TREE_OPERATOR_NOT_ABOVE; - break; - case 'NOT_ABOVE_STRICT': - $iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT; - break; - } - - if ($sFromClass == $sJoinClassAlias) - { - $oReceiver = $aJoinItems[$sToClass]; - $oNewComer = $aJoinItems[$sFromClass]; - $oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode); - } - else - { - $oReceiver = $aJoinItems[$sFromClass]; - $oNewComer = $aJoinItems[$sToClass]; - $oReceiver->AddCondition_PointingTo_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode); - } - } - $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false /* leave unresolved fields */); - } - - // Check and prepare the select information - $this->m_aSelectedClasses = array(); - foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails) - { - $sClassToSelect = $oClassDetails->GetValue(); - $this->m_aSelectedClasses[$sClassToSelect] = $aAliases[$sClassToSelect]; - } - $this->m_aClasses = $aAliases; - } - - //////////////////////////////////////////////////////////////////////////// - // - // Construction of the SQL queries - // - //////////////////////////////////////////////////////////////////////////// - - public function MakeDeleteQuery($aArgs = array()) - { - $aModifierProperties = MetaModel::MakeModifierProperties($this); - $oBuild = new QueryBuilderContext($this, $aModifierProperties); - $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => array()), array()); - $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); - $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); - $oSQLQuery->OptimizeJoins(array()); - $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); - $sRet = $oSQLQuery->RenderDelete($aScalarArgs); - return $sRet; - } - - public function MakeUpdateQuery($aValues, $aArgs = array()) - { - // $aValues is an array of $sAttCode => $value - $aModifierProperties = MetaModel::MakeModifierProperties($this); - $oBuild = new QueryBuilderContext($this, $aModifierProperties); - $aRequested = array(); // Requested attributes are the updated attributes - foreach ($aValues as $sAttCode => $value) - { - $aRequested[$sAttCode] = MetaModel::GetAttributeDef($this->GetClass(), $sAttCode); - } - $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => $aRequested), $aValues); - $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); - $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); - $oSQLQuery->OptimizeJoins(array()); - $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); - $sRet = $oSQLQuery->RenderUpdate($aScalarArgs); - return $sRet; - } - - public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) - { - // Hide objects that are not visible to the current user - // - $oSearch = $this; - if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered()) - { - $oVisibleObjects = UserRights::GetSelectFilter($this->GetClass(), $this->GetModifierProperties('UserRightsGetSelectFilter')); - if ($oVisibleObjects === false) - { - // Make sure this is a valid search object, saying NO for all - $oVisibleObjects = DBObjectSearch::FromEmptySet($this->GetClass()); - } - if (is_object($oVisibleObjects)) - { - $oVisibleObjects->AllowAllData(); - $oSearch = $this->Intersect($oVisibleObjects); - $oSearch->SetDataFiltered(); - } - } - - // Compute query modifiers properties (can be set in the search itself, by the context, etc.) - // - $aModifierProperties = MetaModel::MakeModifierProperties($oSearch); - - // Create a unique cache id - // - $aContextData = array(); - $bCanCache = true; - if (self::$m_bQueryCacheEnabled || self::$m_bTraceQueries) - { - if (isset($_SERVER['REQUEST_URI'])) - { - $aContextData['sRequestUri'] = $_SERVER['REQUEST_URI']; - } - else if (isset($_SERVER['SCRIPT_NAME'])) - { - $aContextData['sRequestUri'] = $_SERVER['SCRIPT_NAME']; - } - else - { - $aContextData['sRequestUri'] = ''; - } - - // Need to identify the query - $sOqlQuery = $oSearch->ToOql(false, null, true); - if ((strpos($sOqlQuery, '`id` IN (') !== false) || (strpos($sOqlQuery, '`id` NOT IN (') !== false)) - { - // Requests containing "id IN" are not worth caching - $bCanCache = false; - } - - $aContextData['sOqlQuery'] = $sOqlQuery; - - if (count($aModifierProperties)) - { - array_multisort($aModifierProperties); - $sModifierProperties = json_encode($aModifierProperties); - } - else - { - $sModifierProperties = ''; - } - $aContextData['sModifierProperties'] = $sModifierProperties; - - $sRawId = $sOqlQuery.$sModifierProperties; - if (!is_null($aAttToLoad)) - { - $sRawId .= json_encode($aAttToLoad); - } - $aContextData['aAttToLoad'] = $aAttToLoad; - if (!is_null($aGroupByExpr)) - { - foreach($aGroupByExpr as $sAlias => $oExpr) - { - $sRawId .= 'g:'.$sAlias.'!'.$oExpr->Render(); - } - } - if (!is_null($aSelectExpr)) - { - foreach($aSelectExpr as $sAlias => $oExpr) - { - $sRawId .= 'se:'.$sAlias.'!'.$oExpr->Render(); - } - } - $aContextData['aGroupByExpr'] = $aGroupByExpr; - $aContextData['aSelectExpr'] = $aSelectExpr; - $sRawId .= $bGetCount; - $aContextData['bGetCount'] = $bGetCount; - if (is_array($aSelectedClasses)) - { - $sRawId .= implode(',', $aSelectedClasses); // Unions may alter the list of selected columns - } - $aContextData['aSelectedClasses'] = $aSelectedClasses; - $bIsArchiveMode = $oSearch->GetArchiveMode(); - $sRawId .= $bIsArchiveMode ? '--arch' : ''; - $bShowObsoleteData = $oSearch->GetShowObsoleteData(); - $sRawId .= $bShowObsoleteData ? '--obso' : ''; - $aContextData['bIsArchiveMode'] = $bIsArchiveMode; - $aContextData['bShowObsoleteData'] = $bShowObsoleteData; - $sOqlId = md5($sRawId); - } - else - { - $sOqlQuery = "SELECTING... ".$oSearch->GetClass(); - $sOqlId = "query id ? n/a"; - } - - - // Query caching - // - $sOqlAPCCacheId = null; - if (self::$m_bQueryCacheEnabled) - { - // Warning: using directly the query string as the key to the hash array can FAIL if the string - // is long and the differences are only near the end... so it's safer (but not bullet proof?) - // to use a hash (like md5) of the string as the key ! - // - // Example of two queries that were found as similar by the hash array: - // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTO' AND CustomerContract.customer_id = 2 - // and - // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTR' AND CustomerContract.customer_id = 2 - // the only difference is R instead or O at position 285 (TTR instead of TTO)... - // - if (array_key_exists($sOqlId, self::$m_aQueryStructCache)) - { - // hit! - - $oSQLQuery = unserialize(serialize(self::$m_aQueryStructCache[$sOqlId])); - // Note: cloning is not enough because the subtree is made of objects - } - elseif (self::$m_bUseAPCCache) - { - // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter - // - $sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-query-cache-'.$sOqlId; - $oKPI = new ExecutionKPI(); - $result = apc_fetch($sOqlAPCCacheId); - $oKPI->ComputeStats('Query APC (fetch)', $sOqlQuery); - - if (is_object($result)) - { - $oSQLQuery = $result; - self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery; - } - } - } - - if (!isset($oSQLQuery)) - { - $oKPI = new ExecutionKPI(); - $oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); - $oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery); - - if (self::$m_bQueryCacheEnabled) - { - if ($bCanCache && self::$m_bUseAPCCache) - { - $oSQLQuery->m_aContextData = $aContextData; - $oKPI = new ExecutionKPI(); - apc_store($sOqlAPCCacheId, $oSQLQuery, self::$m_iQueryCacheTTL); - $oKPI->ComputeStats('Query APC (store)', $sOqlQuery); - } - - self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery->DeepClone(); - } - } - return $oSQLQuery; - } - - /** - * @param array $aAttToLoad - * @param bool $bGetCount - * @param array $aModifierProperties - * @param array $aGroupByExpr - * @param array $aSelectedClasses - * @param array $aSelectExpr - * - * @return null|SQLObjectQuery - * @throws \CoreException - */ - protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) - { - $oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); - - $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array()); - $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); - if (is_array($aGroupByExpr)) - { - $aCols = $oBuild->m_oQBExpressions->GetGroupBy(); - $oSQLQuery->SetGroupBy($aCols); - $oSQLQuery->SetSelect($aCols); - } - else - { - $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); - } - if ($aSelectExpr) - { - // Get the fields corresponding to the select expressions - foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr) - { - if (key_exists($sAlias, $aSelectExpr)) - { - $oSQLQuery->AddSelect($sAlias, $oExpr); - } - } - } - - $aMandatoryTables = null; - if (self::$m_bOptimizeQueries) - { - if ($bGetCount) - { - // Simplify the query if just getting the count - $oSQLQuery->SetSelect(array()); - } - $oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables); - $oSQLQuery->OptimizeJoins($aMandatoryTables); - } - // Filter tables as late as possible: do not interfere with the optimization process - foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions) - { - if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables)) - { - foreach ($aConditions as $oCondition) - { - $oSQLQuery->AddCondition($oCondition); - } - } - } - - return $oSQLQuery; - } - - - /** - * @param $oBuild - * @param null $aAttToLoad - * @param array $aValues - * @return null|SQLObjectQuery - * @throws \CoreException - */ - protected function MakeSQLObjectQuery(&$oBuild, $aAttToLoad = null, $aValues = array()) - { - // Note: query class might be different than the class of the filter - // -> this occurs when we are linking our class to an external class (referenced by, or pointing to) - $sClass = $this->GetFirstJoinedClass(); - $sClassAlias = $this->GetFirstJoinedClassAlias(); - - $bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses()); - - //self::DbgTrace("Entering: ".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY")); - - //$sRootClass = MetaModel::GetRootClass($sClass); - $sKeyField = MetaModel::DBGetKey($sClass); - - if ($bIsOnQueriedClass) - { - // default to the whole list of attributes + the very std id/finalclass - $oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias)); - if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad)) - { - $sSelectedClass = $oBuild->GetSelectedClass($sClassAlias); - $aAttList = MetaModel::ListAttributeDefs($sSelectedClass); - } - else - { - $aAttList = $aAttToLoad[$sClassAlias]; - } - foreach ($aAttList as $sAttCode => $oAttDef) - { - if (!$oAttDef->IsScalar()) continue; - // keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue; - - if ($oAttDef->IsBasedOnOQLExpression()) - { - $oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode, new FieldExpression($sAttCode, $sClassAlias)); - } - else - { - foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) - { - $oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias)); - } - } - } - } - //echo "

    oQBExpr ".__LINE__.":

    \n".print_r($oBuild->m_oQBExpressions, true)."

    \n"; - $aExpectedAtts = array(); // array of (attcode => fieldexpression) - //echo "

    ".__LINE__.": GetUnresolvedFields($sClassAlias, ...)

    \n"; - $oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts); - - // Compute a clear view of required joins (from the current class) - // Build the list of external keys: - // -> ext keys required by an explicit join - // -> ext keys mentionned in a 'pointing to' condition - // -> ext keys required for an external field - // -> ext keys required for a friendly name - // - $aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef)) - // - // Optimization: could be partially computed once for all (cached) ? - // - - if ($bIsOnQueriedClass) - { - // Get all Ext keys for the queried class (??) - foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode) - { - $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); - $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); - } - } - // Get all Ext keys used by the filter - foreach ($this->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo) - { - if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo)) - { - $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); - $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); - } - } - - $aFNJoinAlias = array(); // array of (subclass => alias) - foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression) - { - if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue; - $oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode); - if ($oAttDef->IsBasedOnOQLExpression()) - { - // To optimize: detect a restriction on child classes in the condition expression - // e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine') - $oExpression = static::GetPolymorphicExpression($sClass, $sExpectedAttCode); - - $aRequiredFields = array(); - $oExpression->GetUnresolvedFields('', $aRequiredFields); - $aTranslateFields = array(); - foreach($aRequiredFields as $sSubClass => $aFields) - { - foreach($aFields as $sAttCode => $oField) - { - $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); - if ($oAttDef->IsExternalKey()) - { - $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); - $aExtKeys[$sClassOfAttribute][$sAttCode] = array(); - } - elseif ($oAttDef->IsExternalField()) - { - $sKeyAttCode = $oAttDef->GetKeyAttCode(); - $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode); - $aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef; - } - else - { - $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); - } - - if (MetaModel::IsParentClass($sClassOfAttribute, $sClass)) - { - // The attribute is part of the standard query - // - $sAliasForAttribute = $sClassAlias; - } - else - { - // The attribute will be available from an additional outer join - // For each subclass (table) one single join is enough - // - if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias)) - { - $sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute); - $aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute; - } - else - { - $sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute]; - } - } - - $aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute); - } - } - $oExpression = $oExpression->Translate($aTranslateFields, false); - - $aTranslateNow = array(); - $aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression; - $oBuild->m_oQBExpressions->Translate($aTranslateNow, false); - } - } - - // Add the ext fields used in the select (eventually adds an external key) - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) - { - if ($oAttDef->IsExternalField()) - { - if (array_key_exists($sAttCode, $aExpectedAtts)) - { - // Add the external attribute - $sKeyAttCode = $oAttDef->GetKeyAttCode(); - $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); - $aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef; - } - } - } - - // First query built from the root, adding all tables including the leaf - // Before N.1065 we were joining from the leaf first, but this wasn't a good choice : - // most of the time (obsolescence, friendlyname, ...) we want to get a root attribute ! - // - $oSelectBase = null; - $aClassHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, true); - $bIsClassStandaloneClass = (count($aClassHierarchy) == 1); - foreach($aClassHierarchy as $sSomeClass) - { - if (!MetaModel::HasTable($sSomeClass)) - { - continue; - } - - self::DbgTrace("Adding join from root to leaf: $sSomeClass... let's call MakeSQLObjectQuerySingleTable()"); - $oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSomeClass, $aExtKeys, $aValues); - if (is_null($oSelectBase)) - { - $oSelectBase = $oSelectParentTable; - if (!$bIsClassStandaloneClass && (MetaModel::IsRootClass($sSomeClass))) - { - // As we're linking to root class first, we're adding a where clause on the finalClass attribute : - // COALESCE($sRootClassFinalClass IN ('$sExpectedClasses'), 1) - // If we don't, the child classes can be removed in the query optimisation phase, including the leaf that was queried - // So we still need to filter records to only those corresponding to the child classes ! - // The coalesce is mandatory if we have a polymorphic query (left join) - $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); - $sFinalClassSqlColumnName = MetaModel::DBGetClassField($sSomeClass); - $oClassExpr = new FieldExpression($sFinalClassSqlColumnName, $oSelectBase->GetTableAlias()); - $oInExpression = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); - $oTrueExpression = new TrueExpression(); - $aCoalesceAttr = array($oInExpression, $oTrueExpression); - $oFinalClassRestriction = new FunctionExpression("COALESCE", $aCoalesceAttr); - - $oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction); - } - } - else - { - $oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass)); - } - } - - // Filter on objects referencing me - // - foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) - { - foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) - { - foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) - { - foreach ($aFilters as $oForeignFilter) - { - $oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); - - self::DbgTrace("Referenced by foreign key: $sForeignExtKeyAttCode... let's call MakeSQLObjectQuery()"); - //self::DbgTrace($oForeignFilter); - //self::DbgTrace($oForeignFilter->ToOQL()); - //self::DbgTrace($oSelectForeign); - //self::DbgTrace($oSelectForeign->RenderSelect(array())); - - $sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias(); - $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias)); - - if ($oForeignKeyAttDef instanceof AttributeObjectKey) - { - $sClassAttCode = $oForeignKeyAttDef->Get('class_attcode'); - - // Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass') - $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); - $oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias); - $oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); - $oBuild->m_oQBExpressions->AddCondition($oClassRestriction); - } - - $oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); - - $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); - $sForeignKeyTable = $oJoinExpr->GetParent(); - $sForeignKeyColumn = $oJoinExpr->GetName(); - - if ($iOperatorCode == TREE_OPERATOR_EQUALS) - { - $oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable); - } - else - { - // Hierarchical key - $KeyLeft = $oForeignKeyAttDef->GetSQLLeft(); - $KeyRight = $oForeignKeyAttDef->GetSQLRight(); - - $oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true); - } - } - } - } - } - - // Additional JOINS for Friendly names - // - foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias) - { - $oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias); - $oSelectFN = $oSubClassFilter->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSubClass, $aExtKeys, array()); - $oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass)); - } - - // That's all... cross fingers and we'll get some working query - - //MyHelpers::var_dump_html($oSelectBase, true); - //MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true); - if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml(); - return $oSelectBase; - } - - protected function MakeSQLObjectQuerySingleTable(&$oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues) - { - // $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields)) - - // Prepare the query for a single table (compound objects) - // Ignores the items (attributes/filters) that are not on the target table - // Perform an (inner or left) join for every external key (and specify the expected fields) - // - // Returns an SQLQuery - // - $sTargetClass = $this->GetFirstJoinedClass(); - $sTargetAlias = $this->GetFirstJoinedClassAlias(); - $sTable = MetaModel::DBGetTable($sTableClass); - $sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable); - - $aTranslation = array(); - $aExpectedAtts = array(); - $oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts); - - $bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses()); - - self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY")); - - // 1 - SELECT and UPDATE - // - // Note: no need for any values nor fields for foreign Classes (ie not the queried Class) - // - $aUpdateValues = array(); - - - // 1/a - Get the key and friendly name - // - // We need one pkey to be the key, let's take the first one available - $oSelectedIdField = null; - $oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias); - $aTranslation[$sTargetAlias]['id'] = $oIdField; - - if ($bIsOnQueriedClass) - { - // Add this field to the list of queried fields (required for the COUNT to work fine) - $oSelectedIdField = $oIdField; - } - - // 1/b - Get the other attributes - // - foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) - { - // Skip this attribute if not defined in this table - if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue; - - // Skip this attribute if not made of SQL columns - if (count($oAttDef->GetSQLExpressions()) == 0) continue; - - // Update... - // - if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues)) - { - assert ($oAttDef->IsBasedOnDBColumns()); - foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue) - { - $aUpdateValues[$sColumn] = $sValue; - } - } - } - - // 2 - The SQL query, for this table only - // - $oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField); - - // 3 - Resolve expected expressions (translation table: alias.attcode => table.column) - // - foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) - { - // Skip this attribute if not defined in this table - if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue; - - // Select... - // - if ($oAttDef->IsExternalField()) - { - // skip, this will be handled in the joined tables (done hereabove) - } - else - { - // standard field, or external key - // add it to the output - foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) - { - if (array_key_exists($sAttCode.$sColId, $aExpectedAtts)) - { - $oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias); - foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier) - { - $oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase); - } - $aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp; - } - } - } - } - - // 4 - The external keys -> joins... - // - $aAllPointingTo = $this->GetCriteria_PointingTo(); - - if (array_key_exists($sTableClass, $aExtKeys)) - { - foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields) - { - $oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode); - - $aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode); - if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo)) - { - // The join was not explicitely defined in the filter, - // we need to do it now - $sKeyClass = $oKeyAttDef->GetTargetClass(); - $sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass); - $oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias); - - $aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter; - } - } - } - - foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - foreach($aFilter as $oExtFilter) - { - if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it - // The aliases should not conflict because normalization occured while building the filter - $oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode); - $sKeyClass = $oExtFilter->GetFirstJoinedClass(); - $sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias(); - - // Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree - - if ($iOperatorCode == TREE_OPERATOR_EQUALS) - { - if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass])) - { - // Specify expected attributes for the target class query - // ... and use the current alias ! - $aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...) - foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt) - { - $oExtAttDef = $oAtt->GetExtAttDef(); - if ($oExtAttDef->IsBasedOnOQLExpression()) - { - $aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression($oExtAttDef->GetCode(), $sKeyClassAlias); - } - else - { - $sExtAttCode = $oAtt->GetExtAttCode(); - // Translate mainclass.extfield => remoteclassalias.remotefieldcode - $oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode); - foreach ($oRemoteAttDef->GetSQLExpressions() as $sColId => $sRemoteAttExpr) - { - $aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias); - } - } - } - - if ($oKeyAttDef instanceof AttributeObjectKey) - { - // Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass') - $sClassAttCode = $oKeyAttDef->Get('class_attcode'); - $oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode); - foreach ($oClassAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) - { - $aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias); - } - - $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL)); - $oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias); - $oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); - $oBuild->m_oQBExpressions->AddCondition($oClassRestriction); - } - - // Translate prior to recursing - // - $oBuild->m_oQBExpressions->Translate($aTranslateNow, false); - - self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()"); - $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias)); - - $oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); - - $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); - $sExternalKeyTable = $oJoinExpr->GetParent(); - $sExternalKeyField = $oJoinExpr->GetName(); - - $aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) - $sLocalKeyField = current($aCols); // get the first column for an external key - - self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField"); - if ($oKeyAttDef->IsNullAllowed()) - { - $oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField); - } - else - { - $oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable); - } - } - } - elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass) - { - $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias)); - $oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); - $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); - $sExternalKeyTable = $oJoinExpr->GetParent(); - $sExternalKeyField = $oJoinExpr->GetName(); - $sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft() - $sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight() - - $LocalKeyLeft = $oKeyAttDef->GetSQLLeft(); - $LocalKeyRight = $oKeyAttDef->GetSQLRight(); - - $oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode); - } - } - } - } - - // Translate the selected columns - // - $oBuild->m_oQBExpressions->Translate($aTranslation, false); - - // Filter out archived records - // - if (MetaModel::IsArchivable($sTableClass)) - { - if (!$oBuild->GetRootFilter()->GetArchiveMode()) - { - $bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses()); - if ($bIsOnJoinedClass) - { - if (MetaModel::IsParentClass($sTableClass, $sTargetClass)) - { - $oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0)); - $oBuild->AddFilteredTable($sTableAlias, $oNotArchived); - } - } - } - } - return $oSelectBase; - } - - /** - * Get the expression for the class and its subclasses (if finalclass = 'subclass' ...) - * Simplifies the final expression by grouping classes having the same expression - * @param $sClass - * @param $sAttCode - * @return \FunctionExpression|mixed|null - * @throws \CoreException -*/ - static public function GetPolymorphicExpression($sClass, $sAttCode) - { - $oExpression = ExpressionCache::GetCachedExpression($sClass, $sAttCode); - if (!empty($oExpression)) - { - return $oExpression; - } - - // 1st step - get all of the required expressions (instantiable classes) - // and group them using their OQL representation - // - $aExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes) - foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass) - { - if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue; - - $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); - $oSubClassExp = $oAttDef->GetOQLExpression($sSubClass); - - // 3rd step - position the attributes in the hierarchy of classes - // - $oSubClassExp->Browse(function($oNode) use ($sSubClass) { - if ($oNode instanceof FieldExpression) - { - $sAttCode = $oNode->GetName(); - $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); - if ($oAttDef->IsExternalField()) - { - $sKeyAttCode = $oAttDef->GetKeyAttCode(); - $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode); - } - else - { - $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); - } - $sParent = MetaModel::GetAttributeOrigin($sClassOfAttribute, $oNode->GetName()); - $oNode->SetParent($sParent); - } - }); - - $sSignature = $oSubClassExp->Render(); - if (!array_key_exists($sSignature, $aExpressions)) - { - $aExpressions[$sSignature] = array( - 'expression' => $oSubClassExp, - 'classes' => array(), - ); - } - $aExpressions[$sSignature]['classes'][] = $sSubClass; - } - - // 2nd step - build the final name expression depending on the finalclass - // - if (count($aExpressions) == 1) - { - $aExpData = reset($aExpressions); - $oExpression = $aExpData['expression']; - } - else - { - $oExpression = null; - foreach ($aExpressions as $sSignature => $aExpData) - { - $oClassListExpr = ListExpression::FromScalars($aExpData['classes']); - $oClassExpr = new FieldExpression('finalclass', $sClass); - $oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); - - if (is_null($oExpression)) - { - $oExpression = $aExpData['expression']; - } - else - { - $oExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oExpression)); - } - } - } - return $oExpression; - } -} + +// + +// Dev hack for disabling the some query build optimizations (Folding/Merging) +define('ENABLE_OPT', true); + +class DBObjectSearch extends DBSearch +{ + private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters) + private $m_aSelectedClasses; // selected for the output (alias => class name) + private $m_oSearchCondition; + private $m_aParams; + private $m_aPointingTo; + private $m_aReferencedBy; + + // By default, some information may be hidden to the current user + // But it may happen that we need to disable that feature + protected $m_bAllowAllData = false; + protected $m_bDataFiltered = false; + + public function __construct($sClass, $sClassAlias = null) + { + parent::__construct(); + + if (is_null($sClassAlias)) $sClassAlias = $sClass; + if(!is_string($sClass)) throw new Exception('DBObjectSearch::__construct called with a non-string parameter: $sClass = '.print_r($sClass, true)); + if(!MetaModel::IsValidClass($sClass)) throw new Exception('DBObjectSearch::__construct called for an invalid class: "'.$sClass.'"'); + + $this->m_aSelectedClasses = array($sClassAlias => $sClass); + $this->m_aClasses = array($sClassAlias => $sClass); + $this->m_oSearchCondition = new TrueExpression; + $this->m_aParams = array(); + $this->m_aPointingTo = array(); + $this->m_aReferencedBy = array(); + } + + public function AllowAllData($bAllowAllData = true) {$this->m_bAllowAllData = $bAllowAllData;} + public function IsAllDataAllowed() {return $this->m_bAllowAllData;} + protected function IsDataFiltered() {return $this->m_bDataFiltered; } + protected function SetDataFiltered() {$this->m_bDataFiltered = true;} + + // Create a search definition that leads to 0 result, still a valid search object + static public function FromEmptySet($sClass) + { + $oResultFilter = new DBObjectSearch($sClass); + $oResultFilter->m_oSearchCondition = new FalseExpression; + return $oResultFilter; + } + + + public function GetJoinedClasses() {return $this->m_aClasses;} + + public function GetClassName($sAlias) + { + if (array_key_exists($sAlias, $this->m_aSelectedClasses)) + { + return $this->m_aSelectedClasses[$sAlias]; + } + else + { + throw new CoreException("Invalid class alias '$sAlias'"); + } + } + + public function GetClass() + { + return reset($this->m_aSelectedClasses); + } + public function GetClassAlias() + { + reset($this->m_aSelectedClasses); + return key($this->m_aSelectedClasses); + } + + public function GetFirstJoinedClass() + { + return reset($this->m_aClasses); + } + public function GetFirstJoinedClassAlias() + { + reset($this->m_aClasses); + return key($this->m_aClasses); + } + + /** + * Change the class (only subclasses are supported as of now, because the conditions must fit the new class) + * Defaults to the first selected class (most of the time it is also the first joined class + * + * @param $sNewClass + * @param null $sAlias + * + * @throws \CoreException + */ + public function ChangeClass($sNewClass, $sAlias = null) + { + if (is_null($sAlias)) + { + $sAlias = $this->GetClassAlias(); + } + else + { + if (!array_key_exists($sAlias, $this->m_aSelectedClasses)) + { + // discard silently - necessary when recursing on the related nodes (see code below) + return; + } + } + $sCurrClass = $this->GetClassName($sAlias); + if ($sNewClass == $sCurrClass) + { + // Skip silently + return; + } + if (!MetaModel::IsParentClass($sCurrClass, $sNewClass)) + { + throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted."); + } + + // Change for this node + // + $this->m_aSelectedClasses[$sAlias] = $sNewClass; + $this->m_aClasses[$sAlias] = $sNewClass; + + // Change for all the related node (yes, this was necessary with some queries - strange effects otherwise) + // + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oExtFilter) + { + $oExtFilter->ChangeClass($sNewClass, $sAlias); + } + } + } + foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $oForeignFilter->ChangeClass($sNewClass, $sAlias); + } + } + } + } + } + + public function GetSelectedClasses() + { + return $this->m_aSelectedClasses; + } + + /** + * @param array $aSelectedClasses array of aliases + * @throws CoreException + */ + public function SetSelectedClasses($aSelectedClasses) + { + $this->m_aSelectedClasses = array(); + foreach ($aSelectedClasses as $sAlias) + { + if (!array_key_exists($sAlias, $this->m_aClasses)) + { + throw new CoreException("SetSelectedClasses: Invalid class alias $sAlias"); + } + $this->m_aSelectedClasses[$sAlias] = $this->m_aClasses[$sAlias]; + } + } + + /** + * Change any alias of the query tree + * + * @param $sOldName + * @param $sNewName + * + * @return bool True if the alias has been found and changed + * @throws \Exception + */ + public function RenameAlias($sOldName, $sNewName) + { + $bFound = false; + if (array_key_exists($sOldName, $this->m_aClasses)) + { + $bFound = true; + } + if (array_key_exists($sNewName, $this->m_aClasses)) + { + throw new Exception("RenameAlias: alias '$sNewName' already used"); + } + + $aClasses = array(); + foreach ($this->m_aClasses as $sAlias => $sClass) + { + if ($sAlias === $sOldName) + { + $aClasses[$sNewName] = $sClass; + } + else + { + $aClasses[$sAlias] = $sClass; + } + } + $this->m_aClasses = $aClasses; + + $aSelectedClasses = array(); + foreach ($this->m_aSelectedClasses as $sAlias => $sClass) + { + if ($sAlias === $sOldName) + { + $aSelectedClasses[$sNewName] = $sClass; + } + else + { + $aSelectedClasses[$sAlias] = $sClass; + } + } + $this->m_aSelectedClasses = $aSelectedClasses; + + $this->m_oSearchCondition->RenameAlias($sOldName, $sNewName); + + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oExtFilter) + { + $bFound = $oExtFilter->RenameAlias($sOldName, $sNewName) || $bFound; + } + } + } + foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $bFound = $oForeignFilter->RenameAlias($sOldName, $sNewName) || $bFound; + } + } + } + } + return $bFound; + } + + public function SetModifierProperty($sPluginClass, $sProperty, $value) + { + $this->m_aModifierProperties[$sPluginClass][$sProperty] = $value; + } + + public function GetModifierProperties($sPluginClass) + { + if (array_key_exists($sPluginClass, $this->m_aModifierProperties)) + { + return $this->m_aModifierProperties[$sPluginClass]; + } + else + { + return array(); + } + } + + public function IsAny() + { + if (!$this->m_oSearchCondition->IsTrue()) return false; + if (count($this->m_aPointingTo) > 0) return false; + if (count($this->m_aReferencedBy) > 0) return false; + return true; + } + + protected function TransferConditionExpression($oFilter, $aTranslation) + { + // Prevent collisions in the parameter names by renaming them if needed + foreach($this->m_aParams as $sParam => $value) + { + if (array_key_exists($sParam, $oFilter->m_aParams) && ($value != $oFilter->m_aParams[$sParam])) + { + // Generate a new and unique name for the collinding parameter + $index = 1; + while(array_key_exists($sParam.$index, $oFilter->m_aParams)) + { + $index++; + } + $secondValue = $oFilter->m_aParams[$sParam]; + $oFilter->RenameParam($sParam, $sParam.$index); + unset($oFilter->m_aParams[$sParam]); + $oFilter->m_aParams[$sParam.$index] = $secondValue; + } + } + $oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */); + $this->AddConditionExpression($oTranslated); + $this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams); + } + + protected function RenameParam($sOldName, $sNewName) + { + $this->m_oSearchCondition->RenameParam($sOldName, $sNewName); + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oExtFilter) + { + $oExtFilter->RenameParam($sOldName, $sNewName); + } + } + } + foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $oForeignFilter->RenameParam($sOldName, $sNewName); + } + } + } + } + } + + public function ResetCondition() + { + $this->m_oSearchCondition = new TrueExpression(); + // ? is that usefull/enough, do I need to rebuild the list after the subqueries ? + } + + public function MergeConditionExpression($oExpression) + { + $this->m_oSearchCondition = $this->m_oSearchCondition->LogOr($oExpression); + } + + public function AddConditionExpression($oExpression) + { + $this->m_oSearchCondition = $this->m_oSearchCondition->LogAnd($oExpression); + } + + public function AddNameCondition($sName) + { + $oValueExpr = new ScalarExpression($sName); + $oNameExpr = new FieldExpression('friendlyname', $this->GetClassAlias()); + $oNewCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr); + $this->AddConditionExpression($oNewCondition); + } + + /** + * @param string $sFilterCode + * @param mixed $value + * @param string $sOpCode operator to use : 'IN', 'NOT IN', 'Contains',' Begins with', 'Finishes with', ... + * @param bool $bParseSearchString + * + * @throws \CoreException + * + * @see AddConditionForInOperatorUsingParam for IN/NOT IN queries with lots of params + */ + public function AddCondition($sFilterCode, $value, $sOpCode = null, $bParseSearchString = false) + { + MyHelpers::CheckKeyInArray('filter code in class: '.$this->GetClass(), $sFilterCode, MetaModel::GetClassFilterDefs($this->GetClass())); + + $oField = new FieldExpression($sFilterCode, $this->GetClassAlias()); + if (empty($sOpCode)) + { + if ($sFilterCode == 'id') + { + $sOpCode = '='; + } + else + { + $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); + $oNewCondition = $oAttDef->GetSmartConditionExpression($value, $oField, $this->m_aParams); + $this->AddConditionExpression($oNewCondition); + return; + } + } + // Parse search strings if needed and if the filter code corresponds to a valid attcode + if($bParseSearchString && MetaModel::IsValidAttCode($this->GetClass(), $sFilterCode)) + { + $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); + $value = $oAttDef->ParseSearchString($value); + } + + // Preserve backward compatibility - quick n'dirty way to change that API semantic + // + switch($sOpCode) + { + case 'SameDay': + case 'SameMonth': + case 'SameYear': + case 'Today': + case '>|': + case '<|': + case '=|': + throw new CoreException('Deprecated operator, please consider using OQL (SQL) expressions like "(TO_DAYS(NOW()) - TO_DAYS(x)) AS AgeDays"', array('operator' => $sOpCode)); + break; + + case 'IN': + if (!is_array($value)) $value = array($value); + if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.'); + $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; + $sOQLCondition = $oField->Render()." IN $sListExpr"; + break; + + case 'NOTIN': + if (!is_array($value)) $value = array($value); + if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.'); + $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; + $sOQLCondition = $oField->Render()." NOT IN $sListExpr"; + break; + + case 'Contains': + $this->m_aParams[$sFilterCode] = "%$value%"; + $sOperator = 'LIKE'; + break; + + case 'Begins with': + $this->m_aParams[$sFilterCode] = "$value%"; + $sOperator = 'LIKE'; + break; + + case 'Finishes with': + $this->m_aParams[$sFilterCode] = "%$value"; + $sOperator = 'LIKE'; + break; + + default: + if ($value === null) + { + switch ($sOpCode) + { + case '=': + $sOpCode = '*Expression*'; + $oExpression = new FunctionExpression('ISNULL', array($oField)); + break; + case '!=': + $sOpCode = '*Expression*'; + $oExpression = new FunctionExpression('ISNULL', array($oField)); + $oExpression = new BinaryExpression($oExpression, '=', new ScalarExpression(0)); + break; + default: + throw new Exception("AddCondition on null value: unsupported operator '$sOpCode''"); + } + } + else + { + $this->m_aParams[$sFilterCode] = $value; + $sOperator = $sOpCode; + } + } + + switch($sOpCode) + { + case '*Expression*': + $oNewCondition = $oExpression; + break; + case "IN": + case "NOTIN": + // this will parse all of the values... Can take forever if there are lots of them ! + // In this case using a parameter is far better : WHERE ... IN (:my_param) + $oNewCondition = Expression::FromOQL($sOQLCondition); + break; + + case 'Contains': + case 'Begins with': + case 'Finishes with': + default: + $oRightExpr = new VariableExpression($sFilterCode); + $oNewCondition = new BinaryExpression($oField, $sOperator, $oRightExpr); + } + + $this->AddConditionExpression($oNewCondition); + } + + /** + * @param string $sFilterCode attribute code to use + * @param array $aValues + * @param bool $bPositiveMatch if true will add a IN filter, else a NOT IN + * + * @throws \CoreException + * + * @since 2.5 N°1418 + */ + public function AddConditionForInOperatorUsingParam($sFilterCode, $aValues, $bPositiveMatch = true) + { + $oFieldExpression = new FieldExpression($sFilterCode, $this->GetClassAlias()); + + $sOperator = $bPositiveMatch ? 'IN' : 'NOT IN'; + + $sInParamName = $this->GenerateUniqueParamName(); + $oParamExpression = new VariableExpression($sInParamName); + $this->GetInternalParamsByRef()[$sInParamName] = $aValues; + + $oListExpression = new ListExpression(array($oParamExpression)); + + $oInCondition = new BinaryExpression($oFieldExpression, $sOperator, $oListExpression); + $this->AddConditionExpression($oInCondition); + } + + /** + * @return string a unique param name + */ + private function GenerateUniqueParamName() { + $iExistingParamsNb = count($this->m_aParams); + $iCurrentArrayParamNb = $iExistingParamsNb + 1; + $sParamName = 'param'.$iCurrentArrayParamNb; + + if (isset($this->m_aParams[$sParamName])) { + $sParamName .= '_'.microtime(true) . '_' .rand(0,100); + } + + return $sParamName; + } + + /** + * Specify a condition on external keys or link sets + * @param string $sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively + * Example: infra_list->ci_id->location_id->country + * @param $value + * @return void + * @throws \CoreException + * @throws \CoreWarning + */ + public function AddConditionAdvanced($sAttSpec, $value) + { + $sClass = $this->GetClass(); + + $iPos = strpos($sAttSpec, '->'); + if ($iPos !== false) + { + $sAttCode = substr($sAttSpec, 0, $iPos); + $sSubSpec = substr($sAttSpec, $iPos + 2); + + if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + throw new Exception("Invalid attribute code '$sClass/$sAttCode' in condition specification '$sAttSpec'"); + } + + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef->IsLinkSet()) + { + $sTargetClass = $oAttDef->GetLinkedClass(); + $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + + $oNewFilter = new DBObjectSearch($sTargetClass); + $oNewFilter->AddConditionAdvanced($sSubSpec, $value); + + $this->AddCondition_ReferencedBy($oNewFilter, $sExtKeyToMe); + } + elseif ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) + { + $sTargetClass = $oAttDef->GetTargetClass(EXTKEY_ABSOLUTE); + + $oNewFilter = new DBObjectSearch($sTargetClass); + $oNewFilter->AddConditionAdvanced($sSubSpec, $value); + + $this->AddCondition_PointingTo($oNewFilter, $sAttCode); + } + else + { + throw new Exception("Attribute specification '$sAttSpec', '$sAttCode' should be either a link set or an external key"); + } + } + else + { + // $sAttSpec is an attribute code + // + if (is_array($value)) + { + $oField = new FieldExpression($sAttSpec, $this->GetClass()); + $oListExpr = ListExpression::FromScalars($value); + $oInValues = new BinaryExpression($oField, 'IN', $oListExpr); + + $this->AddConditionExpression($oInValues); + } + else + { + $this->AddCondition($sAttSpec, $value); + } + } + } + + public function AddCondition_FullText($sNeedle) + { + // Transform the full text condition into additional condition expression + $aFullTextFields = array(); + foreach (MetaModel::ListAttributeDefs($this->GetClass()) as $sAttCode => $oAttDef) + { + if (!$oAttDef->IsScalar()) continue; + if ($oAttDef->IsExternalKey()) continue; + $aFullTextFields[] = new FieldExpression($sAttCode, $this->GetClassAlias()); + } + $oTextFields = new CharConcatWSExpression(' ', $aFullTextFields); + + $sQueryParam = 'needle'; + $oFlexNeedle = new CharConcatExpression(array(new ScalarExpression('%'), new VariableExpression($sQueryParam), new ScalarExpression('%'))); + + $oNewCond = new BinaryExpression($oTextFields, 'LIKE', $oFlexNeedle); + $this->AddConditionExpression($oNewCond); + $this->m_aParams[$sQueryParam] = $sNeedle; + } + + protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true) + { + if ($bTranslateMainAlias) + { + $sOrigAlias = $this->GetFirstJoinedClassAlias(); + if (array_key_exists($sOrigAlias, $aClassAliases)) + { + $sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetFirstJoinedClass()); + if (isset($this->m_aSelectedClasses[$sOrigAlias])) + { + $this->m_aSelectedClasses[$sNewAlias] = $this->GetFirstJoinedClass(); + unset($this->m_aSelectedClasses[$sOrigAlias]); + } + + // TEMPORARY ALGORITHM (m_aClasses is not correctly updated, it is not possible to add a subtree onto a subnode) + // Replace the element at the same position (unset + set is not enough because the hash array is ordered) + $aPrevList = $this->m_aClasses; + $this->m_aClasses = array(); + foreach ($aPrevList as $sSomeAlias => $sSomeClass) + { + if ($sSomeAlias == $sOrigAlias) + { + $this->m_aClasses[$sNewAlias] = $sSomeClass; // note: GetFirstJoinedClass now returns '' !!! + } + else + { + $this->m_aClasses[$sSomeAlias] = $sSomeClass; + } + } + + // Translate the condition expression with the new alias + $aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias; + } + + // add the alias into the filter aliases list + $aClassAliases[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass(); + } + + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oFilter) + { + $oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); + } + } + } + + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); + } + } + } + } + } + + + // Browse the tree nodes recursively + // + protected function GetNode($sAlias) + { + if ($this->GetFirstJoinedClassAlias() == $sAlias) + { + return $this; + } + else + { + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oFilter) + { + $ret = $oFilter->GetNode($sAlias); + if (is_object($ret)) + { + return $ret; + } + } + } + } + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $ret = $oForeignFilter->GetNode($sAlias); + if (is_object($ret)) + { + return $ret; + } + } + } + } + } + } + // Not found + return null; + } + + /** + * Helper to + * - convert a translation table (format optimized for the translation in an expression tree) into simple hash + * - compile over an eventually existing map + * + * @param array $aRealiasingMap Map to update + * @param array $aAliasTranslation Translation table resulting from calls to MergeWith_InNamespace + * @return void of => + */ + protected function UpdateRealiasingMap(&$aRealiasingMap, $aAliasTranslation) + { + if ($aRealiasingMap !== null) + { + foreach ($aAliasTranslation as $sPrevAlias => $aRules) + { + if (isset($aRules['*'])) + { + $sNewAlias = $aRules['*']; + $sOriginalAlias = array_search($sPrevAlias, $aRealiasingMap); + if ($sOriginalAlias !== false) + { + $aRealiasingMap[$sOriginalAlias] = $sNewAlias; + } + else + { + $aRealiasingMap[$sPrevAlias] = $sNewAlias; + } + } + } + } + } + + /** + * Completes the list of alias=>class by browsing the whole structure recursively + * This a workaround to handle some cases in which the list of classes is not correctly updated. + * This code should disappear as soon as DBObjectSearch get split between a container search class and a Node class + * + * @param array $aClasses List to be completed + */ + protected function RecomputeClassList(&$aClasses) + { + $aClasses[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass(); + + // Recurse in the query tree + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oFilter) + { + $oFilter->RecomputeClassList($aClasses); + } + } + } + + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $oForeignFilter->RecomputeClassList($aClasses); + } + } + } + } + } + + /** + * @param DBObjectSearch $oFilter + * @param $sExtKeyAttCode + * @param int $iOperatorCode + * @param null $aRealiasingMap array of => , for each alias that has changed + * @throws CoreException + * @throws CoreWarning + */ + public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) + { + if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode)) + { + throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}'"); + } + $oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode); + if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass())) + { + throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); + } + if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey)) + { + throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey"); + } + // Note: though it seems to be a good practice to clone the given source filter + // (as it was done and fixed an issue in Intersect()) + // this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge) + // root cause: FromOQL relies on the fact that the passed filter can be modified later + // NO: $oFilter = $oFilter->DeepClone(); + // See also: Trac #639, and self::AddCondition_ReferencedBy() + $aAliasTranslation = array(); + $res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode); + $this->TransferConditionExpression($oFilter, $aAliasTranslation); + $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); + + if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass())) + { + if (isset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode])) + { + foreach ($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode] as $oRemoteFilter) + { + if ($this->GetClass() == $oRemoteFilter->GetClass()) + { + // Optimization - fold sibling query + $aAliasTranslation = array(); + $this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation); + unset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode]); + $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false); + $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); + break; + } + } + } + } + $this->RecomputeClassList($this->m_aClasses); + return $res; + } + + protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode) + { + // Find the node on which the new tree must be attached (most of the time it is "this") + $oReceivingFilter = $this->GetNode($this->GetClassAlias()); + + $bMerged = false; + if (ENABLE_OPT && isset($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode])) + { + foreach ($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] as $oExisting) + { + if ($oExisting->GetClass() == $oFilter->GetClass()) + { + $oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation); + $bMerged = true; + break; + } + } + } + if (!$bMerged) + { + $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); + $oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][] = $oFilter; + } + } + + /** + * @param DBObjectSearch $oFilter + * @param $sForeignExtKeyAttCode + * @param int $iOperatorCode + * @param null $aRealiasingMap array of => , for each alias that has changed + * @return void + * @throws \CoreException + */ + public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) + { + $sForeignClass = $oFilter->GetClass(); + if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode)) + { + throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}'"); + } + $oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); + if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass())) + { + // à refaire en spécifique dans FromOQL + throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); + } + + // Note: though it seems to be a good practice to clone the given source filter + // (as it was done and fixed an issue in Intersect()) + // this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge) + // root cause: FromOQL relies on the fact that the passed filter can be modified later + // NO: $oFilter = $oFilter->DeepClone(); + // See also: Trac #639, and self::AddCondition_PointingTo() + $aAliasTranslation = array(); + $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode); + $this->TransferConditionExpression($oFilter, $aAliasTranslation); + $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); + + if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass())) + { + if (isset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode])) + { + foreach ($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode] as $oRemoteFilter) + { + if ($this->GetClass() == $oRemoteFilter->GetClass()) + { + // Optimization - fold sibling query + $aAliasTranslation = array(); + $this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation); + unset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode]); + $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false); + $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); + break; + } + } + } + } + $this->RecomputeClassList($this->m_aClasses); + } + + protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode) + { + $sForeignClass = $oFilter->GetClass(); + + // Find the node on which the new tree must be attached (most of the time it is "this") + $oReceivingFilter = $this->GetNode($this->GetClassAlias()); + + $bMerged = false; + if (ENABLE_OPT && isset($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode])) + { + foreach ($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode] as $oExisting) + { + if ($oExisting->GetClass() == $oFilter->GetClass()) + { + $oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation); + $bMerged = true; + break; + } + } + } + if (!$bMerged) + { + $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); + $oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode][] = $oFilter; + } + } + + public function Intersect(DBSearch $oFilter) + { + if ($oFilter instanceof DBUnionSearch) + { + // Develop! + $aFilters = $oFilter->GetSearches(); + } + else + { + $aFilters = array($oFilter); + } + + $aSearches = array(); + foreach ($aFilters as $oRightFilter) + { + // Limitation: the queried class must be the first declared class + if ($this->GetFirstJoinedClassAlias() != $this->GetClassAlias()) + { + throw new CoreException("Limitation: cannot merge two queries if the queried class ({$this->GetClass()} AS {$this->GetClassAlias()}) is not the first joined class ({$this->GetFirstJoinedClass()} AS {$this->GetFirstJoinedClassAlias()})"); + } + if ($oRightFilter->GetFirstJoinedClassAlias() != $oRightFilter->GetClassAlias()) + { + throw new CoreException("Limitation: cannot merge two queries if the queried class ({$oRightFilter->GetClass()} AS {$oRightFilter->GetClassAlias()}) is not the first joined class ({$oRightFilter->GetFirstJoinedClass()} AS {$oRightFilter->GetFirstJoinedClassAlias()})"); + } + + $oLeftFilter = $this->DeepClone(); + $oRightFilter = $oRightFilter->DeepClone(); + + $bAllowAllData = ($oLeftFilter->IsAllDataAllowed() && $oRightFilter->IsAllDataAllowed()); + if ($bAllowAllData) + { + $oLeftFilter->AllowAllData(); + } + + if ($oLeftFilter->GetClass() != $oRightFilter->GetClass()) + { + if (MetaModel::IsParentClass($oLeftFilter->GetClass(), $oRightFilter->GetClass())) + { + // Specialize $oLeftFilter + $oLeftFilter->ChangeClass($oRightFilter->GetClass()); + } + elseif (MetaModel::IsParentClass($oRightFilter->GetClass(), $oLeftFilter->GetClass())) + { + // Specialize $oRightFilter + $oRightFilter->ChangeClass($oLeftFilter->GetClass()); + } + else + { + throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'"); + } + } + + $aAliasTranslation = array(); + $oLeftFilter->MergeWith_InNamespace($oRightFilter, $oLeftFilter->m_aClasses, $aAliasTranslation); + $oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation); + $aSearches[] = $oLeftFilter; + } + if (count($aSearches) == 1) + { + // return a DBObjectSearch + return $aSearches[0]; + } + else + { + return new DBUnionSearch($aSearches); + } + } + + protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation) + { + if ($this->GetClass() != $oFilter->GetClass()) + { + throw new CoreException("Attempting to merge a filter of class '{$this->GetClass()}' with a filter of class '{$oFilter->GetClass()}'"); + } + + // Translate search condition into our aliasing scheme + $aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias(); + + foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oExtFilter) + { + $this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); + } + } + } + foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); + } + } + } + } + } + + public function GetCriteria() {return $this->m_oSearchCondition;} + public function GetCriteria_FullText() {throw new Exception("Removed GetCriteria_FullText");} + public function GetCriteria_PointingTo($sKeyAttCode = "") + { + if (empty($sKeyAttCode)) + { + return $this->m_aPointingTo; + } + if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array(); + return $this->m_aPointingTo[$sKeyAttCode]; + } + protected function GetCriteria_ReferencedBy() + { + return $this->m_aReferencedBy; + } + + public function SetInternalParams($aParams) + { + return $this->m_aParams = $aParams; + } + + /** + * @return array warning : array returned by value + * @see self::GetInternalParamsByRef to get the attribute by reference + */ + public function GetInternalParams() + { + return $this->m_aParams; + } + + /** + * @return array + * @see http://php.net/manual/en/language.references.return.php + * @since 2.5.1 N°1582 + */ + public function &GetInternalParamsByRef() + { + return $this->m_aParams; + } + + /** + * @param string $sKey + * @param mixed $value + * @param bool $bDoNotOverride + * + * @throws \CoreUnexpectedValue if $bDoNotOverride and $sKey already exists + */ + public function AddInternalParam($sKey, $value, $bDoNotOverride = false) + { + if ($bDoNotOverride) + { + if (array_key_exists($sKey, $this->m_aParams)) + { + throw new CoreUnexpectedValue("The key $sKey already exists with value : ".$this->m_aParams[$sKey]); + } + } + + $this->m_aParams[$sKey] = $value; + } + + public function GetQueryParams($bExcludeMagicParams = true) + { + $aParams = array(); + $this->m_oSearchCondition->Render($aParams, true); + + if ($bExcludeMagicParams) + { + $aRet = array(); + + // Make the list of acceptable arguments... could be factorized with run_query, into oSearch->GetQueryParams($bExclude magic params) + $aNakedMagicArguments = array(); + foreach (MetaModel::PrepareQueryArguments(array()) as $sArgName => $value) + { + $iPos = strpos($sArgName, '->object()'); + if ($iPos === false) + { + $aNakedMagicArguments[$sArgName] = $value; + } + else + { + $aNakedMagicArguments[substr($sArgName, 0, $iPos)] = true; + } + } + foreach ($aParams as $sParam => $foo) + { + $iPos = strpos($sParam, '->'); + if ($iPos === false) + { + $sRefName = $sParam; + } + else + { + $sRefName = substr($sParam, 0, $iPos); + } + if (!array_key_exists($sRefName, $aNakedMagicArguments)) + { + $aRet[$sParam] = $foo; + } + } + } + + return $aRet; + } + + public function ListConstantFields() + { + return $this->m_oSearchCondition->ListConstantFields(); + } + + /** + * Turn the parameters (:xxx) into scalar values in order to easily + * serialize a search + * @param $aArgs +*/ + public function ApplyParameters($aArgs) + { + $this->m_oSearchCondition->ApplyParameters(array_merge($this->m_aParams, $aArgs)); + } + + public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false) + { + // Currently unused, but could be useful later + $bRetrofitParams = false; + + if ($bDevelopParams) + { + if (is_null($aContextParams)) + { + $aParams = array_merge($this->m_aParams); + } + else + { + $aParams = array_merge($aContextParams, $this->m_aParams); + } + $aParams = MetaModel::PrepareQueryArguments($aParams); + } + else + { + // Leave it as is, the rendering will be made with parameters in clear + $aParams = null; + } + + $aSelectedAliases = array(); + foreach ($this->m_aSelectedClasses as $sAlias => $sClass) + { + $aSelectedAliases[] = '`' . $sAlias . '`'; + } + $sSelectedClasses = implode(', ', $aSelectedAliases); + $sRes = 'SELECT '.$sSelectedClasses.' FROM'; + + $sRes .= ' ' . $this->GetFirstJoinedClass() . ' AS `' . $this->GetFirstJoinedClassAlias() . '`'; + $sRes .= $this->ToOQL_Joins(); + $sRes .= " WHERE ".$this->m_oSearchCondition->Render($aParams, $bRetrofitParams); + + if ($bWithAllowAllFlag && $this->m_bAllowAllData) + { + $sRes .= " ALLOW ALL DATA"; + } + return $sRes; + } + + protected function OperatorCodeToOQL($iOperatorCode) + { + switch($iOperatorCode) + { + case TREE_OPERATOR_EQUALS: + $sOperator = ' = '; + break; + + case TREE_OPERATOR_BELOW: + $sOperator = ' BELOW '; + break; + + case TREE_OPERATOR_BELOW_STRICT: + $sOperator = ' BELOW STRICT '; + break; + + case TREE_OPERATOR_NOT_BELOW: + $sOperator = ' NOT BELOW '; + break; + + case TREE_OPERATOR_NOT_BELOW_STRICT: + $sOperator = ' NOT BELOW STRICT '; + break; + + case TREE_OPERATOR_ABOVE: + $sOperator = ' ABOVE '; + break; + + case TREE_OPERATOR_ABOVE_STRICT: + $sOperator = ' ABOVE STRICT '; + break; + + case TREE_OPERATOR_NOT_ABOVE: + $sOperator = ' NOT ABOVE '; + break; + + case TREE_OPERATOR_NOT_ABOVE_STRICT: + $sOperator = ' NOT ABOVE STRICT '; + break; + + } + return $sOperator; + } + + protected function ToOQL_Joins() + { + $sRes = ''; + foreach($this->m_aPointingTo as $sExtKey => $aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + $sOperator = $this->OperatorCodeToOQL($iOperatorCode); + foreach($aFilter as $oFilter) + { + $sRes .= ' JOIN ' . $oFilter->GetFirstJoinedClass() . ' AS `' . $oFilter->GetFirstJoinedClassAlias() . '` ON `' . $this->GetFirstJoinedClassAlias() . '`.' . $sExtKey . $sOperator . '`' . $oFilter->GetFirstJoinedClassAlias() . '`.id'; + $sRes .= $oFilter->ToOQL_Joins(); + } + } + } + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + $sOperator = $this->OperatorCodeToOQL($iOperatorCode); + foreach ($aFilters as $oForeignFilter) + { + $sRes .= ' JOIN ' . $oForeignFilter->GetFirstJoinedClass() . ' AS `' . $oForeignFilter->GetFirstJoinedClassAlias() . '` ON `' . $oForeignFilter->GetFirstJoinedClassAlias() . '`.' . $sForeignExtKeyAttCode . $sOperator . '`' . $this->GetFirstJoinedClassAlias() . '`.id'; + $sRes .= $oForeignFilter->ToOQL_Joins(); + } + } + } + } + return $sRes; + } + + protected function OQLExpressionToCondition($sQuery, $oExpression, $aClassAliases) + { + if ($oExpression instanceof BinaryOqlExpression) + { + $sOperator = $oExpression->GetOperator(); + $oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases); + $oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases); + return new BinaryExpression($oLeft, $sOperator, $oRight); + } + elseif ($oExpression instanceof FieldOqlExpression) + { + $sClassAlias = $oExpression->GetParent(); + $sFltCode = $oExpression->GetName(); + if (empty($sClassAlias)) + { + // Need to find the right alias + // Build an array of field => array of aliases + $aFieldClasses = array(); + foreach($aClassAliases as $sAlias => $sReal) + { + foreach(MetaModel::GetFiltersList($sReal) as $sAnFltCode) + { + $aFieldClasses[$sAnFltCode][] = $sAlias; + } + } + $sClassAlias = $aFieldClasses[$sFltCode][0]; + } + return new FieldExpression($sFltCode, $sClassAlias); + } + elseif ($oExpression instanceof VariableOqlExpression) + { + return new VariableExpression($oExpression->GetName()); + } + elseif ($oExpression instanceof TrueOqlExpression) + { + return new TrueExpression; + } + elseif ($oExpression instanceof ScalarOqlExpression) + { + return new ScalarExpression($oExpression->GetValue()); + } + elseif ($oExpression instanceof ListOqlExpression) + { + $aItems = array(); + foreach ($oExpression->GetItems() as $oItemExpression) + { + $aItems[] = $this->OQLExpressionToCondition($sQuery, $oItemExpression, $aClassAliases); + } + return new ListExpression($aItems); + } + elseif ($oExpression instanceof FunctionOqlExpression) + { + $aArgs = array(); + foreach ($oExpression->GetArgs() as $oArgExpression) + { + $aArgs[] = $this->OQLExpressionToCondition($sQuery, $oArgExpression, $aClassAliases); + } + return new FunctionExpression($oExpression->GetVerb(), $aArgs); + } + elseif ($oExpression instanceof IntervalOqlExpression) + { + return new IntervalExpression($oExpression->GetValue(), $oExpression->GetUnit()); + } + else + { + throw new CoreException('Unknown expression type', array('class'=>get_class($oExpression), 'query'=>$sQuery)); + } + } + + public function InitFromOqlQuery(OqlQuery $oOqlQuery, $sQuery) + { + $oModelReflection = new ModelReflectionRuntime(); + $sClass = $oOqlQuery->GetClass($oModelReflection); + $sClassAlias = $oOqlQuery->GetClassAlias(); + + $aAliases = array($sClassAlias => $sClass); + + // Note: the condition must be built here, it may be altered later on when optimizing some joins + $oConditionTree = $oOqlQuery->GetCondition(); + if ($oConditionTree instanceof Expression) + { + $aRawAliases = array($sClassAlias => $sClass); + $aJoinSpecs = $oOqlQuery->GetJoins(); + if (is_array($aJoinSpecs)) + { + foreach ($aJoinSpecs as $oJoinSpec) + { + $aRawAliases[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass(); + } + } + $this->m_oSearchCondition = $this->OQLExpressionToCondition($sQuery, $oConditionTree, $aRawAliases); + } + + // Maintain an array of filters, because the flat list is in fact referring to a tree + // And this will be an easy way to dispatch the conditions + // $this will be referenced by the other filters, or the other way around... + $aJoinItems = array($sClassAlias => $this); + + $aJoinSpecs = $oOqlQuery->GetJoins(); + if (is_array($aJoinSpecs)) + { + $aAliasTranslation = array(); + foreach ($aJoinSpecs as $oJoinSpec) + { + $sJoinClass = $oJoinSpec->GetClass(); + $sJoinClassAlias = $oJoinSpec->GetClassAlias(); + if (isset($aAliasTranslation[$sJoinClassAlias]['*'])) + { + $sJoinClassAlias = $aAliasTranslation[$sJoinClassAlias]['*']; + } + + // Assumption: ext key on the left only !!! + // normalization should take care of this + $oLeftField = $oJoinSpec->GetLeftField(); + $sFromClass = $oLeftField->GetParent(); + if (isset($aAliasTranslation[$sFromClass]['*'])) + { + $sFromClass = $aAliasTranslation[$sFromClass]['*']; + } + $sExtKeyAttCode = $oLeftField->GetName(); + + $oRightField = $oJoinSpec->GetRightField(); + $sToClass = $oRightField->GetParent(); + if (isset($aAliasTranslation[$sToClass]['*'])) + { + $sToClass = $aAliasTranslation[$sToClass]['*']; + } + + $aAliases[$sJoinClassAlias] = $sJoinClass; + $aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias); + + $sOperator = $oJoinSpec->GetOperator(); + switch($sOperator) + { + case '=': + default: + $iOperatorCode = TREE_OPERATOR_EQUALS; + break; + case 'BELOW': + $iOperatorCode = TREE_OPERATOR_BELOW; + break; + case 'BELOW_STRICT': + $iOperatorCode = TREE_OPERATOR_BELOW_STRICT; + break; + case 'NOT_BELOW': + $iOperatorCode = TREE_OPERATOR_NOT_BELOW; + break; + case 'NOT_BELOW_STRICT': + $iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT; + break; + case 'ABOVE': + $iOperatorCode = TREE_OPERATOR_ABOVE; + break; + case 'ABOVE_STRICT': + $iOperatorCode = TREE_OPERATOR_ABOVE_STRICT; + break; + case 'NOT_ABOVE': + $iOperatorCode = TREE_OPERATOR_NOT_ABOVE; + break; + case 'NOT_ABOVE_STRICT': + $iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT; + break; + } + + if ($sFromClass == $sJoinClassAlias) + { + $oReceiver = $aJoinItems[$sToClass]; + $oNewComer = $aJoinItems[$sFromClass]; + $oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode); + } + else + { + $oReceiver = $aJoinItems[$sFromClass]; + $oNewComer = $aJoinItems[$sToClass]; + $oReceiver->AddCondition_PointingTo_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode); + } + } + $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false /* leave unresolved fields */); + } + + // Check and prepare the select information + $this->m_aSelectedClasses = array(); + foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails) + { + $sClassToSelect = $oClassDetails->GetValue(); + $this->m_aSelectedClasses[$sClassToSelect] = $aAliases[$sClassToSelect]; + } + $this->m_aClasses = $aAliases; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Construction of the SQL queries + // + //////////////////////////////////////////////////////////////////////////// + + public function MakeDeleteQuery($aArgs = array()) + { + $aModifierProperties = MetaModel::MakeModifierProperties($this); + $oBuild = new QueryBuilderContext($this, $aModifierProperties); + $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => array()), array()); + $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); + $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); + $oSQLQuery->OptimizeJoins(array()); + $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); + $sRet = $oSQLQuery->RenderDelete($aScalarArgs); + return $sRet; + } + + public function MakeUpdateQuery($aValues, $aArgs = array()) + { + // $aValues is an array of $sAttCode => $value + $aModifierProperties = MetaModel::MakeModifierProperties($this); + $oBuild = new QueryBuilderContext($this, $aModifierProperties); + $aRequested = array(); // Requested attributes are the updated attributes + foreach ($aValues as $sAttCode => $value) + { + $aRequested[$sAttCode] = MetaModel::GetAttributeDef($this->GetClass(), $sAttCode); + } + $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => $aRequested), $aValues); + $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); + $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); + $oSQLQuery->OptimizeJoins(array()); + $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); + $sRet = $oSQLQuery->RenderUpdate($aScalarArgs); + return $sRet; + } + + public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) + { + // Hide objects that are not visible to the current user + // + $oSearch = $this; + if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered()) + { + $oVisibleObjects = UserRights::GetSelectFilter($this->GetClass(), $this->GetModifierProperties('UserRightsGetSelectFilter')); + if ($oVisibleObjects === false) + { + // Make sure this is a valid search object, saying NO for all + $oVisibleObjects = DBObjectSearch::FromEmptySet($this->GetClass()); + } + if (is_object($oVisibleObjects)) + { + $oVisibleObjects->AllowAllData(); + $oSearch = $this->Intersect($oVisibleObjects); + $oSearch->SetDataFiltered(); + } + } + + // Compute query modifiers properties (can be set in the search itself, by the context, etc.) + // + $aModifierProperties = MetaModel::MakeModifierProperties($oSearch); + + // Create a unique cache id + // + $aContextData = array(); + $bCanCache = true; + if (self::$m_bQueryCacheEnabled || self::$m_bTraceQueries) + { + if (isset($_SERVER['REQUEST_URI'])) + { + $aContextData['sRequestUri'] = $_SERVER['REQUEST_URI']; + } + else if (isset($_SERVER['SCRIPT_NAME'])) + { + $aContextData['sRequestUri'] = $_SERVER['SCRIPT_NAME']; + } + else + { + $aContextData['sRequestUri'] = ''; + } + + // Need to identify the query + $sOqlQuery = $oSearch->ToOql(false, null, true); + if ((strpos($sOqlQuery, '`id` IN (') !== false) || (strpos($sOqlQuery, '`id` NOT IN (') !== false)) + { + // Requests containing "id IN" are not worth caching + $bCanCache = false; + } + + $aContextData['sOqlQuery'] = $sOqlQuery; + + if (count($aModifierProperties)) + { + array_multisort($aModifierProperties); + $sModifierProperties = json_encode($aModifierProperties); + } + else + { + $sModifierProperties = ''; + } + $aContextData['sModifierProperties'] = $sModifierProperties; + + $sRawId = $sOqlQuery.$sModifierProperties; + if (!is_null($aAttToLoad)) + { + $sRawId .= json_encode($aAttToLoad); + } + $aContextData['aAttToLoad'] = $aAttToLoad; + if (!is_null($aGroupByExpr)) + { + foreach($aGroupByExpr as $sAlias => $oExpr) + { + $sRawId .= 'g:'.$sAlias.'!'.$oExpr->Render(); + } + } + if (!is_null($aSelectExpr)) + { + foreach($aSelectExpr as $sAlias => $oExpr) + { + $sRawId .= 'se:'.$sAlias.'!'.$oExpr->Render(); + } + } + $aContextData['aGroupByExpr'] = $aGroupByExpr; + $aContextData['aSelectExpr'] = $aSelectExpr; + $sRawId .= $bGetCount; + $aContextData['bGetCount'] = $bGetCount; + if (is_array($aSelectedClasses)) + { + $sRawId .= implode(',', $aSelectedClasses); // Unions may alter the list of selected columns + } + $aContextData['aSelectedClasses'] = $aSelectedClasses; + $bIsArchiveMode = $oSearch->GetArchiveMode(); + $sRawId .= $bIsArchiveMode ? '--arch' : ''; + $bShowObsoleteData = $oSearch->GetShowObsoleteData(); + $sRawId .= $bShowObsoleteData ? '--obso' : ''; + $aContextData['bIsArchiveMode'] = $bIsArchiveMode; + $aContextData['bShowObsoleteData'] = $bShowObsoleteData; + $sOqlId = md5($sRawId); + } + else + { + $sOqlQuery = "SELECTING... ".$oSearch->GetClass(); + $sOqlId = "query id ? n/a"; + } + + + // Query caching + // + $sOqlAPCCacheId = null; + if (self::$m_bQueryCacheEnabled) + { + // Warning: using directly the query string as the key to the hash array can FAIL if the string + // is long and the differences are only near the end... so it's safer (but not bullet proof?) + // to use a hash (like md5) of the string as the key ! + // + // Example of two queries that were found as similar by the hash array: + // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTO' AND CustomerContract.customer_id = 2 + // and + // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTR' AND CustomerContract.customer_id = 2 + // the only difference is R instead or O at position 285 (TTR instead of TTO)... + // + if (array_key_exists($sOqlId, self::$m_aQueryStructCache)) + { + // hit! + + $oSQLQuery = unserialize(serialize(self::$m_aQueryStructCache[$sOqlId])); + // Note: cloning is not enough because the subtree is made of objects + } + elseif (self::$m_bUseAPCCache) + { + // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter + // + $sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-query-cache-'.$sOqlId; + $oKPI = new ExecutionKPI(); + $result = apc_fetch($sOqlAPCCacheId); + $oKPI->ComputeStats('Query APC (fetch)', $sOqlQuery); + + if (is_object($result)) + { + $oSQLQuery = $result; + self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery; + } + } + } + + if (!isset($oSQLQuery)) + { + $oKPI = new ExecutionKPI(); + $oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); + $oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery); + + if (self::$m_bQueryCacheEnabled) + { + if ($bCanCache && self::$m_bUseAPCCache) + { + $oSQLQuery->m_aContextData = $aContextData; + $oKPI = new ExecutionKPI(); + apc_store($sOqlAPCCacheId, $oSQLQuery, self::$m_iQueryCacheTTL); + $oKPI->ComputeStats('Query APC (store)', $sOqlQuery); + } + + self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery->DeepClone(); + } + } + return $oSQLQuery; + } + + /** + * @param array $aAttToLoad + * @param bool $bGetCount + * @param array $aModifierProperties + * @param array $aGroupByExpr + * @param array $aSelectedClasses + * @param array $aSelectExpr + * + * @return null|SQLObjectQuery + * @throws \CoreException + */ + protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) + { + $oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); + + $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array()); + $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); + if (is_array($aGroupByExpr)) + { + $aCols = $oBuild->m_oQBExpressions->GetGroupBy(); + $oSQLQuery->SetGroupBy($aCols); + $oSQLQuery->SetSelect($aCols); + } + else + { + $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); + } + if ($aSelectExpr) + { + // Get the fields corresponding to the select expressions + foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr) + { + if (key_exists($sAlias, $aSelectExpr)) + { + $oSQLQuery->AddSelect($sAlias, $oExpr); + } + } + } + + $aMandatoryTables = null; + if (self::$m_bOptimizeQueries) + { + if ($bGetCount) + { + // Simplify the query if just getting the count + $oSQLQuery->SetSelect(array()); + } + $oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables); + $oSQLQuery->OptimizeJoins($aMandatoryTables); + } + // Filter tables as late as possible: do not interfere with the optimization process + foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions) + { + if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables)) + { + foreach ($aConditions as $oCondition) + { + $oSQLQuery->AddCondition($oCondition); + } + } + } + + return $oSQLQuery; + } + + + /** + * @param $oBuild + * @param null $aAttToLoad + * @param array $aValues + * @return null|SQLObjectQuery + * @throws \CoreException + */ + protected function MakeSQLObjectQuery(&$oBuild, $aAttToLoad = null, $aValues = array()) + { + // Note: query class might be different than the class of the filter + // -> this occurs when we are linking our class to an external class (referenced by, or pointing to) + $sClass = $this->GetFirstJoinedClass(); + $sClassAlias = $this->GetFirstJoinedClassAlias(); + + $bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses()); + + //self::DbgTrace("Entering: ".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY")); + + //$sRootClass = MetaModel::GetRootClass($sClass); + $sKeyField = MetaModel::DBGetKey($sClass); + + if ($bIsOnQueriedClass) + { + // default to the whole list of attributes + the very std id/finalclass + $oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias)); + if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad)) + { + $sSelectedClass = $oBuild->GetSelectedClass($sClassAlias); + $aAttList = MetaModel::ListAttributeDefs($sSelectedClass); + } + else + { + $aAttList = $aAttToLoad[$sClassAlias]; + } + foreach ($aAttList as $sAttCode => $oAttDef) + { + if (!$oAttDef->IsScalar()) continue; + // keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue; + + if ($oAttDef->IsBasedOnOQLExpression()) + { + $oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode, new FieldExpression($sAttCode, $sClassAlias)); + } + else + { + foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) + { + $oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias)); + } + } + } + } + //echo "

    oQBExpr ".__LINE__.":

    \n".print_r($oBuild->m_oQBExpressions, true)."

    \n"; + $aExpectedAtts = array(); // array of (attcode => fieldexpression) + //echo "

    ".__LINE__.": GetUnresolvedFields($sClassAlias, ...)

    \n"; + $oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts); + + // Compute a clear view of required joins (from the current class) + // Build the list of external keys: + // -> ext keys required by an explicit join + // -> ext keys mentionned in a 'pointing to' condition + // -> ext keys required for an external field + // -> ext keys required for a friendly name + // + $aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef)) + // + // Optimization: could be partially computed once for all (cached) ? + // + + if ($bIsOnQueriedClass) + { + // Get all Ext keys for the queried class (??) + foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode) + { + $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); + $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); + } + } + // Get all Ext keys used by the filter + foreach ($this->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo) + { + if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo)) + { + $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); + $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); + } + } + + $aFNJoinAlias = array(); // array of (subclass => alias) + foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression) + { + if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue; + $oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode); + if ($oAttDef->IsBasedOnOQLExpression()) + { + // To optimize: detect a restriction on child classes in the condition expression + // e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine') + $oExpression = static::GetPolymorphicExpression($sClass, $sExpectedAttCode); + + $aRequiredFields = array(); + $oExpression->GetUnresolvedFields('', $aRequiredFields); + $aTranslateFields = array(); + foreach($aRequiredFields as $sSubClass => $aFields) + { + foreach($aFields as $sAttCode => $oField) + { + $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); + if ($oAttDef->IsExternalKey()) + { + $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); + $aExtKeys[$sClassOfAttribute][$sAttCode] = array(); + } + elseif ($oAttDef->IsExternalField()) + { + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode); + $aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef; + } + else + { + $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); + } + + if (MetaModel::IsParentClass($sClassOfAttribute, $sClass)) + { + // The attribute is part of the standard query + // + $sAliasForAttribute = $sClassAlias; + } + else + { + // The attribute will be available from an additional outer join + // For each subclass (table) one single join is enough + // + if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias)) + { + $sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute); + $aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute; + } + else + { + $sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute]; + } + } + + $aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute); + } + } + $oExpression = $oExpression->Translate($aTranslateFields, false); + + $aTranslateNow = array(); + $aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression; + $oBuild->m_oQBExpressions->Translate($aTranslateNow, false); + } + } + + // Add the ext fields used in the select (eventually adds an external key) + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) + { + if ($oAttDef->IsExternalField()) + { + if (array_key_exists($sAttCode, $aExpectedAtts)) + { + // Add the external attribute + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); + $aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef; + } + } + } + + // First query built from the root, adding all tables including the leaf + // Before N.1065 we were joining from the leaf first, but this wasn't a good choice : + // most of the time (obsolescence, friendlyname, ...) we want to get a root attribute ! + // + $oSelectBase = null; + $aClassHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, true); + $bIsClassStandaloneClass = (count($aClassHierarchy) == 1); + foreach($aClassHierarchy as $sSomeClass) + { + if (!MetaModel::HasTable($sSomeClass)) + { + continue; + } + + self::DbgTrace("Adding join from root to leaf: $sSomeClass... let's call MakeSQLObjectQuerySingleTable()"); + $oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSomeClass, $aExtKeys, $aValues); + if (is_null($oSelectBase)) + { + $oSelectBase = $oSelectParentTable; + if (!$bIsClassStandaloneClass && (MetaModel::IsRootClass($sSomeClass))) + { + // As we're linking to root class first, we're adding a where clause on the finalClass attribute : + // COALESCE($sRootClassFinalClass IN ('$sExpectedClasses'), 1) + // If we don't, the child classes can be removed in the query optimisation phase, including the leaf that was queried + // So we still need to filter records to only those corresponding to the child classes ! + // The coalesce is mandatory if we have a polymorphic query (left join) + $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); + $sFinalClassSqlColumnName = MetaModel::DBGetClassField($sSomeClass); + $oClassExpr = new FieldExpression($sFinalClassSqlColumnName, $oSelectBase->GetTableAlias()); + $oInExpression = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); + $oTrueExpression = new TrueExpression(); + $aCoalesceAttr = array($oInExpression, $oTrueExpression); + $oFinalClassRestriction = new FunctionExpression("COALESCE", $aCoalesceAttr); + + $oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction); + } + } + else + { + $oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass)); + } + } + + // Filter on objects referencing me + // + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) + { + foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) + { + foreach ($aFilters as $oForeignFilter) + { + $oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); + + self::DbgTrace("Referenced by foreign key: $sForeignExtKeyAttCode... let's call MakeSQLObjectQuery()"); + //self::DbgTrace($oForeignFilter); + //self::DbgTrace($oForeignFilter->ToOQL()); + //self::DbgTrace($oSelectForeign); + //self::DbgTrace($oSelectForeign->RenderSelect(array())); + + $sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias(); + $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias)); + + if ($oForeignKeyAttDef instanceof AttributeObjectKey) + { + $sClassAttCode = $oForeignKeyAttDef->Get('class_attcode'); + + // Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass') + $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); + $oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias); + $oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); + $oBuild->m_oQBExpressions->AddCondition($oClassRestriction); + } + + $oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); + + $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); + $sForeignKeyTable = $oJoinExpr->GetParent(); + $sForeignKeyColumn = $oJoinExpr->GetName(); + + if ($iOperatorCode == TREE_OPERATOR_EQUALS) + { + $oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable); + } + else + { + // Hierarchical key + $KeyLeft = $oForeignKeyAttDef->GetSQLLeft(); + $KeyRight = $oForeignKeyAttDef->GetSQLRight(); + + $oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true); + } + } + } + } + } + + // Additional JOINS for Friendly names + // + foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias) + { + $oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias); + $oSelectFN = $oSubClassFilter->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSubClass, $aExtKeys, array()); + $oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass)); + } + + // That's all... cross fingers and we'll get some working query + + //MyHelpers::var_dump_html($oSelectBase, true); + //MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true); + if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml(); + return $oSelectBase; + } + + protected function MakeSQLObjectQuerySingleTable(&$oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues) + { + // $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields)) + + // Prepare the query for a single table (compound objects) + // Ignores the items (attributes/filters) that are not on the target table + // Perform an (inner or left) join for every external key (and specify the expected fields) + // + // Returns an SQLQuery + // + $sTargetClass = $this->GetFirstJoinedClass(); + $sTargetAlias = $this->GetFirstJoinedClassAlias(); + $sTable = MetaModel::DBGetTable($sTableClass); + $sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable); + + $aTranslation = array(); + $aExpectedAtts = array(); + $oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts); + + $bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses()); + + self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY")); + + // 1 - SELECT and UPDATE + // + // Note: no need for any values nor fields for foreign Classes (ie not the queried Class) + // + $aUpdateValues = array(); + + + // 1/a - Get the key and friendly name + // + // We need one pkey to be the key, let's take the first one available + $oSelectedIdField = null; + $oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias); + $aTranslation[$sTargetAlias]['id'] = $oIdField; + + if ($bIsOnQueriedClass) + { + // Add this field to the list of queried fields (required for the COUNT to work fine) + $oSelectedIdField = $oIdField; + } + + // 1/b - Get the other attributes + // + foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue; + + // Skip this attribute if not made of SQL columns + if (count($oAttDef->GetSQLExpressions()) == 0) continue; + + // Update... + // + if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues)) + { + assert ($oAttDef->IsBasedOnDBColumns()); + foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue) + { + $aUpdateValues[$sColumn] = $sValue; + } + } + } + + // 2 - The SQL query, for this table only + // + $oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField); + + // 3 - Resolve expected expressions (translation table: alias.attcode => table.column) + // + foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) + { + // Skip this attribute if not defined in this table + if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue; + + // Select... + // + if ($oAttDef->IsExternalField()) + { + // skip, this will be handled in the joined tables (done hereabove) + } + else + { + // standard field, or external key + // add it to the output + foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) + { + if (array_key_exists($sAttCode.$sColId, $aExpectedAtts)) + { + $oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias); + foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier) + { + $oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase); + } + $aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp; + } + } + } + } + + // 4 - The external keys -> joins... + // + $aAllPointingTo = $this->GetCriteria_PointingTo(); + + if (array_key_exists($sTableClass, $aExtKeys)) + { + foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields) + { + $oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode); + + $aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode); + if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo)) + { + // The join was not explicitely defined in the filter, + // we need to do it now + $sKeyClass = $oKeyAttDef->GetTargetClass(); + $sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass); + $oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias); + + $aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter; + } + } + } + + foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oExtFilter) + { + if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it + // The aliases should not conflict because normalization occured while building the filter + $oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode); + $sKeyClass = $oExtFilter->GetFirstJoinedClass(); + $sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias(); + + // Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree + + if ($iOperatorCode == TREE_OPERATOR_EQUALS) + { + if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass])) + { + // Specify expected attributes for the target class query + // ... and use the current alias ! + $aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...) + foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt) + { + $oExtAttDef = $oAtt->GetExtAttDef(); + if ($oExtAttDef->IsBasedOnOQLExpression()) + { + $aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression($oExtAttDef->GetCode(), $sKeyClassAlias); + } + else + { + $sExtAttCode = $oAtt->GetExtAttCode(); + // Translate mainclass.extfield => remoteclassalias.remotefieldcode + $oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode); + foreach ($oRemoteAttDef->GetSQLExpressions() as $sColId => $sRemoteAttExpr) + { + $aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias); + } + } + } + + if ($oKeyAttDef instanceof AttributeObjectKey) + { + // Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass') + $sClassAttCode = $oKeyAttDef->Get('class_attcode'); + $oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode); + foreach ($oClassAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) + { + $aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias); + } + + $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL)); + $oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias); + $oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); + $oBuild->m_oQBExpressions->AddCondition($oClassRestriction); + } + + // Translate prior to recursing + // + $oBuild->m_oQBExpressions->Translate($aTranslateNow, false); + + self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()"); + $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias)); + + $oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); + + $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); + $sExternalKeyTable = $oJoinExpr->GetParent(); + $sExternalKeyField = $oJoinExpr->GetName(); + + $aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) + $sLocalKeyField = current($aCols); // get the first column for an external key + + self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField"); + if ($oKeyAttDef->IsNullAllowed()) + { + $oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField); + } + else + { + $oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable); + } + } + } + elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass) + { + $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias)); + $oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); + $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); + $sExternalKeyTable = $oJoinExpr->GetParent(); + $sExternalKeyField = $oJoinExpr->GetName(); + $sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft() + $sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight() + + $LocalKeyLeft = $oKeyAttDef->GetSQLLeft(); + $LocalKeyRight = $oKeyAttDef->GetSQLRight(); + + $oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode); + } + } + } + } + + // Translate the selected columns + // + $oBuild->m_oQBExpressions->Translate($aTranslation, false); + + // Filter out archived records + // + if (MetaModel::IsArchivable($sTableClass)) + { + if (!$oBuild->GetRootFilter()->GetArchiveMode()) + { + $bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses()); + if ($bIsOnJoinedClass) + { + if (MetaModel::IsParentClass($sTableClass, $sTargetClass)) + { + $oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0)); + $oBuild->AddFilteredTable($sTableAlias, $oNotArchived); + } + } + } + } + return $oSelectBase; + } + + /** + * Get the expression for the class and its subclasses (if finalclass = 'subclass' ...) + * Simplifies the final expression by grouping classes having the same expression + * @param $sClass + * @param $sAttCode + * @return \FunctionExpression|mixed|null + * @throws \CoreException +*/ + static public function GetPolymorphicExpression($sClass, $sAttCode) + { + $oExpression = ExpressionCache::GetCachedExpression($sClass, $sAttCode); + if (!empty($oExpression)) + { + return $oExpression; + } + + // 1st step - get all of the required expressions (instantiable classes) + // and group them using their OQL representation + // + $aExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes) + foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass) + { + if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue; + + $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); + $oSubClassExp = $oAttDef->GetOQLExpression($sSubClass); + + // 3rd step - position the attributes in the hierarchy of classes + // + $oSubClassExp->Browse(function($oNode) use ($sSubClass) { + if ($oNode instanceof FieldExpression) + { + $sAttCode = $oNode->GetName(); + $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); + if ($oAttDef->IsExternalField()) + { + $sKeyAttCode = $oAttDef->GetKeyAttCode(); + $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode); + } + else + { + $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); + } + $sParent = MetaModel::GetAttributeOrigin($sClassOfAttribute, $oNode->GetName()); + $oNode->SetParent($sParent); + } + }); + + $sSignature = $oSubClassExp->Render(); + if (!array_key_exists($sSignature, $aExpressions)) + { + $aExpressions[$sSignature] = array( + 'expression' => $oSubClassExp, + 'classes' => array(), + ); + } + $aExpressions[$sSignature]['classes'][] = $sSubClass; + } + + // 2nd step - build the final name expression depending on the finalclass + // + if (count($aExpressions) == 1) + { + $aExpData = reset($aExpressions); + $oExpression = $aExpData['expression']; + } + else + { + $oExpression = null; + foreach ($aExpressions as $sSignature => $aExpData) + { + $oClassListExpr = ListExpression::FromScalars($aExpData['classes']); + $oClassExpr = new FieldExpression('finalclass', $sClass); + $oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); + + if (is_null($oExpression)) + { + $oExpression = $aExpData['expression']; + } + else + { + $oExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oExpression)); + } + } + } + return $oExpression; + } +} diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index d4b19ccfd..79718081a 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -1,1685 +1,1685 @@ - - -require_once('dbobjectiterator.php'); - -/** - * Object set management - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * A set of persistent objects, could be heterogeneous as long as the objects in the set have a common ancestor class - * - * @package iTopORM - */ -class DBObjectSet implements iDBObjectSetIterator -{ - /** - * @var array - */ - protected $m_aAddedIds; // Ids of objects added (discrete lists) - /** - * @var array array of (row => array of (classalias) => object/null) storing the objects added "in memory" - */ - protected $m_aAddedObjects; - /** - * @var array - */ - protected $m_aArgs; - /** - * @var array - */ - protected $m_aAttToLoad; - /** - * @var array - */ - protected $m_aOrderBy; - /** - * @var bool True when the filter has been used OR the set is built step by step (AddObject...) - */ - public $m_bLoaded; - /** - * @var int Total number of rows for the query without LIMIT. null if unknown yet - */ - protected $m_iNumTotalDBRows; - /** - * @var int Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default - */ - protected $m_iNumLoadedDBRows; - /** - * @var int - */ - protected $m_iCurrRow; - /** - * @var DBSearch - */ - protected $m_oFilter; - /** - * @var mysqli_result - */ - protected $m_oSQLResult; - protected $m_bSort; - - /** - * Create a new set based on a Search definition. - * - * @param DBSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported) - * @param array $aOrderBy Array of '[.]attcode' => bAscending - * @param array $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value - * @param array $aExtendedDataSpec - * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count) - * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count) - * @param bool $bSort if false no order by is done - */ - public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bSort = true) - { - $this->m_oFilter = $oFilter->DeepClone(); - $this->m_aAddedIds = array(); - $this->m_aOrderBy = $aOrderBy; - $this->m_aArgs = $aArgs; - $this->m_aAttToLoad = null; - $this->m_aExtendedDataSpec = $aExtendedDataSpec; - $this->m_iLimitCount = $iLimitCount; - $this->m_iLimitStart = $iLimitStart; - $this->m_bSort = $bSort; - - $this->m_iNumTotalDBRows = null; - $this->m_iNumLoadedDBRows = 0; - $this->m_bLoaded = false; - $this->m_aAddedObjects = array(); - $this->m_iCurrRow = 0; - $this->m_oSQLResult = null; - } - - public function __destruct() - { - if (is_object($this->m_oSQLResult)) - { - $this->m_oSQLResult->free(); - } - } - - /** - * @return string - * - * @throws \Exception - * @throws \CoreException - * @throws \MissingQueryArgument - */ - public function __toString() - { - $sRet = ''; - $this->Rewind(); - $sRet .= "Set (".$this->m_oFilter->ToOQL().")
    \n"; - $sRet .= "Query:
    ".$this->m_oFilter->MakeSelectQuery().")
    \n"; - - $sRet .= $this->Count()." records
    \n"; - if ($this->Count() > 0) - { - $sRet .= "
      \n"; - while ($oObj = $this->Fetch()) - { - $sRet .= "
    • ".$oObj->__toString()."
    • \n"; - } - $sRet .= "
    \n"; - } - return $sRet; - } - - public function __clone() - { - $this->m_oFilter = $this->m_oFilter->DeepClone(); - - $this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet - $this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default - $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...) - $this->m_iCurrRow = 0; - $this->m_oSQLResult = null; - } - - /** - * Called when unserializing a DBObjectSet - */ - public function __wakeup() - { - $this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet - $this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default - $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...) - $this->m_iCurrRow = 0; - $this->m_oSQLResult = null; - } - - public function SetShowObsoleteData($bShow) - { - $this->m_oFilter->SetShowObsoleteData($bShow); - } - - public function GetShowObsoleteData() - { - return $this->m_oFilter->GetShowObsoleteData(); - } - - /** - * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB - * - * @param array $aAttToLoad Format: alias => array of attribute_codes - * - * @return void - * - * @throws \Exception - * @throws \CoreException - */ - public function OptimizeColumnLoad($aAttToLoad) - { - if (is_null($aAttToLoad)) - { - $this->m_aAttToLoad = null; - } - else - { - // Complete the attribute list with the attribute codes - $aAttToLoadWithAttDef = array(); - foreach($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) - { - $aAttToLoadWithAttDef[$sClassAlias] = array(); - if (array_key_exists($sClassAlias, $aAttToLoad)) - { - $aAttList = $aAttToLoad[$sClassAlias]; - foreach($aAttList as $sAttToLoad) - { - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad); - $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad] = $oAttDef; - if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) - { - // Add the external key friendly name anytime - $oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_friendlyname'); - $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_friendlyname'] = $oFriendlyNameAttDef; - - if (MetaModel::IsArchivable($oAttDef->GetTargetClass(EXTKEY_ABSOLUTE))) - { - // Add the archive flag if necessary - $oArchiveFlagAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_archive_flag'); - $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_archive_flag'] = $oArchiveFlagAttDef; - } - - if (MetaModel::IsObsoletable($oAttDef->GetTargetClass(EXTKEY_ABSOLUTE))) - { - // Add the obsolescence flag if necessary - $oObsoleteFlagAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_obsolescence_flag'); - $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_obsolescence_flag'] = $oObsoleteFlagAttDef; - } - } - } - } - // Add the friendly name anytime - $oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, 'friendlyname'); - $aAttToLoadWithAttDef[$sClassAlias]['friendlyname'] = $oFriendlyNameAttDef; - - if (MetaModel::IsArchivable($sClass)) - { - // Add the archive flag if necessary - $oArchiveFlagAttDef = MetaModel::GetAttributeDef($sClass, 'archive_flag'); - $aAttToLoadWithAttDef[$sClassAlias]['archive_flag'] = $oArchiveFlagAttDef; - } - - if (MetaModel::IsObsoletable($sClass)) - { - // Add the obsolescence flag if necessary - $oObsoleteFlagAttDef = MetaModel::GetAttributeDef($sClass, 'obsolescence_flag'); - $aAttToLoadWithAttDef[$sClassAlias]['obsolescence_flag'] = $oObsoleteFlagAttDef; - } - - // Make sure that the final class is requested anytime, whatever the specification (needed for object construction!) - if (!MetaModel::IsStandaloneClass($sClass) && !array_key_exists('finalclass', $aAttToLoadWithAttDef[$sClassAlias])) - { - $aAttToLoadWithAttDef[$sClassAlias]['finalclass'] = MetaModel::GetAttributeDef($sClass, 'finalclass'); - } - } - - $this->m_aAttToLoad = $aAttToLoadWithAttDef; - } - } - - /** - * Create a set (in-memory) containing just the given object - * - * @param \DBobject $oObject - * - * @return \DBObjectSet The singleton set - * - * @throws \Exception - */ - static public function FromObject($oObject) - { - $oRetSet = self::FromScratch(get_class($oObject)); - $oRetSet->AddObject($oObject); - return $oRetSet; - } - - /** - * Create an empty set (in-memory), for the given class (and its subclasses) of objects - * - * @param string $sClass The class (or an ancestor) for the objects to be added in this set - * - * @return \DBObjectSet The empty set - * - * @throws \Exception - */ - static public function FromScratch($sClass) - { - $oFilter = new DBObjectSearch($sClass); - $oFilter->AddConditionExpression(new FalseExpression()); - $oRetSet = new self($oFilter); - $oRetSet->m_bLoaded = true; // no DB load - $oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB - return $oRetSet; - } - - /** - * Create a set (in-memory) with just one column (i.e. one object per row) and filled with the given array of objects - * - * @param string $sClass The class of the objects (must be a common ancestor to all objects in the set) - * @param array $aObjects The list of objects to add into the set - * - * @return \DBObjectSet - * - * @throws \Exception - */ - static public function FromArray($sClass, $aObjects) - { - $oRetSet = self::FromScratch($sClass); - $oRetSet->AddObjectArray($aObjects, $sClass); - return $oRetSet; - } - - /** - * Create a set in-memory with several classes of objects per row (with one alias per "column") - * - * Limitation: - * The filter/OQL query representing such a set can not be rebuilt (only the first column will be taken into account) - * - * @param array $aClasses Format: array of (alias => class) - * @param array $aObjects Format: array of (array of (classalias => object)) - * - * @return \DBObjectSet - * - * @throws \Exception - */ - static public function FromArrayAssoc($aClasses, $aObjects) - { - // In a perfect world, we should create a complete tree of DBObjectSearch, - // but as we lack most of the information related to the objects, - // let's create one search definition corresponding only to the first column - $sClass = reset($aClasses); - $sAlias = key($aClasses); - $oFilter = new DBObjectSearch($sClass, $sAlias); - - $oRetSet = new self($oFilter); - $oRetSet->m_bLoaded = true; // no DB load - $oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB - - foreach($aObjects as $rowIndex => $aObjectsByClassAlias) - { - $oRetSet->AddObjectExtended($aObjectsByClassAlias); - } - return $oRetSet; - } - - /** - * @param $oObject - * @param string $sLinkSetAttCode - * @param string $sExtKeyToRemote - * - * @return \DBObjectSet - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - */static public function FromLinkSet($oObject, $sLinkSetAttCode, $sExtKeyToRemote) - { - $oLinkAttCode = MetaModel::GetAttributeDef(get_class($oObject), $sLinkSetAttCode); - $oExtKeyAttDef = MetaModel::GetAttributeDef($oLinkAttCode->GetLinkedClass(), $sExtKeyToRemote); - $sTargetClass = $oExtKeyAttDef->GetTargetClass(); - - $oLinkSet = $oObject->Get($sLinkSetAttCode); - $aTargets = array(); - while ($oLink = $oLinkSet->Fetch()) - { - $aTargets[] = MetaModel::GetObject($sTargetClass, $oLink->Get($sExtKeyToRemote)); - } - - return self::FromArray($sTargetClass, $aTargets); - } - - /** - * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. - * - * @param bool $bWithId - * - * @return array - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function ToArray($bWithId = true) - { - $aRet = array(); - $this->Rewind(); - while ($oObject = $this->Fetch()) - { - if ($bWithId) - { - $aRet[$oObject->GetKey()] = $oObject; - } - else - { - $aRet[] = $oObject; - } - } - return $aRet; - } - - /** - * @return array - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function ToArrayOfValues() - { - if (!$this->m_bLoaded) $this->Load(); - $this->Rewind(); - - $aSelectedClasses = $this->m_oFilter->GetSelectedClasses(); - - $aRet = array(); - $iRow = 0; - while($aObjects = $this->FetchAssoc()) - { - foreach($aObjects as $sClassAlias => $oObject) - { - if (is_null($oObject)) - { - $aRet[$iRow][$sClassAlias.'.'.'id'] = null; - } - else - { - $aRet[$iRow][$sClassAlias.'.'.'id'] = $oObject->GetKey(); - } - if (is_null($oObject)) - { - $sClass = $aSelectedClasses[$sClassAlias]; - } - else - { - $sClass = get_class($oObject); - } - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsScalar()) - { - $sAttName = $sClassAlias.'.'.$sAttCode; - if (is_null($oObject)) - { - $aRet[$iRow][$sAttName] = null; - } - else - { - $aRet[$iRow][$sAttName] = $oObject->Get($sAttCode); - } - } - } - } - $iRow++; - } - return $aRet; - } - - /** - * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. - * - * @param string $sAttCode - * @param bool $bWithId - * - * @return array - * - * @throws \Exception - * @throws \CoreException - */ - public function GetColumnAsArray($sAttCode, $bWithId = true) - { - $aRet = array(); - $this->Rewind(); - while ($oObject = $this->Fetch()) - { - if ($bWithId) - { - $aRet[$oObject->GetKey()] = $oObject->Get($sAttCode); - } - else - { - $aRet[] = $oObject->Get($sAttCode); - } - } - return $aRet; - } - - /** - * Retrieve the DBSearch corresponding to the objects present in this set - * - * Limitation: - * This method will NOT work for sets with several columns (i.e. several objects per row) - * - * @return \DBObjectSearch - * - * @throws \CoreException - */ - public function GetFilter() - { - // Make sure that we carry on the parameters of the set with the filter - $oFilter = $this->m_oFilter->DeepClone(); - $oFilter->SetShowObsoleteData(true); - // Note: the arguments found within a set can be object (but not in a filter) - // That's why PrepareQueryArguments must be invoked there - $oFilter->SetInternalParams(array_merge($oFilter->GetInternalParams(), $this->m_aArgs)); - - if (count($this->m_aAddedIds) == 0) - { - return $oFilter; - } - else - { - $oIdListExpr = ListExpression::FromScalars(array_keys($this->m_aAddedIds)); - $oIdExpr = new FieldExpression('id', $oFilter->GetClassAlias()); - $oIdInList = new BinaryExpression($oIdExpr, 'IN', $oIdListExpr); - $oFilter->MergeConditionExpression($oIdInList); - return $oFilter; - } - } - - /** - * The (common ancestor) class of the objects in the first column of this set - * - * @return string The class of the objects in the first column - */ - public function GetClass() - { - return $this->m_oFilter->GetClass(); - } - - /** - * The alias for the class of the objects in the first column of this set - * - * @return string The alias of the class in the first column - */ - public function GetClassAlias() - { - return $this->m_oFilter->GetClassAlias(); - } - - /** - * The list of all classes (one per column) which are part of this set - * - * @return array Format: alias => class - */ - public function GetSelectedClasses() - { - return $this->m_oFilter->GetSelectedClasses(); - } - - /** - * The root class (i.e. highest ancestor in the MeaModel class hierarchy) for the first column on this set - * - * @return string The root class for the objects in the first column of the set - * - * @throws \CoreException - */ - public function GetRootClass() - { - return MetaModel::GetRootClass($this->GetClass()); - } - - /** - * The arguments used for building this set - * - * @return array Format: parameter_name => value - */ - public function GetArgs() - { - return $this->m_aArgs; - } - - /** - * Sets the limits for loading the rows from the DB. Equivalent to MySQL's LIMIT start,count clause. - * @param int $iLimitCount The number of rows to load - * @param int $iLimitStart The index of the first row to load - */ - public function SetLimit($iLimitCount, $iLimitStart = 0) - { - $this->m_iLimitCount = $iLimitCount; - $this->m_iLimitStart = $iLimitStart; - } - - /** - * Sets the sort order for loading the rows from the DB. Changing the order by causes a Reload. - * - * @param array $aOrderBy Format: [alias.]attcode => boolean (true = ascending, false = descending) - * - * @throws \MySQLException - */ - public function SetOrderBy($aOrderBy) - { - if ($this->m_aOrderBy != $aOrderBy) - { - $this->m_aOrderBy = $aOrderBy; - if ($this->m_bLoaded) - { - $this->m_bLoaded = false; - $this->Load(); - } - } - } - - /** - * Sets the sort order for loading the rows from the DB. Changing the order by causes a Reload. - * - * @param array $aAliases Format: alias => boolean (true = ascending, false = descending). If omitted, then it defaults to all the selected classes - * - * @throws \CoreException - * @throws \MySQLException - */ - public function SetOrderByClasses($aAliases = null) - { - if ($aAliases === null) - { - $aAliases = array(); - foreach ($this->GetSelectedClasses() as $sAlias => $sClass) - { - $aAliases[$sAlias] = true; - } - } - - $aAttributes = array(); - foreach ($aAliases as $sAlias => $bClassDirection) - { - foreach (MetaModel::GetOrderByDefault($this->m_oFilter->GetClassName($sAlias)) as $sAttCode => $bAttributeDirection) - { - $bDirection = $bClassDirection ? $bAttributeDirection : !$bAttributeDirection; - $aAttributes[$sAlias.'.'.$sAttCode] = $bDirection; - } - } - $this->SetOrderBy($aAttributes); - } - - /** - * Returns the 'count' limit for loading the rows from the DB - * - * @return int - */ - public function GetLimitCount() - { - return $this->m_iLimitCount; - } - - /** - * Returns the 'start' limit for loading the rows from the DB - * - * @return int - */ - public function GetLimitStart() - { - return $this->m_iLimitStart; - } - - /** - * Get the sort order used for loading this set from the database - * - * Limitation: the sort order has no effect on objects added in-memory - * - * @return array Format: field_code => boolean (true = ascending, false = descending) - * - * @throws \CoreException - */ - public function GetRealSortOrder() - { - if (!$this->m_bSort) - { - // No order by - return array(); - } - // Get the class default sort order if not specified with the API - // - if (empty($this->m_aOrderBy)) - { - return MetaModel::GetOrderByDefault($this->m_oFilter->GetClass()); - } - else - { - return $this->m_aOrderBy; - } - } - - /** - * Loads the set from the database. Actually performs the SQL query to retrieve the records from the DB. - * - * @throws \Exception - * @throws \MySQLException - */ - public function Load() - { - if ($this->m_bLoaded) return; - // Note: it is mandatory to set this value now, to protect against reentrance - $this->m_bLoaded = true; - - $sSQL = $this->_makeSelectQuery($this->m_aAttToLoad); - - if (is_object($this->m_oSQLResult)) - { - // Free previous resultset if any - $this->m_oSQLResult->free(); - $this->m_oSQLResult = null; - } - - try - { - $this->m_oSQLResult = CMDBSource::Query($sSQL); - } catch (MySQLException $e) - { - // 1116 = ER_TOO_MANY_TABLES - // https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_too_many_tables - if ($e->getCode() != 1116) - { - throw $e; - } - - // N.689 Workaround for the 61 max joins in MySQL : full lazy load ! - $aAttToLoad = array(); - foreach($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) - { - $aAttToLoad[$sClassAlias] = array(); - $bIsAbstractClass = MetaModel::IsAbstract($sClass); - $bIsClassWithChildren = MetaModel::HasChildrenClasses($sClass); - if ($bIsAbstractClass || $bIsClassWithChildren) - { - // we need finalClass field at least to be able to instantiate the real corresponding object ! - $aAttToLoad[$sClassAlias]['finalclass'] = MetaModel::GetAttributeDef($sClass, 'finalclass'); - } - } - $sSQL = $this->_makeSelectQuery($aAttToLoad); - $this->m_oSQLResult = CMDBSource::Query($sSQL); // may fail again - } - - if ($this->m_oSQLResult === false) return; - - if ((($this->m_iLimitCount == 0) || ($this->m_iLimitCount > $this->m_oSQLResult->num_rows)) && ($this->m_iLimitStart == 0)) - { - $this->m_iNumTotalDBRows = $this->m_oSQLResult->num_rows; - } - - $this->m_iNumLoadedDBRows = $this->m_oSQLResult->num_rows; - } - - /** - * @param string[] $aAttToLoad - * - * @return string SQL query - * - * @throws \CoreException - * @throws \MissingQueryArgument - */ - private function _makeSelectQuery($aAttToLoad) - { - if ($this->m_iLimitCount > 0) - { - $sSQL = $this->m_oFilter->MakeSelectQuery($this->GetRealSortOrder(), $this->m_aArgs, $aAttToLoad, - $this->m_aExtendedDataSpec, $this->m_iLimitCount, $this->m_iLimitStart); - } - else - { - $sSQL = $this->m_oFilter->MakeSelectQuery($this->GetRealSortOrder(), $this->m_aArgs, $aAttToLoad, - $this->m_aExtendedDataSpec); - } - - return $sSQL; - } - - /** - * The total number of rows in this set. Independently of the SetLimit used for loading the set and taking into - * account the rows added in-memory. - * - * May actually perform the SQL query SELECT COUNT... if the set was not previously loaded, or loaded with a - * SetLimit - * - * @return int The total number of rows for this set. - * - * @throws \CoreException - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public function Count() - { - if (is_null($this->m_iNumTotalDBRows)) - { - $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true); - $resQuery = CMDBSource::Query($sSQL); - if (!$resQuery) return 0; - - $aRow = CMDBSource::FetchArray($resQuery); - CMDBSource::FreeResult($resQuery); - $this->m_iNumTotalDBRows = intval($aRow['COUNT']); - } - - return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ?? - } - - /** Check if the count exceeds a given limit - * @param $iLimit - * - * @return bool - * - * @throws \CoreException - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public function CountExceeds($iLimit) - { - if (is_null($this->m_iNumTotalDBRows)) - { - $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); - $resQuery = CMDBSource::Query($sSQL); - if ($resQuery) - { - $aRow = CMDBSource::FetchArray($resQuery); - CMDBSource::FreeResult($resQuery); - $iCount = intval($aRow['COUNT']); - } - else - { - $iCount = 0; - } - } - else - { - $iCount = $this->m_iNumTotalDBRows; - } - - return ($iCount > $iLimit); - } - - /** Count only up to the given limit - * @param $iLimit - * - * @return int - * - * @throws \CoreException - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public function CountWithLimit($iLimit) - { - if (is_null($this->m_iNumTotalDBRows)) - { - $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); - $resQuery = CMDBSource::Query($sSQL); - if ($resQuery) - { - $aRow = CMDBSource::FetchArray($resQuery); - CMDBSource::FreeResult($resQuery); - $iCount = intval($aRow['COUNT']); - } - else - { - $iCount = 0; - } - } - else - { - $iCount = $this->m_iNumTotalDBRows; - } - - return $iCount; - } - - /** - * Number of rows available in memory (loaded from DB + added in memory) - * - * @return number The number of rows available for Fetch'ing - */ - protected function CountLoaded() - { - return $this->m_iNumLoadedDBRows + count($this->m_aAddedObjects); - } - - /** - * Fetch the object (with the given class alias) at the current position in the set and move the cursor to the next position. - * - * @param string $sRequestedClassAlias The class alias to fetch (if there are several objects/classes per row) - * - * @return \DBObject The fetched object or null when at the end - * - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function Fetch($sRequestedClassAlias = '') - { - if (!$this->m_bLoaded) $this->Load(); - - if ($this->m_iCurrRow >= $this->CountLoaded()) - { - return null; - } - - if (strlen($sRequestedClassAlias) == 0) - { - $sRequestedClassAlias = $this->m_oFilter->GetClassAlias(); - } - - if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows) - { - // Pick the row from the database - $aRow = CMDBSource::FetchArray($this->m_oSQLResult); - foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) - { - if ($sRequestedClassAlias == $sClassAlias) - { - if (is_null($aRow[$sClassAlias.'id'])) - { - $oRetObj = null; - } - else - { - $oRetObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec); - } - break; - } - } - } - else - { - // Pick the row from the objects added *in memory* - $oRetObj = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sRequestedClassAlias]; - } - $this->m_iCurrRow++; - return $oRetObj; - } - - /** - * Fetch the whole row of objects (if several classes have been specified in the query) and move the cursor to the next position - * - * @return array An associative with the format 'classAlias' => $oObj representing the current row of the set. Returns null when at the end. - * - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function FetchAssoc() - { - if (!$this->m_bLoaded) $this->Load(); - - if ($this->m_iCurrRow >= $this->CountLoaded()) - { - return null; - } - - if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows) - { - // Pick the row from the database - $aRow = CMDBSource::FetchArray($this->m_oSQLResult); - $aRetObjects = array(); - foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) - { - if (is_null($aRow[$sClassAlias.'id'])) - { - $oObj = null; - } - else - { - $oObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec); - } - $aRetObjects[$sClassAlias] = $oObj; - } - } - else - { - // Pick the row from the objects added *in memory* - $aRetObjects = array(); - foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) - { - $aRetObjects[$sClassAlias] = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sClassAlias]; - } - } - $this->m_iCurrRow++; - return $aRetObjects; - } - - /** - * Position the cursor (for iterating in the set) to the first position (equivalent to Seek(0)) - * - * @throws \Exception - */ - public function Rewind() - { - if ($this->m_bLoaded) - { - $this->Seek(0); - } - } - - /** - * Position the cursor (for iterating in the set) to the given position - * - * @param int $iRow - * - * @return int|mixed - * - * @throws \CoreException - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public function Seek($iRow) - { - if (!$this->m_bLoaded) $this->Load(); - - $this->m_iCurrRow = min($iRow, $this->Count()); - if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows) - { - $this->m_oSQLResult->data_seek($this->m_iCurrRow); - } - return $this->m_iCurrRow; - } - - /** - * Add an object to the current set (in-memory only, nothing is written to the database) - * - * Limitation: - * Sets with several objects per row are NOT supported - * - * @param \DBObject $oObject The object to add - * @param string $sClassAlias The alias for the class of the object - * - * @throws \MySQLException - */ - public function AddObject($oObject, $sClassAlias = '') - { - if (!$this->m_bLoaded) $this->Load(); - - if (strlen($sClassAlias) == 0) - { - $sClassAlias = $this->m_oFilter->GetClassAlias(); - } - - $iNextPos = count($this->m_aAddedObjects); - $this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject; - if (!is_null($oObject)) - { - $this->m_aAddedIds[$oObject->GetKey()] = true; - } - } - - /** - * Add a hash containig objects into the current set. - * - * The expected format for the hash is: $aObjectArray[$idx][$sClassAlias] => $oObject - * Limitation: - * The aliases MUST match the ones used in the current set - * Only the ID of the objects associated to the first alias (column) is remembered.. in case we have to rebuild a filter - * - * @param array $aObjectArray - * - * @throws \MySQLException - */ - protected function AddObjectExtended($aObjectArray) - { - if (!$this->m_bLoaded) $this->Load(); - - $iNextPos = count($this->m_aAddedObjects); - - $sFirstAlias = $this->m_oFilter->GetClassAlias(); - - foreach ($aObjectArray as $sClassAlias => $oObject) - { - $this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject; - - if (!is_null($oObject) && ($sFirstAlias == $sClassAlias)) - { - $this->m_aAddedIds[$oObject->GetKey()] = true; - } - } - } - - /** - * Add an array of objects into the current set - * - * Limitation: - * Sets with several classes per row are not supported (use AddObjectExtended instead) - * - * @param array $aObjects The array of objects to add - * @param string $sClassAlias The Alias of the class for the added objects - * - * @throws \MySQLException - */ - public function AddObjectArray($aObjects, $sClassAlias = '') - { - if (!$this->m_bLoaded) $this->Load(); - - // #@# todo - add a check on the object class ? - foreach ($aObjects as $oObj) - { - $this->AddObject($oObj, $sClassAlias); - } - } - - /** - * Append a given set to the current object. (This method used to be named Merge) - * - * Limitation: - * The added objects are not checked for duplicates (i.e. one cann add several times the same object, or add an object already present in the set). - * - * @param \DBObjectSet $oObjectSet The set to append - * - * @throws \CoreException - */ - public function Append(DBObjectSet $oObjectSet) - { - if ($this->GetRootClass() != $oObjectSet->GetRootClass()) - { - throw new CoreException("Could not merge two objects sets if they don't have the same root class"); - } - if (!$this->m_bLoaded) $this->Load(); - - $oObjectSet->Seek(0); - while ($oObject = $oObjectSet->Fetch()) - { - $this->AddObject($oObject); - } - } - - /** - * Create a set containing the objects present in both the current set and another specified set - * - * Limitations: - * Will NOT work if only a subset of the sets was loaded with SetLimit. - * Works only with sets made of objects loaded from the database since the comparison is based on the objects identifiers - * - * @param \DBObjectSet $oObjectSet The set to intersect with. The current position inside the set will be lost (= at the end) - * - * @return \DBObjectSet A new set of objects, containing the objects present in both sets (based on their identifier) - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - */ - public function CreateIntersect(DBObjectSet $oObjectSet) - { - if ($this->GetRootClass() != $oObjectSet->GetRootClass()) - { - throw new CoreException("Could not 'intersect' two objects sets if they don't have the same root class"); - } - if (!$this->m_bLoaded) $this->Load(); - - $aId2Row = array(); - $iCurrPos = $this->m_iCurrRow; // Save the cursor - $idx = 0; - while($oObj = $this->Fetch()) - { - $aId2Row[$oObj->GetKey()] = $idx; - $idx++; - } - - $oNewSet = DBObjectSet::FromScratch($this->GetClass()); - - $oObjectSet->Seek(0); - while ($oObject = $oObjectSet->Fetch()) - { - if (array_key_exists($oObject->GetKey(), $aId2Row)) - { - $oNewSet->AddObject($oObject); - } - } - $this->Seek($iCurrPos); // Restore the cursor - return $oNewSet; - } - - /** - * Compare two sets of objects to determine if their content is identical or not. - * - * Limitation: - * Works only for sets of 1 column (i.e. one class of object selected) - * - * @param \DBObjectSet $oObjectSet - * @param array $aExcludeColumns The list of columns to exclude frop the comparison - * - * @return boolean True if the sets are identical, false otherwise - * - * @throws \CoreException - */ - public function HasSameContents(DBObjectSet $oObjectSet, $aExcludeColumns = array()) - { - $oComparator = new DBObjectSetComparator($this, $oObjectSet, $aExcludeColumns); - return $oComparator->SetsAreEquivalent(); - } - - /** - * Build a new set (in memory) made of objects of the given set which are NOT present in the current set - * - * Limitations: - * The objects inside the set must be written in the database since the comparison is based on their identifiers - * Sets with several objects per row are NOT supported - * - * @param \DBObjectSet $oObjectSet - * - * @return \DBObjectSet The "delta" set. - * - * @throws \Exception - * @throws \CoreException - */ - public function CreateDelta(DBObjectSet $oObjectSet) - { - if ($this->GetRootClass() != $oObjectSet->GetRootClass()) - { - throw new CoreException("Could not 'delta' two objects sets if they don't have the same root class"); - } - if (!$this->m_bLoaded) $this->Load(); - - $aId2Row = array(); - $iCurrPos = $this->m_iCurrRow; // Save the cursor - $idx = 0; - while($oObj = $this->Fetch()) - { - $aId2Row[$oObj->GetKey()] = $idx; - $idx++; - } - - $oNewSet = DBObjectSet::FromScratch($this->GetClass()); - - $oObjectSet->Seek(0); - while ($oObject = $oObjectSet->Fetch()) - { - if (!array_key_exists($oObject->GetKey(), $aId2Row)) - { - $oNewSet->AddObject($oObject); - } - } - $this->Seek($iCurrPos); // Restore the cursor - return $oNewSet; - } - - /** - * Will be deprecated soon - use MetaModel::GetRelatedObjectsDown/Up instead to take redundancy into account - * - * @throws \Exception - */ - public function GetRelatedObjects($sRelCode, $iMaxDepth = 99) - { - $aRelatedObjs = array(); - - $aVisited = array(); // optimization for consecutive calls of MetaModel::GetRelatedObjects - $this->Seek(0); - while ($oObject = $this->Fetch()) - { - $aMore = $oObject->GetRelatedObjects($sRelCode, $iMaxDepth, $aVisited); - foreach ($aMore as $sClass => $aRelated) - { - foreach ($aRelated as $iObj => $oObj) - { - if (!isset($aRelatedObjs[$sClass][$iObj])) - { - $aRelatedObjs[$sClass][$iObj] = $oObj; - } - } - } - } - return $aRelatedObjs; - } - - /** - * Compute the "RelatedObjects" (forward or "down" direction) for the set - * for the specified relation - * - * @param string $sRelCode The code of the relation to use for the computation - * @param int $iMaxDepth Maximum recursion depth - * @param bool $bEnableRedundancy - * - * @return \RelationGraph The graph of all the related objects - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) - { - $oGraph = new RelationGraph(); - $this->Rewind(); - while($oObj = $this->Fetch()) - { - $oGraph->AddSourceObject($oObj); - } - $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy); - return $oGraph; - } - - /** - * Compute the "RelatedObjects" (reverse or "up" direction) for the set - * for the specified relation - * - * @param string $sRelCode The code of the relation to use for the computation - * @param int $iMaxDepth Maximum recursion depth - * @param bool $bEnableRedundancy - * - * @return \RelationGraph The graph of all the related objects - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) - { - $oGraph = new RelationGraph(); - $this->Rewind(); - while($oObj = $this->Fetch()) - { - $oGraph->AddSinkObject($oObj); - } - $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); - return $oGraph; - } - - /** - * Builds an object that contains the values that are common to all the objects - * in the set. If for a given attribute, objects in the set have various values - * then the resulting object will contain null for this value. - * - * @param array $aValues Hash Output: the distribution of the values, in the set, for each attribute - * - * @return \DBObject The object with the common values - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MySQLException - */ - public function ComputeCommonObject(&$aValues) - { - $sClass = $this->GetClass(); - $aList = MetaModel::ListAttributeDefs($sClass); - $aValues = array(); - foreach($aList as $sAttCode => $oAttDef) - { - if ($oAttDef->IsScalar()) - { - $aValues[$sAttCode] = array(); - } - } - $this->Rewind(); - while($oObj = $this->Fetch()) - { - foreach($aList as $sAttCode => $oAttDef) - { - if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) - { - $currValue = $oObj->Get($sAttCode); - if (is_object($currValue)) continue; // Skip non scalar values... - if(!array_key_exists($currValue, $aValues[$sAttCode])) - { - $aValues[$sAttCode][$currValue] = array('count' => 1, 'display' => $oObj->GetAsHTML($sAttCode)); - } - else - { - $aValues[$sAttCode][$currValue]['count']++; - } - } - } - } - - foreach($aValues as $sAttCode => $aMultiValues) - { - if (count($aMultiValues) > 1) - { - uasort($aValues[$sAttCode], 'HashCountComparison'); - } - } - - - // Now create an object that has values for the homogenous values only - $oCommonObj = new $sClass(); // @@ What if the class is abstract ? - $aComments = array(); - - $iFormId = cmdbAbstractObject::GetNextFormId(); // Identifier that prefixes all the form fields - $sReadyScript = ''; - $aDependsOn = array(); - $sFormPrefix = '2_'; - foreach($aList as $sAttCode => $oAttDef) - { - if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) - { - if ($oAttDef->GetEditClass() == 'One Way Password') - { - $oCommonObj->Set($sAttCode, null); - } - else - { - $iCount = count($aValues[$sAttCode]); - if ($iCount == 1) - { - // Homogenous value - reset($aValues[$sAttCode]); - $aKeys = array_keys($aValues[$sAttCode]); - $currValue = $aKeys[0]; // The only value is the first key - $oCommonObj->Set($sAttCode, $currValue); - } - else - { - // Non-homogenous value - $oCommonObj->Set($sAttCode, null); - } - } - } - } - $this->Rewind(); - return $oCommonObj; - } - - /** - * List the constant fields (and their value) in the given query - * @return array [Alias][AttCode] => value - */ - public function ListConstantFields() - { - // The complete list of arguments will include magic arguments (e.g. current_user->attcode) - $aScalarArgs = MetaModel::PrepareQueryArguments($this->m_oFilter->GetInternalParams(), $this->m_aArgs); - $aConst = $this->m_oFilter->ListConstantFields(); - - foreach($aConst as $sClassAlias => $aVals) - { - foreach($aVals as $sCode => $oExpr) - { - if (is_object($oExpr)) // Array_merge_recursive creates an array when the same key is present multiple times... ignore them - { - $oScalarExpr = $oExpr->GetAsScalar($aScalarArgs); - $aConst[$sClassAlias][$sCode] = $oScalarExpr->GetValue(); - } - } - } - return $aConst; - } - - public function ApplyParameters() - { - $aAllArgs = MetaModel::PrepareQueryArguments($this->m_oFilter->GetInternalParams(), $this->m_aArgs); - $this->m_oFilter->ApplyParameters($aAllArgs); - } -} - -/** - * Helper function to perform a custom sort of a hash array - */ -function HashCountComparison($a, $b) // Sort descending on 'count' -{ - if ($a['count'] == $b['count']) - { - return 0; - } - return ($a['count'] > $b['count']) ? -1 : 1; -} - -/** - * Helper class to compare the content of two DBObjectSets based on the fingerprints of the contained objects - * The FIRST SET MUST BE LOADED FROM THE DATABASE, the second one can be a set of objects in memory - * When computing the actual differences, the algorithm tries to preserve as much as possible the EXISTING - * objects (i.e. prefers 'modified' to 'removed' + 'added') - * - * LIMITATIONS: - * - only DBObjectSets with one column (i.e. one class of object selected) are supported - * - the first set must be the one loaded from the database - */ -class DBObjectSetComparator -{ - protected $aFingerprints1; - protected $aFingerprints2; - protected $aIDs1; - protected $aIDs2; - protected $aExcludedColumns; - - /** - * @var iDBObjectSetIterator - */ - protected $oSet1; - /** - * @var iDBObjectSetIterator - */ - protected $oSet2; - - protected $sAdditionalKeyColumn; - protected $aAdditionalKeys; - - /** - * Initializes the comparator - * @param iDBObjectSetIterator $oSet1 The first set of objects to compare, or null - * @param iDBObjectSetIterator $oSet2 The second set of objects to compare, or null - * @param array $aExcludedColumns The list of columns (= attribute codes) to exclude from the comparison - * @param string $sAdditionalKeyColumn The attribute code of an additional column to be considered as a key indentifying the object (useful for n:n links) - */ - public function __construct(iDBObjectSetIterator $oSet1, iDBObjectSetIterator $oSet2, $aExcludedColumns = array(), $sAdditionalKeyColumn = null) - { - $this->aFingerprints1 = null; - $this->aFingerprints2 = null; - $this->aIDs1 = array(); - $this->aIDs2 = array(); - $this->aExcludedColumns = $aExcludedColumns; - $this->sAdditionalKeyColumn = $sAdditionalKeyColumn; - $this->aAdditionalKeys = null; - $this->oSet1 = $oSet1; - $this->oSet2 = $oSet2; - } - - /** - * Builds the lists of fingerprints and initializes internal structures, if it was not already done - * - * @throws \CoreException - */ - protected function ComputeFingerprints() - { - if ($this->aFingerprints1 === null) - { - $this->aFingerprints1 = array(); - $this->aFingerprints2 = array(); - $this->aAdditionalKeys = array(); - - if ($this->oSet1 !== null) - { - $this->oSet1->Rewind(); - while($oObj = $this->oSet1->Fetch()) - { - $sFingerprint = $oObj->Fingerprint($this->aExcludedColumns); - $this->aFingerprints1[$sFingerprint] = $oObj; - if (!$oObj->IsNew()) - { - $this->aIDs1[$oObj->GetKey()] = $oObj; - } - } - $this->oSet1->Rewind(); - } - - if ($this->oSet2 !== null) - { - $this->oSet2->Rewind(); - while($oObj = $this->oSet2->Fetch()) - { - $sFingerprint = $oObj->Fingerprint($this->aExcludedColumns); - $this->aFingerprints2[$sFingerprint] = $oObj; - if (!$oObj->IsNew()) - { - $this->aIDs2[$oObj->GetKey()] = $oObj; - } - - if ($this->sAdditionalKeyColumn !== null) - { - $this->aAdditionalKeys[$oObj->Get($this->sAdditionalKeyColumn)] = $oObj; - } - } - $this->oSet2->Rewind(); - } - } - } - - /** - * Tells if the sets are equivalent or not. Returns as soon as the first difference is found. - * @return boolean true if the set have an equivalent content, false otherwise - * - * @throws \CoreException - */ - public function SetsAreEquivalent() - { - if (($this->oSet1 === null) && ($this->oSet2 === null)) - { - // Both sets are empty, they are equal - return true; - } - else if (($this->oSet1 === null) || ($this->oSet2 === null)) - { - // one of them is empty, they are different - return false; - } - - if (($this->oSet1->GetRootClass() != $this->oSet2->GetRootClass()) || ($this->oSet1->Count() != $this->oSet2->Count())) return false; - - $this->ComputeFingerprints(); - - // Check that all objects in Set1 are also in Set2 - foreach($this->aFingerprints1 as $sFingerprint => $oObj) - { - if (!array_key_exists($sFingerprint, $this->aFingerprints2)) - { - return false; - } - } - - // Vice versa - // Check that all objects in Set2 are also in Set1 - foreach($this->aFingerprints2 as $sFingerprint => $oObj) - { - if (!array_key_exists($sFingerprint, $this->aFingerprints1)) - { - return false; - } - } - - return true; - } - - /** - * Get the list of differences between the two sets. In ordeer to write back into the database only the minimum changes - * THE FIRST SET MUST BE THE ONE LOADED FROM THE DATABASE - * Returns a hash: 'added' => DBObject(s), 'removed' => DBObject(s), 'modified' => DBObjects(s) - * @return array - * - * @throws \Exception - * @throws \CoreException - */ - public function GetDifferences() - { - $aResult = array('added' => array(), 'removed' => array(), 'modified' => array()); - $this->ComputeFingerprints(); - - // Check that all objects in Set1 are also in Set2 - foreach($this->aFingerprints1 as $sFingerprint => $oObj) - { - // Beware: the elements from the first set MUST come from the database, otherwise the result will be irrelevant - if ($oObj->IsNew()) throw new Exception('Cannot compute differences when elements from the first set are NOT in the database'); - if (array_key_exists($oObj->GetKey(), $this->aIDs2) && ($this->aIDs2[$oObj->GetKey()]->IsModified())) - { - // The very same object exists in both set, but was modified since its load - $aResult['modified'][$oObj->GetKey()] = $this->aIDs2[$oObj->GetKey()]; - } - else if (($this->sAdditionalKeyColumn !== null) && array_key_exists($oObj->Get($this->sAdditionalKeyColumn), $this->aAdditionalKeys)) - { - // Special case for n:n links where the link is recreated between the very same 2 objects, but some of its attributes are modified - // Let's consider this as a "modification" instead of "deletion" + "creation" in order to have a "clean" history for the objects - $oDestObj = $this->aAdditionalKeys[$oObj->Get($this->sAdditionalKeyColumn)]; - $oCloneObj = $this->CopyFrom($oObj, $oDestObj); - $aResult['modified'][$oObj->GetKey()] = $oCloneObj; - // Mark this as processed, so that the pass on aFingerprints2 below ignores this object - $sNewFingerprint = $oDestObj->Fingerprint($this->aExcludedColumns); - $this->aFingerprints2[$sNewFingerprint] = $oCloneObj; - } - else if (!array_key_exists($sFingerprint, $this->aFingerprints2)) - { - $aResult['removed'][] = $oObj; - } - } - - // Vice versa - // Check that all objects in Set2 are also in Set1 - foreach($this->aFingerprints2 as $sFingerprint => $oObj) - { - if (array_key_exists($oObj->GetKey(), $this->aIDs1) && ($oObj->IsModified())) - { - // Already marked as modified above - //$aResult['modified'][$oObj->GetKey()] = $oObj; - } - else if (!array_key_exists($sFingerprint, $this->aFingerprints1)) - { - $aResult['added'][] = $oObj; - } - } - return $aResult; - } - - /** - * Helpr to clone (in memory) an object and to apply to it the values taken from a second object - * - * @param \DBObject $oObjToClone - * @param \DBObject $oObjWithValues - * - * @return \DBObject The modified clone - * - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - */ - protected function CopyFrom($oObjToClone, $oObjWithValues) - { - $oObj = MetaModel::GetObject(get_class($oObjToClone), $oObjToClone->GetKey()); - foreach(MetaModel::ListAttributeDefs(get_class($oObj)) as $sAttCode => $oAttDef) - { - if (!in_array($sAttCode, $this->aExcludedColumns) && $oAttDef->IsWritable()) - { - $oObj->Set($sAttCode, $oObjWithValues->Get($sAttCode)); - } - } - return $oObj; - } + + +require_once('dbobjectiterator.php'); + +/** + * Object set management + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * A set of persistent objects, could be heterogeneous as long as the objects in the set have a common ancestor class + * + * @package iTopORM + */ +class DBObjectSet implements iDBObjectSetIterator +{ + /** + * @var array + */ + protected $m_aAddedIds; // Ids of objects added (discrete lists) + /** + * @var array array of (row => array of (classalias) => object/null) storing the objects added "in memory" + */ + protected $m_aAddedObjects; + /** + * @var array + */ + protected $m_aArgs; + /** + * @var array + */ + protected $m_aAttToLoad; + /** + * @var array + */ + protected $m_aOrderBy; + /** + * @var bool True when the filter has been used OR the set is built step by step (AddObject...) + */ + public $m_bLoaded; + /** + * @var int Total number of rows for the query without LIMIT. null if unknown yet + */ + protected $m_iNumTotalDBRows; + /** + * @var int Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default + */ + protected $m_iNumLoadedDBRows; + /** + * @var int + */ + protected $m_iCurrRow; + /** + * @var DBSearch + */ + protected $m_oFilter; + /** + * @var mysqli_result + */ + protected $m_oSQLResult; + protected $m_bSort; + + /** + * Create a new set based on a Search definition. + * + * @param DBSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported) + * @param array $aOrderBy Array of '[.]attcode' => bAscending + * @param array $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value + * @param array $aExtendedDataSpec + * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count) + * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count) + * @param bool $bSort if false no order by is done + */ + public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bSort = true) + { + $this->m_oFilter = $oFilter->DeepClone(); + $this->m_aAddedIds = array(); + $this->m_aOrderBy = $aOrderBy; + $this->m_aArgs = $aArgs; + $this->m_aAttToLoad = null; + $this->m_aExtendedDataSpec = $aExtendedDataSpec; + $this->m_iLimitCount = $iLimitCount; + $this->m_iLimitStart = $iLimitStart; + $this->m_bSort = $bSort; + + $this->m_iNumTotalDBRows = null; + $this->m_iNumLoadedDBRows = 0; + $this->m_bLoaded = false; + $this->m_aAddedObjects = array(); + $this->m_iCurrRow = 0; + $this->m_oSQLResult = null; + } + + public function __destruct() + { + if (is_object($this->m_oSQLResult)) + { + $this->m_oSQLResult->free(); + } + } + + /** + * @return string + * + * @throws \Exception + * @throws \CoreException + * @throws \MissingQueryArgument + */ + public function __toString() + { + $sRet = ''; + $this->Rewind(); + $sRet .= "Set (".$this->m_oFilter->ToOQL().")
    \n"; + $sRet .= "Query:
    ".$this->m_oFilter->MakeSelectQuery().")
    \n"; + + $sRet .= $this->Count()." records
    \n"; + if ($this->Count() > 0) + { + $sRet .= "
      \n"; + while ($oObj = $this->Fetch()) + { + $sRet .= "
    • ".$oObj->__toString()."
    • \n"; + } + $sRet .= "
    \n"; + } + return $sRet; + } + + public function __clone() + { + $this->m_oFilter = $this->m_oFilter->DeepClone(); + + $this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet + $this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default + $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...) + $this->m_iCurrRow = 0; + $this->m_oSQLResult = null; + } + + /** + * Called when unserializing a DBObjectSet + */ + public function __wakeup() + { + $this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet + $this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default + $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...) + $this->m_iCurrRow = 0; + $this->m_oSQLResult = null; + } + + public function SetShowObsoleteData($bShow) + { + $this->m_oFilter->SetShowObsoleteData($bShow); + } + + public function GetShowObsoleteData() + { + return $this->m_oFilter->GetShowObsoleteData(); + } + + /** + * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB + * + * @param array $aAttToLoad Format: alias => array of attribute_codes + * + * @return void + * + * @throws \Exception + * @throws \CoreException + */ + public function OptimizeColumnLoad($aAttToLoad) + { + if (is_null($aAttToLoad)) + { + $this->m_aAttToLoad = null; + } + else + { + // Complete the attribute list with the attribute codes + $aAttToLoadWithAttDef = array(); + foreach($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) + { + $aAttToLoadWithAttDef[$sClassAlias] = array(); + if (array_key_exists($sClassAlias, $aAttToLoad)) + { + $aAttList = $aAttToLoad[$sClassAlias]; + foreach($aAttList as $sAttToLoad) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad); + $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad] = $oAttDef; + if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) + { + // Add the external key friendly name anytime + $oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_friendlyname'); + $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_friendlyname'] = $oFriendlyNameAttDef; + + if (MetaModel::IsArchivable($oAttDef->GetTargetClass(EXTKEY_ABSOLUTE))) + { + // Add the archive flag if necessary + $oArchiveFlagAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_archive_flag'); + $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_archive_flag'] = $oArchiveFlagAttDef; + } + + if (MetaModel::IsObsoletable($oAttDef->GetTargetClass(EXTKEY_ABSOLUTE))) + { + // Add the obsolescence flag if necessary + $oObsoleteFlagAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_obsolescence_flag'); + $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_obsolescence_flag'] = $oObsoleteFlagAttDef; + } + } + } + } + // Add the friendly name anytime + $oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, 'friendlyname'); + $aAttToLoadWithAttDef[$sClassAlias]['friendlyname'] = $oFriendlyNameAttDef; + + if (MetaModel::IsArchivable($sClass)) + { + // Add the archive flag if necessary + $oArchiveFlagAttDef = MetaModel::GetAttributeDef($sClass, 'archive_flag'); + $aAttToLoadWithAttDef[$sClassAlias]['archive_flag'] = $oArchiveFlagAttDef; + } + + if (MetaModel::IsObsoletable($sClass)) + { + // Add the obsolescence flag if necessary + $oObsoleteFlagAttDef = MetaModel::GetAttributeDef($sClass, 'obsolescence_flag'); + $aAttToLoadWithAttDef[$sClassAlias]['obsolescence_flag'] = $oObsoleteFlagAttDef; + } + + // Make sure that the final class is requested anytime, whatever the specification (needed for object construction!) + if (!MetaModel::IsStandaloneClass($sClass) && !array_key_exists('finalclass', $aAttToLoadWithAttDef[$sClassAlias])) + { + $aAttToLoadWithAttDef[$sClassAlias]['finalclass'] = MetaModel::GetAttributeDef($sClass, 'finalclass'); + } + } + + $this->m_aAttToLoad = $aAttToLoadWithAttDef; + } + } + + /** + * Create a set (in-memory) containing just the given object + * + * @param \DBobject $oObject + * + * @return \DBObjectSet The singleton set + * + * @throws \Exception + */ + static public function FromObject($oObject) + { + $oRetSet = self::FromScratch(get_class($oObject)); + $oRetSet->AddObject($oObject); + return $oRetSet; + } + + /** + * Create an empty set (in-memory), for the given class (and its subclasses) of objects + * + * @param string $sClass The class (or an ancestor) for the objects to be added in this set + * + * @return \DBObjectSet The empty set + * + * @throws \Exception + */ + static public function FromScratch($sClass) + { + $oFilter = new DBObjectSearch($sClass); + $oFilter->AddConditionExpression(new FalseExpression()); + $oRetSet = new self($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + $oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB + return $oRetSet; + } + + /** + * Create a set (in-memory) with just one column (i.e. one object per row) and filled with the given array of objects + * + * @param string $sClass The class of the objects (must be a common ancestor to all objects in the set) + * @param array $aObjects The list of objects to add into the set + * + * @return \DBObjectSet + * + * @throws \Exception + */ + static public function FromArray($sClass, $aObjects) + { + $oRetSet = self::FromScratch($sClass); + $oRetSet->AddObjectArray($aObjects, $sClass); + return $oRetSet; + } + + /** + * Create a set in-memory with several classes of objects per row (with one alias per "column") + * + * Limitation: + * The filter/OQL query representing such a set can not be rebuilt (only the first column will be taken into account) + * + * @param array $aClasses Format: array of (alias => class) + * @param array $aObjects Format: array of (array of (classalias => object)) + * + * @return \DBObjectSet + * + * @throws \Exception + */ + static public function FromArrayAssoc($aClasses, $aObjects) + { + // In a perfect world, we should create a complete tree of DBObjectSearch, + // but as we lack most of the information related to the objects, + // let's create one search definition corresponding only to the first column + $sClass = reset($aClasses); + $sAlias = key($aClasses); + $oFilter = new DBObjectSearch($sClass, $sAlias); + + $oRetSet = new self($oFilter); + $oRetSet->m_bLoaded = true; // no DB load + $oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB + + foreach($aObjects as $rowIndex => $aObjectsByClassAlias) + { + $oRetSet->AddObjectExtended($aObjectsByClassAlias); + } + return $oRetSet; + } + + /** + * @param $oObject + * @param string $sLinkSetAttCode + * @param string $sExtKeyToRemote + * + * @return \DBObjectSet + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + */static public function FromLinkSet($oObject, $sLinkSetAttCode, $sExtKeyToRemote) + { + $oLinkAttCode = MetaModel::GetAttributeDef(get_class($oObject), $sLinkSetAttCode); + $oExtKeyAttDef = MetaModel::GetAttributeDef($oLinkAttCode->GetLinkedClass(), $sExtKeyToRemote); + $sTargetClass = $oExtKeyAttDef->GetTargetClass(); + + $oLinkSet = $oObject->Get($sLinkSetAttCode); + $aTargets = array(); + while ($oLink = $oLinkSet->Fetch()) + { + $aTargets[] = MetaModel::GetObject($sTargetClass, $oLink->Get($sExtKeyToRemote)); + } + + return self::FromArray($sTargetClass, $aTargets); + } + + /** + * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. + * + * @param bool $bWithId + * + * @return array + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function ToArray($bWithId = true) + { + $aRet = array(); + $this->Rewind(); + while ($oObject = $this->Fetch()) + { + if ($bWithId) + { + $aRet[$oObject->GetKey()] = $oObject; + } + else + { + $aRet[] = $oObject; + } + } + return $aRet; + } + + /** + * @return array + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function ToArrayOfValues() + { + if (!$this->m_bLoaded) $this->Load(); + $this->Rewind(); + + $aSelectedClasses = $this->m_oFilter->GetSelectedClasses(); + + $aRet = array(); + $iRow = 0; + while($aObjects = $this->FetchAssoc()) + { + foreach($aObjects as $sClassAlias => $oObject) + { + if (is_null($oObject)) + { + $aRet[$iRow][$sClassAlias.'.'.'id'] = null; + } + else + { + $aRet[$iRow][$sClassAlias.'.'.'id'] = $oObject->GetKey(); + } + if (is_null($oObject)) + { + $sClass = $aSelectedClasses[$sClassAlias]; + } + else + { + $sClass = get_class($oObject); + } + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar()) + { + $sAttName = $sClassAlias.'.'.$sAttCode; + if (is_null($oObject)) + { + $aRet[$iRow][$sAttName] = null; + } + else + { + $aRet[$iRow][$sAttName] = $oObject->Get($sAttCode); + } + } + } + } + $iRow++; + } + return $aRet; + } + + /** + * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. + * + * @param string $sAttCode + * @param bool $bWithId + * + * @return array + * + * @throws \Exception + * @throws \CoreException + */ + public function GetColumnAsArray($sAttCode, $bWithId = true) + { + $aRet = array(); + $this->Rewind(); + while ($oObject = $this->Fetch()) + { + if ($bWithId) + { + $aRet[$oObject->GetKey()] = $oObject->Get($sAttCode); + } + else + { + $aRet[] = $oObject->Get($sAttCode); + } + } + return $aRet; + } + + /** + * Retrieve the DBSearch corresponding to the objects present in this set + * + * Limitation: + * This method will NOT work for sets with several columns (i.e. several objects per row) + * + * @return \DBObjectSearch + * + * @throws \CoreException + */ + public function GetFilter() + { + // Make sure that we carry on the parameters of the set with the filter + $oFilter = $this->m_oFilter->DeepClone(); + $oFilter->SetShowObsoleteData(true); + // Note: the arguments found within a set can be object (but not in a filter) + // That's why PrepareQueryArguments must be invoked there + $oFilter->SetInternalParams(array_merge($oFilter->GetInternalParams(), $this->m_aArgs)); + + if (count($this->m_aAddedIds) == 0) + { + return $oFilter; + } + else + { + $oIdListExpr = ListExpression::FromScalars(array_keys($this->m_aAddedIds)); + $oIdExpr = new FieldExpression('id', $oFilter->GetClassAlias()); + $oIdInList = new BinaryExpression($oIdExpr, 'IN', $oIdListExpr); + $oFilter->MergeConditionExpression($oIdInList); + return $oFilter; + } + } + + /** + * The (common ancestor) class of the objects in the first column of this set + * + * @return string The class of the objects in the first column + */ + public function GetClass() + { + return $this->m_oFilter->GetClass(); + } + + /** + * The alias for the class of the objects in the first column of this set + * + * @return string The alias of the class in the first column + */ + public function GetClassAlias() + { + return $this->m_oFilter->GetClassAlias(); + } + + /** + * The list of all classes (one per column) which are part of this set + * + * @return array Format: alias => class + */ + public function GetSelectedClasses() + { + return $this->m_oFilter->GetSelectedClasses(); + } + + /** + * The root class (i.e. highest ancestor in the MeaModel class hierarchy) for the first column on this set + * + * @return string The root class for the objects in the first column of the set + * + * @throws \CoreException + */ + public function GetRootClass() + { + return MetaModel::GetRootClass($this->GetClass()); + } + + /** + * The arguments used for building this set + * + * @return array Format: parameter_name => value + */ + public function GetArgs() + { + return $this->m_aArgs; + } + + /** + * Sets the limits for loading the rows from the DB. Equivalent to MySQL's LIMIT start,count clause. + * @param int $iLimitCount The number of rows to load + * @param int $iLimitStart The index of the first row to load + */ + public function SetLimit($iLimitCount, $iLimitStart = 0) + { + $this->m_iLimitCount = $iLimitCount; + $this->m_iLimitStart = $iLimitStart; + } + + /** + * Sets the sort order for loading the rows from the DB. Changing the order by causes a Reload. + * + * @param array $aOrderBy Format: [alias.]attcode => boolean (true = ascending, false = descending) + * + * @throws \MySQLException + */ + public function SetOrderBy($aOrderBy) + { + if ($this->m_aOrderBy != $aOrderBy) + { + $this->m_aOrderBy = $aOrderBy; + if ($this->m_bLoaded) + { + $this->m_bLoaded = false; + $this->Load(); + } + } + } + + /** + * Sets the sort order for loading the rows from the DB. Changing the order by causes a Reload. + * + * @param array $aAliases Format: alias => boolean (true = ascending, false = descending). If omitted, then it defaults to all the selected classes + * + * @throws \CoreException + * @throws \MySQLException + */ + public function SetOrderByClasses($aAliases = null) + { + if ($aAliases === null) + { + $aAliases = array(); + foreach ($this->GetSelectedClasses() as $sAlias => $sClass) + { + $aAliases[$sAlias] = true; + } + } + + $aAttributes = array(); + foreach ($aAliases as $sAlias => $bClassDirection) + { + foreach (MetaModel::GetOrderByDefault($this->m_oFilter->GetClassName($sAlias)) as $sAttCode => $bAttributeDirection) + { + $bDirection = $bClassDirection ? $bAttributeDirection : !$bAttributeDirection; + $aAttributes[$sAlias.'.'.$sAttCode] = $bDirection; + } + } + $this->SetOrderBy($aAttributes); + } + + /** + * Returns the 'count' limit for loading the rows from the DB + * + * @return int + */ + public function GetLimitCount() + { + return $this->m_iLimitCount; + } + + /** + * Returns the 'start' limit for loading the rows from the DB + * + * @return int + */ + public function GetLimitStart() + { + return $this->m_iLimitStart; + } + + /** + * Get the sort order used for loading this set from the database + * + * Limitation: the sort order has no effect on objects added in-memory + * + * @return array Format: field_code => boolean (true = ascending, false = descending) + * + * @throws \CoreException + */ + public function GetRealSortOrder() + { + if (!$this->m_bSort) + { + // No order by + return array(); + } + // Get the class default sort order if not specified with the API + // + if (empty($this->m_aOrderBy)) + { + return MetaModel::GetOrderByDefault($this->m_oFilter->GetClass()); + } + else + { + return $this->m_aOrderBy; + } + } + + /** + * Loads the set from the database. Actually performs the SQL query to retrieve the records from the DB. + * + * @throws \Exception + * @throws \MySQLException + */ + public function Load() + { + if ($this->m_bLoaded) return; + // Note: it is mandatory to set this value now, to protect against reentrance + $this->m_bLoaded = true; + + $sSQL = $this->_makeSelectQuery($this->m_aAttToLoad); + + if (is_object($this->m_oSQLResult)) + { + // Free previous resultset if any + $this->m_oSQLResult->free(); + $this->m_oSQLResult = null; + } + + try + { + $this->m_oSQLResult = CMDBSource::Query($sSQL); + } catch (MySQLException $e) + { + // 1116 = ER_TOO_MANY_TABLES + // https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_too_many_tables + if ($e->getCode() != 1116) + { + throw $e; + } + + // N.689 Workaround for the 61 max joins in MySQL : full lazy load ! + $aAttToLoad = array(); + foreach($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) + { + $aAttToLoad[$sClassAlias] = array(); + $bIsAbstractClass = MetaModel::IsAbstract($sClass); + $bIsClassWithChildren = MetaModel::HasChildrenClasses($sClass); + if ($bIsAbstractClass || $bIsClassWithChildren) + { + // we need finalClass field at least to be able to instantiate the real corresponding object ! + $aAttToLoad[$sClassAlias]['finalclass'] = MetaModel::GetAttributeDef($sClass, 'finalclass'); + } + } + $sSQL = $this->_makeSelectQuery($aAttToLoad); + $this->m_oSQLResult = CMDBSource::Query($sSQL); // may fail again + } + + if ($this->m_oSQLResult === false) return; + + if ((($this->m_iLimitCount == 0) || ($this->m_iLimitCount > $this->m_oSQLResult->num_rows)) && ($this->m_iLimitStart == 0)) + { + $this->m_iNumTotalDBRows = $this->m_oSQLResult->num_rows; + } + + $this->m_iNumLoadedDBRows = $this->m_oSQLResult->num_rows; + } + + /** + * @param string[] $aAttToLoad + * + * @return string SQL query + * + * @throws \CoreException + * @throws \MissingQueryArgument + */ + private function _makeSelectQuery($aAttToLoad) + { + if ($this->m_iLimitCount > 0) + { + $sSQL = $this->m_oFilter->MakeSelectQuery($this->GetRealSortOrder(), $this->m_aArgs, $aAttToLoad, + $this->m_aExtendedDataSpec, $this->m_iLimitCount, $this->m_iLimitStart); + } + else + { + $sSQL = $this->m_oFilter->MakeSelectQuery($this->GetRealSortOrder(), $this->m_aArgs, $aAttToLoad, + $this->m_aExtendedDataSpec); + } + + return $sSQL; + } + + /** + * The total number of rows in this set. Independently of the SetLimit used for loading the set and taking into + * account the rows added in-memory. + * + * May actually perform the SQL query SELECT COUNT... if the set was not previously loaded, or loaded with a + * SetLimit + * + * @return int The total number of rows for this set. + * + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public function Count() + { + if (is_null($this->m_iNumTotalDBRows)) + { + $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true); + $resQuery = CMDBSource::Query($sSQL); + if (!$resQuery) return 0; + + $aRow = CMDBSource::FetchArray($resQuery); + CMDBSource::FreeResult($resQuery); + $this->m_iNumTotalDBRows = intval($aRow['COUNT']); + } + + return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ?? + } + + /** Check if the count exceeds a given limit + * @param $iLimit + * + * @return bool + * + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public function CountExceeds($iLimit) + { + if (is_null($this->m_iNumTotalDBRows)) + { + $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); + $resQuery = CMDBSource::Query($sSQL); + if ($resQuery) + { + $aRow = CMDBSource::FetchArray($resQuery); + CMDBSource::FreeResult($resQuery); + $iCount = intval($aRow['COUNT']); + } + else + { + $iCount = 0; + } + } + else + { + $iCount = $this->m_iNumTotalDBRows; + } + + return ($iCount > $iLimit); + } + + /** Count only up to the given limit + * @param $iLimit + * + * @return int + * + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public function CountWithLimit($iLimit) + { + if (is_null($this->m_iNumTotalDBRows)) + { + $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); + $resQuery = CMDBSource::Query($sSQL); + if ($resQuery) + { + $aRow = CMDBSource::FetchArray($resQuery); + CMDBSource::FreeResult($resQuery); + $iCount = intval($aRow['COUNT']); + } + else + { + $iCount = 0; + } + } + else + { + $iCount = $this->m_iNumTotalDBRows; + } + + return $iCount; + } + + /** + * Number of rows available in memory (loaded from DB + added in memory) + * + * @return number The number of rows available for Fetch'ing + */ + protected function CountLoaded() + { + return $this->m_iNumLoadedDBRows + count($this->m_aAddedObjects); + } + + /** + * Fetch the object (with the given class alias) at the current position in the set and move the cursor to the next position. + * + * @param string $sRequestedClassAlias The class alias to fetch (if there are several objects/classes per row) + * + * @return \DBObject The fetched object or null when at the end + * + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function Fetch($sRequestedClassAlias = '') + { + if (!$this->m_bLoaded) $this->Load(); + + if ($this->m_iCurrRow >= $this->CountLoaded()) + { + return null; + } + + if (strlen($sRequestedClassAlias) == 0) + { + $sRequestedClassAlias = $this->m_oFilter->GetClassAlias(); + } + + if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows) + { + // Pick the row from the database + $aRow = CMDBSource::FetchArray($this->m_oSQLResult); + foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) + { + if ($sRequestedClassAlias == $sClassAlias) + { + if (is_null($aRow[$sClassAlias.'id'])) + { + $oRetObj = null; + } + else + { + $oRetObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec); + } + break; + } + } + } + else + { + // Pick the row from the objects added *in memory* + $oRetObj = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sRequestedClassAlias]; + } + $this->m_iCurrRow++; + return $oRetObj; + } + + /** + * Fetch the whole row of objects (if several classes have been specified in the query) and move the cursor to the next position + * + * @return array An associative with the format 'classAlias' => $oObj representing the current row of the set. Returns null when at the end. + * + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function FetchAssoc() + { + if (!$this->m_bLoaded) $this->Load(); + + if ($this->m_iCurrRow >= $this->CountLoaded()) + { + return null; + } + + if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows) + { + // Pick the row from the database + $aRow = CMDBSource::FetchArray($this->m_oSQLResult); + $aRetObjects = array(); + foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) + { + if (is_null($aRow[$sClassAlias.'id'])) + { + $oObj = null; + } + else + { + $oObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec); + } + $aRetObjects[$sClassAlias] = $oObj; + } + } + else + { + // Pick the row from the objects added *in memory* + $aRetObjects = array(); + foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass) + { + $aRetObjects[$sClassAlias] = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sClassAlias]; + } + } + $this->m_iCurrRow++; + return $aRetObjects; + } + + /** + * Position the cursor (for iterating in the set) to the first position (equivalent to Seek(0)) + * + * @throws \Exception + */ + public function Rewind() + { + if ($this->m_bLoaded) + { + $this->Seek(0); + } + } + + /** + * Position the cursor (for iterating in the set) to the given position + * + * @param int $iRow + * + * @return int|mixed + * + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public function Seek($iRow) + { + if (!$this->m_bLoaded) $this->Load(); + + $this->m_iCurrRow = min($iRow, $this->Count()); + if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows) + { + $this->m_oSQLResult->data_seek($this->m_iCurrRow); + } + return $this->m_iCurrRow; + } + + /** + * Add an object to the current set (in-memory only, nothing is written to the database) + * + * Limitation: + * Sets with several objects per row are NOT supported + * + * @param \DBObject $oObject The object to add + * @param string $sClassAlias The alias for the class of the object + * + * @throws \MySQLException + */ + public function AddObject($oObject, $sClassAlias = '') + { + if (!$this->m_bLoaded) $this->Load(); + + if (strlen($sClassAlias) == 0) + { + $sClassAlias = $this->m_oFilter->GetClassAlias(); + } + + $iNextPos = count($this->m_aAddedObjects); + $this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject; + if (!is_null($oObject)) + { + $this->m_aAddedIds[$oObject->GetKey()] = true; + } + } + + /** + * Add a hash containig objects into the current set. + * + * The expected format for the hash is: $aObjectArray[$idx][$sClassAlias] => $oObject + * Limitation: + * The aliases MUST match the ones used in the current set + * Only the ID of the objects associated to the first alias (column) is remembered.. in case we have to rebuild a filter + * + * @param array $aObjectArray + * + * @throws \MySQLException + */ + protected function AddObjectExtended($aObjectArray) + { + if (!$this->m_bLoaded) $this->Load(); + + $iNextPos = count($this->m_aAddedObjects); + + $sFirstAlias = $this->m_oFilter->GetClassAlias(); + + foreach ($aObjectArray as $sClassAlias => $oObject) + { + $this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject; + + if (!is_null($oObject) && ($sFirstAlias == $sClassAlias)) + { + $this->m_aAddedIds[$oObject->GetKey()] = true; + } + } + } + + /** + * Add an array of objects into the current set + * + * Limitation: + * Sets with several classes per row are not supported (use AddObjectExtended instead) + * + * @param array $aObjects The array of objects to add + * @param string $sClassAlias The Alias of the class for the added objects + * + * @throws \MySQLException + */ + public function AddObjectArray($aObjects, $sClassAlias = '') + { + if (!$this->m_bLoaded) $this->Load(); + + // #@# todo - add a check on the object class ? + foreach ($aObjects as $oObj) + { + $this->AddObject($oObj, $sClassAlias); + } + } + + /** + * Append a given set to the current object. (This method used to be named Merge) + * + * Limitation: + * The added objects are not checked for duplicates (i.e. one cann add several times the same object, or add an object already present in the set). + * + * @param \DBObjectSet $oObjectSet The set to append + * + * @throws \CoreException + */ + public function Append(DBObjectSet $oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + throw new CoreException("Could not merge two objects sets if they don't have the same root class"); + } + if (!$this->m_bLoaded) $this->Load(); + + $oObjectSet->Seek(0); + while ($oObject = $oObjectSet->Fetch()) + { + $this->AddObject($oObject); + } + } + + /** + * Create a set containing the objects present in both the current set and another specified set + * + * Limitations: + * Will NOT work if only a subset of the sets was loaded with SetLimit. + * Works only with sets made of objects loaded from the database since the comparison is based on the objects identifiers + * + * @param \DBObjectSet $oObjectSet The set to intersect with. The current position inside the set will be lost (= at the end) + * + * @return \DBObjectSet A new set of objects, containing the objects present in both sets (based on their identifier) + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public function CreateIntersect(DBObjectSet $oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + throw new CoreException("Could not 'intersect' two objects sets if they don't have the same root class"); + } + if (!$this->m_bLoaded) $this->Load(); + + $aId2Row = array(); + $iCurrPos = $this->m_iCurrRow; // Save the cursor + $idx = 0; + while($oObj = $this->Fetch()) + { + $aId2Row[$oObj->GetKey()] = $idx; + $idx++; + } + + $oNewSet = DBObjectSet::FromScratch($this->GetClass()); + + $oObjectSet->Seek(0); + while ($oObject = $oObjectSet->Fetch()) + { + if (array_key_exists($oObject->GetKey(), $aId2Row)) + { + $oNewSet->AddObject($oObject); + } + } + $this->Seek($iCurrPos); // Restore the cursor + return $oNewSet; + } + + /** + * Compare two sets of objects to determine if their content is identical or not. + * + * Limitation: + * Works only for sets of 1 column (i.e. one class of object selected) + * + * @param \DBObjectSet $oObjectSet + * @param array $aExcludeColumns The list of columns to exclude frop the comparison + * + * @return boolean True if the sets are identical, false otherwise + * + * @throws \CoreException + */ + public function HasSameContents(DBObjectSet $oObjectSet, $aExcludeColumns = array()) + { + $oComparator = new DBObjectSetComparator($this, $oObjectSet, $aExcludeColumns); + return $oComparator->SetsAreEquivalent(); + } + + /** + * Build a new set (in memory) made of objects of the given set which are NOT present in the current set + * + * Limitations: + * The objects inside the set must be written in the database since the comparison is based on their identifiers + * Sets with several objects per row are NOT supported + * + * @param \DBObjectSet $oObjectSet + * + * @return \DBObjectSet The "delta" set. + * + * @throws \Exception + * @throws \CoreException + */ + public function CreateDelta(DBObjectSet $oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + throw new CoreException("Could not 'delta' two objects sets if they don't have the same root class"); + } + if (!$this->m_bLoaded) $this->Load(); + + $aId2Row = array(); + $iCurrPos = $this->m_iCurrRow; // Save the cursor + $idx = 0; + while($oObj = $this->Fetch()) + { + $aId2Row[$oObj->GetKey()] = $idx; + $idx++; + } + + $oNewSet = DBObjectSet::FromScratch($this->GetClass()); + + $oObjectSet->Seek(0); + while ($oObject = $oObjectSet->Fetch()) + { + if (!array_key_exists($oObject->GetKey(), $aId2Row)) + { + $oNewSet->AddObject($oObject); + } + } + $this->Seek($iCurrPos); // Restore the cursor + return $oNewSet; + } + + /** + * Will be deprecated soon - use MetaModel::GetRelatedObjectsDown/Up instead to take redundancy into account + * + * @throws \Exception + */ + public function GetRelatedObjects($sRelCode, $iMaxDepth = 99) + { + $aRelatedObjs = array(); + + $aVisited = array(); // optimization for consecutive calls of MetaModel::GetRelatedObjects + $this->Seek(0); + while ($oObject = $this->Fetch()) + { + $aMore = $oObject->GetRelatedObjects($sRelCode, $iMaxDepth, $aVisited); + foreach ($aMore as $sClass => $aRelated) + { + foreach ($aRelated as $iObj => $oObj) + { + if (!isset($aRelatedObjs[$sClass][$iObj])) + { + $aRelatedObjs[$sClass][$iObj] = $oObj; + } + } + } + } + return $aRelatedObjs; + } + + /** + * Compute the "RelatedObjects" (forward or "down" direction) for the set + * for the specified relation + * + * @param string $sRelCode The code of the relation to use for the computation + * @param int $iMaxDepth Maximum recursion depth + * @param bool $bEnableRedundancy + * + * @return \RelationGraph The graph of all the related objects + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) + { + $oGraph = new RelationGraph(); + $this->Rewind(); + while($oObj = $this->Fetch()) + { + $oGraph->AddSourceObject($oObj); + } + $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy); + return $oGraph; + } + + /** + * Compute the "RelatedObjects" (reverse or "up" direction) for the set + * for the specified relation + * + * @param string $sRelCode The code of the relation to use for the computation + * @param int $iMaxDepth Maximum recursion depth + * @param bool $bEnableRedundancy + * + * @return \RelationGraph The graph of all the related objects + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) + { + $oGraph = new RelationGraph(); + $this->Rewind(); + while($oObj = $this->Fetch()) + { + $oGraph->AddSinkObject($oObj); + } + $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); + return $oGraph; + } + + /** + * Builds an object that contains the values that are common to all the objects + * in the set. If for a given attribute, objects in the set have various values + * then the resulting object will contain null for this value. + * + * @param array $aValues Hash Output: the distribution of the values, in the set, for each attribute + * + * @return \DBObject The object with the common values + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + */ + public function ComputeCommonObject(&$aValues) + { + $sClass = $this->GetClass(); + $aList = MetaModel::ListAttributeDefs($sClass); + $aValues = array(); + foreach($aList as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar()) + { + $aValues[$sAttCode] = array(); + } + } + $this->Rewind(); + while($oObj = $this->Fetch()) + { + foreach($aList as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) + { + $currValue = $oObj->Get($sAttCode); + if (is_object($currValue)) continue; // Skip non scalar values... + if(!array_key_exists($currValue, $aValues[$sAttCode])) + { + $aValues[$sAttCode][$currValue] = array('count' => 1, 'display' => $oObj->GetAsHTML($sAttCode)); + } + else + { + $aValues[$sAttCode][$currValue]['count']++; + } + } + } + } + + foreach($aValues as $sAttCode => $aMultiValues) + { + if (count($aMultiValues) > 1) + { + uasort($aValues[$sAttCode], 'HashCountComparison'); + } + } + + + // Now create an object that has values for the homogenous values only + $oCommonObj = new $sClass(); // @@ What if the class is abstract ? + $aComments = array(); + + $iFormId = cmdbAbstractObject::GetNextFormId(); // Identifier that prefixes all the form fields + $sReadyScript = ''; + $aDependsOn = array(); + $sFormPrefix = '2_'; + foreach($aList as $sAttCode => $oAttDef) + { + if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) + { + if ($oAttDef->GetEditClass() == 'One Way Password') + { + $oCommonObj->Set($sAttCode, null); + } + else + { + $iCount = count($aValues[$sAttCode]); + if ($iCount == 1) + { + // Homogenous value + reset($aValues[$sAttCode]); + $aKeys = array_keys($aValues[$sAttCode]); + $currValue = $aKeys[0]; // The only value is the first key + $oCommonObj->Set($sAttCode, $currValue); + } + else + { + // Non-homogenous value + $oCommonObj->Set($sAttCode, null); + } + } + } + } + $this->Rewind(); + return $oCommonObj; + } + + /** + * List the constant fields (and their value) in the given query + * @return array [Alias][AttCode] => value + */ + public function ListConstantFields() + { + // The complete list of arguments will include magic arguments (e.g. current_user->attcode) + $aScalarArgs = MetaModel::PrepareQueryArguments($this->m_oFilter->GetInternalParams(), $this->m_aArgs); + $aConst = $this->m_oFilter->ListConstantFields(); + + foreach($aConst as $sClassAlias => $aVals) + { + foreach($aVals as $sCode => $oExpr) + { + if (is_object($oExpr)) // Array_merge_recursive creates an array when the same key is present multiple times... ignore them + { + $oScalarExpr = $oExpr->GetAsScalar($aScalarArgs); + $aConst[$sClassAlias][$sCode] = $oScalarExpr->GetValue(); + } + } + } + return $aConst; + } + + public function ApplyParameters() + { + $aAllArgs = MetaModel::PrepareQueryArguments($this->m_oFilter->GetInternalParams(), $this->m_aArgs); + $this->m_oFilter->ApplyParameters($aAllArgs); + } +} + +/** + * Helper function to perform a custom sort of a hash array + */ +function HashCountComparison($a, $b) // Sort descending on 'count' +{ + if ($a['count'] == $b['count']) + { + return 0; + } + return ($a['count'] > $b['count']) ? -1 : 1; +} + +/** + * Helper class to compare the content of two DBObjectSets based on the fingerprints of the contained objects + * The FIRST SET MUST BE LOADED FROM THE DATABASE, the second one can be a set of objects in memory + * When computing the actual differences, the algorithm tries to preserve as much as possible the EXISTING + * objects (i.e. prefers 'modified' to 'removed' + 'added') + * + * LIMITATIONS: + * - only DBObjectSets with one column (i.e. one class of object selected) are supported + * - the first set must be the one loaded from the database + */ +class DBObjectSetComparator +{ + protected $aFingerprints1; + protected $aFingerprints2; + protected $aIDs1; + protected $aIDs2; + protected $aExcludedColumns; + + /** + * @var iDBObjectSetIterator + */ + protected $oSet1; + /** + * @var iDBObjectSetIterator + */ + protected $oSet2; + + protected $sAdditionalKeyColumn; + protected $aAdditionalKeys; + + /** + * Initializes the comparator + * @param iDBObjectSetIterator $oSet1 The first set of objects to compare, or null + * @param iDBObjectSetIterator $oSet2 The second set of objects to compare, or null + * @param array $aExcludedColumns The list of columns (= attribute codes) to exclude from the comparison + * @param string $sAdditionalKeyColumn The attribute code of an additional column to be considered as a key indentifying the object (useful for n:n links) + */ + public function __construct(iDBObjectSetIterator $oSet1, iDBObjectSetIterator $oSet2, $aExcludedColumns = array(), $sAdditionalKeyColumn = null) + { + $this->aFingerprints1 = null; + $this->aFingerprints2 = null; + $this->aIDs1 = array(); + $this->aIDs2 = array(); + $this->aExcludedColumns = $aExcludedColumns; + $this->sAdditionalKeyColumn = $sAdditionalKeyColumn; + $this->aAdditionalKeys = null; + $this->oSet1 = $oSet1; + $this->oSet2 = $oSet2; + } + + /** + * Builds the lists of fingerprints and initializes internal structures, if it was not already done + * + * @throws \CoreException + */ + protected function ComputeFingerprints() + { + if ($this->aFingerprints1 === null) + { + $this->aFingerprints1 = array(); + $this->aFingerprints2 = array(); + $this->aAdditionalKeys = array(); + + if ($this->oSet1 !== null) + { + $this->oSet1->Rewind(); + while($oObj = $this->oSet1->Fetch()) + { + $sFingerprint = $oObj->Fingerprint($this->aExcludedColumns); + $this->aFingerprints1[$sFingerprint] = $oObj; + if (!$oObj->IsNew()) + { + $this->aIDs1[$oObj->GetKey()] = $oObj; + } + } + $this->oSet1->Rewind(); + } + + if ($this->oSet2 !== null) + { + $this->oSet2->Rewind(); + while($oObj = $this->oSet2->Fetch()) + { + $sFingerprint = $oObj->Fingerprint($this->aExcludedColumns); + $this->aFingerprints2[$sFingerprint] = $oObj; + if (!$oObj->IsNew()) + { + $this->aIDs2[$oObj->GetKey()] = $oObj; + } + + if ($this->sAdditionalKeyColumn !== null) + { + $this->aAdditionalKeys[$oObj->Get($this->sAdditionalKeyColumn)] = $oObj; + } + } + $this->oSet2->Rewind(); + } + } + } + + /** + * Tells if the sets are equivalent or not. Returns as soon as the first difference is found. + * @return boolean true if the set have an equivalent content, false otherwise + * + * @throws \CoreException + */ + public function SetsAreEquivalent() + { + if (($this->oSet1 === null) && ($this->oSet2 === null)) + { + // Both sets are empty, they are equal + return true; + } + else if (($this->oSet1 === null) || ($this->oSet2 === null)) + { + // one of them is empty, they are different + return false; + } + + if (($this->oSet1->GetRootClass() != $this->oSet2->GetRootClass()) || ($this->oSet1->Count() != $this->oSet2->Count())) return false; + + $this->ComputeFingerprints(); + + // Check that all objects in Set1 are also in Set2 + foreach($this->aFingerprints1 as $sFingerprint => $oObj) + { + if (!array_key_exists($sFingerprint, $this->aFingerprints2)) + { + return false; + } + } + + // Vice versa + // Check that all objects in Set2 are also in Set1 + foreach($this->aFingerprints2 as $sFingerprint => $oObj) + { + if (!array_key_exists($sFingerprint, $this->aFingerprints1)) + { + return false; + } + } + + return true; + } + + /** + * Get the list of differences between the two sets. In ordeer to write back into the database only the minimum changes + * THE FIRST SET MUST BE THE ONE LOADED FROM THE DATABASE + * Returns a hash: 'added' => DBObject(s), 'removed' => DBObject(s), 'modified' => DBObjects(s) + * @return array + * + * @throws \Exception + * @throws \CoreException + */ + public function GetDifferences() + { + $aResult = array('added' => array(), 'removed' => array(), 'modified' => array()); + $this->ComputeFingerprints(); + + // Check that all objects in Set1 are also in Set2 + foreach($this->aFingerprints1 as $sFingerprint => $oObj) + { + // Beware: the elements from the first set MUST come from the database, otherwise the result will be irrelevant + if ($oObj->IsNew()) throw new Exception('Cannot compute differences when elements from the first set are NOT in the database'); + if (array_key_exists($oObj->GetKey(), $this->aIDs2) && ($this->aIDs2[$oObj->GetKey()]->IsModified())) + { + // The very same object exists in both set, but was modified since its load + $aResult['modified'][$oObj->GetKey()] = $this->aIDs2[$oObj->GetKey()]; + } + else if (($this->sAdditionalKeyColumn !== null) && array_key_exists($oObj->Get($this->sAdditionalKeyColumn), $this->aAdditionalKeys)) + { + // Special case for n:n links where the link is recreated between the very same 2 objects, but some of its attributes are modified + // Let's consider this as a "modification" instead of "deletion" + "creation" in order to have a "clean" history for the objects + $oDestObj = $this->aAdditionalKeys[$oObj->Get($this->sAdditionalKeyColumn)]; + $oCloneObj = $this->CopyFrom($oObj, $oDestObj); + $aResult['modified'][$oObj->GetKey()] = $oCloneObj; + // Mark this as processed, so that the pass on aFingerprints2 below ignores this object + $sNewFingerprint = $oDestObj->Fingerprint($this->aExcludedColumns); + $this->aFingerprints2[$sNewFingerprint] = $oCloneObj; + } + else if (!array_key_exists($sFingerprint, $this->aFingerprints2)) + { + $aResult['removed'][] = $oObj; + } + } + + // Vice versa + // Check that all objects in Set2 are also in Set1 + foreach($this->aFingerprints2 as $sFingerprint => $oObj) + { + if (array_key_exists($oObj->GetKey(), $this->aIDs1) && ($oObj->IsModified())) + { + // Already marked as modified above + //$aResult['modified'][$oObj->GetKey()] = $oObj; + } + else if (!array_key_exists($sFingerprint, $this->aFingerprints1)) + { + $aResult['added'][] = $oObj; + } + } + return $aResult; + } + + /** + * Helpr to clone (in memory) an object and to apply to it the values taken from a second object + * + * @param \DBObject $oObjToClone + * @param \DBObject $oObjWithValues + * + * @return \DBObject The modified clone + * + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + */ + protected function CopyFrom($oObjToClone, $oObjWithValues) + { + $oObj = MetaModel::GetObject(get_class($oObjToClone), $oObjToClone->GetKey()); + foreach(MetaModel::ListAttributeDefs(get_class($oObj)) as $sAttCode => $oAttDef) + { + if (!in_array($sAttCode, $this->aExcludedColumns) && $oAttDef->IsWritable()) + { + $oObj->Set($sAttCode, $oObjWithValues->Get($sAttCode)); + } + } + return $oObj; + } } \ No newline at end of file diff --git a/core/dbproperty.class.inc.php b/core/dbproperty.class.inc.php index 6be724cae..3081e34fc 100644 --- a/core/dbproperty.class.inc.php +++ b/core/dbproperty.class.inc.php @@ -1,160 +1,160 @@ - - - -/** - * Database properties - manage database instances in a complex installation - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * A database property - * - * @package iTopORM - */ -class DBProperty extends DBObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "cloud", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_db_properties", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeDateTime("change_date", array("allowed_values"=>null, "sql"=>"change_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("change_comment", array("allowed_values"=>null, "sql"=>"change_comment", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - } - - /** - * Helper to check wether the table has been created into the DB - * (this table did not exist in 1.0.1 and older versions) - */ - public static function IsInstalled() - { - $sTable = MetaModel::DBGetTable(__CLASS__); - if (CMDBSource::IsTable($sTable)) - { - return true; - } - else - { - return false; - } - return false; - } - - public static function SetProperty($sName, $sValue, $sComment = '', $sDescription = null) - { - try - { - $oSearch = DBObjectSearch::FromOQL('SELECT DBProperty WHERE name = :name'); - $oSet = new DBObjectSet($oSearch, array(), array('name' => $sName)); - if ($oSet->Count() == 0) - { - $oProp = new DBProperty(); - $oProp->Set('name', $sName); - $oProp->Set('description', $sDescription); - $oProp->Set('value', $sValue); - $oProp->Set('change_date', time()); - $oProp->Set('change_comment', $sComment); - $oProp->DBInsert(); - } - elseif ($oSet->Count() == 1) - { - $oProp = $oSet->fetch(); - if (!is_null($sDescription)) - { - $oProp->Set('description', $sDescription); - } - $oProp->Set('value', $sValue); - $oProp->Set('change_date', time()); - $oProp->Set('change_comment', $sComment); - $oProp->DBUpdate(); - } - else - { - // Houston... - throw new CoreException('duplicate db property'); - } - } - catch (MySQLException $e) - { - // This might be because the table could not be found, - // let's check it and discard silently if this is really the case - if (self::IsInstalled()) - { - throw $e; - } - IssueLog::Error('Attempting to write a DBProperty while the module has not been installed'); - } - } - - public static function GetProperty($sName, $default = null) - { - try - { - $oSearch = DBObjectSearch::FromOQL('SELECT DBProperty WHERE name = :name'); - $oSet = new DBObjectSet($oSearch, array(), array('name' => $sName)); - $iCount = $oSet->Count(); - if ($iCount == 0) - { - //throw new CoreException('unknown db property', array('name' => $sName)); - $sValue = $default; - } - elseif ($iCount == 1) - { - $oProp = $oSet->fetch(); - $sValue = $oProp->Get('value'); - } - else - { - // $iCount > 1 - // Houston... - throw new CoreException('duplicate db property', array('name' => $sName, 'count' => $iCount)); - } - } - catch (MySQLException $e) - { - // This might be because the table could not be found, - // let's check it and discard silently if this is really the case - if (self::IsInstalled()) - { - throw $e; - } - $sValue = $default; - } - return $sValue; - } -} - -?> + + + +/** + * Database properties - manage database instances in a complex installation + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * A database property + * + * @package iTopORM + */ +class DBProperty extends DBObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "cloud", + "key_type" => "autoincrement", + "name_attcode" => "name", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_db_properties", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("value", array("allowed_values"=>null, "sql"=>"value", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeDateTime("change_date", array("allowed_values"=>null, "sql"=>"change_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("change_comment", array("allowed_values"=>null, "sql"=>"change_comment", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + } + + /** + * Helper to check wether the table has been created into the DB + * (this table did not exist in 1.0.1 and older versions) + */ + public static function IsInstalled() + { + $sTable = MetaModel::DBGetTable(__CLASS__); + if (CMDBSource::IsTable($sTable)) + { + return true; + } + else + { + return false; + } + return false; + } + + public static function SetProperty($sName, $sValue, $sComment = '', $sDescription = null) + { + try + { + $oSearch = DBObjectSearch::FromOQL('SELECT DBProperty WHERE name = :name'); + $oSet = new DBObjectSet($oSearch, array(), array('name' => $sName)); + if ($oSet->Count() == 0) + { + $oProp = new DBProperty(); + $oProp->Set('name', $sName); + $oProp->Set('description', $sDescription); + $oProp->Set('value', $sValue); + $oProp->Set('change_date', time()); + $oProp->Set('change_comment', $sComment); + $oProp->DBInsert(); + } + elseif ($oSet->Count() == 1) + { + $oProp = $oSet->fetch(); + if (!is_null($sDescription)) + { + $oProp->Set('description', $sDescription); + } + $oProp->Set('value', $sValue); + $oProp->Set('change_date', time()); + $oProp->Set('change_comment', $sComment); + $oProp->DBUpdate(); + } + else + { + // Houston... + throw new CoreException('duplicate db property'); + } + } + catch (MySQLException $e) + { + // This might be because the table could not be found, + // let's check it and discard silently if this is really the case + if (self::IsInstalled()) + { + throw $e; + } + IssueLog::Error('Attempting to write a DBProperty while the module has not been installed'); + } + } + + public static function GetProperty($sName, $default = null) + { + try + { + $oSearch = DBObjectSearch::FromOQL('SELECT DBProperty WHERE name = :name'); + $oSet = new DBObjectSet($oSearch, array(), array('name' => $sName)); + $iCount = $oSet->Count(); + if ($iCount == 0) + { + //throw new CoreException('unknown db property', array('name' => $sName)); + $sValue = $default; + } + elseif ($iCount == 1) + { + $oProp = $oSet->fetch(); + $sValue = $oProp->Get('value'); + } + else + { + // $iCount > 1 + // Houston... + throw new CoreException('duplicate db property', array('name' => $sName, 'count' => $iCount)); + } + } + catch (MySQLException $e) + { + // This might be because the table could not be found, + // let's check it and discard silently if this is really the case + if (self::IsInstalled()) + { + throw $e; + } + $sValue = $default; + } + return $sValue; + } +} + +?> diff --git a/core/dbunionsearch.class.php b/core/dbunionsearch.class.php index d0c967671..f45ff7fab 100644 --- a/core/dbunionsearch.class.php +++ b/core/dbunionsearch.class.php @@ -1,601 +1,601 @@ - - - -/** - * A union of DBObjectSearches - * - * @copyright Copyright (C) 2015-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class DBUnionSearch extends DBSearch -{ - protected $aSearches; // source queries - protected $aSelectedClasses; // alias => classes (lowest common ancestors) computed at construction - - public function __construct($aSearches) - { - if (count ($aSearches) == 0) - { - throw new CoreException('A DBUnionSearch must be made of at least one search'); - } - - $this->aSearches = array(); - foreach ($aSearches as $oSearch) - { - if ($oSearch instanceof DBUnionSearch) - { - foreach ($oSearch->aSearches as $oSubSearch) - { - $this->aSearches[] = $oSubSearch->DeepClone(); - } - } - else - { - $this->aSearches[] = $oSearch->DeepClone(); - } - } - - $this->ComputeSelectedClasses(); - } - - public function AllowAllData() - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AllowAllData(); - } - } - public function IsAllDataAllowed() - { - foreach ($this->aSearches as $oSearch) - { - if ($oSearch->IsAllDataAllowed() === false) return false; - } - return true; - } - - public function SetArchiveMode($bEnable) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->SetArchiveMode($bEnable); - } - parent::SetArchiveMode($bEnable); - } - - public function SetShowObsoleteData($bShow) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->SetShowObsoleteData($bShow); - } - parent::SetShowObsoleteData($bShow); - } - - /** - * Find the lowest common ancestor for each of the selected class - */ - protected function ComputeSelectedClasses() - { - // 1 - Collect all the column/classes - $aColumnToClasses = array(); - foreach ($this->aSearches as $iPos => $oSearch) - { - $aSelected = array_values($oSearch->GetSelectedClasses()); - - if ($iPos != 0) - { - if (count($aSelected) < count($aColumnToClasses)) - { - throw new Exception('Too few selected classes in the subquery #'.($iPos+1)); - } - if (count($aSelected) > count($aColumnToClasses)) - { - throw new Exception('Too many selected classes in the subquery #'.($iPos+1)); - } - } - - foreach ($aSelected as $iColumn => $sClass) - { - $aColumnToClasses[$iColumn][] = $sClass; - } - } - - // 2 - Build the index column => alias - $oFirstSearch = $this->aSearches[0]; - $aColumnToAlias = array_keys($oFirstSearch->GetSelectedClasses()); - - // 3 - Compute alias => lowest common ancestor - $this->aSelectedClasses = array(); - foreach ($aColumnToClasses as $iColumn => $aClasses) - { - $sAlias = $aColumnToAlias[$iColumn]; - $sAncestor = MetaModel::GetLowestCommonAncestor($aClasses); - if (is_null($sAncestor)) - { - throw new Exception('Could not find a common ancestor for the column '.($iColumn+1).' (Classes: '.implode(', ', $aClasses).')'); - } - $this->aSelectedClasses[$sAlias] = $sAncestor; - } - } - - public function GetSearches() - { - return $this->aSearches; - } - - /** - * Limited to the selected classes - */ - public function GetClassName($sAlias) - { - if (array_key_exists($sAlias, $this->aSelectedClasses)) - { - return $this->aSelectedClasses[$sAlias]; - } - else - { - throw new CoreException("Invalid class alias '$sAlias'"); - } - } - - public function GetClass() - { - return reset($this->aSelectedClasses); - } - - public function GetClassAlias() - { - reset($this->aSelectedClasses); - return key($this->aSelectedClasses); - } - - - /** - * Change the class (only subclasses are supported as of now, because the conditions must fit the new class) - * Defaults to the first selected class - * Only the selected classes can be changed - */ - public function ChangeClass($sNewClass, $sAlias = null) - { - if (is_null($sAlias)) - { - $sAlias = $this->GetClassAlias(); - } - elseif (!array_key_exists($sAlias, $this->aSelectedClasses)) - { - // discard silently - necessary when recursing (??? copied from DBObjectSearch) - return; - } - - // 1 - identify the impacted column - $iColumn = array_search($sAlias, array_keys($this->aSelectedClasses)); - - // 2 - change for each search - foreach ($this->aSearches as $oSearch) - { - $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); - $sSearchAlias = $aSearchAliases[$iColumn]; - $oSearch->ChangeClass($sNewClass, $sSearchAlias); - } - - // 3 - record the change - $this->aSelectedClasses[$sAlias] = $sNewClass; - } - - public function GetSelectedClasses() - { - return $this->aSelectedClasses; - } - - /** - * @param array $aSelectedClasses array of aliases - * @throws CoreException - */ - public function SetSelectedClasses($aSelectedClasses) - { - // 1 - change for each search - foreach ($this->aSearches as $oSearch) - { - // Throws an exception if not valid - $oSearch->SetSelectedClasses($aSelectedClasses); - } - // 2 - update the lowest common ancestors - $this->ComputeSelectedClasses(); - } - - /** - * Change any alias of the query tree - * - * @param $sOldName - * @param $sNewName - * @return bool True if the alias has been found and changed - */ - public function RenameAlias($sOldName, $sNewName) - { - $bRet = false; - foreach ($this->aSearches as $oSearch) - { - $bRet = $oSearch->RenameAlias($sOldName, $sNewName) || $bRet; - } - return $bRet; - } - - public function IsAny() - { - $bIsAny = true; - foreach ($this->aSearches as $oSearch) - { - if (!$oSearch->IsAny()) - { - $bIsAny = false; - break; - } - } - return $bIsAny; - } - - public function ResetCondition() - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->ResetCondition(); - } - } - - public function MergeConditionExpression($oExpression) - { - $aAliases = array_keys($this->aSelectedClasses); - foreach ($this->aSearches as $iSearchIndex => $oSearch) - { - $oClonedExpression = $oExpression->DeepClone(); - if ($iSearchIndex != 0) - { - foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias) - { - $oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias); - } - } - $oSearch->MergeConditionExpression($oClonedExpression); - } - } - - public function AddConditionExpression($oExpression) - { - $aAliases = array_keys($this->aSelectedClasses); - foreach ($this->aSearches as $iSearchIndex => $oSearch) - { - $oClonedExpression = $oExpression->DeepClone(); - if ($iSearchIndex != 0) - { - foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias) - { - $oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias); - } - } - $oSearch->AddConditionExpression($oClonedExpression); - } - } - - public function AddNameCondition($sName) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AddNameCondition($sName); - } - } - - public function AddCondition($sFilterCode, $value, $sOpCode = null) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AddCondition($sFilterCode, $value, $sOpCode); - } - } - - /** - * Specify a condition on external keys or link sets - * @param sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively - * Example: infra_list->ci_id->location_id->country - * @param value The value to match (can be an array => IN(val1, val2...) - * @return void - */ - public function AddConditionAdvanced($sAttSpec, $value) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AddConditionAdvanced($sAttSpec, $value); - } - } - - public function AddCondition_FullText($sFullText) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AddCondition_FullText($sFullText); - } - } - - /** - * @param DBObjectSearch $oFilter - * @param $sExtKeyAttCode - * @param int $iOperatorCode - * @param null $aRealiasingMap array of => , for each alias that has changed - * @throws CoreException - * @throws CoreWarning - */ - public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); - } - } - - /** - * @param DBObjectSearch $oFilter - * @param $sForeignExtKeyAttCode - * @param int $iOperatorCode - * @param null $aRealiasingMap array of => , for each alias that has changed - */ - public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->AddCondition_ReferencedBy($oFilter, $sForeignExtKeyAttCode, $iOperatorCode, $aRealiasingMap); - } - } - - public function Intersect(DBSearch $oFilter) - { - $aSearches = array(); - foreach ($this->aSearches as $oSearch) - { - $aSearches[] = $oSearch->Intersect($oFilter); - } - return new DBUnionSearch($aSearches); - } - - public function SetInternalParams($aParams) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->SetInternalParams($aParams); - } - } - - public function GetInternalParams() - { - $aParams = array(); - foreach ($this->aSearches as $oSearch) - { - $aParams = array_merge($oSearch->GetInternalParams(), $aParams); - } - return $aParams; - } - - public function GetQueryParams($bExcludeMagicParams = true) - { - $aParams = array(); - foreach ($this->aSearches as $oSearch) - { - $aParams = array_merge($oSearch->GetQueryParams($bExcludeMagicParams), $aParams); - } - return $aParams; - } - - public function ListConstantFields() - { - // Somewhat complex to implement for unions, for a poor benefit - return array(); - } - - /** - * Turn the parameters (:xxx) into scalar values in order to easily - * serialize a search - */ - public function ApplyParameters($aArgs) - { - foreach ($this->aSearches as $oSearch) - { - $oSearch->ApplyParameters($aArgs); - } - } - - /** - * Overloads for query building - */ - public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false) - { - $aSubQueries = array(); - foreach ($this->aSearches as $oSearch) - { - $aSubQueries[] = $oSearch->ToOQL($bDevelopParams, $aContextParams, $bWithAllowAllFlag); - } - $sRet = implode(' UNION ', $aSubQueries); - return $sRet; - } - - /** - * Returns a new DBUnionSearch object where duplicates queries have been removed based on their OQLs - * - * @return \DBUnionSearch - */ - public function RemoveDuplicateQueries() - { - $aQueries = array(); - $aSearches = array(); - - foreach ($this->GetSearches() as $oTmpSearch) - { - $sQuery = $oTmpSearch->ToOQL(true); - if (!in_array($sQuery, $aQueries)) - { - $aQueries[] = $sQuery; - $aSearches[] = $oTmpSearch; - } - } - - $oNewSearch = new DBUnionSearch($aSearches); - - return $oNewSearch; - } - - //////////////////////////////////////////////////////////////////////////// - // - // Construction of the SQL queries - // - //////////////////////////////////////////////////////////////////////////// - - public function MakeDeleteQuery($aArgs = array()) - { - throw new Exception('MakeDeleteQuery is not implemented for the unions!'); - } - - public function MakeUpdateQuery($aValues, $aArgs = array()) - { - throw new Exception('MakeUpdateQuery is not implemented for the unions!'); - } - - public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) - { - if (count($this->aSearches) == 1) - { - return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, $aSelectExpr); - } - - $aSQLQueries = array(); - $aAliases = array_keys($this->aSelectedClasses); - $aQueryAttToLoad = null; - $aUnionQuerySelectExpr = array(); - foreach ($this->aSearches as $iSearch => $oSearch) - { - $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); - - // The selected classes from the query build perspective are the lowest common ancestors amongst the various queries - // (used when it comes to determine which attributes must be selected) - $aSearchSelectedClasses = array(); - foreach ($aSearchAliases as $iColumn => $sSearchAlias) - { - $sAlias = $aAliases[$iColumn]; - $aSearchSelectedClasses[$sSearchAlias] = $this->aSelectedClasses[$sAlias]; - } - - if ($bGetCount) - { - // Select only ids for the count to allow optimization of joins - foreach($aSearchAliases as $sSearchAlias) - { - $aQueryAttToLoad[$sSearchAlias] = array(); - } - } - else - { - if (is_null($aAttToLoad)) - { - $aQueryAttToLoad = null; - } - else - { - // (Eventually) Transform the aliases - $aQueryAttToLoad = array(); - foreach($aAttToLoad as $sAlias => $aAttributes) - { - $iColumn = array_search($sAlias, $aAliases); - $sQueryAlias = ($iColumn === false) ? $sAlias : $aSearchAliases[$iColumn]; - $aQueryAttToLoad[$sQueryAlias] = $aAttributes; - } - } - } - - if (is_null($aGroupByExpr)) - { - $aQueryGroupByExpr = null; - } - else - { - // Clone (and eventually transform) the group by expressions - $aQueryGroupByExpr = array(); - $aTranslationData = array(); - $aQueryColumns = array_keys($oSearch->GetSelectedClasses()); - foreach ($aAliases as $iColumn => $sAlias) - { - $sQueryAlias = $aQueryColumns[$iColumn]; - $aTranslationData[$sAlias]['*'] = $sQueryAlias; - $aQueryGroupByExpr[$sAlias.'id'] = new FieldExpression('id', $sQueryAlias); - } - foreach ($aGroupByExpr as $sExpressionAlias => $oExpression) - { - $aQueryGroupByExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); - } - } - - if (is_null($aSelectExpr)) - { - $aQuerySelectExpr = null; - } - else - { - $aQuerySelectExpr = array(); - $aTranslationData = array(); - $aQueryColumns = array_keys($oSearch->GetSelectedClasses()); - foreach($aAliases as $iColumn => $sAlias) - { - $sQueryAlias = $aQueryColumns[$iColumn]; - $aTranslationData[$sAlias]['*'] = $sQueryAlias; - } - foreach($aSelectExpr as $sExpressionAlias => $oExpression) - { - $oExpression->Browse(function ($oNode) use (&$aQuerySelectExpr, &$aTranslationData) - { - if ($oNode instanceof FieldExpression) - { - $sAlias = $oNode->GetParent()."__".$oNode->GetName(); - if (!key_exists($sAlias, $aQuerySelectExpr)) - { - $aQuerySelectExpr[$sAlias] = $oNode->Translate($aTranslationData, false, false); - } - $aTranslationData[$oNode->GetParent()][$oNode->GetName()] = new FieldExpression($sAlias); - } - }); - // Only done for the first select as aliases are named after the first query - if (!array_key_exists($sExpressionAlias, $aUnionQuerySelectExpr)) - { - $aUnionQuerySelectExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); - } - } - } - $oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses, $aQuerySelectExpr); - if (count($aSearchAliases) > 1) - { - // Necessary to make sure that selected columns will match throughout all the queries - // (default order of selected fields depending on the order of JOINS) - $oSubQuery->SortSelectedFields(); - } - $aSQLQueries[] = $oSubQuery; - } - - $oSQLQuery = new SQLUnionQuery($aSQLQueries, $aGroupByExpr, $aUnionQuerySelectExpr); - //MyHelpers::var_dump_html($oSQLQuery, true); - //MyHelpers::var_dump_html($oSQLQuery->RenderSelect(), true); - if (self::$m_bDebugQuery) $oSQLQuery->DisplayHtml(); - return $oSQLQuery; - } -} + + + +/** + * A union of DBObjectSearches + * + * @copyright Copyright (C) 2015-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class DBUnionSearch extends DBSearch +{ + protected $aSearches; // source queries + protected $aSelectedClasses; // alias => classes (lowest common ancestors) computed at construction + + public function __construct($aSearches) + { + if (count ($aSearches) == 0) + { + throw new CoreException('A DBUnionSearch must be made of at least one search'); + } + + $this->aSearches = array(); + foreach ($aSearches as $oSearch) + { + if ($oSearch instanceof DBUnionSearch) + { + foreach ($oSearch->aSearches as $oSubSearch) + { + $this->aSearches[] = $oSubSearch->DeepClone(); + } + } + else + { + $this->aSearches[] = $oSearch->DeepClone(); + } + } + + $this->ComputeSelectedClasses(); + } + + public function AllowAllData() + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AllowAllData(); + } + } + public function IsAllDataAllowed() + { + foreach ($this->aSearches as $oSearch) + { + if ($oSearch->IsAllDataAllowed() === false) return false; + } + return true; + } + + public function SetArchiveMode($bEnable) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->SetArchiveMode($bEnable); + } + parent::SetArchiveMode($bEnable); + } + + public function SetShowObsoleteData($bShow) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->SetShowObsoleteData($bShow); + } + parent::SetShowObsoleteData($bShow); + } + + /** + * Find the lowest common ancestor for each of the selected class + */ + protected function ComputeSelectedClasses() + { + // 1 - Collect all the column/classes + $aColumnToClasses = array(); + foreach ($this->aSearches as $iPos => $oSearch) + { + $aSelected = array_values($oSearch->GetSelectedClasses()); + + if ($iPos != 0) + { + if (count($aSelected) < count($aColumnToClasses)) + { + throw new Exception('Too few selected classes in the subquery #'.($iPos+1)); + } + if (count($aSelected) > count($aColumnToClasses)) + { + throw new Exception('Too many selected classes in the subquery #'.($iPos+1)); + } + } + + foreach ($aSelected as $iColumn => $sClass) + { + $aColumnToClasses[$iColumn][] = $sClass; + } + } + + // 2 - Build the index column => alias + $oFirstSearch = $this->aSearches[0]; + $aColumnToAlias = array_keys($oFirstSearch->GetSelectedClasses()); + + // 3 - Compute alias => lowest common ancestor + $this->aSelectedClasses = array(); + foreach ($aColumnToClasses as $iColumn => $aClasses) + { + $sAlias = $aColumnToAlias[$iColumn]; + $sAncestor = MetaModel::GetLowestCommonAncestor($aClasses); + if (is_null($sAncestor)) + { + throw new Exception('Could not find a common ancestor for the column '.($iColumn+1).' (Classes: '.implode(', ', $aClasses).')'); + } + $this->aSelectedClasses[$sAlias] = $sAncestor; + } + } + + public function GetSearches() + { + return $this->aSearches; + } + + /** + * Limited to the selected classes + */ + public function GetClassName($sAlias) + { + if (array_key_exists($sAlias, $this->aSelectedClasses)) + { + return $this->aSelectedClasses[$sAlias]; + } + else + { + throw new CoreException("Invalid class alias '$sAlias'"); + } + } + + public function GetClass() + { + return reset($this->aSelectedClasses); + } + + public function GetClassAlias() + { + reset($this->aSelectedClasses); + return key($this->aSelectedClasses); + } + + + /** + * Change the class (only subclasses are supported as of now, because the conditions must fit the new class) + * Defaults to the first selected class + * Only the selected classes can be changed + */ + public function ChangeClass($sNewClass, $sAlias = null) + { + if (is_null($sAlias)) + { + $sAlias = $this->GetClassAlias(); + } + elseif (!array_key_exists($sAlias, $this->aSelectedClasses)) + { + // discard silently - necessary when recursing (??? copied from DBObjectSearch) + return; + } + + // 1 - identify the impacted column + $iColumn = array_search($sAlias, array_keys($this->aSelectedClasses)); + + // 2 - change for each search + foreach ($this->aSearches as $oSearch) + { + $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); + $sSearchAlias = $aSearchAliases[$iColumn]; + $oSearch->ChangeClass($sNewClass, $sSearchAlias); + } + + // 3 - record the change + $this->aSelectedClasses[$sAlias] = $sNewClass; + } + + public function GetSelectedClasses() + { + return $this->aSelectedClasses; + } + + /** + * @param array $aSelectedClasses array of aliases + * @throws CoreException + */ + public function SetSelectedClasses($aSelectedClasses) + { + // 1 - change for each search + foreach ($this->aSearches as $oSearch) + { + // Throws an exception if not valid + $oSearch->SetSelectedClasses($aSelectedClasses); + } + // 2 - update the lowest common ancestors + $this->ComputeSelectedClasses(); + } + + /** + * Change any alias of the query tree + * + * @param $sOldName + * @param $sNewName + * @return bool True if the alias has been found and changed + */ + public function RenameAlias($sOldName, $sNewName) + { + $bRet = false; + foreach ($this->aSearches as $oSearch) + { + $bRet = $oSearch->RenameAlias($sOldName, $sNewName) || $bRet; + } + return $bRet; + } + + public function IsAny() + { + $bIsAny = true; + foreach ($this->aSearches as $oSearch) + { + if (!$oSearch->IsAny()) + { + $bIsAny = false; + break; + } + } + return $bIsAny; + } + + public function ResetCondition() + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->ResetCondition(); + } + } + + public function MergeConditionExpression($oExpression) + { + $aAliases = array_keys($this->aSelectedClasses); + foreach ($this->aSearches as $iSearchIndex => $oSearch) + { + $oClonedExpression = $oExpression->DeepClone(); + if ($iSearchIndex != 0) + { + foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias) + { + $oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias); + } + } + $oSearch->MergeConditionExpression($oClonedExpression); + } + } + + public function AddConditionExpression($oExpression) + { + $aAliases = array_keys($this->aSelectedClasses); + foreach ($this->aSearches as $iSearchIndex => $oSearch) + { + $oClonedExpression = $oExpression->DeepClone(); + if ($iSearchIndex != 0) + { + foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias) + { + $oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias); + } + } + $oSearch->AddConditionExpression($oClonedExpression); + } + } + + public function AddNameCondition($sName) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AddNameCondition($sName); + } + } + + public function AddCondition($sFilterCode, $value, $sOpCode = null) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AddCondition($sFilterCode, $value, $sOpCode); + } + } + + /** + * Specify a condition on external keys or link sets + * @param sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively + * Example: infra_list->ci_id->location_id->country + * @param value The value to match (can be an array => IN(val1, val2...) + * @return void + */ + public function AddConditionAdvanced($sAttSpec, $value) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AddConditionAdvanced($sAttSpec, $value); + } + } + + public function AddCondition_FullText($sFullText) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AddCondition_FullText($sFullText); + } + } + + /** + * @param DBObjectSearch $oFilter + * @param $sExtKeyAttCode + * @param int $iOperatorCode + * @param null $aRealiasingMap array of => , for each alias that has changed + * @throws CoreException + * @throws CoreWarning + */ + public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); + } + } + + /** + * @param DBObjectSearch $oFilter + * @param $sForeignExtKeyAttCode + * @param int $iOperatorCode + * @param null $aRealiasingMap array of => , for each alias that has changed + */ + public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->AddCondition_ReferencedBy($oFilter, $sForeignExtKeyAttCode, $iOperatorCode, $aRealiasingMap); + } + } + + public function Intersect(DBSearch $oFilter) + { + $aSearches = array(); + foreach ($this->aSearches as $oSearch) + { + $aSearches[] = $oSearch->Intersect($oFilter); + } + return new DBUnionSearch($aSearches); + } + + public function SetInternalParams($aParams) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->SetInternalParams($aParams); + } + } + + public function GetInternalParams() + { + $aParams = array(); + foreach ($this->aSearches as $oSearch) + { + $aParams = array_merge($oSearch->GetInternalParams(), $aParams); + } + return $aParams; + } + + public function GetQueryParams($bExcludeMagicParams = true) + { + $aParams = array(); + foreach ($this->aSearches as $oSearch) + { + $aParams = array_merge($oSearch->GetQueryParams($bExcludeMagicParams), $aParams); + } + return $aParams; + } + + public function ListConstantFields() + { + // Somewhat complex to implement for unions, for a poor benefit + return array(); + } + + /** + * Turn the parameters (:xxx) into scalar values in order to easily + * serialize a search + */ + public function ApplyParameters($aArgs) + { + foreach ($this->aSearches as $oSearch) + { + $oSearch->ApplyParameters($aArgs); + } + } + + /** + * Overloads for query building + */ + public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false) + { + $aSubQueries = array(); + foreach ($this->aSearches as $oSearch) + { + $aSubQueries[] = $oSearch->ToOQL($bDevelopParams, $aContextParams, $bWithAllowAllFlag); + } + $sRet = implode(' UNION ', $aSubQueries); + return $sRet; + } + + /** + * Returns a new DBUnionSearch object where duplicates queries have been removed based on their OQLs + * + * @return \DBUnionSearch + */ + public function RemoveDuplicateQueries() + { + $aQueries = array(); + $aSearches = array(); + + foreach ($this->GetSearches() as $oTmpSearch) + { + $sQuery = $oTmpSearch->ToOQL(true); + if (!in_array($sQuery, $aQueries)) + { + $aQueries[] = $sQuery; + $aSearches[] = $oTmpSearch; + } + } + + $oNewSearch = new DBUnionSearch($aSearches); + + return $oNewSearch; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Construction of the SQL queries + // + //////////////////////////////////////////////////////////////////////////// + + public function MakeDeleteQuery($aArgs = array()) + { + throw new Exception('MakeDeleteQuery is not implemented for the unions!'); + } + + public function MakeUpdateQuery($aValues, $aArgs = array()) + { + throw new Exception('MakeUpdateQuery is not implemented for the unions!'); + } + + public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) + { + if (count($this->aSearches) == 1) + { + return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, $aSelectExpr); + } + + $aSQLQueries = array(); + $aAliases = array_keys($this->aSelectedClasses); + $aQueryAttToLoad = null; + $aUnionQuerySelectExpr = array(); + foreach ($this->aSearches as $iSearch => $oSearch) + { + $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); + + // The selected classes from the query build perspective are the lowest common ancestors amongst the various queries + // (used when it comes to determine which attributes must be selected) + $aSearchSelectedClasses = array(); + foreach ($aSearchAliases as $iColumn => $sSearchAlias) + { + $sAlias = $aAliases[$iColumn]; + $aSearchSelectedClasses[$sSearchAlias] = $this->aSelectedClasses[$sAlias]; + } + + if ($bGetCount) + { + // Select only ids for the count to allow optimization of joins + foreach($aSearchAliases as $sSearchAlias) + { + $aQueryAttToLoad[$sSearchAlias] = array(); + } + } + else + { + if (is_null($aAttToLoad)) + { + $aQueryAttToLoad = null; + } + else + { + // (Eventually) Transform the aliases + $aQueryAttToLoad = array(); + foreach($aAttToLoad as $sAlias => $aAttributes) + { + $iColumn = array_search($sAlias, $aAliases); + $sQueryAlias = ($iColumn === false) ? $sAlias : $aSearchAliases[$iColumn]; + $aQueryAttToLoad[$sQueryAlias] = $aAttributes; + } + } + } + + if (is_null($aGroupByExpr)) + { + $aQueryGroupByExpr = null; + } + else + { + // Clone (and eventually transform) the group by expressions + $aQueryGroupByExpr = array(); + $aTranslationData = array(); + $aQueryColumns = array_keys($oSearch->GetSelectedClasses()); + foreach ($aAliases as $iColumn => $sAlias) + { + $sQueryAlias = $aQueryColumns[$iColumn]; + $aTranslationData[$sAlias]['*'] = $sQueryAlias; + $aQueryGroupByExpr[$sAlias.'id'] = new FieldExpression('id', $sQueryAlias); + } + foreach ($aGroupByExpr as $sExpressionAlias => $oExpression) + { + $aQueryGroupByExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); + } + } + + if (is_null($aSelectExpr)) + { + $aQuerySelectExpr = null; + } + else + { + $aQuerySelectExpr = array(); + $aTranslationData = array(); + $aQueryColumns = array_keys($oSearch->GetSelectedClasses()); + foreach($aAliases as $iColumn => $sAlias) + { + $sQueryAlias = $aQueryColumns[$iColumn]; + $aTranslationData[$sAlias]['*'] = $sQueryAlias; + } + foreach($aSelectExpr as $sExpressionAlias => $oExpression) + { + $oExpression->Browse(function ($oNode) use (&$aQuerySelectExpr, &$aTranslationData) + { + if ($oNode instanceof FieldExpression) + { + $sAlias = $oNode->GetParent()."__".$oNode->GetName(); + if (!key_exists($sAlias, $aQuerySelectExpr)) + { + $aQuerySelectExpr[$sAlias] = $oNode->Translate($aTranslationData, false, false); + } + $aTranslationData[$oNode->GetParent()][$oNode->GetName()] = new FieldExpression($sAlias); + } + }); + // Only done for the first select as aliases are named after the first query + if (!array_key_exists($sExpressionAlias, $aUnionQuerySelectExpr)) + { + $aUnionQuerySelectExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); + } + } + } + $oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses, $aQuerySelectExpr); + if (count($aSearchAliases) > 1) + { + // Necessary to make sure that selected columns will match throughout all the queries + // (default order of selected fields depending on the order of JOINS) + $oSubQuery->SortSelectedFields(); + } + $aSQLQueries[] = $oSubQuery; + } + + $oSQLQuery = new SQLUnionQuery($aSQLQueries, $aGroupByExpr, $aUnionQuerySelectExpr); + //MyHelpers::var_dump_html($oSQLQuery, true); + //MyHelpers::var_dump_html($oSQLQuery->RenderSelect(), true); + if (self::$m_bDebugQuery) $oSQLQuery->DisplayHtml(); + return $oSQLQuery; + } +} diff --git a/core/deletionplan.class.inc.php b/core/deletionplan.class.inc.php index 3f0d5d68d..447b458f1 100644 --- a/core/deletionplan.class.inc.php +++ b/core/deletionplan.class.inc.php @@ -1,298 +1,298 @@ - - - -/** - * Algorithm to delete object(s) and maintain data integrity - * - * @copyright Copyright (C) 2010-2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class DeleteException extends CoreException -{ -} - -/** - * Deletion plan (other objects to be deleted/modified, eventual issues, etc.) - * - * @package iTopORM - */ -class DeletionPlan -{ - //protected $m_aIssues; - - protected $m_bFoundStopper; - protected $m_bFoundSecurityIssue; - protected $m_bFoundManualDelete; - protected $m_bFoundManualOperation; - - protected $m_iToDelete; - protected $m_iToUpdate; - - protected $m_aToDelete; - protected $m_aToUpdate; - - protected static $m_aModeUpdate = array( - DEL_SILENT => array( - DEL_SILENT => DEL_SILENT, - DEL_AUTO => DEL_AUTO, - DEL_MANUAL => DEL_MANUAL - ), - DEL_MANUAL => array( - DEL_SILENT => DEL_MANUAL, - DEL_AUTO => DEL_AUTO, - DEL_MANUAL => DEL_MANUAL - ), - DEL_AUTO => array( - DEL_SILENT => DEL_AUTO, - DEL_AUTO => DEL_AUTO, - DEL_MANUAL => DEL_AUTO - ) - ); - - public function __construct() - { - $this->m_iToDelete = 0; - $this->m_iToUpdate = 0; - - $this->m_aToDelete = array(); - $this->m_aToUpdate = array(); - - $this->m_bFoundStopper = false; - $this->m_bFoundSecurityIssue = false; - $this->m_bFoundManualDelete = false; - $this->m_bFoundManualOperation = false; - } - - public function ComputeResults() - { - $this->m_iToDelete = 0; - $this->m_iToUpdate = 0; - - foreach($this->m_aToDelete as $sClass => $aToDelete) - { - foreach($aToDelete as $iId => $aData) - { - $this->m_iToDelete++; - if (isset($aData['issue'])) - { - $this->m_bFoundStopper = true; - $this->m_bFoundManualOperation = true; - if (isset($aData['issue_security'])) - { - $this->m_bFoundSecurityIssue = true; - } - } - if ($aData['mode'] == DEL_MANUAL) - { - $this->m_aToDelete[$sClass][$iId]['issue'] = $sClass.'::'.$iId.' '.Dict::S('UI:Delete:MustBeDeletedManually'); - $this->m_bFoundStopper = true; - $this->m_bFoundManualDelete = true; - } - } - } - - // Getting and setting time limit are not symetric: - // www.php.net/manual/fr/function.set-time-limit.php#72305 - $iPreviousTimeLimit = ini_get('max_execution_time'); - $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); - foreach($this->m_aToUpdate as $sClass => $aToUpdate) - { - foreach($aToUpdate as $iId => $aData) - { - set_time_limit($iLoopTimeLimit); - $this->m_iToUpdate++; - - $oObject = $aData['to_reset']; - $aExtKeyLabels = array(); - foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) - { - $oObject->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]); - $aExtKeyLabels[] = $aRemoteAttDef->GetLabel(); - } - $this->m_aToUpdate[$sClass][$iId]['attributes_list'] = implode(', ', $aExtKeyLabels); - - list($bRes, $aIssues, $bSecurityIssues) = $oObject->CheckToWrite(); - if (!$bRes) - { - $this->m_aToUpdate[$sClass][$iId]['issue'] = implode(', ', $aIssues); - $this->m_bFoundStopper = true; - - if ($bSecurityIssues) - { - $this->m_aToUpdate[$sClass][$iId]['issue_security'] = true; - $this->m_bFoundSecurityIssue = true; - } - } - } - } - set_time_limit($iPreviousTimeLimit); - } - - public function GetIssues() - { - $aIssues = array(); - foreach ($this->m_aToDelete as $sClass => $aToDelete) - { - foreach ($aToDelete as $iId => $aData) - { - if (isset($aData['issue'])) - { - $aIssues[] = $aData['issue']; - } - } - } - foreach ($this->m_aToUpdate as $sClass => $aToUpdate) - { - foreach ($aToUpdate as $iId => $aData) - { - if (isset($aData['issue'])) - { - $aIssues[] = $aData['issue']; - } - } - } - return $aIssues; - } - - public function ListDeletes() - { - return $this->m_aToDelete; - } - - public function ListUpdates() - { - return $this->m_aToUpdate; - } - - public function GetTargetCount() - { - return $this->m_iToDelete + $this->m_iToUpdate; - } - - public function FoundStopper() - { - return $this->m_bFoundStopper; - } - - public function FoundSecurityIssue() - { - return $this->m_bFoundSecurityIssue; - } - - public function FoundManualOperation() - { - return $this->m_bFoundManualOperation; - } - - public function FoundManualDelete() - { - return $this->m_bFoundManualDelete; - } - - public function FoundManualUpdate() - { - } - - public function AddToDelete($oObject, $iDeletionMode = null) - { - if (is_null($iDeletionMode)) - { - $bRequestedExplicitely = true; - $iDeletionMode = DEL_AUTO; - } - else - { - $bRequestedExplicitely = false; - } - - $sClass = get_class($oObject); - $iId = $oObject->GetKey(); - - if (isset($this->m_aToUpdate[$sClass][$iId])) - { - unset($this->m_aToUpdate[$sClass][$iId]); - } - - if (isset($this->m_aToDelete[$sClass][$iId])) - { - if ($this->m_aToDelete[$sClass][$iId]['requested_explicitely']) - { - // No change: let it in mode DEL_AUTO - } - else - { - $iPrevDeletionMode = $this->m_aToDelete[$sClass][$iId]['mode']; - $iNewDeletionMode = self::$m_aModeUpdate[$iPrevDeletionMode][$iDeletionMode]; - $this->m_aToDelete[$sClass][$iId]['mode'] = $iNewDeletionMode; - - if ($bRequestedExplicitely) - { - // This object was in the root list - $this->m_aToDelete[$sClass][$iId]['requested_explicitely'] = true; - $this->m_aToDelete[$sClass][$iId]['mode'] = DEL_AUTO; - } - } - } - else - { - $this->m_aToDelete[$sClass][$iId] = array( - 'to_delete' => $oObject, - 'mode' => $iDeletionMode, - 'requested_explicitely' => $bRequestedExplicitely, - ); - } - } - - public function SetDeletionIssues($oObject, $aIssues, $bSecurityIssue) - { - if (count($aIssues) > 0) - { - $sClass = get_class($oObject); - $iId = $oObject->GetKey(); - $this->m_aToDelete[$sClass][$iId]['issue'] = implode(', ', $aIssues); - if ($bSecurityIssue) - { - $this->m_aToDelete[$sClass][$iId]['issue_security'] = true; - } - } - } - - public function AddToUpdate($oObject, $oAttDef, $value = 0) - { - $sClass = get_class($oObject); - $iId = $oObject->GetKey(); - if (isset($this->m_aToDelete[$sClass][$iId])) - { - // skip... it should be deleted anyhow ! - } - else - { - if (!isset($this->m_aToUpdate[$sClass][$iId])) - { - $this->m_aToUpdate[$sClass][$iId] = array( - 'to_reset' => $oObject, - ); - } - $this->m_aToUpdate[$sClass][$iId]['attributes'][$oAttDef->GetCode()] = $oAttDef; - $this->m_aToUpdate[$sClass][$iId]['values'][$oAttDef->GetCode()] = $value; - } - } -} -?> + + + +/** + * Algorithm to delete object(s) and maintain data integrity + * + * @copyright Copyright (C) 2010-2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class DeleteException extends CoreException +{ +} + +/** + * Deletion plan (other objects to be deleted/modified, eventual issues, etc.) + * + * @package iTopORM + */ +class DeletionPlan +{ + //protected $m_aIssues; + + protected $m_bFoundStopper; + protected $m_bFoundSecurityIssue; + protected $m_bFoundManualDelete; + protected $m_bFoundManualOperation; + + protected $m_iToDelete; + protected $m_iToUpdate; + + protected $m_aToDelete; + protected $m_aToUpdate; + + protected static $m_aModeUpdate = array( + DEL_SILENT => array( + DEL_SILENT => DEL_SILENT, + DEL_AUTO => DEL_AUTO, + DEL_MANUAL => DEL_MANUAL + ), + DEL_MANUAL => array( + DEL_SILENT => DEL_MANUAL, + DEL_AUTO => DEL_AUTO, + DEL_MANUAL => DEL_MANUAL + ), + DEL_AUTO => array( + DEL_SILENT => DEL_AUTO, + DEL_AUTO => DEL_AUTO, + DEL_MANUAL => DEL_AUTO + ) + ); + + public function __construct() + { + $this->m_iToDelete = 0; + $this->m_iToUpdate = 0; + + $this->m_aToDelete = array(); + $this->m_aToUpdate = array(); + + $this->m_bFoundStopper = false; + $this->m_bFoundSecurityIssue = false; + $this->m_bFoundManualDelete = false; + $this->m_bFoundManualOperation = false; + } + + public function ComputeResults() + { + $this->m_iToDelete = 0; + $this->m_iToUpdate = 0; + + foreach($this->m_aToDelete as $sClass => $aToDelete) + { + foreach($aToDelete as $iId => $aData) + { + $this->m_iToDelete++; + if (isset($aData['issue'])) + { + $this->m_bFoundStopper = true; + $this->m_bFoundManualOperation = true; + if (isset($aData['issue_security'])) + { + $this->m_bFoundSecurityIssue = true; + } + } + if ($aData['mode'] == DEL_MANUAL) + { + $this->m_aToDelete[$sClass][$iId]['issue'] = $sClass.'::'.$iId.' '.Dict::S('UI:Delete:MustBeDeletedManually'); + $this->m_bFoundStopper = true; + $this->m_bFoundManualDelete = true; + } + } + } + + // Getting and setting time limit are not symetric: + // www.php.net/manual/fr/function.set-time-limit.php#72305 + $iPreviousTimeLimit = ini_get('max_execution_time'); + $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); + foreach($this->m_aToUpdate as $sClass => $aToUpdate) + { + foreach($aToUpdate as $iId => $aData) + { + set_time_limit($iLoopTimeLimit); + $this->m_iToUpdate++; + + $oObject = $aData['to_reset']; + $aExtKeyLabels = array(); + foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) + { + $oObject->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]); + $aExtKeyLabels[] = $aRemoteAttDef->GetLabel(); + } + $this->m_aToUpdate[$sClass][$iId]['attributes_list'] = implode(', ', $aExtKeyLabels); + + list($bRes, $aIssues, $bSecurityIssues) = $oObject->CheckToWrite(); + if (!$bRes) + { + $this->m_aToUpdate[$sClass][$iId]['issue'] = implode(', ', $aIssues); + $this->m_bFoundStopper = true; + + if ($bSecurityIssues) + { + $this->m_aToUpdate[$sClass][$iId]['issue_security'] = true; + $this->m_bFoundSecurityIssue = true; + } + } + } + } + set_time_limit($iPreviousTimeLimit); + } + + public function GetIssues() + { + $aIssues = array(); + foreach ($this->m_aToDelete as $sClass => $aToDelete) + { + foreach ($aToDelete as $iId => $aData) + { + if (isset($aData['issue'])) + { + $aIssues[] = $aData['issue']; + } + } + } + foreach ($this->m_aToUpdate as $sClass => $aToUpdate) + { + foreach ($aToUpdate as $iId => $aData) + { + if (isset($aData['issue'])) + { + $aIssues[] = $aData['issue']; + } + } + } + return $aIssues; + } + + public function ListDeletes() + { + return $this->m_aToDelete; + } + + public function ListUpdates() + { + return $this->m_aToUpdate; + } + + public function GetTargetCount() + { + return $this->m_iToDelete + $this->m_iToUpdate; + } + + public function FoundStopper() + { + return $this->m_bFoundStopper; + } + + public function FoundSecurityIssue() + { + return $this->m_bFoundSecurityIssue; + } + + public function FoundManualOperation() + { + return $this->m_bFoundManualOperation; + } + + public function FoundManualDelete() + { + return $this->m_bFoundManualDelete; + } + + public function FoundManualUpdate() + { + } + + public function AddToDelete($oObject, $iDeletionMode = null) + { + if (is_null($iDeletionMode)) + { + $bRequestedExplicitely = true; + $iDeletionMode = DEL_AUTO; + } + else + { + $bRequestedExplicitely = false; + } + + $sClass = get_class($oObject); + $iId = $oObject->GetKey(); + + if (isset($this->m_aToUpdate[$sClass][$iId])) + { + unset($this->m_aToUpdate[$sClass][$iId]); + } + + if (isset($this->m_aToDelete[$sClass][$iId])) + { + if ($this->m_aToDelete[$sClass][$iId]['requested_explicitely']) + { + // No change: let it in mode DEL_AUTO + } + else + { + $iPrevDeletionMode = $this->m_aToDelete[$sClass][$iId]['mode']; + $iNewDeletionMode = self::$m_aModeUpdate[$iPrevDeletionMode][$iDeletionMode]; + $this->m_aToDelete[$sClass][$iId]['mode'] = $iNewDeletionMode; + + if ($bRequestedExplicitely) + { + // This object was in the root list + $this->m_aToDelete[$sClass][$iId]['requested_explicitely'] = true; + $this->m_aToDelete[$sClass][$iId]['mode'] = DEL_AUTO; + } + } + } + else + { + $this->m_aToDelete[$sClass][$iId] = array( + 'to_delete' => $oObject, + 'mode' => $iDeletionMode, + 'requested_explicitely' => $bRequestedExplicitely, + ); + } + } + + public function SetDeletionIssues($oObject, $aIssues, $bSecurityIssue) + { + if (count($aIssues) > 0) + { + $sClass = get_class($oObject); + $iId = $oObject->GetKey(); + $this->m_aToDelete[$sClass][$iId]['issue'] = implode(', ', $aIssues); + if ($bSecurityIssue) + { + $this->m_aToDelete[$sClass][$iId]['issue_security'] = true; + } + } + } + + public function AddToUpdate($oObject, $oAttDef, $value = 0) + { + $sClass = get_class($oObject); + $iId = $oObject->GetKey(); + if (isset($this->m_aToDelete[$sClass][$iId])) + { + // skip... it should be deleted anyhow ! + } + else + { + if (!isset($this->m_aToUpdate[$sClass][$iId])) + { + $this->m_aToUpdate[$sClass][$iId] = array( + 'to_reset' => $oObject, + ); + } + $this->m_aToUpdate[$sClass][$iId]['attributes'][$oAttDef->GetCode()] = $oAttDef; + $this->m_aToUpdate[$sClass][$iId]['values'][$oAttDef->GetCode()] = $value; + } + } +} +?> diff --git a/core/designdocument.class.inc.php b/core/designdocument.class.inc.php index 2be363414..03ec5d5f1 100644 --- a/core/designdocument.class.inc.php +++ b/core/designdocument.class.inc.php @@ -1,282 +1,282 @@ - - * - */ - -/** - * Design document and associated nodes - * @package Core - */ - -namespace Combodo\iTop; - -use \DOMDocument; -use \DOMFormatException; - -/** - * Class \Combodo\iTop\DesignDocument - * - * A design document is the DOM tree that modelize behaviors. One of its - * characteristics is that it can be altered by the mean of the same kind of document. - * - */ -class DesignDocument extends DOMDocument -{ - /** - * @throws \Exception - */ - public function __construct() - { - parent::__construct('1.0', 'UTF-8'); - $this->Init(); - } - - /** - * Overloadable. Called prior to data loading. - */ - protected function Init() - { - $this->registerNodeClass('DOMElement', '\Combodo\iTop\DesignElement'); - - $this->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS) - $this->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect - } - - /** - * Overload of the standard API - * - * @param $filename - * @param int $options - */ - public function load($filename, $options = 0) - { - parent::load($filename, LIBXML_NOBLANKS); - } - - /** - * Overload of the standard API - * - * @param $filename - * @param int $options - * - * @return int - */ - public function save($filename, $options = 0) - { - $this->documentElement->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance"); - return parent::save($filename, LIBXML_NOBLANKS); - } - - /** - * Create an HTML representation of the DOM, for debugging purposes - * @param bool|false $bReturnRes Echoes or returns the HTML representation - * @return mixed void or the HTML representation of the DOM - */ - public function Dump($bReturnRes = false) - { - $sXml = $this->saveXML(); - if ($bReturnRes) - { - return $sXml; - } - - echo "
    \n";
    -		echo htmlentities($sXml);
    -		echo "
    \n"; - - return ''; - } - - /** - * Quote and escape strings for use within an XPath expression - * Usage: DesignDocument::GetNodes('class[@id='.DesignDocument::XPathQuote($sId).']'); - * @param string $sValue The value to be quoted - * @return string to be used within an XPath - */ - public static function XPathQuote($sValue) - { - if (strpos($sValue, '"') !== false) - { - $aParts = explode('"', $sValue); - $sRet = 'concat("'.implode('", \'"\', "', $aParts).'")'; - } - else - { - $sRet = '"'.$sValue.'"'; - } - return $sRet; - } - - /** - * Extracts some nodes from the DOM - * @param string $sXPath A XPath expression - * @param DesignElement $oContextNode The node to start the search from - * @return \DOMNodeList - */ - public function GetNodes($sXPath, $oContextNode = null) - { - $oXPath = new \DOMXPath($this); - if (is_null($oContextNode)) - { - $oResult = $oXPath->query($sXPath); - } - else - { - $oResult = $oXPath->query($sXPath, $oContextNode); - } - return $oResult; - } - - /** - * An alternative to getNodePath, that gives the id of nodes instead of the position within the children - * @param DesignElement $oNode The node to describe - * @return string - */ - public static function GetItopNodePath($oNode) - { - if ($oNode instanceof \DOMDocument) return ''; - if (is_null($oNode)) return ''; - - $sId = $oNode->getAttribute('id'); - $sNodeDesc = ($sId != '') ? $oNode->nodeName.'['.$sId.']' : $oNode->nodeName; - return self::GetItopNodePath($oNode->parentNode).'/'.$sNodeDesc; - } -} - -/** - * DesignElement: helper to read/change the DOM - * @package ModelFactory - */ -class DesignElement extends \DOMElement -{ - /** - * Extracts some nodes from the DOM - * @param string $sXPath A XPath expression - * @return \DOMNodeList - */ - public function GetNodes($sXPath) - { - return $this->ownerDocument->GetNodes($sXPath, $this); - } - - /** - * Create an HTML representation of the DOM, for debugging purposes - * - * @param bool|false $bReturnRes Echoes or returns the HTML representation - * - * @return mixed void or the HTML representation of the DOM - * @throws \Exception - */ - public function Dump($bReturnRes = false) - { - $oDoc = new DesignDocument(); - $oClone = $oDoc->importNode($this->cloneNode(true), true); - $oDoc->appendChild($oClone); - - $sXml = $oDoc->saveXML($oClone); - if ($bReturnRes) - { - return $sXml; - } - echo "
    \n";
    -		echo htmlentities($sXml);
    -		echo "
    \n"; - return ''; - } - /** - * Returns the node directly under the given node - * @param $sTagName - * @param bool|true $bMustExist - * @return \MFElement - * @throws DOMFormatException - */ - public function GetUniqueElement($sTagName, $bMustExist = true) - { - $oNode = null; - foreach($this->childNodes as $oChildNode) - { - if ($oChildNode->nodeName == $sTagName) - { - $oNode = $oChildNode; - break; - } - } - if ($bMustExist && is_null($oNode)) - { - throw new DOMFormatException('Missing unique tag: '.$sTagName); - } - return $oNode; - } - - /** - * Returns the node directly under the current node, or null if missing - * @param $sTagName - * @return \MFElement - * @throws DOMFormatException - */ - public function GetOptionalElement($sTagName) - { - return $this->GetUniqueElement($sTagName, false); - } - - /** - * Returns the TEXT of the current node (possibly from several child nodes) - * @param null $sDefault - * @return null|string - */ - public function GetText($sDefault = null) - { - $sText = null; - foreach($this->childNodes as $oChildNode) - { - if ($oChildNode instanceof \DOMText) - { - if (is_null($sText)) $sText = ''; - $sText .= $oChildNode->wholeText; - } - } - if (is_null($sText)) - { - return $sDefault; - } - else - { - return $sText; - } - } - - /** - * Get the TEXT value from a child node - * - * @param string $sTagName - * @param string|null $sDefault - * - * @return string - * @throws \DOMFormatException - */ - public function GetChildText($sTagName, $sDefault = null) - { - $sRet = $sDefault; - if ($oChild = $this->GetOptionalElement($sTagName)) - { - $sRet = $oChild->GetText($sDefault); - } - return $sRet; - } -} + + * + */ + +/** + * Design document and associated nodes + * @package Core + */ + +namespace Combodo\iTop; + +use \DOMDocument; +use \DOMFormatException; + +/** + * Class \Combodo\iTop\DesignDocument + * + * A design document is the DOM tree that modelize behaviors. One of its + * characteristics is that it can be altered by the mean of the same kind of document. + * + */ +class DesignDocument extends DOMDocument +{ + /** + * @throws \Exception + */ + public function __construct() + { + parent::__construct('1.0', 'UTF-8'); + $this->Init(); + } + + /** + * Overloadable. Called prior to data loading. + */ + protected function Init() + { + $this->registerNodeClass('DOMElement', '\Combodo\iTop\DesignElement'); + + $this->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS) + $this->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect + } + + /** + * Overload of the standard API + * + * @param $filename + * @param int $options + */ + public function load($filename, $options = 0) + { + parent::load($filename, LIBXML_NOBLANKS); + } + + /** + * Overload of the standard API + * + * @param $filename + * @param int $options + * + * @return int + */ + public function save($filename, $options = 0) + { + $this->documentElement->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance"); + return parent::save($filename, LIBXML_NOBLANKS); + } + + /** + * Create an HTML representation of the DOM, for debugging purposes + * @param bool|false $bReturnRes Echoes or returns the HTML representation + * @return mixed void or the HTML representation of the DOM + */ + public function Dump($bReturnRes = false) + { + $sXml = $this->saveXML(); + if ($bReturnRes) + { + return $sXml; + } + + echo "
    \n";
    +		echo htmlentities($sXml);
    +		echo "
    \n"; + + return ''; + } + + /** + * Quote and escape strings for use within an XPath expression + * Usage: DesignDocument::GetNodes('class[@id='.DesignDocument::XPathQuote($sId).']'); + * @param string $sValue The value to be quoted + * @return string to be used within an XPath + */ + public static function XPathQuote($sValue) + { + if (strpos($sValue, '"') !== false) + { + $aParts = explode('"', $sValue); + $sRet = 'concat("'.implode('", \'"\', "', $aParts).'")'; + } + else + { + $sRet = '"'.$sValue.'"'; + } + return $sRet; + } + + /** + * Extracts some nodes from the DOM + * @param string $sXPath A XPath expression + * @param DesignElement $oContextNode The node to start the search from + * @return \DOMNodeList + */ + public function GetNodes($sXPath, $oContextNode = null) + { + $oXPath = new \DOMXPath($this); + if (is_null($oContextNode)) + { + $oResult = $oXPath->query($sXPath); + } + else + { + $oResult = $oXPath->query($sXPath, $oContextNode); + } + return $oResult; + } + + /** + * An alternative to getNodePath, that gives the id of nodes instead of the position within the children + * @param DesignElement $oNode The node to describe + * @return string + */ + public static function GetItopNodePath($oNode) + { + if ($oNode instanceof \DOMDocument) return ''; + if (is_null($oNode)) return ''; + + $sId = $oNode->getAttribute('id'); + $sNodeDesc = ($sId != '') ? $oNode->nodeName.'['.$sId.']' : $oNode->nodeName; + return self::GetItopNodePath($oNode->parentNode).'/'.$sNodeDesc; + } +} + +/** + * DesignElement: helper to read/change the DOM + * @package ModelFactory + */ +class DesignElement extends \DOMElement +{ + /** + * Extracts some nodes from the DOM + * @param string $sXPath A XPath expression + * @return \DOMNodeList + */ + public function GetNodes($sXPath) + { + return $this->ownerDocument->GetNodes($sXPath, $this); + } + + /** + * Create an HTML representation of the DOM, for debugging purposes + * + * @param bool|false $bReturnRes Echoes or returns the HTML representation + * + * @return mixed void or the HTML representation of the DOM + * @throws \Exception + */ + public function Dump($bReturnRes = false) + { + $oDoc = new DesignDocument(); + $oClone = $oDoc->importNode($this->cloneNode(true), true); + $oDoc->appendChild($oClone); + + $sXml = $oDoc->saveXML($oClone); + if ($bReturnRes) + { + return $sXml; + } + echo "
    \n";
    +		echo htmlentities($sXml);
    +		echo "
    \n"; + return ''; + } + /** + * Returns the node directly under the given node + * @param $sTagName + * @param bool|true $bMustExist + * @return \MFElement + * @throws DOMFormatException + */ + public function GetUniqueElement($sTagName, $bMustExist = true) + { + $oNode = null; + foreach($this->childNodes as $oChildNode) + { + if ($oChildNode->nodeName == $sTagName) + { + $oNode = $oChildNode; + break; + } + } + if ($bMustExist && is_null($oNode)) + { + throw new DOMFormatException('Missing unique tag: '.$sTagName); + } + return $oNode; + } + + /** + * Returns the node directly under the current node, or null if missing + * @param $sTagName + * @return \MFElement + * @throws DOMFormatException + */ + public function GetOptionalElement($sTagName) + { + return $this->GetUniqueElement($sTagName, false); + } + + /** + * Returns the TEXT of the current node (possibly from several child nodes) + * @param null $sDefault + * @return null|string + */ + public function GetText($sDefault = null) + { + $sText = null; + foreach($this->childNodes as $oChildNode) + { + if ($oChildNode instanceof \DOMText) + { + if (is_null($sText)) $sText = ''; + $sText .= $oChildNode->wholeText; + } + } + if (is_null($sText)) + { + return $sDefault; + } + else + { + return $sText; + } + } + + /** + * Get the TEXT value from a child node + * + * @param string $sTagName + * @param string|null $sDefault + * + * @return string + * @throws \DOMFormatException + */ + public function GetChildText($sTagName, $sDefault = null) + { + $sRet = $sDefault; + if ($oChild = $this->GetOptionalElement($sTagName)) + { + $sRet = $oChild->GetText($sDefault); + } + return $sRet; + } +} diff --git a/core/email.class.inc.php b/core/email.class.inc.php index 9bc500385..3f6616f71 100644 --- a/core/email.class.inc.php +++ b/core/email.class.inc.php @@ -1,540 +1,540 @@ - - - -/** - * Send an email (abstraction for synchronous/asynchronous modes) - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/lib/swiftmailer/lib/swift_required.php'); - -Swift_Preferences::getInstance()->setCharset('UTF-8'); - - -define ('EMAIL_SEND_OK', 0); -define ('EMAIL_SEND_PENDING', 1); -define ('EMAIL_SEND_ERROR', 2); - -class EMail -{ - // Serialization formats - const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object. - // Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string - const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed) - - protected static $m_oConfig = null; - protected $m_aData; // For storing data to serialize - - public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE) - { - if (is_null(self::$m_oConfig)) - { - self::$m_oConfig = new Config($sConfigFile); - } - } - - protected $m_oMessage; - - public function __construct() - { - $this->m_aData = array(); - $this->m_oMessage = Swift_Message::newInstance(); - $this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label')); - } - - /** - * Custom serialization method - * No longer use the brute force "serialize" method since - * 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field) - * 2) The size tends to be quite big (sometimes ten times the size of the email) - */ - public function SerializeV2() - { - return serialize($this->m_aData); - } - - /** - * Custom de-serialization method - * @param string $sSerializedMessage The serialized representation of the message - */ - static public function UnSerializeV2($sSerializedMessage) - { - $aData = unserialize($sSerializedMessage); - $oMessage = new Email(); - - if (array_key_exists('body', $aData)) - { - $oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']); - } - if (array_key_exists('message_id', $aData)) - { - $oMessage->SetMessageId($aData['message_id']); - } - if (array_key_exists('bcc', $aData)) - { - $oMessage->SetRecipientBCC($aData['bcc']); - } - if (array_key_exists('cc', $aData)) - { - $oMessage->SetRecipientCC($aData['cc']); - } - if (array_key_exists('from', $aData)) - { - $oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']); - } - if (array_key_exists('reply_to', $aData)) - { - $oMessage->SetRecipientReplyTo($aData['reply_to']); - } - if (array_key_exists('to', $aData)) - { - $oMessage->SetRecipientTO($aData['to']); - } - if (array_key_exists('subject', $aData)) - { - $oMessage->SetSubject($aData['subject']); - } - - - if (array_key_exists('headers', $aData)) - { - foreach($aData['headers'] as $sKey => $sValue) - { - $oMessage->AddToHeader($sKey, $sValue); - } - } - if (array_key_exists('parts', $aData)) - { - foreach($aData['parts'] as $aPart) - { - $oMessage->AddPart($aPart['text'], $aPart['mimeType']); - } - } - if (array_key_exists('attachments', $aData)) - { - foreach($aData['attachments'] as $aAttachment) - { - $oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']); - } - } - return $oMessage; - } - - protected function SendAsynchronous(&$aIssues, $oLog = null) - { - try - { - AsyncSendEmail::AddToQueue($this, $oLog); - } - catch(Exception $e) - { - $aIssues = array($e->GetMessage()); - return EMAIL_SEND_ERROR; - } - $aIssues = array(); - return EMAIL_SEND_PENDING; - } - - protected function SendSynchronous(&$aIssues, $oLog = null) - { - // If the body of the message is in HTML, embed all images based on attachments - $this->EmbedInlineImages(); - - $this->LoadConfig(); - - $sTransport = self::$m_oConfig->Get('email_transport'); - switch ($sTransport) - { - case 'SMTP': - $sHost = self::$m_oConfig->Get('email_transport_smtp.host'); - $sPort = self::$m_oConfig->Get('email_transport_smtp.port'); - $sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption'); - $sUserName = self::$m_oConfig->Get('email_transport_smtp.username'); - $sPassword = self::$m_oConfig->Get('email_transport_smtp.password'); - - $oTransport = Swift_SmtpTransport::newInstance($sHost, $sPort, $sEncryption); - if (strlen($sUserName) > 0) - { - $oTransport->setUsername($sUserName); - $oTransport->setPassword($sPassword); - } - break; - - case 'Null': - $oTransport = Swift_NullTransport::newInstance(); - break; - - case 'LogFile': - $oTransport = Swift_LogFileTransport::newInstance(); - $oTransport->setLogFile(APPROOT.'log/mail.log'); - break; - - case 'PHPMail': - default: - $oTransport = Swift_MailTransport::newInstance(); - } - - $oMailer = Swift_Mailer::newInstance($oTransport); - - $aFailedRecipients = array(); - $this->m_oMessage->setMaxLineLength(0); - $oKPI = new ExecutionKPI(); - try - { - $iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients); - if ($iSent === 0) - { - // Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!! - IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients)); - $aIssues = array('Some recipients were invalid.'); - $oKPI->ComputeStats('Email Sent', 'Error received'); - return EMAIL_SEND_ERROR; - } - else - { - $aIssues = array(); - $oKPI->ComputeStats('Email Sent', 'Succeded'); - return EMAIL_SEND_OK; - } - } - catch (Exception $e) - { - $oKPI->ComputeStats('Email Sent', 'Error received'); - throw $e; - } - } - - /** - * Reprocess the body of the message (if it is an HTML message) - * to replace the URL of images based on attachments by a link - * to an embedded image (i.e. cid:....) - */ - protected function EmbedInlineImages() - { - if ($this->m_aData['body']['mimeType'] == 'text/html') - { - $oDOMDoc = new DOMDocument(); - $oDOMDoc->preserveWhitespace = true; - @$oDOMDoc->loadHTML(''.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified - - $oXPath = new DOMXPath($oDOMDoc); - $sXPath = "//img[@data-img-id]"; - $oImagesList = $oXPath->query($sXPath); - - if ($oImagesList->length != 0) - { - foreach($oImagesList as $oImg) - { - $iAttId = $oImg->getAttribute('data-img-id'); - $oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */); - if ($oAttachment) - { - $oDoc = $oAttachment->Get('contents'); - $oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType()); - $sCid = $this->m_oMessage->embed($oSwiftImage); - $oImg->setAttribute('src', $sCid); - } - } - } - $sHtmlBody = $oDOMDoc->saveHTML(); - $this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8'); - } - } - - public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null) - { - //select a default sender if none is provided. - if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){ - $this->SetRecipientFrom($this->m_aData['to']); - } - - if ($bForceSynchronous) - { - return $this->SendSynchronous($aIssues, $oLog); - } - else - { - $bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous'); - if ($bConfigASYNC) - { - return $this->SendAsynchronous($aIssues, $oLog); - } - else - { - return $this->SendSynchronous($aIssues, $oLog); - } - } - } - - public function AddToHeader($sKey, $sValue) - { - if (!array_key_exists('headers', $this->m_aData)) - { - $this->m_aData['headers'] = array(); - } - $this->m_aData['headers'][$sKey] = $sValue; - - if (strlen($sValue) > 0) - { - $oHeaders = $this->m_oMessage->getHeaders(); - switch(strtolower($sKey)) - { - default: - $oHeaders->addTextHeader($sKey, $sValue); - } - } - } - - public function SetMessageId($sId) - { - $this->m_aData['message_id'] = $sId; - - // Note: Swift will add the angle brackets for you - // so let's remove the angle brackets if present, for historical reasons - $sId = str_replace(array('<', '>'), '', $sId); - - $oMsgId = $this->m_oMessage->getHeaders()->get('Message-ID'); - $oMsgId->SetId($sId); - } - - public function SetReferences($sReferences) - { - $this->AddToHeader('References', $sReferences); - } - - public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null) - { - if (($sMimeType === 'text/html') && ($sCustomStyles !== null)) - { - require_once(APPROOT.'lib/emogrifier/Classes/Emogrifier.php'); - $emogrifier = new \Pelago\Emogrifier($sBody, $sCustomStyles); - $sBody = $emogrifier->emogrify(); // Adds html/body tags if not already present - } - $this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType); - $this->m_oMessage->setBody($sBody, $sMimeType); - } - - public function AddPart($sText, $sMimeType = 'text/html') - { - if (!array_key_exists('parts', $this->m_aData)) - { - $this->m_aData['parts'] = array(); - } - $this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType); - $this->m_oMessage->addPart($sText, $sMimeType); - } - - public function AddAttachment($data, $sFileName, $sMimeType) - { - if (!array_key_exists('attachments', $this->m_aData)) - { - $this->m_aData['attachments'] = array(); - } - $this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType); - $this->m_oMessage->attach(Swift_Attachment::newInstance($data, $sFileName, $sMimeType)); - } - - public function SetSubject($sSubject) - { - $this->m_aData['subject'] = $sSubject; - $this->m_oMessage->setSubject($sSubject); - } - - public function GetSubject() - { - return $this->m_oMessage->getSubject(); - } - - /** - * Helper to transform and sanitize addresses - * - get rid of empty addresses - */ - protected function AddressStringToArray($sAddressCSVList) - { - $aAddresses = array(); - foreach(explode(',', $sAddressCSVList) as $sAddress) - { - $sAddress = trim($sAddress); - if (strlen($sAddress) > 0) - { - $aAddresses[] = $sAddress; - } - } - return $aAddresses; - } - - public function SetRecipientTO($sAddress) - { - $this->m_aData['to'] = $sAddress; - if (!empty($sAddress)) - { - $aAddresses = $this->AddressStringToArray($sAddress); - $this->m_oMessage->setTo($aAddresses); - } - } - - public function GetRecipientTO($bAsString = false) - { - $aRes = $this->m_oMessage->getTo(); - if ($aRes === null) - { - // There is no "To" header field - $aRes = array(); - } - if ($bAsString) - { - $aStrings = array(); - foreach ($aRes as $sEmail => $sName) - { - if (is_null($sName)) - { - $aStrings[] = $sEmail; - } - else - { - $sName = str_replace(array('<', '>'), '', $sName); - $aStrings[] = "$sName <$sEmail>"; - } - } - return implode(', ', $aStrings); - } - else - { - return $aRes; - } - } - - public function SetRecipientCC($sAddress) - { - $this->m_aData['cc'] = $sAddress; - if (!empty($sAddress)) - { - $aAddresses = $this->AddressStringToArray($sAddress); - $this->m_oMessage->setCc($aAddresses); - } - } - - public function SetRecipientBCC($sAddress) - { - $this->m_aData['bcc'] = $sAddress; - if (!empty($sAddress)) - { - $aAddresses = $this->AddressStringToArray($sAddress); - $this->m_oMessage->setBcc($aAddresses); - } - } - - public function SetRecipientFrom($sAddress, $sLabel = '') - { - $this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel); - if ($sLabel != '') - { - $this->m_oMessage->setFrom(array($sAddress => $sLabel)); - } - else if (!empty($sAddress)) - { - $this->m_oMessage->setFrom($sAddress); - } - } - - public function SetRecipientReplyTo($sAddress) - { - $this->m_aData['reply_to'] = $sAddress; - if (!empty($sAddress)) - { - $this->m_oMessage->setReplyTo($sAddress); - } - } - -} - -///////////////////////////////////////////////////////////////////////////////////// - -/** - * Extension to SwiftMailer: "debug" transport that pretends messages have been sent, - * but just log them to a file. - * - * @package Swift - * @author Denis Flaven - */ -class Swift_Transport_LogFileTransport extends Swift_Transport_NullTransport -{ - protected $sLogFile; - - /** - * Sends the given message. - * - * @param Swift_Mime_Message $message - * @param string[] $failedRecipients An array of failures by-reference - * - * @return int The number of sent emails - */ - public function send(Swift_Mime_Message $message, &$failedRecipients = null) - { - $hFile = @fopen($this->sLogFile, 'a'); - if ($hFile) - { - $sTxt = "================== ".date('Y-m-d H:i:s')." ==================\n"; - $sTxt .= $message->toString()."\n"; - - @fwrite($hFile, $sTxt); - @fclose($hFile); - } - - return parent::send($message, $failedRecipients); - } - - public function setLogFile($sFilename) - { - $this->sLogFile = $sFilename; - } -} - -/** - * Pretends messages have been sent, but just log them to a file. - * - * @package Swift - * @author Denis Flaven - */ -class Swift_LogFileTransport extends Swift_Transport_LogFileTransport -{ - /** - * Create a new LogFileTransport. - */ - public function __construct() - { - call_user_func_array( - array($this, 'Swift_Transport_LogFileTransport::__construct'), - Swift_DependencyContainer::getInstance() - ->createDependenciesFor('transport.null') - ); - } - - /** - * Create a new LogFileTransport instance. - * - * @return Swift_LogFileTransport - */ - public static function newInstance() - { - return new self(); - } + + + +/** + * Send an email (abstraction for synchronous/asynchronous modes) + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/lib/swiftmailer/lib/swift_required.php'); + +Swift_Preferences::getInstance()->setCharset('UTF-8'); + + +define ('EMAIL_SEND_OK', 0); +define ('EMAIL_SEND_PENDING', 1); +define ('EMAIL_SEND_ERROR', 2); + +class EMail +{ + // Serialization formats + const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object. + // Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string + const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed) + + protected static $m_oConfig = null; + protected $m_aData; // For storing data to serialize + + public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE) + { + if (is_null(self::$m_oConfig)) + { + self::$m_oConfig = new Config($sConfigFile); + } + } + + protected $m_oMessage; + + public function __construct() + { + $this->m_aData = array(); + $this->m_oMessage = Swift_Message::newInstance(); + $this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label')); + } + + /** + * Custom serialization method + * No longer use the brute force "serialize" method since + * 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field) + * 2) The size tends to be quite big (sometimes ten times the size of the email) + */ + public function SerializeV2() + { + return serialize($this->m_aData); + } + + /** + * Custom de-serialization method + * @param string $sSerializedMessage The serialized representation of the message + */ + static public function UnSerializeV2($sSerializedMessage) + { + $aData = unserialize($sSerializedMessage); + $oMessage = new Email(); + + if (array_key_exists('body', $aData)) + { + $oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']); + } + if (array_key_exists('message_id', $aData)) + { + $oMessage->SetMessageId($aData['message_id']); + } + if (array_key_exists('bcc', $aData)) + { + $oMessage->SetRecipientBCC($aData['bcc']); + } + if (array_key_exists('cc', $aData)) + { + $oMessage->SetRecipientCC($aData['cc']); + } + if (array_key_exists('from', $aData)) + { + $oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']); + } + if (array_key_exists('reply_to', $aData)) + { + $oMessage->SetRecipientReplyTo($aData['reply_to']); + } + if (array_key_exists('to', $aData)) + { + $oMessage->SetRecipientTO($aData['to']); + } + if (array_key_exists('subject', $aData)) + { + $oMessage->SetSubject($aData['subject']); + } + + + if (array_key_exists('headers', $aData)) + { + foreach($aData['headers'] as $sKey => $sValue) + { + $oMessage->AddToHeader($sKey, $sValue); + } + } + if (array_key_exists('parts', $aData)) + { + foreach($aData['parts'] as $aPart) + { + $oMessage->AddPart($aPart['text'], $aPart['mimeType']); + } + } + if (array_key_exists('attachments', $aData)) + { + foreach($aData['attachments'] as $aAttachment) + { + $oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']); + } + } + return $oMessage; + } + + protected function SendAsynchronous(&$aIssues, $oLog = null) + { + try + { + AsyncSendEmail::AddToQueue($this, $oLog); + } + catch(Exception $e) + { + $aIssues = array($e->GetMessage()); + return EMAIL_SEND_ERROR; + } + $aIssues = array(); + return EMAIL_SEND_PENDING; + } + + protected function SendSynchronous(&$aIssues, $oLog = null) + { + // If the body of the message is in HTML, embed all images based on attachments + $this->EmbedInlineImages(); + + $this->LoadConfig(); + + $sTransport = self::$m_oConfig->Get('email_transport'); + switch ($sTransport) + { + case 'SMTP': + $sHost = self::$m_oConfig->Get('email_transport_smtp.host'); + $sPort = self::$m_oConfig->Get('email_transport_smtp.port'); + $sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption'); + $sUserName = self::$m_oConfig->Get('email_transport_smtp.username'); + $sPassword = self::$m_oConfig->Get('email_transport_smtp.password'); + + $oTransport = Swift_SmtpTransport::newInstance($sHost, $sPort, $sEncryption); + if (strlen($sUserName) > 0) + { + $oTransport->setUsername($sUserName); + $oTransport->setPassword($sPassword); + } + break; + + case 'Null': + $oTransport = Swift_NullTransport::newInstance(); + break; + + case 'LogFile': + $oTransport = Swift_LogFileTransport::newInstance(); + $oTransport->setLogFile(APPROOT.'log/mail.log'); + break; + + case 'PHPMail': + default: + $oTransport = Swift_MailTransport::newInstance(); + } + + $oMailer = Swift_Mailer::newInstance($oTransport); + + $aFailedRecipients = array(); + $this->m_oMessage->setMaxLineLength(0); + $oKPI = new ExecutionKPI(); + try + { + $iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients); + if ($iSent === 0) + { + // Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!! + IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients)); + $aIssues = array('Some recipients were invalid.'); + $oKPI->ComputeStats('Email Sent', 'Error received'); + return EMAIL_SEND_ERROR; + } + else + { + $aIssues = array(); + $oKPI->ComputeStats('Email Sent', 'Succeded'); + return EMAIL_SEND_OK; + } + } + catch (Exception $e) + { + $oKPI->ComputeStats('Email Sent', 'Error received'); + throw $e; + } + } + + /** + * Reprocess the body of the message (if it is an HTML message) + * to replace the URL of images based on attachments by a link + * to an embedded image (i.e. cid:....) + */ + protected function EmbedInlineImages() + { + if ($this->m_aData['body']['mimeType'] == 'text/html') + { + $oDOMDoc = new DOMDocument(); + $oDOMDoc->preserveWhitespace = true; + @$oDOMDoc->loadHTML(''.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified + + $oXPath = new DOMXPath($oDOMDoc); + $sXPath = "//img[@data-img-id]"; + $oImagesList = $oXPath->query($sXPath); + + if ($oImagesList->length != 0) + { + foreach($oImagesList as $oImg) + { + $iAttId = $oImg->getAttribute('data-img-id'); + $oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */); + if ($oAttachment) + { + $oDoc = $oAttachment->Get('contents'); + $oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType()); + $sCid = $this->m_oMessage->embed($oSwiftImage); + $oImg->setAttribute('src', $sCid); + } + } + } + $sHtmlBody = $oDOMDoc->saveHTML(); + $this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8'); + } + } + + public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null) + { + //select a default sender if none is provided. + if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){ + $this->SetRecipientFrom($this->m_aData['to']); + } + + if ($bForceSynchronous) + { + return $this->SendSynchronous($aIssues, $oLog); + } + else + { + $bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous'); + if ($bConfigASYNC) + { + return $this->SendAsynchronous($aIssues, $oLog); + } + else + { + return $this->SendSynchronous($aIssues, $oLog); + } + } + } + + public function AddToHeader($sKey, $sValue) + { + if (!array_key_exists('headers', $this->m_aData)) + { + $this->m_aData['headers'] = array(); + } + $this->m_aData['headers'][$sKey] = $sValue; + + if (strlen($sValue) > 0) + { + $oHeaders = $this->m_oMessage->getHeaders(); + switch(strtolower($sKey)) + { + default: + $oHeaders->addTextHeader($sKey, $sValue); + } + } + } + + public function SetMessageId($sId) + { + $this->m_aData['message_id'] = $sId; + + // Note: Swift will add the angle brackets for you + // so let's remove the angle brackets if present, for historical reasons + $sId = str_replace(array('<', '>'), '', $sId); + + $oMsgId = $this->m_oMessage->getHeaders()->get('Message-ID'); + $oMsgId->SetId($sId); + } + + public function SetReferences($sReferences) + { + $this->AddToHeader('References', $sReferences); + } + + public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null) + { + if (($sMimeType === 'text/html') && ($sCustomStyles !== null)) + { + require_once(APPROOT.'lib/emogrifier/Classes/Emogrifier.php'); + $emogrifier = new \Pelago\Emogrifier($sBody, $sCustomStyles); + $sBody = $emogrifier->emogrify(); // Adds html/body tags if not already present + } + $this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType); + $this->m_oMessage->setBody($sBody, $sMimeType); + } + + public function AddPart($sText, $sMimeType = 'text/html') + { + if (!array_key_exists('parts', $this->m_aData)) + { + $this->m_aData['parts'] = array(); + } + $this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType); + $this->m_oMessage->addPart($sText, $sMimeType); + } + + public function AddAttachment($data, $sFileName, $sMimeType) + { + if (!array_key_exists('attachments', $this->m_aData)) + { + $this->m_aData['attachments'] = array(); + } + $this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType); + $this->m_oMessage->attach(Swift_Attachment::newInstance($data, $sFileName, $sMimeType)); + } + + public function SetSubject($sSubject) + { + $this->m_aData['subject'] = $sSubject; + $this->m_oMessage->setSubject($sSubject); + } + + public function GetSubject() + { + return $this->m_oMessage->getSubject(); + } + + /** + * Helper to transform and sanitize addresses + * - get rid of empty addresses + */ + protected function AddressStringToArray($sAddressCSVList) + { + $aAddresses = array(); + foreach(explode(',', $sAddressCSVList) as $sAddress) + { + $sAddress = trim($sAddress); + if (strlen($sAddress) > 0) + { + $aAddresses[] = $sAddress; + } + } + return $aAddresses; + } + + public function SetRecipientTO($sAddress) + { + $this->m_aData['to'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setTo($aAddresses); + } + } + + public function GetRecipientTO($bAsString = false) + { + $aRes = $this->m_oMessage->getTo(); + if ($aRes === null) + { + // There is no "To" header field + $aRes = array(); + } + if ($bAsString) + { + $aStrings = array(); + foreach ($aRes as $sEmail => $sName) + { + if (is_null($sName)) + { + $aStrings[] = $sEmail; + } + else + { + $sName = str_replace(array('<', '>'), '', $sName); + $aStrings[] = "$sName <$sEmail>"; + } + } + return implode(', ', $aStrings); + } + else + { + return $aRes; + } + } + + public function SetRecipientCC($sAddress) + { + $this->m_aData['cc'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setCc($aAddresses); + } + } + + public function SetRecipientBCC($sAddress) + { + $this->m_aData['bcc'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setBcc($aAddresses); + } + } + + public function SetRecipientFrom($sAddress, $sLabel = '') + { + $this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel); + if ($sLabel != '') + { + $this->m_oMessage->setFrom(array($sAddress => $sLabel)); + } + else if (!empty($sAddress)) + { + $this->m_oMessage->setFrom($sAddress); + } + } + + public function SetRecipientReplyTo($sAddress) + { + $this->m_aData['reply_to'] = $sAddress; + if (!empty($sAddress)) + { + $this->m_oMessage->setReplyTo($sAddress); + } + } + +} + +///////////////////////////////////////////////////////////////////////////////////// + +/** + * Extension to SwiftMailer: "debug" transport that pretends messages have been sent, + * but just log them to a file. + * + * @package Swift + * @author Denis Flaven + */ +class Swift_Transport_LogFileTransport extends Swift_Transport_NullTransport +{ + protected $sLogFile; + + /** + * Sends the given message. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int The number of sent emails + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $hFile = @fopen($this->sLogFile, 'a'); + if ($hFile) + { + $sTxt = "================== ".date('Y-m-d H:i:s')." ==================\n"; + $sTxt .= $message->toString()."\n"; + + @fwrite($hFile, $sTxt); + @fclose($hFile); + } + + return parent::send($message, $failedRecipients); + } + + public function setLogFile($sFilename) + { + $this->sLogFile = $sFilename; + } +} + +/** + * Pretends messages have been sent, but just log them to a file. + * + * @package Swift + * @author Denis Flaven + */ +class Swift_LogFileTransport extends Swift_Transport_LogFileTransport +{ + /** + * Create a new LogFileTransport. + */ + public function __construct() + { + call_user_func_array( + array($this, 'Swift_Transport_LogFileTransport::__construct'), + Swift_DependencyContainer::getInstance() + ->createDependenciesFor('transport.null') + ); + } + + /** + * Create a new LogFileTransport instance. + * + * @return Swift_LogFileTransport + */ + public static function newInstance() + { + return new self(); + } } \ No newline at end of file diff --git a/core/event.class.inc.php b/core/event.class.inc.php index cc126b3f2..fba748ca4 100644 --- a/core/event.class.inc.php +++ b/core/event.class.inc.php @@ -1,437 +1,437 @@ - - - -/** - * Persistent class Event and derived - * Application internal events - * There is also a file log - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class Event extends DBObject implements iDisplay -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "display_template" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); -// MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'message', 'userinfo')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'finalclass', 'message')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - /** - * Maps the given context parameter name to the appropriate filter/search code for this class - * @param string $sContextParam Name of the context parameter, i.e. 'org_id' - * @return string Filter code, i.e. 'customer_id' - */ - public static function MapContextParam($sContextParam) - { - if ($sContextParam == 'menu') - { - return null; - } - else - { - return $sContextParam; - } - } - - /** - * This function returns a 'hilight' CSS class, used to hilight a given row in a table - * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL, - * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE - * To Be overridden by derived classes - * @param void - * @return String The desired higlight class for the object/row - */ - public function GetHilightClass() - { - // Possible return values are: - // HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE - return HILIGHT_CLASS_NONE; // Not hilighted by default - } - - public static function GetUIPage() - { - return 'UI.php'; - } - - function DisplayDetails(WebPage $oPage, $bEditMode = false) - { - // Object's details - //$this->DisplayBareHeader($oPage, $bEditMode); - $oPage->AddTabContainer(OBJECT_PROPERTIES_TAB); - $oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB); - $oPage->SetCurrentTab(Dict::S('UI:PropertiesTab')); - $this->DisplayBareProperties($oPage, $bEditMode); - } - - function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) - { - if ($bEditMode) return array(); // Not editable - - $aDetails = array(); - $sClass = get_class($this); - $aZList = MetaModel::FlattenZlist(MetaModel::GetZListItems($sClass, 'details')); - foreach( $aZList as $sAttCode) - { - $sDisplayValue = $this->GetAsHTML($sAttCode); - $aDetails[] = array('label' => ''.MetaModel::GetLabel($sClass, $sAttCode).'', 'value' => $sDisplayValue); - } - $oPage->Details($aDetails); - return array(); - } -} - -class EventNotification extends Event -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_notification", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - "order_by_default" => array('date' => false), - 'indexes' => array( - array('object_id'), - ) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> "", "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> "", "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values"=>null, "sql"=>"object_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'message', 'userinfo', 'trigger_id', 'action_id', 'object_id')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'message')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - -} - -class EventNotificationEmail extends EventNotification -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_email", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("cc", array("allowed_values"=>null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("from", array("allowed_values"=>null, "sql"=>"from", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeHTML("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeTable("attachments", array("allowed_values"=>null, "sql"=>"attachments", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'message', 'trigger_id', 'action_id', 'object_id', 'to', 'cc', 'bcc', 'from', 'subject', 'body', 'attachments')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'message', 'to', 'subject', 'attachments')); // Attributes to be displayed for a list - - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - -} - -class EventIssue extends Event -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_issue", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("issue", array("allowed_values"=>null, "sql"=>"issue", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("impact", array("allowed_values"=>null, "sql"=>"impact", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("page", array("allowed_values"=>null, "sql"=>"page", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributePropertySet("arguments_post", array("allowed_values"=>null, "sql"=>"arguments_post", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributePropertySet("arguments_get", array("allowed_values"=>null, "sql"=>"arguments_get", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeTable("callstack", array("allowed_values"=>null, "sql"=>"callstack", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributePropertySet("data", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'issue', 'impact', 'page', 'arguments_post', 'arguments_get', 'callstack', 'data')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'issue', 'impact')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - protected function OnInsert() - { - // Init page information: name, arguments - // - $this->Set('page', @$GLOBALS['_SERVER']['SCRIPT_NAME']); - - if (array_key_exists('_GET', $GLOBALS) && is_array($GLOBALS['_GET'])) - { - $this->Set('arguments_get', $GLOBALS['_GET']); - } - else - { - $this->Set('arguments_get', array()); - } - - if (array_key_exists('_POST', $GLOBALS) && is_array($GLOBALS['_POST'])) - { - $aPost = array(); - foreach($GLOBALS['_POST'] as $sKey => $sValue) - { - if (is_string($sValue)) - { - if (strlen($sValue) < 256) - { - $aPost[$sKey] = $sValue; - } - else - { - $aPost[$sKey] = "!long string: ".strlen($sValue). " chars"; - } - } - else - { - // Not a string (avoid warnings in case the value cannot be easily casted into a string) - $aPost[$sKey] = @(string) $sValue; - } - } - $this->Set('arguments_post', $aPost); - } - else - { - $this->Set('arguments_post', array()); - } - - $sLength = strlen($this->Get('issue')); - if ($sLength > 255) - { - $this->Set('issue', substr($this->Get('issue'), 0, 200)." -truncated ($sLength chars)"); - } - - $sLength = strlen($this->Get('impact')); - if ($sLength > 255) - { - $this->Set('impact', substr($this->Get('impact'), 0, 200)." -truncated ($sLength chars)"); - } - - $sLength = strlen($this->Get('page')); - if ($sLength > 255) - { - $this->Set('page', substr($this->Get('page'), 0, 200)." -truncated ($sLength chars)"); - } - } -} - - -class EventWebService extends Event -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_webservice", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("verb", array("allowed_values"=>null, "sql"=>"verb", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - //MetaModel::Init_AddAttribute(new AttributeStructure("arguments", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeBoolean("result", array("allowed_values"=>null, "sql"=>"result", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("log_info", array("allowed_values"=>null, "sql"=>"log_info", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("log_warning", array("allowed_values"=>null, "sql"=>"log_warning", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("log_error", array("allowed_values"=>null, "sql"=>"log_error", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("data", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'verb', 'result', 'log_info', 'log_warning', 'log_error', 'data')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'verb', 'result')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class EventRestService extends Event -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_restservice", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("operation", array("allowed_values"=>null, "sql"=>"operation", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("json_input", array("allowed_values"=>null, "sql"=>"json_input", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeInteger("code", array("allowed_values"=>null, "sql"=>"code", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("json_output", array("allowed_values"=>null, "sql"=>"json_output", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("provider", array("allowed_values"=>null, "sql"=>"provider", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'operation', 'version', 'json_input', 'message', 'code', 'json_output', 'provider')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'operation', 'message')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class EventLoginUsage extends Event -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_loginusage", - "db_key_field" => "id", - "db_finalclass_field" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); - $aZList = array('date', 'user_id'); - if (MetaModel::IsValidAttCode('Contact', 'name')) - { - MetaModel::Init_AddAttribute(new AttributeExternalField("contact_name", array("allowed_values"=>null, "extkey_attcode"=>"user_id", "target_attcode"=>"contactid", "is_null_allowed"=>true, "depends_on"=>array()))); - $aZList[] = 'contact_name'; - } - if (MetaModel::IsValidAttCode('Contact', 'email')) - { - MetaModel::Init_AddAttribute(new AttributeExternalField("contact_email", array("allowed_values"=>null, "extkey_attcode"=>"user_id", "target_attcode"=>"email", "is_null_allowed"=>true, "depends_on"=>array()))); - $aZList[] = 'contact_email'; - } - // Display lists - MetaModel::Init_SetZListItems('details', array_merge($aZList, array('userinfo', 'message'))); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array_merge($aZList, array('userinfo'))); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', $aZList); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class EventOnObject extends Event -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb,view_in_gui", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_event_onobject", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - "order_by_default" => array('date' => false) - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("obj_class", array("allowed_values"=>null, "sql"=>"obj_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("obj_key", array("allowed_values"=>null, "sql"=>"obj_key", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'obj_class', 'obj_key', 'message')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'obj_class', 'obj_key', 'message')); // Attributes to be displayed for a list - } -} + + + +/** + * Persistent class Event and derived + * Application internal events + * There is also a file log + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class Event extends DBObject implements iDisplay +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); +// MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'message', 'userinfo')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'finalclass', 'message')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + /** + * Maps the given context parameter name to the appropriate filter/search code for this class + * @param string $sContextParam Name of the context parameter, i.e. 'org_id' + * @return string Filter code, i.e. 'customer_id' + */ + public static function MapContextParam($sContextParam) + { + if ($sContextParam == 'menu') + { + return null; + } + else + { + return $sContextParam; + } + } + + /** + * This function returns a 'hilight' CSS class, used to hilight a given row in a table + * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL, + * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE + * To Be overridden by derived classes + * @param void + * @return String The desired higlight class for the object/row + */ + public function GetHilightClass() + { + // Possible return values are: + // HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE + return HILIGHT_CLASS_NONE; // Not hilighted by default + } + + public static function GetUIPage() + { + return 'UI.php'; + } + + function DisplayDetails(WebPage $oPage, $bEditMode = false) + { + // Object's details + //$this->DisplayBareHeader($oPage, $bEditMode); + $oPage->AddTabContainer(OBJECT_PROPERTIES_TAB); + $oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB); + $oPage->SetCurrentTab(Dict::S('UI:PropertiesTab')); + $this->DisplayBareProperties($oPage, $bEditMode); + } + + function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) + { + if ($bEditMode) return array(); // Not editable + + $aDetails = array(); + $sClass = get_class($this); + $aZList = MetaModel::FlattenZlist(MetaModel::GetZListItems($sClass, 'details')); + foreach( $aZList as $sAttCode) + { + $sDisplayValue = $this->GetAsHTML($sAttCode); + $aDetails[] = array('label' => ''.MetaModel::GetLabel($sClass, $sAttCode).'', 'value' => $sDisplayValue); + } + $oPage->Details($aDetails); + return array(); + } +} + +class EventNotification extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_notification", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + "order_by_default" => array('date' => false), + 'indexes' => array( + array('object_id'), + ) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> "", "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> "", "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values"=>null, "sql"=>"object_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'message', 'userinfo', 'trigger_id', 'action_id', 'object_id')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'message')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + +} + +class EventNotificationEmail extends EventNotification +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_email", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("cc", array("allowed_values"=>null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("from", array("allowed_values"=>null, "sql"=>"from", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeHTML("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTable("attachments", array("allowed_values"=>null, "sql"=>"attachments", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'message', 'trigger_id', 'action_id', 'object_id', 'to', 'cc', 'bcc', 'from', 'subject', 'body', 'attachments')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'message', 'to', 'subject', 'attachments')); // Attributes to be displayed for a list + + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + +} + +class EventIssue extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_issue", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("issue", array("allowed_values"=>null, "sql"=>"issue", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("impact", array("allowed_values"=>null, "sql"=>"impact", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("page", array("allowed_values"=>null, "sql"=>"page", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("arguments_post", array("allowed_values"=>null, "sql"=>"arguments_post", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("arguments_get", array("allowed_values"=>null, "sql"=>"arguments_get", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTable("callstack", array("allowed_values"=>null, "sql"=>"callstack", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributePropertySet("data", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'issue', 'impact', 'page', 'arguments_post', 'arguments_get', 'callstack', 'data')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'issue', 'impact')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + protected function OnInsert() + { + // Init page information: name, arguments + // + $this->Set('page', @$GLOBALS['_SERVER']['SCRIPT_NAME']); + + if (array_key_exists('_GET', $GLOBALS) && is_array($GLOBALS['_GET'])) + { + $this->Set('arguments_get', $GLOBALS['_GET']); + } + else + { + $this->Set('arguments_get', array()); + } + + if (array_key_exists('_POST', $GLOBALS) && is_array($GLOBALS['_POST'])) + { + $aPost = array(); + foreach($GLOBALS['_POST'] as $sKey => $sValue) + { + if (is_string($sValue)) + { + if (strlen($sValue) < 256) + { + $aPost[$sKey] = $sValue; + } + else + { + $aPost[$sKey] = "!long string: ".strlen($sValue). " chars"; + } + } + else + { + // Not a string (avoid warnings in case the value cannot be easily casted into a string) + $aPost[$sKey] = @(string) $sValue; + } + } + $this->Set('arguments_post', $aPost); + } + else + { + $this->Set('arguments_post', array()); + } + + $sLength = strlen($this->Get('issue')); + if ($sLength > 255) + { + $this->Set('issue', substr($this->Get('issue'), 0, 200)." -truncated ($sLength chars)"); + } + + $sLength = strlen($this->Get('impact')); + if ($sLength > 255) + { + $this->Set('impact', substr($this->Get('impact'), 0, 200)." -truncated ($sLength chars)"); + } + + $sLength = strlen($this->Get('page')); + if ($sLength > 255) + { + $this->Set('page', substr($this->Get('page'), 0, 200)." -truncated ($sLength chars)"); + } + } +} + + +class EventWebService extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_webservice", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("verb", array("allowed_values"=>null, "sql"=>"verb", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + //MetaModel::Init_AddAttribute(new AttributeStructure("arguments", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeBoolean("result", array("allowed_values"=>null, "sql"=>"result", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("log_info", array("allowed_values"=>null, "sql"=>"log_info", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("log_warning", array("allowed_values"=>null, "sql"=>"log_warning", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("log_error", array("allowed_values"=>null, "sql"=>"log_error", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("data", array("allowed_values"=>null, "sql"=>"data", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'verb', 'result', 'log_info', 'log_warning', 'log_error', 'data')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'verb', 'result')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class EventRestService extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_restservice", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("operation", array("allowed_values"=>null, "sql"=>"operation", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("json_input", array("allowed_values"=>null, "sql"=>"json_input", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeInteger("code", array("allowed_values"=>null, "sql"=>"code", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeText("json_output", array("allowed_values"=>null, "sql"=>"json_output", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("provider", array("allowed_values"=>null, "sql"=>"provider", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'operation', 'version', 'json_input', 'message', 'code', 'json_output', 'provider')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'operation', 'message')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class EventLoginUsage extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_loginusage", + "db_key_field" => "id", + "db_finalclass_field" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=> "", "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); + $aZList = array('date', 'user_id'); + if (MetaModel::IsValidAttCode('Contact', 'name')) + { + MetaModel::Init_AddAttribute(new AttributeExternalField("contact_name", array("allowed_values"=>null, "extkey_attcode"=>"user_id", "target_attcode"=>"contactid", "is_null_allowed"=>true, "depends_on"=>array()))); + $aZList[] = 'contact_name'; + } + if (MetaModel::IsValidAttCode('Contact', 'email')) + { + MetaModel::Init_AddAttribute(new AttributeExternalField("contact_email", array("allowed_values"=>null, "extkey_attcode"=>"user_id", "target_attcode"=>"email", "is_null_allowed"=>true, "depends_on"=>array()))); + $aZList[] = 'contact_email'; + } + // Display lists + MetaModel::Init_SetZListItems('details', array_merge($aZList, array('userinfo', 'message'))); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array_merge($aZList, array('userinfo'))); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', $aZList); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class EventOnObject extends Event +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_event_onobject", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + "order_by_default" => array('date' => false) + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("obj_class", array("allowed_values"=>null, "sql"=>"obj_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("obj_key", array("allowed_values"=>null, "sql"=>"obj_key", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'obj_class', 'obj_key', 'message')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'obj_class', 'obj_key', 'message')); // Attributes to be displayed for a list + } +} diff --git a/core/expression.class.inc.php b/core/expression.class.inc.php index 1e76b9bda..1135cff4c 100644 --- a/core/expression.class.inc.php +++ b/core/expression.class.inc.php @@ -1,4 +1,4 @@ - -// - -class ExpressionCache -{ - static private $aCache = array(); - - static public function GetCachedExpression($sClass, $sAttCode) - { - // read current cache - @include_once (static::GetCacheFileName()); - - $oExpr = null; - $sKey = static::GetKey($sClass, $sAttCode); - if (array_key_exists($sKey, static::$aCache)) - { - $oExpr = static::$aCache[$sKey]; - } - else - { - if (class_exists('ExpressionCacheData')) - { - if (array_key_exists($sKey, ExpressionCacheData::$aCache)) - { - $sVal = ExpressionCacheData::$aCache[$sKey]; - $oExpr = unserialize($sVal); - static::$aCache[$sKey] = $oExpr; - } - } - } - return $oExpr; - } - - - static public function Warmup() - { - $sFilePath = static::GetCacheFileName(); - - if (!is_file($sFilePath)) - { - $content = << '".serialize($oExpr)."',\n"; - } - - /** - * @param $sClass - * @param $sAttCode - * @return string - */ - static private function GetKey($sClass, $sAttCode) - { - return $sClass.'::'.$sAttCode; - } - - public static function GetCacheFileName() - { - return utils::GetCachePath().'expressioncache.php'; - } - -} - - - + +// + +class ExpressionCache +{ + static private $aCache = array(); + + static public function GetCachedExpression($sClass, $sAttCode) + { + // read current cache + @include_once (static::GetCacheFileName()); + + $oExpr = null; + $sKey = static::GetKey($sClass, $sAttCode); + if (array_key_exists($sKey, static::$aCache)) + { + $oExpr = static::$aCache[$sKey]; + } + else + { + if (class_exists('ExpressionCacheData')) + { + if (array_key_exists($sKey, ExpressionCacheData::$aCache)) + { + $sVal = ExpressionCacheData::$aCache[$sKey]; + $oExpr = unserialize($sVal); + static::$aCache[$sKey] = $oExpr; + } + } + } + return $oExpr; + } + + + static public function Warmup() + { + $sFilePath = static::GetCacheFileName(); + + if (!is_file($sFilePath)) + { + $content = << '".serialize($oExpr)."',\n"; + } + + /** + * @param $sClass + * @param $sAttCode + * @return string + */ + static private function GetKey($sClass, $sAttCode) + { + return $sClass.'::'.$sAttCode; + } + + public static function GetCacheFileName() + { + return utils::GetCachePath().'expressioncache.php'; + } + +} + + + diff --git a/core/filterdef.class.inc.php b/core/filterdef.class.inc.php index 0465038aa..47ee1802e 100644 --- a/core/filterdef.class.inc.php +++ b/core/filterdef.class.inc.php @@ -1,212 +1,212 @@ - - - -/** - * Definition of a filter - * Most of the time, a filter corresponds to an attribute, but we could imagine other search criteria - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - - -require_once('MyHelpers.class.inc.php'); - - -/** - * Definition of a filter (could be made out of an existing attribute, or from an expression) - * - * @package iTopORM - */ -abstract class FilterDefinition -{ - abstract public function GetType(); - abstract public function GetTypeDesc(); - - protected $m_sCode; - private $m_aParams = array(); - protected function Get($sParamName) {return $this->m_aParams[$sParamName];} - - public function __construct($sCode, $aParams = array()) - { - $this->m_sCode = $sCode; - $this->m_aParams = $aParams; - $this->ConsistencyCheck(); - } - - // to be overloaded - static protected function ListExpectedParams() - { - return array(); - } - - private function ConsistencyCheck() - { - // Check that any mandatory param has been specified - // - $aExpectedParams = $this->ListExpectedParams(); - foreach($aExpectedParams as $sParamName) - { - if (!array_key_exists($sParamName, $this->m_aParams)) - { - $aBacktrace = debug_backtrace(); - $sTargetClass = $aBacktrace[2]["class"]; - $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; - throw new CoreException("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); - } - } - } - - public function GetCode() {return $this->m_sCode;} - abstract public function GetLabel(); - abstract public function GetValuesDef(); - - // returns an array of opcode=>oplabel (e.g. "differs from") - abstract public function GetOperators(); - // returns an opcode - abstract public function GetLooseOperator(); - abstract public function GetSQLExpressions(); - - // Wrapper - no need for overloading this one - public function GetOpDescription($sOpCode) - { - $aOperators = $this->GetOperators(); - if (!array_key_exists($sOpCode, $aOperators)) - { - throw new CoreException("Unknown operator '$sOpCode'"); - } - - return $aOperators[$sOpCode]; - } -} - -/** - * Match against the object unique identifier - * - * @package iTopORM - */ -class FilterPrivateKey extends FilterDefinition -{ - static protected function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("id_field")); - } - - public function GetType() {return "PrivateKey";} - public function GetTypeDesc() {return "Match against object identifier";} - - public function GetLabel() - { - return "Object Private Key"; - } - - public function GetValuesDef() - { - return null; - } - - public function GetOperators() - { - return array( - "="=>"equals", - "!="=>"differs from", - "IN"=>"in", - "NOTIN"=>"not in" - ); - } - public function GetLooseOperator() - { - return "IN"; - } - - public function GetSQLExpressions() - { - return array( - '' => $this->Get("id_field"), - ); - } -} - -/** - * Match against an existing attribute (the attribute type will determine the available operators) - * - * @package iTopORM - */ -class FilterFromAttribute extends FilterDefinition -{ - static protected function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("refattribute")); - } - - public function __construct($oRefAttribute, $sSuffix = '') - { - // In this very specific case, the code is the one of the attribute - // (this to get a very very simple syntax upon declaration) - $aParam = array(); - $aParam["refattribute"] = $oRefAttribute; - parent::__construct($oRefAttribute->GetCode().$sSuffix, $aParam); - } - - public function GetType() {return "Basic";} - public function GetTypeDesc() {return "Match against field contents";} - - public function __GetRefAttribute() // for checking purposes only !!! - { - return $oAttDef = $this->Get("refattribute"); - } - - public function GetLabel() - { - $oAttDef = $this->Get("refattribute"); - return $oAttDef->GetLabel(); - } - - public function GetValuesDef() - { - $oAttDef = $this->Get("refattribute"); - return $oAttDef->GetValuesDef(); - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $oAttDef = $this->Get("refattribute"); - return $oAttDef->GetAllowedValues($aArgs, $sContains); - } - - public function GetOperators() - { - $oAttDef = $this->Get("refattribute"); - return $oAttDef->GetBasicFilterOperators(); - } - public function GetLooseOperator() - { - $oAttDef = $this->Get("refattribute"); - return $oAttDef->GetBasicFilterLooseOperator(); - } - - public function GetSQLExpressions() - { - $oAttDef = $this->Get("refattribute"); - return $oAttDef->GetSQLExpressions(); - } -} - -?> + + + +/** + * Definition of a filter + * Most of the time, a filter corresponds to an attribute, but we could imagine other search criteria + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + + +require_once('MyHelpers.class.inc.php'); + + +/** + * Definition of a filter (could be made out of an existing attribute, or from an expression) + * + * @package iTopORM + */ +abstract class FilterDefinition +{ + abstract public function GetType(); + abstract public function GetTypeDesc(); + + protected $m_sCode; + private $m_aParams = array(); + protected function Get($sParamName) {return $this->m_aParams[$sParamName];} + + public function __construct($sCode, $aParams = array()) + { + $this->m_sCode = $sCode; + $this->m_aParams = $aParams; + $this->ConsistencyCheck(); + } + + // to be overloaded + static protected function ListExpectedParams() + { + return array(); + } + + private function ConsistencyCheck() + { + // Check that any mandatory param has been specified + // + $aExpectedParams = $this->ListExpectedParams(); + foreach($aExpectedParams as $sParamName) + { + if (!array_key_exists($sParamName, $this->m_aParams)) + { + $aBacktrace = debug_backtrace(); + $sTargetClass = $aBacktrace[2]["class"]; + $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; + throw new CoreException("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); + } + } + } + + public function GetCode() {return $this->m_sCode;} + abstract public function GetLabel(); + abstract public function GetValuesDef(); + + // returns an array of opcode=>oplabel (e.g. "differs from") + abstract public function GetOperators(); + // returns an opcode + abstract public function GetLooseOperator(); + abstract public function GetSQLExpressions(); + + // Wrapper - no need for overloading this one + public function GetOpDescription($sOpCode) + { + $aOperators = $this->GetOperators(); + if (!array_key_exists($sOpCode, $aOperators)) + { + throw new CoreException("Unknown operator '$sOpCode'"); + } + + return $aOperators[$sOpCode]; + } +} + +/** + * Match against the object unique identifier + * + * @package iTopORM + */ +class FilterPrivateKey extends FilterDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("id_field")); + } + + public function GetType() {return "PrivateKey";} + public function GetTypeDesc() {return "Match against object identifier";} + + public function GetLabel() + { + return "Object Private Key"; + } + + public function GetValuesDef() + { + return null; + } + + public function GetOperators() + { + return array( + "="=>"equals", + "!="=>"differs from", + "IN"=>"in", + "NOTIN"=>"not in" + ); + } + public function GetLooseOperator() + { + return "IN"; + } + + public function GetSQLExpressions() + { + return array( + '' => $this->Get("id_field"), + ); + } +} + +/** + * Match against an existing attribute (the attribute type will determine the available operators) + * + * @package iTopORM + */ +class FilterFromAttribute extends FilterDefinition +{ + static protected function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), array("refattribute")); + } + + public function __construct($oRefAttribute, $sSuffix = '') + { + // In this very specific case, the code is the one of the attribute + // (this to get a very very simple syntax upon declaration) + $aParam = array(); + $aParam["refattribute"] = $oRefAttribute; + parent::__construct($oRefAttribute->GetCode().$sSuffix, $aParam); + } + + public function GetType() {return "Basic";} + public function GetTypeDesc() {return "Match against field contents";} + + public function __GetRefAttribute() // for checking purposes only !!! + { + return $oAttDef = $this->Get("refattribute"); + } + + public function GetLabel() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetLabel(); + } + + public function GetValuesDef() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetValuesDef(); + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetAllowedValues($aArgs, $sContains); + } + + public function GetOperators() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetBasicFilterOperators(); + } + public function GetLooseOperator() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetBasicFilterLooseOperator(); + } + + public function GetSQLExpressions() + { + $oAttDef = $this->Get("refattribute"); + return $oAttDef->GetSQLExpressions(); + } +} + +?> diff --git a/core/kpi.class.inc.php b/core/kpi.class.inc.php index 3dd6f018e..91b414ab5 100644 --- a/core/kpi.class.inc.php +++ b/core/kpi.class.inc.php @@ -1,401 +1,401 @@ - - - -/** - * Measures operations duration, memory usage, etc. (and some other KPIs) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class ExecutionKPI -{ - static protected $m_bEnabled_Duration = false; - static protected $m_bEnabled_Memory = false; - static protected $m_bBlameCaller = false; - static protected $m_sAllowedUser = '*'; - - static protected $m_aStats = array(); // Recurrent operations - static protected $m_aExecData = array(); // One shot operations - - protected $m_fStarted = null; - protected $m_iInitialMemory = null; - - static public function EnableDuration($iLevel) - { - if ($iLevel > 0) - { - self::$m_bEnabled_Duration = true; - if ($iLevel > 1) - { - self::$m_bBlameCaller = true; - } - } - } - - static public function EnableMemory($iLevel) - { - if ($iLevel > 0) - { - self::$m_bEnabled_Memory = true; - } - } - - /** - * @param string sUser A user login or * for all users - */ - static public function SetAllowedUser($sUser) - { - self::$m_sAllowedUser = $sUser; - } - - static public function IsEnabled() - { - if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) - { - if ((self::$m_sAllowedUser == '*') || (UserRights::GetUser() == trim(self::$m_sAllowedUser))) - { - return true; - } - } - return false; - } - - static public function GetDescription() - { - $aFeatures = array(); - if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration'; - if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage'; - $sFeatures = implode(', ', $aFeatures); - $sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'"; - return "KPI logging is active for $sFor. Measures: $sFeatures"; - } - - static public function ReportStats() - { - if (!self::IsEnabled()) return; - - global $fItopStarted; - $sExecId = microtime(); // id to differentiate the hrefs! - - $aBeginTimes = array(); - foreach (self::$m_aExecData as $aOpStats) - { - $aBeginTimes[] = $aOpStats['time_begin']; - } - array_multisort($aBeginTimes, self::$m_aExecData); - - $sTableStyle = 'background-color: #ccc; margin: 10px;'; - - self::Report("
    "); - self::Report("
    "); - self::Report("

    KPIs - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")

    "); - self::Report("

    ".date('Y-m-d H:i:s', $fItopStarted)."

    "); - self::Report("

    log_kpi_user_id: ".MetaModel::GetConfig()->Get('log_kpi_user_id')."

    "); - self::Report("
    "); - self::Report(""); - self::Report(""); - self::Report(" "); - self::Report(""); - foreach (self::$m_aExecData as $aOpStats) - { - $sOperation = $aOpStats['op']; - $sBegin = round($aOpStats['time_begin'], 3); - $sEnd = round($aOpStats['time_end'], 3); - $fDuration = $aOpStats['time_end'] - $aOpStats['time_begin']; - $sDuration = round($fDuration, 3); - - $sMemBegin = 'n/a'; - $sMemEnd = 'n/a'; - $sMemPeak = 'n/a'; - if (isset($aOpStats['mem_begin'])) - { - $sMemBegin = self::MemStr($aOpStats['mem_begin']); - $sMemEnd = self::MemStr($aOpStats['mem_end']); - if (isset($aOpStats['mem_peak'])) - { - $sMemPeak = self::MemStr($aOpStats['mem_peak']); - } - } - - self::Report(""); - self::Report(" "); - self::Report(""); - } - self::Report("
    OperationBeginEndDurationMemory startMemory endMemory peak
    $sOperation$sBegin$sEnd$sDuration$sMemBegin$sMemEnd$sMemPeak
    "); - self::Report("
    "); - - $aConsolidatedStats = array(); - foreach (self::$m_aStats as $sOperation => $aOpStats) - { - $fTotalOp = 0; - $iTotalOp = 0; - $fMinOp = null; - $fMaxOp = 0; - $sMaxOpArguments = null; - foreach ($aOpStats as $sArguments => $aEvents) - { - foreach ($aEvents as $aEventData) - { - $fDuration = $aEventData['time']; - $fTotalOp += $fDuration; - $iTotalOp++; - - $fMinOp = is_null($fMinOp) ? $fDuration : min($fMinOp, $fDuration); - if ($fDuration > $fMaxOp) - { - $sMaxOpArguments = $sArguments; - $fMaxOp = $fDuration; - } - } - } - $aConsolidatedStats[$sOperation] = array( - 'count' => $iTotalOp, - 'duration' => $fTotalOp, - 'min' => $fMinOp, - 'max' => $fMaxOp, - 'avg' => $fTotalOp / $iTotalOp, - 'max_args' => $sMaxOpArguments - ); - } - - self::Report("
    "); - self::Report(""); - self::Report(""); - self::Report(" "); - self::Report(""); - foreach ($aConsolidatedStats as $sOperation => $aOpStats) - { - $sOperation = ''.$sOperation.''; - $sCount = $aOpStats['count']; - $sDuration = round($aOpStats['duration'], 3); - $sMin = round($aOpStats['min'], 3); - $sMax = ''.round($aOpStats['max'], 3).''; - $sAvg = round($aOpStats['avg'], 3); - - self::Report(""); - self::Report(" "); - self::Report(""); - } - self::Report("
    OperationCountDurationMinMaxAvg
    $sOperation$sCount$sDuration$sMin$sMax$sAvg
    "); - self::Report("
    "); - - self::Report("
    "); - - self::Report("

    Next page stats

    "); - - // Report operation details - foreach (self::$m_aStats as $sOperation => $aOpStats) - { - $sOperationHtml = ''.$sOperation.''; - self::Report("

    $sOperationHtml

    "); - self::Report(""); - self::Report(""); - self::Report(" "); - self::Report(""); - foreach ($aOpStats as $sArguments => $aEvents) - { - $sHtmlArguments = '
    '.$sArguments.'
    '; - if ($aConsolidatedStats[$sOperation]['max_args'] == $sArguments) - { - $sHtmlArguments = ''.$sHtmlArguments.''; - } - if (isset($aEvents[0]['callers'])) - { - $sHtmlArguments .= '
    '; - $sHtmlArguments .= '
    Operation details (+ blame caller if log_kpi_duration = 2)CountDurationMinMax
    '; - $sHtmlArguments .= ''; - - foreach ($aEvents[0]['callers'] as $aCall) - { - $sHtmlArguments .= ''; - $sHtmlArguments .= ''; - $sHtmlArguments .= ''; - $sHtmlArguments .= ''; - } - $sHtmlArguments .= '
    Call stack for the FIRST caller
    '.$aCall['Function'].''.$aCall['File'].':'.$aCall['Line'].'
    '; - $sHtmlArguments .= '
    '; - } - - $fTotalInter = 0; - $fMinInter = null; - $fMaxInter = 0; - foreach ($aEvents as $aEventData) - { - $fDuration = $aEventData['time']; - $fTotalInter += $fDuration; - $fMinInter = is_null($fMinInter) ? $fDuration : min($fMinInter, $fDuration); - $fMaxInter = max($fMaxInter, $fDuration); - } - - $iCountInter = count($aEvents); - $sTotalInter = round($fTotalInter, 3); - $sMinInter = round($fMinInter, 3); - $sMaxInter = round($fMaxInter, 3); - self::Report(""); - self::Report(" $sHtmlArguments$iCountInter$sTotalInter$sMinInter$sMaxInter"); - self::Report(""); - } - self::Report(""); - self::Report("

    Back to page stats

    "); - } - self::Report(' '); - } - - - public function __construct() - { - $this->ResetCounters(); - } - - // Get the duration since startup, and reset the counter for the next measure - // - public function ComputeAndReport($sOperationDesc) - { - global $fItopStarted; - - $aNewEntry = null; - - if (self::$m_bEnabled_Duration) - { - $fStopped = MyHelpers::getmicrotime(); - $aNewEntry = array( - 'op' => $sOperationDesc, - 'time_begin' => $this->m_fStarted - $fItopStarted, - 'time_end' => $fStopped - $fItopStarted, - ); - // Reset for the next operation (if the object is recycled) - $this->m_fStarted = $fStopped; - } - - if (self::$m_bEnabled_Memory) - { - $iCurrentMemory = self::memory_get_usage(); - if (is_null($aNewEntry)) - { - $aNewEntry = array('op' => $sOperationDesc); - } - $aNewEntry['mem_begin'] = $this->m_iInitialMemory; - $aNewEntry['mem_end'] = $iCurrentMemory; - if (function_exists('memory_get_peak_usage')) - { - $aNewEntry['mem_peak'] = memory_get_peak_usage(); - } - // Reset for the next operation (if the object is recycled) - $this->m_iInitialMemory = $iCurrentMemory; - } - - if (!is_null($aNewEntry)) - { - self::$m_aExecData[] = $aNewEntry; - } - $this->ResetCounters(); - } - - public function ComputeStats($sOperation, $sArguments) - { - if (self::$m_bEnabled_Duration) - { - $fStopped = MyHelpers::getmicrotime(); - $fDuration = $fStopped - $this->m_fStarted; - if (self::$m_bBlameCaller) - { - self::$m_aStats[$sOperation][$sArguments][] = array( - 'time' => $fDuration, - 'callers' => MyHelpers::get_callstack(1), - ); - } - else - { - self::$m_aStats[$sOperation][$sArguments][] = array( - 'time' => $fDuration - ); - } - } - } - - protected function ResetCounters() - { - if (self::$m_bEnabled_Duration) - { - $this->m_fStarted = MyHelpers::getmicrotime(); - } - - if (self::$m_bEnabled_Memory) - { - $this->m_iInitialMemory = self::memory_get_usage(); - } - } - - const HtmlReportFile = 'log/kpi.html'; - - static protected function Report($sText) - { - file_put_contents(APPROOT.self::HtmlReportFile, "$sText\n", FILE_APPEND | LOCK_EX); - } - - static protected function MemStr($iMemory) - { - return round($iMemory / 1024).' Kb'; - } - - static protected function memory_get_usage() - { - if (function_exists('memory_get_usage')) - { - return memory_get_usage(true); - } - - // Copied from the PHP manual - // - //If its Windows - //Tested on Win XP Pro SP2. Should work on Win 2003 Server too - //Doesn't work for 2000 - //If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642 - if (substr(PHP_OS,0,3) == 'WIN') - { - $output = array(); - exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output); - - return preg_replace( '/[\D]/', '', $output[5] ) * 1024; - } - else - { - //We now assume the OS is UNIX - //Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4 - //This should work on most UNIX systems - $pid = getmypid(); - exec("ps -eo%mem,rss,pid | grep $pid", $output); - $output = explode(" ", $output[0]); - //rss is given in 1024 byte units - return $output[1] * 1024; - } - } - - static public function memory_get_peak_usage($bRealUsage = false) - { - if (function_exists('memory_get_peak_usage')) - { - return memory_get_peak_usage($bRealUsage); - } - // PHP > 5.2.1 - this verb depends on a compilation option - return 0; - } -} - + + + +/** + * Measures operations duration, memory usage, etc. (and some other KPIs) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class ExecutionKPI +{ + static protected $m_bEnabled_Duration = false; + static protected $m_bEnabled_Memory = false; + static protected $m_bBlameCaller = false; + static protected $m_sAllowedUser = '*'; + + static protected $m_aStats = array(); // Recurrent operations + static protected $m_aExecData = array(); // One shot operations + + protected $m_fStarted = null; + protected $m_iInitialMemory = null; + + static public function EnableDuration($iLevel) + { + if ($iLevel > 0) + { + self::$m_bEnabled_Duration = true; + if ($iLevel > 1) + { + self::$m_bBlameCaller = true; + } + } + } + + static public function EnableMemory($iLevel) + { + if ($iLevel > 0) + { + self::$m_bEnabled_Memory = true; + } + } + + /** + * @param string sUser A user login or * for all users + */ + static public function SetAllowedUser($sUser) + { + self::$m_sAllowedUser = $sUser; + } + + static public function IsEnabled() + { + if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) + { + if ((self::$m_sAllowedUser == '*') || (UserRights::GetUser() == trim(self::$m_sAllowedUser))) + { + return true; + } + } + return false; + } + + static public function GetDescription() + { + $aFeatures = array(); + if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration'; + if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage'; + $sFeatures = implode(', ', $aFeatures); + $sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'"; + return "KPI logging is active for $sFor. Measures: $sFeatures"; + } + + static public function ReportStats() + { + if (!self::IsEnabled()) return; + + global $fItopStarted; + $sExecId = microtime(); // id to differentiate the hrefs! + + $aBeginTimes = array(); + foreach (self::$m_aExecData as $aOpStats) + { + $aBeginTimes[] = $aOpStats['time_begin']; + } + array_multisort($aBeginTimes, self::$m_aExecData); + + $sTableStyle = 'background-color: #ccc; margin: 10px;'; + + self::Report("
    "); + self::Report("
    "); + self::Report("

    KPIs - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")

    "); + self::Report("

    ".date('Y-m-d H:i:s', $fItopStarted)."

    "); + self::Report("

    log_kpi_user_id: ".MetaModel::GetConfig()->Get('log_kpi_user_id')."

    "); + self::Report("
    "); + self::Report(""); + self::Report(""); + self::Report(" "); + self::Report(""); + foreach (self::$m_aExecData as $aOpStats) + { + $sOperation = $aOpStats['op']; + $sBegin = round($aOpStats['time_begin'], 3); + $sEnd = round($aOpStats['time_end'], 3); + $fDuration = $aOpStats['time_end'] - $aOpStats['time_begin']; + $sDuration = round($fDuration, 3); + + $sMemBegin = 'n/a'; + $sMemEnd = 'n/a'; + $sMemPeak = 'n/a'; + if (isset($aOpStats['mem_begin'])) + { + $sMemBegin = self::MemStr($aOpStats['mem_begin']); + $sMemEnd = self::MemStr($aOpStats['mem_end']); + if (isset($aOpStats['mem_peak'])) + { + $sMemPeak = self::MemStr($aOpStats['mem_peak']); + } + } + + self::Report(""); + self::Report(" "); + self::Report(""); + } + self::Report("
    OperationBeginEndDurationMemory startMemory endMemory peak
    $sOperation$sBegin$sEnd$sDuration$sMemBegin$sMemEnd$sMemPeak
    "); + self::Report("
    "); + + $aConsolidatedStats = array(); + foreach (self::$m_aStats as $sOperation => $aOpStats) + { + $fTotalOp = 0; + $iTotalOp = 0; + $fMinOp = null; + $fMaxOp = 0; + $sMaxOpArguments = null; + foreach ($aOpStats as $sArguments => $aEvents) + { + foreach ($aEvents as $aEventData) + { + $fDuration = $aEventData['time']; + $fTotalOp += $fDuration; + $iTotalOp++; + + $fMinOp = is_null($fMinOp) ? $fDuration : min($fMinOp, $fDuration); + if ($fDuration > $fMaxOp) + { + $sMaxOpArguments = $sArguments; + $fMaxOp = $fDuration; + } + } + } + $aConsolidatedStats[$sOperation] = array( + 'count' => $iTotalOp, + 'duration' => $fTotalOp, + 'min' => $fMinOp, + 'max' => $fMaxOp, + 'avg' => $fTotalOp / $iTotalOp, + 'max_args' => $sMaxOpArguments + ); + } + + self::Report("
    "); + self::Report(""); + self::Report(""); + self::Report(" "); + self::Report(""); + foreach ($aConsolidatedStats as $sOperation => $aOpStats) + { + $sOperation = ''.$sOperation.''; + $sCount = $aOpStats['count']; + $sDuration = round($aOpStats['duration'], 3); + $sMin = round($aOpStats['min'], 3); + $sMax = ''.round($aOpStats['max'], 3).''; + $sAvg = round($aOpStats['avg'], 3); + + self::Report(""); + self::Report(" "); + self::Report(""); + } + self::Report("
    OperationCountDurationMinMaxAvg
    $sOperation$sCount$sDuration$sMin$sMax$sAvg
    "); + self::Report("
    "); + + self::Report("
    "); + + self::Report("

    Next page stats

    "); + + // Report operation details + foreach (self::$m_aStats as $sOperation => $aOpStats) + { + $sOperationHtml = ''.$sOperation.''; + self::Report("

    $sOperationHtml

    "); + self::Report(""); + self::Report(""); + self::Report(" "); + self::Report(""); + foreach ($aOpStats as $sArguments => $aEvents) + { + $sHtmlArguments = '
    '.$sArguments.'
    '; + if ($aConsolidatedStats[$sOperation]['max_args'] == $sArguments) + { + $sHtmlArguments = ''.$sHtmlArguments.''; + } + if (isset($aEvents[0]['callers'])) + { + $sHtmlArguments .= '
    '; + $sHtmlArguments .= '
    Operation details (+ blame caller if log_kpi_duration = 2)CountDurationMinMax
    '; + $sHtmlArguments .= ''; + + foreach ($aEvents[0]['callers'] as $aCall) + { + $sHtmlArguments .= ''; + $sHtmlArguments .= ''; + $sHtmlArguments .= ''; + $sHtmlArguments .= ''; + } + $sHtmlArguments .= '
    Call stack for the FIRST caller
    '.$aCall['Function'].''.$aCall['File'].':'.$aCall['Line'].'
    '; + $sHtmlArguments .= '
    '; + } + + $fTotalInter = 0; + $fMinInter = null; + $fMaxInter = 0; + foreach ($aEvents as $aEventData) + { + $fDuration = $aEventData['time']; + $fTotalInter += $fDuration; + $fMinInter = is_null($fMinInter) ? $fDuration : min($fMinInter, $fDuration); + $fMaxInter = max($fMaxInter, $fDuration); + } + + $iCountInter = count($aEvents); + $sTotalInter = round($fTotalInter, 3); + $sMinInter = round($fMinInter, 3); + $sMaxInter = round($fMaxInter, 3); + self::Report(""); + self::Report(" $sHtmlArguments$iCountInter$sTotalInter$sMinInter$sMaxInter"); + self::Report(""); + } + self::Report(""); + self::Report("

    Back to page stats

    "); + } + self::Report(' '); + } + + + public function __construct() + { + $this->ResetCounters(); + } + + // Get the duration since startup, and reset the counter for the next measure + // + public function ComputeAndReport($sOperationDesc) + { + global $fItopStarted; + + $aNewEntry = null; + + if (self::$m_bEnabled_Duration) + { + $fStopped = MyHelpers::getmicrotime(); + $aNewEntry = array( + 'op' => $sOperationDesc, + 'time_begin' => $this->m_fStarted - $fItopStarted, + 'time_end' => $fStopped - $fItopStarted, + ); + // Reset for the next operation (if the object is recycled) + $this->m_fStarted = $fStopped; + } + + if (self::$m_bEnabled_Memory) + { + $iCurrentMemory = self::memory_get_usage(); + if (is_null($aNewEntry)) + { + $aNewEntry = array('op' => $sOperationDesc); + } + $aNewEntry['mem_begin'] = $this->m_iInitialMemory; + $aNewEntry['mem_end'] = $iCurrentMemory; + if (function_exists('memory_get_peak_usage')) + { + $aNewEntry['mem_peak'] = memory_get_peak_usage(); + } + // Reset for the next operation (if the object is recycled) + $this->m_iInitialMemory = $iCurrentMemory; + } + + if (!is_null($aNewEntry)) + { + self::$m_aExecData[] = $aNewEntry; + } + $this->ResetCounters(); + } + + public function ComputeStats($sOperation, $sArguments) + { + if (self::$m_bEnabled_Duration) + { + $fStopped = MyHelpers::getmicrotime(); + $fDuration = $fStopped - $this->m_fStarted; + if (self::$m_bBlameCaller) + { + self::$m_aStats[$sOperation][$sArguments][] = array( + 'time' => $fDuration, + 'callers' => MyHelpers::get_callstack(1), + ); + } + else + { + self::$m_aStats[$sOperation][$sArguments][] = array( + 'time' => $fDuration + ); + } + } + } + + protected function ResetCounters() + { + if (self::$m_bEnabled_Duration) + { + $this->m_fStarted = MyHelpers::getmicrotime(); + } + + if (self::$m_bEnabled_Memory) + { + $this->m_iInitialMemory = self::memory_get_usage(); + } + } + + const HtmlReportFile = 'log/kpi.html'; + + static protected function Report($sText) + { + file_put_contents(APPROOT.self::HtmlReportFile, "$sText\n", FILE_APPEND | LOCK_EX); + } + + static protected function MemStr($iMemory) + { + return round($iMemory / 1024).' Kb'; + } + + static protected function memory_get_usage() + { + if (function_exists('memory_get_usage')) + { + return memory_get_usage(true); + } + + // Copied from the PHP manual + // + //If its Windows + //Tested on Win XP Pro SP2. Should work on Win 2003 Server too + //Doesn't work for 2000 + //If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642 + if (substr(PHP_OS,0,3) == 'WIN') + { + $output = array(); + exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output); + + return preg_replace( '/[\D]/', '', $output[5] ) * 1024; + } + else + { + //We now assume the OS is UNIX + //Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4 + //This should work on most UNIX systems + $pid = getmypid(); + exec("ps -eo%mem,rss,pid | grep $pid", $output); + $output = explode(" ", $output[0]); + //rss is given in 1024 byte units + return $output[1] * 1024; + } + } + + static public function memory_get_peak_usage($bRealUsage = false) + { + if (function_exists('memory_get_peak_usage')) + { + return memory_get_peak_usage($bRealUsage); + } + // PHP > 5.2.1 - this verb depends on a compilation option + return 0; + } +} + diff --git a/core/metamodelmodifier.inc.php b/core/metamodelmodifier.inc.php index f1da6b207..008eabcf2 100644 --- a/core/metamodelmodifier.inc.php +++ b/core/metamodelmodifier.inc.php @@ -1,32 +1,32 @@ - - - -/** - * Any extension to hook the initialization of the metamodel - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -interface iOnClassInitialization -{ - public function OnAfterClassInitialization($sClass); -} - -?> + + + +/** + * Any extension to hook the initialization of the metamodel + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +interface iOnClassInitialization +{ + public function OnAfterClassInitialization($sClass); +} + +?> diff --git a/core/modelreflection.class.inc.php b/core/modelreflection.class.inc.php index cb7885d4b..c88352d8d 100644 --- a/core/modelreflection.class.inc.php +++ b/core/modelreflection.class.inc.php @@ -1,288 +1,288 @@ - - - -/** - * Reflection API for the MetaModel (partial) - * - * @copyright Copyright (C) 2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * Exclude the parent class from the list - * - * @package iTopORM - */ -define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1); -/** - * Include the parent class in the list - * - * @package iTopORM -*/ -define('ENUM_CHILD_CLASSES_ALL', 2); - - -abstract class ModelReflection -{ - abstract public function GetClassIcon($sClass, $bImgTag = true); - abstract public function IsValidAttCode($sClass, $sAttCode); - abstract public function GetName($sClass); - abstract public function GetLabel($sClass, $sAttCodeEx); - abstract public function GetValueLabel($sClass, $sAttCode, $sValue); - abstract public function ListAttributes($sClass, $sScope = null); - abstract public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null); - abstract public function GetAllowedValues_att($sClass, $sAttCode); - abstract public function HasChildrenClasses($sClass); - abstract public function GetClasses($sCategories = '', $bExcludeLinks = false); - abstract public function IsValidClass($sClass); - abstract public function IsSameFamilyBranch($sClassA, $sClassB); - abstract public function GetParentClass($sClass); - abstract public function GetFiltersList($sClass); - abstract public function IsValidFilterCode($sClass, $sFilterCode); - - abstract public function GetQuery($sOQL); - - abstract public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false); - - public function DictFormat($sFormatCode /*, ... arguments ....*/) - { - $sLocalizedFormat = $this->DictString($sFormatCode); - $aArguments = func_get_args(); - array_shift($aArguments); - - if ($sLocalizedFormat == $sFormatCode) - { - // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) - return $sFormatCode.' - '.implode(', ', $aArguments); - } - - return vsprintf($sLocalizedFormat, $aArguments); - } - - abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = ''); - - abstract public function GetRootClass($sClass); - abstract public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP); -} - -abstract class QueryReflection -{ - /** - * Throws an exception in case of an invalid syntax - */ - abstract public function __construct($sOQL, ModelReflection $oModelReflection); - - abstract public function GetClass(); - abstract public function GetClassAlias(); -} - - -class ModelReflectionRuntime extends ModelReflection -{ - public function __construct() - { - } - - public function GetClassIcon($sClass, $bImgTag = true) - { - return MetaModel::GetClassIcon($sClass, $bImgTag); - } - - public function IsValidAttCode($sClass, $sAttCode) - { - return MetaModel::IsValidAttCode($sClass, $sAttCode); - } - - public function GetName($sClass) - { - return MetaModel::GetName($sClass); - } - - public function GetLabel($sClass, $sAttCodeEx) - { - return MetaModel::GetLabel($sClass, $sAttCodeEx); - } - - public function GetValueLabel($sClass, $sAttCode, $sValue) - { - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - return $oAttDef->GetValueLabel($sValue); - } - - public function ListAttributes($sClass, $sScope = null) - { - $aScope = null; - if ($sScope != null) - { - $aScope = array(); - foreach (explode(',', $sScope) as $sScopeClass) - { - $aScope[] = trim($sScopeClass); - } - } - $aAttributes = array(); - foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - $sAttributeClass = get_class($oAttDef); - if ($aScope != null) - { - foreach ($aScope as $sScopeClass) - { - if (($sAttributeClass == $sScopeClass) || is_subclass_of($sAttributeClass, $sScopeClass)) - { - $aAttributes[$sAttCode] = $sAttributeClass; - break; - } - } - } - else - { - $aAttributes[$sAttCode] = $sAttributeClass; - } - } - return $aAttributes; - } - - public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null) - { - $ret = $default; - - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $aParams = $oAttDef->GetParams(); - if (array_key_exists($sPropName, $aParams)) - { - $ret = $aParams[$sPropName]; - } - - if ($oAttDef instanceof AttributeHierarchicalKey) - { - if ($sPropName == 'targetclass') - { - $ret = $sClass; - } - } - return $ret; - } - - public function GetAllowedValues_att($sClass, $sAttCode) - { - return MetaModel::GetAllowedValues_att($sClass, $sAttCode); - } - - public function HasChildrenClasses($sClass) - { - return MetaModel::HasChildrenClasses($sClass); - } - - public function GetClasses($sCategories = '', $bExcludeLinks = false) - { - $aClasses = MetaModel::GetClasses($sCategories); - if ($bExcludeLinks) - { - $aExcluded = MetaModel::GetLinkClasses(); - $aRes = array(); - foreach ($aClasses as $sClass) - { - if (!array_key_exists($sClass, $aExcluded)) - { - $aRes[] = $sClass; - } - } - } - else - { - $aRes = $aClasses; - } - return $aRes; - } - - public function IsValidClass($sClass) - { - return MetaModel::IsValidClass($sClass); - } - - public function IsSameFamilyBranch($sClassA, $sClassB) - { - return MetaModel::IsSameFamilyBranch($sClassA, $sClassB); - } - - public function GetParentClass($sClass) - { - return MetaModel::GetParentClass($sClass); - } - - public function GetFiltersList($sClass) - { - return MetaModel::GetFiltersList($sClass); - } - - public function IsValidFilterCode($sClass, $sFilterCode) - { - return MetaModel::IsValidFilterCode($sClass, $sFilterCode); - } - - public function GetQuery($sOQL) - { - return new QueryReflectionRuntime($sOQL, $this); - } - - public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false) - { - return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); - } - - public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '') - { - return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue); - } - - public function GetRootClass($sClass) - { - return MetaModel::GetRootClass($sClass); - } - - public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP) - { - return MetaModel::EnumChildClasses($sClass, $iOption); - } -} - - -class QueryReflectionRuntime extends QueryReflection -{ - protected $oFilter; - - /** - * throws an exception in case of a wrong syntax - */ - public function __construct($sOQL, ModelReflection $oModelReflection) - { - $this->oFilter = DBObjectSearch::FromOQL($sOQL); - } - - public function GetClass() - { - return $this->oFilter->GetClass(); - } - - public function GetClassAlias() - { - return $this->oFilter->GetClassAlias(); - } -} + + + +/** + * Reflection API for the MetaModel (partial) + * + * @copyright Copyright (C) 2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * Exclude the parent class from the list + * + * @package iTopORM + */ +define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1); +/** + * Include the parent class in the list + * + * @package iTopORM +*/ +define('ENUM_CHILD_CLASSES_ALL', 2); + + +abstract class ModelReflection +{ + abstract public function GetClassIcon($sClass, $bImgTag = true); + abstract public function IsValidAttCode($sClass, $sAttCode); + abstract public function GetName($sClass); + abstract public function GetLabel($sClass, $sAttCodeEx); + abstract public function GetValueLabel($sClass, $sAttCode, $sValue); + abstract public function ListAttributes($sClass, $sScope = null); + abstract public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null); + abstract public function GetAllowedValues_att($sClass, $sAttCode); + abstract public function HasChildrenClasses($sClass); + abstract public function GetClasses($sCategories = '', $bExcludeLinks = false); + abstract public function IsValidClass($sClass); + abstract public function IsSameFamilyBranch($sClassA, $sClassB); + abstract public function GetParentClass($sClass); + abstract public function GetFiltersList($sClass); + abstract public function IsValidFilterCode($sClass, $sFilterCode); + + abstract public function GetQuery($sOQL); + + abstract public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false); + + public function DictFormat($sFormatCode /*, ... arguments ....*/) + { + $sLocalizedFormat = $this->DictString($sFormatCode); + $aArguments = func_get_args(); + array_shift($aArguments); + + if ($sLocalizedFormat == $sFormatCode) + { + // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) + return $sFormatCode.' - '.implode(', ', $aArguments); + } + + return vsprintf($sLocalizedFormat, $aArguments); + } + + abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = ''); + + abstract public function GetRootClass($sClass); + abstract public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP); +} + +abstract class QueryReflection +{ + /** + * Throws an exception in case of an invalid syntax + */ + abstract public function __construct($sOQL, ModelReflection $oModelReflection); + + abstract public function GetClass(); + abstract public function GetClassAlias(); +} + + +class ModelReflectionRuntime extends ModelReflection +{ + public function __construct() + { + } + + public function GetClassIcon($sClass, $bImgTag = true) + { + return MetaModel::GetClassIcon($sClass, $bImgTag); + } + + public function IsValidAttCode($sClass, $sAttCode) + { + return MetaModel::IsValidAttCode($sClass, $sAttCode); + } + + public function GetName($sClass) + { + return MetaModel::GetName($sClass); + } + + public function GetLabel($sClass, $sAttCodeEx) + { + return MetaModel::GetLabel($sClass, $sAttCodeEx); + } + + public function GetValueLabel($sClass, $sAttCode, $sValue) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + return $oAttDef->GetValueLabel($sValue); + } + + public function ListAttributes($sClass, $sScope = null) + { + $aScope = null; + if ($sScope != null) + { + $aScope = array(); + foreach (explode(',', $sScope) as $sScopeClass) + { + $aScope[] = trim($sScopeClass); + } + } + $aAttributes = array(); + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + $sAttributeClass = get_class($oAttDef); + if ($aScope != null) + { + foreach ($aScope as $sScopeClass) + { + if (($sAttributeClass == $sScopeClass) || is_subclass_of($sAttributeClass, $sScopeClass)) + { + $aAttributes[$sAttCode] = $sAttributeClass; + break; + } + } + } + else + { + $aAttributes[$sAttCode] = $sAttributeClass; + } + } + return $aAttributes; + } + + public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null) + { + $ret = $default; + + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aParams = $oAttDef->GetParams(); + if (array_key_exists($sPropName, $aParams)) + { + $ret = $aParams[$sPropName]; + } + + if ($oAttDef instanceof AttributeHierarchicalKey) + { + if ($sPropName == 'targetclass') + { + $ret = $sClass; + } + } + return $ret; + } + + public function GetAllowedValues_att($sClass, $sAttCode) + { + return MetaModel::GetAllowedValues_att($sClass, $sAttCode); + } + + public function HasChildrenClasses($sClass) + { + return MetaModel::HasChildrenClasses($sClass); + } + + public function GetClasses($sCategories = '', $bExcludeLinks = false) + { + $aClasses = MetaModel::GetClasses($sCategories); + if ($bExcludeLinks) + { + $aExcluded = MetaModel::GetLinkClasses(); + $aRes = array(); + foreach ($aClasses as $sClass) + { + if (!array_key_exists($sClass, $aExcluded)) + { + $aRes[] = $sClass; + } + } + } + else + { + $aRes = $aClasses; + } + return $aRes; + } + + public function IsValidClass($sClass) + { + return MetaModel::IsValidClass($sClass); + } + + public function IsSameFamilyBranch($sClassA, $sClassB) + { + return MetaModel::IsSameFamilyBranch($sClassA, $sClassB); + } + + public function GetParentClass($sClass) + { + return MetaModel::GetParentClass($sClass); + } + + public function GetFiltersList($sClass) + { + return MetaModel::GetFiltersList($sClass); + } + + public function IsValidFilterCode($sClass, $sFilterCode) + { + return MetaModel::IsValidFilterCode($sClass, $sFilterCode); + } + + public function GetQuery($sOQL) + { + return new QueryReflectionRuntime($sOQL, $this); + } + + public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false) + { + return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); + } + + public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '') + { + return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue); + } + + public function GetRootClass($sClass) + { + return MetaModel::GetRootClass($sClass); + } + + public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP) + { + return MetaModel::EnumChildClasses($sClass, $iOption); + } +} + + +class QueryReflectionRuntime extends QueryReflection +{ + protected $oFilter; + + /** + * throws an exception in case of a wrong syntax + */ + public function __construct($sOQL, ModelReflection $oModelReflection) + { + $this->oFilter = DBObjectSearch::FromOQL($sOQL); + } + + public function GetClass() + { + return $this->oFilter->GetClass(); + } + + public function GetClassAlias() + { + return $this->oFilter->GetClassAlias(); + } +} diff --git a/core/moduledesign.class.inc.php b/core/moduledesign.class.inc.php index 5da0a6274..6aff020d9 100644 --- a/core/moduledesign.class.inc.php +++ b/core/moduledesign.class.inc.php @@ -1,119 +1,119 @@ - - -/** - * Module specific customizations: - * The customizations are done in XML, within a module_design section (itop_design/module_designs/module_design) - * The module reads the cusomtizations by the mean of the ModuleDesign API - * @package Core - */ - -require_once(APPROOT.'application/utils.inc.php'); -require_once(APPROOT.'core/designdocument.class.inc.php'); - - -/** - * Class ModuleDesign - * - * Usage from within a module: - * - * // Fetch the design - * $oDesign = new ModuleDesign('tagada'); - * - * // Read data from the root node - * $oRoot = $oDesign->documentElement; - * $oProperties = $oRoot->GetUniqueElement('properties'); - * $prop1 = $oProperties->GetChildText('property1'); - * $prop2 = $oProperties->GetChildText('property2'); - * - * // Read data by searching the entire DOM - * foreach ($oDesign->GetNodes('/module_design/bricks/brick') as $oBrickNode) - * { - * $sId = $oBrickNode->getAttribute('id'); - * $sType = $oBrickNode->getAttribute('xsi:type'); - * } - * - * // Search starting a given node - * $oBricks = $oDesign->documentElement->GetUniqueElement('bricks'); - * foreach ($oBricks->GetNodes('brick') as $oBrickNode) - * { - * ... - * } - */ -class ModuleDesign extends \Combodo\iTop\DesignDocument -{ - /** - * @param string|null $sDesignSourceId Identifier of the section module_design (generally a module name), null to build an empty design - * @throws Exception - */ - public function __construct($sDesignSourceId = null) - { - parent::__construct(); - - if (!is_null($sDesignSourceId)) - { - $this->LoadFromCompiledDesigns($sDesignSourceId); - } - } - - /** - * Gets the data where the compiler has left them... - * @param $sDesignSourceId String Identifier of the section module_design (generally a module name) - * @throws Exception - */ - protected function LoadFromCompiledDesigns($sDesignSourceId) - { - $sDesignDir = APPROOT.'env-'.utils::GetCurrentEnvironment().'/core/module_designs/'; - $sFile = $sDesignDir.$sDesignSourceId.'.xml'; - if (!file_exists($sFile)) - { - $aFiles = glob($sDesignDir.'/*.xml'); - if (count($aFiles) == 0) - { - $sAvailable = 'none!'; - } - else - { - $aAvailable = array(); - foreach ($aFiles as $sFile) - { - $aAvailable[] = "'".basename($sFile, '.xml')."'"; - } - $sAvailable = implode(', ', $aAvailable); - } - throw new Exception("Could not load module design '$sDesignSourceId'. Available designs: $sAvailable"); - } - - // Silently keep track of errors - libxml_use_internal_errors(true); - libxml_clear_errors(); - $this->load($sFile); - //$bValidated = $oDocument->schemaValidate(APPROOT.'setup/itop_design.xsd'); - $aErrors = libxml_get_errors(); - if (count($aErrors) > 0) - { - $aDisplayErrors = array(); - foreach($aErrors as $oXmlError) - { - $aDisplayErrors[] = 'Line '.$oXmlError->line.': '.$oXmlError->message; - } - - throw new Exception("Invalid XML in '$sFile'. Errors: ".implode(', ', $aDisplayErrors)); - } - } -} + + +/** + * Module specific customizations: + * The customizations are done in XML, within a module_design section (itop_design/module_designs/module_design) + * The module reads the cusomtizations by the mean of the ModuleDesign API + * @package Core + */ + +require_once(APPROOT.'application/utils.inc.php'); +require_once(APPROOT.'core/designdocument.class.inc.php'); + + +/** + * Class ModuleDesign + * + * Usage from within a module: + * + * // Fetch the design + * $oDesign = new ModuleDesign('tagada'); + * + * // Read data from the root node + * $oRoot = $oDesign->documentElement; + * $oProperties = $oRoot->GetUniqueElement('properties'); + * $prop1 = $oProperties->GetChildText('property1'); + * $prop2 = $oProperties->GetChildText('property2'); + * + * // Read data by searching the entire DOM + * foreach ($oDesign->GetNodes('/module_design/bricks/brick') as $oBrickNode) + * { + * $sId = $oBrickNode->getAttribute('id'); + * $sType = $oBrickNode->getAttribute('xsi:type'); + * } + * + * // Search starting a given node + * $oBricks = $oDesign->documentElement->GetUniqueElement('bricks'); + * foreach ($oBricks->GetNodes('brick') as $oBrickNode) + * { + * ... + * } + */ +class ModuleDesign extends \Combodo\iTop\DesignDocument +{ + /** + * @param string|null $sDesignSourceId Identifier of the section module_design (generally a module name), null to build an empty design + * @throws Exception + */ + public function __construct($sDesignSourceId = null) + { + parent::__construct(); + + if (!is_null($sDesignSourceId)) + { + $this->LoadFromCompiledDesigns($sDesignSourceId); + } + } + + /** + * Gets the data where the compiler has left them... + * @param $sDesignSourceId String Identifier of the section module_design (generally a module name) + * @throws Exception + */ + protected function LoadFromCompiledDesigns($sDesignSourceId) + { + $sDesignDir = APPROOT.'env-'.utils::GetCurrentEnvironment().'/core/module_designs/'; + $sFile = $sDesignDir.$sDesignSourceId.'.xml'; + if (!file_exists($sFile)) + { + $aFiles = glob($sDesignDir.'/*.xml'); + if (count($aFiles) == 0) + { + $sAvailable = 'none!'; + } + else + { + $aAvailable = array(); + foreach ($aFiles as $sFile) + { + $aAvailable[] = "'".basename($sFile, '.xml')."'"; + } + $sAvailable = implode(', ', $aAvailable); + } + throw new Exception("Could not load module design '$sDesignSourceId'. Available designs: $sAvailable"); + } + + // Silently keep track of errors + libxml_use_internal_errors(true); + libxml_clear_errors(); + $this->load($sFile); + //$bValidated = $oDocument->schemaValidate(APPROOT.'setup/itop_design.xsd'); + $aErrors = libxml_get_errors(); + if (count($aErrors) > 0) + { + $aDisplayErrors = array(); + foreach($aErrors as $oXmlError) + { + $aDisplayErrors[] = 'Line '.$oXmlError->line.': '.$oXmlError->message; + } + + throw new Exception("Invalid XML in '$sFile'. Errors: ".implode(', ', $aDisplayErrors)); + } + } +} diff --git a/core/modulehandler.class.inc.php b/core/modulehandler.class.inc.php index d56307f63..645755ebd 100644 --- a/core/modulehandler.class.inc.php +++ b/core/modulehandler.class.inc.php @@ -1,52 +1,52 @@ - - - -/** - * Class ModuleHandler - * Defines the API to implement module specific actions during page execution - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -abstract class ModuleHandlerAPI implements ModuleHandlerApiInterface -{ - public static function OnMetaModelStarted() - { - } - - public static function OnMenuCreation() - { - } - - public function __construct() - { - } - -} - - -interface ModuleHandlerApiInterface -{ - public static function OnMetaModelStarted(); - - public static function OnMenuCreation(); - - public function __construct(); //empty params is required in order to be instantiable by MetaModel::InitClasses() + + + +/** + * Class ModuleHandler + * Defines the API to implement module specific actions during page execution + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +abstract class ModuleHandlerAPI implements ModuleHandlerApiInterface +{ + public static function OnMetaModelStarted() + { + } + + public static function OnMenuCreation() + { + } + + public function __construct() + { + } + +} + + +interface ModuleHandlerApiInterface +{ + public static function OnMetaModelStarted(); + + public static function OnMenuCreation(); + + public function __construct(); //empty params is required in order to be instantiable by MetaModel::InitClasses() } \ No newline at end of file diff --git a/core/oql/build/build.cmd b/core/oql/build/build.cmd index 9edd6d3d4..dea9c8781 100644 --- a/core/oql/build/build.cmd +++ b/core/oql/build/build.cmd @@ -1,6 +1,6 @@ -rem must be run with current directory = the directory of the batch -rem PEAR is required to build -php -d include_path=".;C:\iTop\PHP\PEAR" ".\PHP\LexerGenerator\cli.php" ..\oql-lexer.plex -php ".\PHP\ParserGenerator\cli.php" ..\oql-parser.y -php -r "echo date('Y-m-d');" > ..\version.txt +rem must be run with current directory = the directory of the batch +rem PEAR is required to build +php -d include_path=".;C:\iTop\PHP\PEAR" ".\PHP\LexerGenerator\cli.php" ..\oql-lexer.plex +php ".\PHP\ParserGenerator\cli.php" ..\oql-parser.y +php -r "echo date('Y-m-d');" > ..\version.txt pause \ No newline at end of file diff --git a/core/oql/expression.class.inc.php b/core/oql/expression.class.inc.php index 7173a68ae..4800a404c 100644 --- a/core/oql/expression.class.inc.php +++ b/core/oql/expression.class.inc.php @@ -1,2407 +1,2407 @@ - -// - -class MissingQueryArgument extends CoreException -{ -} - - -abstract class Expression -{ - /** - * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects) - **/ - public function DeepClone() - { - return unserialize(serialize($this)); - } - - // recursive translation of identifiers - abstract public function GetUnresolvedFields($sAlias, &$aUnresolved); - - /** - * @param array $aTranslationData - * @param bool $bMatchAll - * @param bool $bMarkFieldsAsResolved - * - * @return Expression Translated expression - */ - abstract public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true); - - /** - * recursive rendering - * - * @deprecated use RenderExpression - * - * @param array $aArgs used as input by default, or used as output if bRetrofitParams set to True - * @param bool $bRetrofitParams - * - * @return array|string - * @throws \MissingQueryArgument - */ - public function Render(&$aArgs = null, $bRetrofitParams = false) - { - return $this->RenderExpression(false, $aArgs, $bRetrofitParams); - } - - /** - * recursive rendering - * - * @param bool $bForSQL generates code for OQL if false, for SQL otherwise - * @param array $aArgs used as input by default, or used as output if bRetrofitParams set to True - * @param bool $bRetrofitParams - * - * @return array|string - * @throws \MissingQueryArgument - */ - abstract public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false); - - /** - * @param DBObjectSearch $oSearch - * @param array $aArgs - * @param AttributeDefinition $oAttDef - * - * @param array $aCtx - * - * @return array parameters for the search form - */ - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - return $this->RenderExpression(false, $aArgs); - } - - public function GetAttDef($aClasses = array()) - { - return null; - } - - /** - * Recursively browse the expression tree - * @param Closure $callback - * @return mixed - */ - abstract public function Browse(Closure $callback); - - abstract public function ApplyParameters($aArgs); - - // recursively builds an array of class => fieldname - abstract public function ListRequiredFields(); - - // recursively list field parents ($aTable = array of sParent => dummy) - abstract public function CollectUsedParents(&$aTable); - - abstract public function IsTrue(); - - // recursively builds an array of [classAlias][fieldName] => value - abstract public function ListConstantFields(); - - public function RequiresField($sClass, $sFieldName) - { - // #@# todo - optimize : this is called quite often when building a single query ! - $aRequired = $this->ListRequiredFields(); - if (!in_array($sClass.'.'.$sFieldName, $aRequired)) return false; - return true; - } - - public function serialize() - { - return base64_encode($this->RenderExpression(false)); - } - - /** - * @param $sValue - * - * @return Expression - * @throws OQLException - */ - static public function unserialize($sValue) - { - return self::FromOQL(base64_decode($sValue)); - } - - /** - * @param $sConditionExpr - * @return Expression - */ - static public function FromOQL($sConditionExpr) - { - static $aCache = array(); - if (array_key_exists($sConditionExpr, $aCache)) - { - return unserialize($aCache[$sConditionExpr]); - } - $oOql = new OqlInterpreter($sConditionExpr); - $oExpression = $oOql->ParseExpression(); - $aCache[$sConditionExpr] = serialize($oExpression); - - return $oExpression; - } - - static public function FromSQL($sSQL) - { - $oSql = new SQLExpression($sSQL); - return $oSql; - } - - /** - * @param Expression $oExpr - * @return Expression - */ - public function LogAnd(Expression $oExpr) - { - if ($this->IsTrue()) return clone $oExpr; - if ($oExpr->IsTrue()) return clone $this; - return new BinaryExpression($this, 'AND', $oExpr); - } - - /** - * @param Expression $oExpr - * @return Expression - */ - public function LogOr(Expression $oExpr) - { - return new BinaryExpression($this, 'OR', $oExpr); - } - - abstract public function RenameParam($sOldName, $sNewName); - abstract public function RenameAlias($sOldName, $sNewName); - - /** - * Make the most relevant label, given the value of the expression - * - * @param DBSearch oFilter The context in which this expression has been used - * @param string sValue The value returned by the query, for this expression - * @param string sDefault The default value if no relevant label could be computed - * - * @return string label - */ - public function MakeValueLabel($oFilter, $sValue, $sDefault) - { - return $sDefault; - } - - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - return array( - 'widget' => AttributeDefinition::SEARCH_WIDGET_TYPE_RAW, - 'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams), - 'label' => $this->Display($oSearch, $aArgs, $oAttDef), - 'source' => get_class($this), - ); - } - - /** - * Split binary expression on given operator - * - * @param Expression $oExpr - * @param string $sOperator - * @param array $aAndExpr - * - * @return array of expressions - */ - public static function Split($oExpr, $sOperator = 'AND', &$aAndExpr = array()) - { - if (($oExpr instanceof BinaryExpression) && ($oExpr->GetOperator() == $sOperator)) - { - static::Split($oExpr->GetLeftExpr(), $sOperator, $aAndExpr); - static::Split($oExpr->GetRightExpr(), $sOperator, $aAndExpr); - } - else - { - $aAndExpr[] = $oExpr; - } - - return $aAndExpr; - } -} - -class SQLExpression extends Expression -{ - protected $m_sSQL; - - public function __construct($sSQL) - { - $this->m_sSQL = $sSQL; - } - - public function IsTrue() - { - return false; - } - - // recursive rendering - public function RenderExpression($bForSql = false, &$aArgs = null, $bRetrofitParams = false) - { - return $this->m_sSQL; - } - - public function Browse(Closure $callback) - { - $callback($this); - } - - public function ApplyParameters($aArgs) - { - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - return clone $this; - } - - public function ListRequiredFields() - { - return array(); - } - - public function CollectUsedParents(&$aTable) - { - } - - public function ListConstantFields() - { - return array(); - } - - public function RenameParam($sOldName, $sNewName) - { - // Do nothing, since there is nothing to rename - } - - public function RenameAlias($sOldName, $sNewName) - { - // Do nothing, since there is nothing to rename - } -} - - - -class BinaryExpression extends Expression -{ - protected $m_oLeftExpr; // filter code or an SQL expression (later?) - protected $m_oRightExpr; - protected $m_sOperator; - - /** - * @param \Expression $oLeftExpr - * @param string $sOperator - * @param \Expression $oRightExpr - * - * @throws \CoreException - */ - public function __construct($oLeftExpr, $sOperator, $oRightExpr) - { - $this->ValidateConstructorParams($oLeftExpr, $sOperator, $oRightExpr); - - $this->m_oLeftExpr = $oLeftExpr; - $this->m_oRightExpr = $oRightExpr; - $this->m_sOperator = $sOperator; - } - - /** - * @param $oLeftExpr - * @param $sOperator - * @param $oRightExpr - * - * @throws \CoreException if one of the parameter is invalid - */ - protected function ValidateConstructorParams($oLeftExpr, $sOperator, $oRightExpr) - { - if (!is_object($oLeftExpr)) - { - throw new CoreException('Expecting an Expression object on the left hand', array('found_type' => gettype($oLeftExpr))); - } - if (!is_object($oRightExpr)) - { - throw new CoreException('Expecting an Expression object on the right hand', array('found_type' => gettype($oRightExpr))); - } - if (!$oLeftExpr instanceof Expression) - { - throw new CoreException('Expecting an Expression object on the left hand', array('found_class' => get_class($oLeftExpr))); - } - if (!$oRightExpr instanceof Expression) - { - throw new CoreException('Expecting an Expression object on the right hand', array('found_class' => get_class($oRightExpr))); - } - if ((($sOperator == "IN") || ($sOperator == "NOT IN")) && !($oRightExpr instanceof ListExpression)) - { - throw new CoreException("Expecting a List Expression object on the right hand for operator $sOperator", - array('found_class' => get_class($oRightExpr))); - } - } - - public function IsTrue() - { - // return true if we are certain that it will be true - if ($this->m_sOperator == 'AND') - { - if ($this->m_oLeftExpr->IsTrue() && $this->m_oRightExpr->IsTrue()) return true; - } - elseif ($this->m_sOperator == 'OR') - { - if ($this->m_oLeftExpr->IsTrue() || $this->m_oRightExpr->IsTrue()) return true; - } - return false; - } - - public function GetLeftExpr() - { - return $this->m_oLeftExpr; - } - - public function GetRightExpr() - { - return $this->m_oRightExpr; - } - - public function GetOperator() - { - return $this->m_sOperator; - } - - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - $sOperator = $this->GetOperator(); - $sLeft = $this->GetLeftExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - $sRight = $this->GetRightExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - return "($sLeft $sOperator $sRight)"; - } - - public function Browse(Closure $callback) - { - $callback($this); - $this->m_oLeftExpr->Browse($callback); - $this->m_oRightExpr->Browse($callback); - } - - public function ApplyParameters($aArgs) - { - if ($this->m_oLeftExpr instanceof VariableExpression) - { - $this->m_oLeftExpr = $this->m_oLeftExpr->GetAsScalar($aArgs); - } - else //if ($this->m_oLeftExpr instanceof Expression) - { - $this->m_oLeftExpr->ApplyParameters($aArgs); - } - if ($this->m_oRightExpr instanceof VariableExpression) - { - $this->m_oRightExpr = $this->m_oRightExpr->GetAsScalar($aArgs); - } - else //if ($this->m_oRightExpr instanceof Expression) - { - $this->m_oRightExpr->ApplyParameters($aArgs); - } - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - $this->GetLeftExpr()->GetUnresolvedFields($sAlias, $aUnresolved); - $this->GetRightExpr()->GetUnresolvedFields($sAlias, $aUnresolved); - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - $oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - return new BinaryExpression($oLeft, $this->GetOperator(), $oRight); - } - - public function ListRequiredFields() - { - $aLeft = $this->GetLeftExpr()->ListRequiredFields(); - $aRight = $this->GetRightExpr()->ListRequiredFields(); - return array_merge($aLeft, $aRight); - } - - public function CollectUsedParents(&$aTable) - { - $this->GetLeftExpr()->CollectUsedParents($aTable); - $this->GetRightExpr()->CollectUsedParents($aTable); - } - - public function GetAttDef($aClasses = array()) - { - $oAttDef = $this->GetLeftExpr()->GetAttDef($aClasses); - if (!is_null($oAttDef)) return $oAttDef; - - return $this->GetRightExpr()->GetAttDef($aClasses); - } - - /** - * List all constant expression of the form = or = : - * Could be extended to support = - */ - public function ListConstantFields() - { - $aResult = array(); - if ($this->m_sOperator == '=') - { - if (($this->m_oLeftExpr instanceof FieldExpression) && ($this->m_oRightExpr instanceof ScalarExpression)) - { - $aResult[$this->m_oLeftExpr->GetParent()][$this->m_oLeftExpr->GetName()] = $this->m_oRightExpr; - } - else if (($this->m_oRightExpr instanceof FieldExpression) && ($this->m_oLeftExpr instanceof ScalarExpression)) - { - $aResult[$this->m_oRightExpr->GetParent()][$this->m_oRightExpr->GetName()] = $this->m_oLeftExpr; - } - else if (($this->m_oLeftExpr instanceof FieldExpression) && ($this->m_oRightExpr instanceof VariableExpression)) - { - $aResult[$this->m_oLeftExpr->GetParent()][$this->m_oLeftExpr->GetName()] = $this->m_oRightExpr; - } - else if (($this->m_oRightExpr instanceof FieldExpression) && ($this->m_oLeftExpr instanceof VariableExpression)) - { - $aResult[$this->m_oRightExpr->GetParent()][$this->m_oRightExpr->GetName()] = $this->m_oLeftExpr; - } - } - else if ($this->m_sOperator == 'AND') - { - // Strictly, this should be done only for the AND operator - $aResult = array_merge_recursive($this->m_oRightExpr->ListConstantFields(), $this->m_oLeftExpr->ListConstantFields()); - } - return $aResult; - } - - public function RenameParam($sOldName, $sNewName) - { - $this->GetLeftExpr()->RenameParam($sOldName, $sNewName); - $this->GetRightExpr()->RenameParam($sOldName, $sNewName); - } - - public function RenameAlias($sOldName, $sNewName) - { - $this->GetLeftExpr()->RenameAlias($sOldName, $sNewName); - $this->GetRightExpr()->RenameAlias($sOldName, $sNewName); - } - - // recursive rendering - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - $bReverseOperator = false; - if (method_exists($oSearch, 'GetJoinedClasses')) - { - $aClasses = $oSearch->GetJoinedClasses(); - } - else - { - $aClasses = array($oSearch->GetClass()); - } - $oLeftExpr = $this->GetLeftExpr(); - if ($oLeftExpr instanceof FieldExpression) - { - $oAttDef = $oLeftExpr->GetAttDef($aClasses); - } - $oRightExpr = $this->GetRightExpr(); - if ($oRightExpr instanceof FieldExpression) - { - $oAttDef = $oRightExpr->GetAttDef($aClasses); - $bReverseOperator = true; - } - - - if ($bReverseOperator) - { - $sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); - $sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); - - // switch left and right expressions so reverse the operator - // Note that the operation is the same so < becomes > and not >= - switch ($this->GetOperator()) - { - case '>': - $sOperator = '<'; - break; - case '<': - $sOperator = '>'; - break; - case '>=': - $sOperator = '<='; - break; - case '<=': - $sOperator = '>='; - break; - default: - $sOperator = $this->GetOperator(); - break; - } - $sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef); - - return "({$sRight}{$sOperator}{$sLeft})"; - } - - $sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); - $sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); - - $sOperator = $this->GetOperator(); - $sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef); - - return "({$sLeft}{$sOperator}{$sRight})"; - } - - private function OperatorToNaturalLanguage($sOperator, $oAttDef) - { - if ($oAttDef instanceof AttributeDateTime) - { - return Dict::S('Expression:Operator:Date:'.$sOperator, " $sOperator "); - } - - return Dict::S('Expression:Operator:'.$sOperator, " $sOperator "); - } - - /** - * @param DBSearch $oSearch - * @param null $aArgs - * @param bool $bRetrofitParams - * @param null $oAttDef - * - * @return array - */ - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - $bReverseOperator = false; - $oLeftExpr = $this->GetLeftExpr(); - $oRightExpr = $this->GetRightExpr(); - - if (method_exists($oSearch, 'GetJoinedClasses')) - { - $aClasses = $oSearch->GetJoinedClasses(); - } - else - { - $aClasses = array($oSearch->GetClass()); - } - - $oAttDef = $oLeftExpr->GetAttDef($aClasses); - if (is_null($oAttDef)) - { - $oAttDef = $oRightExpr->GetAttDef($aClasses); - $bReverseOperator = true; - } - - if (is_null($oAttDef)) - { - return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - } - - - if ($bReverseOperator) - { - $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - // $oAttDef can be different now - $oAttDef = $oRightExpr->GetAttDef($aClasses); - $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - - // switch left and right expressions so reverse the operator - // Note that the operation is the same so < becomes > and not >= - switch ($this->GetOperator()) - { - case '>': - $sOperator = '<'; - break; - case '<': - $sOperator = '>'; - break; - case '>=': - $sOperator = '<='; - break; - case '<=': - $sOperator = '>='; - break; - default: - $sOperator = $this->GetOperator(); - break; - } - $aCriteria = self::MergeCriteria($aCriteriaRight, $aCriteriaLeft, $sOperator); - } - else - { - $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - // $oAttDef can be different now - $oAttDef = $oLeftExpr->GetAttDef($aClasses); - $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - - $aCriteria = self::MergeCriteria($aCriteriaLeft, $aCriteriaRight, $this->GetOperator()); - } - $aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams); - $aCriteria['label'] = $this->Display($oSearch, $aArgs, $oAttDef); - - if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] != $aCriteriaRight['ref'])) - { - // Only one Field is supported in the expressions - $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; - } - - return $aCriteria; - } - - protected static function MergeCriteria($aCriteriaLeft, $aCriteriaRight, $sOperator) - { - $aCriteriaOverride = array(); - $aCriteriaOverride['operator'] = $sOperator; - if ($sOperator == 'OR') - { - if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] == $aCriteriaRight['ref'])) - { - if (isset($aCriteriaLeft['widget']) && isset($aCriteriaRight['widget']) && ($aCriteriaLeft['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY) && ($aCriteriaRight['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY)) - { - $aCriteriaOverride['operator'] = 'IN'; - $aCriteriaOverride['is_hierarchical'] = true; - - if (isset($aCriteriaLeft['values']) && isset($aCriteriaRight['values'])) - { - $aCriteriaOverride['values'] = array_merge($aCriteriaLeft['values'], $aCriteriaRight['values']); - } - } - } - } - - return array_merge($aCriteriaLeft, $aCriteriaRight, $aCriteriaOverride); - } -} - - -/** - * @since 2.6 N°931 tag fields - */ -class MatchExpression extends BinaryExpression -{ - /** @var \FieldExpression */ - protected $m_oLeftExpr; - /** @var \ScalarExpression */ - protected $m_oRightExpr; - - /** - * MatchExpression constructor. - * - * @param \FieldExpression $oLeftExpr - * @param \ScalarExpression $oRightExpr - * - * @throws \CoreException - */ - public function __construct(FieldExpression $oLeftExpr, ScalarExpression $oRightExpr) - { - parent::__construct($oLeftExpr, 'MATCHES', $oRightExpr); - } - - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - $sLeft = $this->GetLeftExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - $sRight = $this->GetRightExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - - if ($bForSQL) - { - $sRet = "MATCH ($sLeft) AGAINST ($sRight IN BOOLEAN MODE)"; - } - else - { - $sRet = "$sLeft MATCHES $sRight"; - } - - return $sRet; - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - $oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - - return new static($oLeft, $oRight); - } -} - - -class UnaryExpression extends Expression -{ - protected $m_value; - - public function __construct($value) - { - $this->m_value = $value; - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return ($this->m_value == 1); - } - - public function GetValue() - { - return $this->m_value; - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - return CMDBSource::Quote($this->m_value); - } - - public function Browse(Closure $callback) - { - $callback($this); - } - - public function ApplyParameters($aArgs) - { - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - return clone $this; - } - - public function ListRequiredFields() - { - return array(); - } - - public function CollectUsedParents(&$aTable) - { - } - - public function ListConstantFields() - { - return array(); - } - - public function RenameParam($sOldName, $sNewName) - { - // Do nothing - // really ? what about :param{$iParamIndex} ?? - } - - public function RenameAlias($sOldName, $sNewName) - { - // Do nothing - } -} - -class ScalarExpression extends UnaryExpression -{ - public function __construct($value) - { - if (!is_scalar($value) && !is_null($value) && (!$value instanceof OqlHexValue)) - { - throw new CoreException('Attempt to create a scalar expression from a non scalar', array('var_type'=>gettype($value))); - } - parent::__construct($value); - } - - /** - * @param array $oSearch - * @param array $aArgs - * @param AttributeDefinition $oAttDef - * - * @param array $aCtx - * - * @return array|string - * @throws \Exception - */ - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - if (!is_null($oAttDef)) - { - if ($oAttDef->IsExternalKey()) - { - try - { - /** @var AttributeExternalKey $oAttDef */ - $sTarget = $oAttDef->GetTargetClass(); - $oObj = MetaModel::GetObject($sTarget, $this->m_value, false); - if (empty($oObj)) - { - return Dict::S('Enum:Undefined'); - } - - return $oObj->Get("friendlyname"); - } catch (CoreException $e) - { - } - } - - if (!($oAttDef instanceof AttributeDateTime)) - { - return $oAttDef->GetAsPlainText($this->m_value); - } - } - - if (strpos($this->m_value, '%') === 0) - { - return ''; - } - - if (isset($aCtx['date_display'])) - { - return $aCtx['date_display']->MakeValueLabel($oSearch, $this->m_value, $this->m_value); - } - - return $this->RenderExpression(false, $aArgs); - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - if (is_null($this->m_value)) - { - $sRet = 'NULL'; - } - else - { - $sRet = CMDBSource::Quote($this->m_value); - } - return $sRet; - } - - public function GetAsScalar($aArgs) - { - return clone $this; - } - - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - $aCriteria = array(); - switch ((string)($this->m_value)) - { - case '%Y-%m-%d': - $aCriteria['unit'] = 'DAY'; - break; - case '%Y-%m': - $aCriteria['unit'] = 'MONTH'; - break; - case '%w': - $aCriteria['unit'] = 'WEEKDAY'; - break; - case '%H': - $aCriteria['unit'] = 'HOUR'; - break; - default: - $aValue = array(); - if (!is_null($oAttDef)) - { - switch (true) - { - case ($oAttDef instanceof AttributeExternalField): - try - { - if ($this->GetValue() != 0) - { - /** @var AttributeExternalKey $oAttDef */ - $sTarget = $oAttDef->GetFinalAttDef()->GetTargetClass(); - $oObj = MetaModel::GetObject($sTarget, $this->GetValue()); - - $aValue['label'] = $oObj->Get("friendlyname"); - } - } - catch (Exception $e) - { - IssueLog::Error($e->getMessage()); - } - break; - case $oAttDef->IsExternalKey(): - try - { - if ($this->GetValue() != 0) - { - /** @var AttributeExternalKey $oAttDef */ - $sTarget = $oAttDef->GetTargetClass(); - $oObj = MetaModel::GetObject($sTarget, $this->GetValue(), true, true); - $aValue['label'] = $oObj->Get("friendlyname"); - } - } - catch (Exception $e) - { - // This object cannot be seen... ignore - } - break; - default: - try - { - $aValue['label'] = $oAttDef->GetAsPlainText($this->GetValue()); - } catch (Exception $e) - { - $aValue['label'] = $this->GetValue(); - } - break; - } - } - if (!empty($aValue)) - { - // only if a label is found - $aValue['value'] = $this->GetValue(); - $aCriteria['values'] = array($aValue); - } - break; - } - $aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams); - return $aCriteria; - } - -} - -class TrueExpression extends ScalarExpression -{ - public function __construct() - { - parent::__construct(1); - } - - public function IsTrue() - { - return true; - } -} - -class FalseExpression extends ScalarExpression -{ - public function __construct() - { - parent::__construct(0); - } - - public function IsTrue() - { - return false; - } -} - -class FieldExpression extends UnaryExpression -{ - protected $m_sParent; - protected $m_sName; - - public function __construct($sName, $sParent = '') - { - parent::__construct("$sParent.$sName"); - - $this->m_sParent = $sParent; - $this->m_sName = $sName; - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return false; - } - - public function GetParent() {return $this->m_sParent;} - public function GetName() {return $this->m_sName;} - - public function SetParent($sParent) - { - $this->m_sParent = $sParent; - $this->m_value = $sParent.'.'.$this->m_sName; - } - - private function GetClassName($aClasses = array()) - { - if (isset($aClasses[$this->m_sParent])) - { - return $aClasses[$this->m_sParent]; - } - else - { - return $this->m_sParent; - } - } - - /** - * @param DBObjectSearch $oSearch - * @param array $aArgs - * @param AttributeDefinition $oAttDef - * - * @param array $aCtx - * - * @return array|string - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - if (empty($this->m_sParent)) - { - return "`{$this->m_sName}`"; - } - if (method_exists($oSearch, 'GetJoinedClasses')) - { - $aClasses = $oSearch->GetJoinedClasses(); - } - else - { - $aClasses = array($oSearch->GetClass()); - } - $sClass = $this->GetClassName($aClasses); - $sAttName = MetaModel::GetLabel($sClass, $this->m_sName); - if ($sClass != $oSearch->GetClass()) - { - $sAttName = MetaModel::GetName($sClass).':'.$sAttName; - } - - return $sAttName; - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - if (empty($this->m_sParent)) - { - return "`{$this->m_sName}`"; - } - return "`{$this->m_sParent}`.`{$this->m_sName}`"; - } - - public function GetAttDef($aClasses = array()) - { - if (!empty($this->m_sParent)) - { - $sClass = $this->GetClassName($aClasses); - $aAttDefs = MetaModel::ListAttributeDefs($sClass); - if (isset($aAttDefs[$this->m_sName])) - { - return $aAttDefs[$this->m_sName]; - } - else - { - if ($this->m_sName == 'id') - { - $aParams = array( - 'default_value' => 0, - 'is_null_allowed' => false, - 'allowed_values' => null, - 'depends_on' => null, - 'sql' => 'id', - ); - - return new AttributeInteger($this->m_sName, $aParams); - } - } - } - - return null; - } - - - public function ListRequiredFields() - { - return array($this->m_sParent.'.'.$this->m_sName); - } - - public function CollectUsedParents(&$aTable) - { - $aTable[$this->m_sParent] = true; - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - if ($this->m_sParent == $sAlias) - { - // Add a reference to the field - $aUnresolved[$this->m_sName] = $this; - } - elseif ($sAlias == '') - { - // An empty alias means "any alias" - // In such a case, the results are indexed differently - $aUnresolved[$this->m_sParent][$this->m_sName] = $this; - } - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - if (!array_key_exists($this->m_sParent, $aTranslationData)) - { - if ($bMatchAll) throw new CoreException('Unknown parent id in translation table', array('parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData))); - - return clone $this; - } - if (!array_key_exists($this->m_sName, $aTranslationData[$this->m_sParent])) - { - if (!array_key_exists('*', $aTranslationData[$this->m_sParent])) - { - // #@# debug - if ($bMatchAll) MyHelpers::var_dump_html($aTranslationData, true); - if ($bMatchAll) throw new CoreException('Unknown name in translation table', array('name' => $this->m_sName, 'parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData[$this->m_sParent]))); - return clone $this; - } - $sNewParent = $aTranslationData[$this->m_sParent]['*']; - $sNewName = $this->m_sName; - if ($bMarkFieldsAsResolved) - { - $oRet = new FieldExpressionResolved($sNewName, $sNewParent); - } - else - { - $oRet = new FieldExpression($sNewName, $sNewParent); - } - } - else - { - $oRet = $aTranslationData[$this->m_sParent][$this->m_sName]; - } - return $oRet; - } - - /** - * Make the most relevant label, given the value of the expression - * - * @param DBSearch oFilter The context in which this expression has been used - * @param string sValue The value returned by the query, for this expression - * @param string sDefault The default value if no relevant label could be computed - * - * @return string label - * @throws \CoreException - */ - public function MakeValueLabel($oFilter, $sValue, $sDefault) - { - $sAttCode = $this->GetName(); - $sParentAlias = $this->GetParent(); - - $aSelectedClasses = $oFilter->GetSelectedClasses(); - $sClass = $aSelectedClasses[$sParentAlias]; - - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - // Set a default value for the general case - $sRes = $oAttDef->GetAsHtml($sValue); - - // Exceptions... - if ($oAttDef->IsExternalKey()) - { - /** @var AttributeExternalKey $oAttDef */ - $sObjClass = $oAttDef->GetTargetClass(); - $iObjKey = (int)$sValue; - if ($iObjKey > 0) - { - $oObject = MetaModel::GetObjectWithArchive($sObjClass, $iObjKey, true, true); - $sRes = $oObject->GetHyperlink(); - } - else - { - // Undefined - $sRes = DBObject::MakeHyperLink($sObjClass, 0); - } - } - elseif ($oAttDef->IsExternalField()) - { - if (is_null($sValue)) - { - $sRes = Dict::S('UI:UndefinedObject'); - } - } - return $sRes; - } - - public function RenameAlias($sOldName, $sNewName) - { - if ($this->m_sParent == $sOldName) - { - $this->m_sParent = $sNewName; - } - } - - private function GetJoinedFilters($oSearch, $iOperatorCodeTarget) - { - $aFilters = array(); - $aPointingToByKey = $oSearch->GetCriteria_PointingTo(); - foreach ($aPointingToByKey as $sExtKey => $aPointingTo) - { - foreach($aPointingTo as $iOperatorCode => $aFilter) - { - if ($iOperatorCode == $iOperatorCodeTarget) - { - foreach($aFilter as $oExtFilter) - { - $aFilters[$sExtKey] = $oExtFilter; - } - } - } - } - return $aFilters; - } - - /** - * @param DBObjectSearch $oSearch - * @param null $aArgs - * @param bool $bRetrofitParams - * @param AttributeDefinition $oAttDef - * - * @return array - */ - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - $aCriteria = array(); - $aCriteria['is_hierarchical'] = false; - // Replace BELOW joins by the corresponding external key for the search - // Try to detect hierarchical links - if ($this->m_sName == 'id') - { - if (method_exists($oSearch, 'GetCriteria_PointingTo')) - { - $aFilters = $this->GetJoinedFilters($oSearch, TREE_OPERATOR_EQUALS); - if (!empty($aFilters)) - { - foreach($aFilters as $sExtKey => $oFilter) - { - $aSubFilters = $this->GetJoinedFilters($oFilter, TREE_OPERATOR_BELOW); - foreach($aSubFilters as $oSubFilter) - { - /** @var \DBObjectSearch $oSubFilter */ - $sClassAlias = $oSubFilter->GetClassAlias(); - if ($sClassAlias == $this->m_sParent) - { - // Hierarchical link detected - // replace current field with the corresponding external key - $this->m_sName = $sExtKey; - $this->m_sParent = $oSearch->GetClassAlias(); - $aCriteria['is_hierarchical'] = true; - } - } - } - } - } - } - - if (method_exists($oSearch, 'GetJoinedClasses')) - { - $oAttDef = $this->GetAttDef($oSearch->GetJoinedClasses()); - } - else - { - $oAttDef = $this->GetAttDef($oSearch->GetSelectedClasses()); - } - if (!is_null($oAttDef)) - { - $sSearchType = $oAttDef->GetSearchType(); - try - { - if ($sSearchType == AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY) - { - if (MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass())) - { - $sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; - } - } - } - catch (CoreException $e) - { - } - } - else - { - $sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE; - } - - $aCriteria['widget'] = $sSearchType; - $aCriteria['ref'] = $this->GetParent().'.'.$this->GetName(); - $aCriteria['class_alias'] = $this->GetParent(); - - return $aCriteria; - } -} - -// Has been resolved into an SQL expression -class FieldExpressionResolved extends FieldExpression -{ - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - return clone $this; - } -} - -class VariableExpression extends UnaryExpression -{ - protected $m_sName; - - public function __construct($sName) - { - parent::__construct($sName); - - $this->m_sName = $sName; - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return false; - } - - public function GetName() - { - return $this->m_sName; - } - - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - $sValue = $this->m_value; - if (!is_null($aArgs) && (array_key_exists($this->m_sName, $aArgs))) - { - $sValue = $aArgs[$this->m_sName]; - } - elseif (($iPos = strpos($this->m_sName, '->')) !== false) - { - $sParamName = substr($this->m_sName, 0, $iPos); - $oObj = null; - $sAttCode = 'id'; - if (array_key_exists($sParamName.'->object()', $aArgs)) - { - $sAttCode = substr($this->m_sName, $iPos + 2); - $oObj = $aArgs[$sParamName.'->object()']; - } - elseif (array_key_exists($sParamName, $aArgs)) - { - $sAttCode = substr($this->m_sName, $iPos + 2); - $oObj = $aArgs[$sParamName]; - } - if (!is_null($oObj)) - { - if ($sAttCode == 'id') - { - $sValue = $oObj->Get("friendlyname"); - } - else - { - $sValue = $oObj->Get($sAttCode); - } - - return $sValue; - } - } - if (!is_null($oAttDef)) - { - if ($oAttDef->IsExternalKey()) - { - try - { - /** @var AttributeExternalKey $oAttDef */ - $sTarget = $oAttDef->GetTargetClass(); - $oObj = MetaModel::GetObject($sTarget, $sValue); - - return $oObj->Get("friendlyname"); - } catch (CoreException $e) - { - } - } - - return $oAttDef->GetAsPlainText($sValue); - } - - return $this->RenderExpression(false, $aArgs); - } - - /** - * @param bool $bForSQL - * @param array $aArgs - * @param bool $bRetrofitParams - * - * @return array|string - * @throws \MissingQueryArgument - */ - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - if (is_null($aArgs)) - { - return ':'.$this->m_sName; - } - elseif (array_key_exists($this->m_sName, $aArgs)) - { - $res = CMDBSource::Quote($aArgs[$this->m_sName]); - if (is_array($res)) - { - $res = implode(', ', $res); - } - return $res; - } - elseif (($iPos = strpos($this->m_sName, '->')) !== false) - { - $sParamName = substr($this->m_sName, 0, $iPos); - if (array_key_exists($sParamName.'->object()', $aArgs)) - { - $sAttCode = substr($this->m_sName, $iPos + 2); - $oObj = $aArgs[$sParamName.'->object()']; - if ($sAttCode == 'id') - { - return CMDBSource::Quote($oObj->GetKey()); - } - return CMDBSource::Quote($oObj->Get($sAttCode)); - } - } - - if ($bRetrofitParams) - { - $aArgs[$this->m_sName] = null; - return ':'.$this->m_sName; - } - else - { - throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs))); - } - } - - public function RenameParam($sOldName, $sNewName) - { - if ($this->m_sName == $sOldName) - { - $this->m_sName = $sNewName; - } - } - - public function GetAsScalar($aArgs) - { - $oRet = null; - if (array_key_exists($this->m_sName, $aArgs)) - { - $oRet = new ScalarExpression($aArgs[$this->m_sName]); - } - elseif (($iPos = strpos($this->m_sName, '->')) !== false) - { - $sParamName = substr($this->m_sName, 0, $iPos); - if (array_key_exists($sParamName.'->object()', $aArgs)) - { - $sAttCode = substr($this->m_sName, $iPos + 2); - $oObj = $aArgs[$sParamName.'->object()']; - if ($sAttCode == 'id') - { - $oRet = new ScalarExpression($oObj->GetKey()); - } - elseif (MetaModel::IsValidAttCode(get_class($oObj), $sAttCode)) - { - $oRet = new ScalarExpression($oObj->Get($sAttCode)); - } - else - { - throw new CoreException("Query argument {$this->m_sName} not matching any attribute of class ".get_class($oObj)); - } - } - } - if (is_null($oRet)) - { - throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs))); - } - return $oRet; - } -} - -// Temporary, until we implement functions and expression casting! -// ... or until we implement a real full text search based in the MATCH() expression -class ListExpression extends Expression -{ - protected $m_aExpressions; - - public function __construct($aExpressions) - { - $this->m_aExpressions = $aExpressions; - } - - public static function FromScalars($aScalars) - { - $aExpressions = array(); - foreach($aScalars as $value) - { - $aExpressions[] = new ScalarExpression($value); - } - return new ListExpression($aExpressions); - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return false; - } - - public function GetItems() - { - return $this->m_aExpressions; - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes[] = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - } - return '('.implode(', ', $aRes).')'; - } - - public function Browse(Closure $callback) - { - $callback($this); - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->Browse($callback); - } - } - - public function ApplyParameters($aArgs) - { - foreach ($this->m_aExpressions as $idx => $oExpr) - { - if ($oExpr instanceof VariableExpression) - { - $this->m_aExpressions[$idx] = $oExpr->GetAsScalar(); - } - else - { - $oExpr->ApplyParameters($aArgs); - } - } - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); - } - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - return new ListExpression($aRes); - } - - public function ListRequiredFields() - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); - } - return $aRes; - } - - public function CollectUsedParents(&$aTable) - { - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->CollectUsedParents($aTable); - } - } - - public function ListConstantFields() - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes = array_merge($aRes, $oExpr->ListConstantFields()); - } - return $aRes; - } - - public function RenameParam($sOldName, $sNewName) - { - foreach ($this->m_aExpressions as $key => $oExpr) - { - $this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName); - } - } - - public function RenameAlias($sOldName, $sNewName) - { - foreach ($this->m_aExpressions as $key => $oExpr) - { - $oExpr->RenameAlias($sOldName, $sNewName); - } - } - - public function GetAttDef($aClasses = array()) - { - foreach($this->m_aExpressions as $oExpression) - { - $oAttDef = $oExpression->GetAttDef($aClasses); - if (!is_null($oAttDef)) return $oAttDef; - } - - return null; - } - - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - $aValues = array(); - - foreach($this->m_aExpressions as $oExpression) - { - $aCrit = $oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - if (array_key_exists('values', $aCrit)) - { - $aValues = array_merge($aValues, $aCrit['values']); - } - } - - return array('values' => $aValues); - } -} - - -class FunctionExpression extends Expression -{ - protected $m_sVerb; - protected $m_aArgs; // array of expressions - - public function __construct($sVerb, $aArgExpressions) - { - $this->m_sVerb = $sVerb; - $this->m_aArgs = $aArgExpressions; - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return false; - } - - public function GetVerb() - { - return $this->m_sVerb; - } - - public function GetArgs() - { - return $this->m_aArgs; - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - $aRes = array(); - foreach ($this->m_aArgs as $iPos => $oExpr) - { - $aRes[] = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - } - return $this->m_sVerb.'('.implode(', ', $aRes).')'; - } - - public function Browse(Closure $callback) - { - $callback($this); - foreach ($this->m_aArgs as $iPos => $oExpr) - { - $oExpr->Browse($callback); - } - } - - public function ApplyParameters($aArgs) - { - foreach ($this->m_aArgs as $idx => $oExpr) - { - if ($oExpr instanceof VariableExpression) - { - $this->m_aArgs[$idx] = $oExpr->GetAsScalar($aArgs); - } - else - { - $oExpr->ApplyParameters($aArgs); - } - } - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - foreach ($this->m_aArgs as $oExpr) - { - $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); - } - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $aRes = array(); - foreach ($this->m_aArgs as $oExpr) - { - $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - return new FunctionExpression($this->m_sVerb, $aRes); - } - - public function ListRequiredFields() - { - $aRes = array(); - foreach ($this->m_aArgs as $oExpr) - { - $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); - } - return $aRes; - } - - public function CollectUsedParents(&$aTable) - { - foreach ($this->m_aArgs as $oExpr) - { - $oExpr->CollectUsedParents($aTable); - } - } - - public function ListConstantFields() - { - $aRes = array(); - foreach ($this->m_aArgs as $oExpr) - { - $aRes = array_merge($aRes, $oExpr->ListConstantFields()); - } - return $aRes; - } - - public function RenameParam($sOldName, $sNewName) - { - foreach ($this->m_aArgs as $key => $oExpr) - { - $this->m_aArgs[$key] = $oExpr->RenameParam($sOldName, $sNewName); - } - } - - public function RenameAlias($sOldName, $sNewName) - { - foreach ($this->m_aArgs as $key => $oExpr) - { - $oExpr->RenameAlias($sOldName, $sNewName); - } - } - - public function GetAttDef($aClasses = array()) - { - foreach($this->m_aArgs as $oExpression) - { - $oAttDef = $oExpression->GetAttDef($aClasses); - if (!is_null($oAttDef)) return $oAttDef; - } - - return null; - } - - /** - * Make the most relevant label, given the value of the expression - * - * @param DBSearch oFilter The context in which this expression has been used - * @param string sValue The value returned by the query, for this expression - * @param string sDefault The default value if no relevant label could be computed - * - * @return string label - */ - public function MakeValueLabel($oFilter, $sValue, $sDefault) - { - static $aWeekDayToString = null; - if (is_null($aWeekDayToString)) - { - // Init the correspondance table - $aWeekDayToString = array( - 0 => Dict::S('DayOfWeek-Sunday'), - 1 => Dict::S('DayOfWeek-Monday'), - 2 => Dict::S('DayOfWeek-Tuesday'), - 3 => Dict::S('DayOfWeek-Wednesday'), - 4 => Dict::S('DayOfWeek-Thursday'), - 5 => Dict::S('DayOfWeek-Friday'), - 6 => Dict::S('DayOfWeek-Saturday') - ); - } - static $aMonthToString = null; - if (is_null($aMonthToString)) - { - // Init the correspondance table - $aMonthToString = array( - 1 => Dict::S('Month-01'), - 2 => Dict::S('Month-02'), - 3 => Dict::S('Month-03'), - 4 => Dict::S('Month-04'), - 5 => Dict::S('Month-05'), - 6 => Dict::S('Month-06'), - 7 => Dict::S('Month-07'), - 8 => Dict::S('Month-08'), - 9 => Dict::S('Month-09'), - 10 => Dict::S('Month-10'), - 11 => Dict::S('Month-11'), - 12 => Dict::S('Month-12'), - ); - } - - $sRes = $sDefault; - if (strtolower($this->m_sVerb) == 'date_format') - { - $oFormatExpr = $this->m_aArgs[1]; - if ($oFormatExpr->Render() == "'%w'") - { - if (isset($aWeekDayToString[(int)$sValue])) - { - $sRes = $aWeekDayToString[(int)$sValue]; - } - } - elseif ($oFormatExpr->Render() == "'%Y-%m'") - { - // yyyy-mm => "yyyy month" - $iMonth = (int) substr($sValue, -2); // the two last chars - $sRes = substr($sValue, 0, 4).' '.$aMonthToString[$iMonth]; - } - elseif ($oFormatExpr->Render() == "'%Y-%m-%d'") - { - // yyyy-mm-dd => "month d" - $iMonth = (int) substr($sValue, 5, 2); - $sRes = $aMonthToString[$iMonth].' '.(int)substr($sValue, -2); - } - elseif ($oFormatExpr->Render() == "'%H'") - { - // H => "H Hour(s)" - $sRes = $sValue.':00'; - } - } - return $sRes; - } - - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - $sOperation = ''; - $sVerb = ''; - switch ($this->m_sVerb) - { - case 'ISNULL': - case 'NOW': - $sVerb = $this->VerbToNaturalLanguage(); - break; - case 'DATE_SUB': - $sVerb = ' -'; - break; - case 'DATE_ADD': - $sVerb = ' +'; - break; - case 'DATE_FORMAT': - $aCtx['date_display'] = $this; - break; - default: - return $this->RenderExpression(false, $aArgs); - } - - foreach($this->m_aArgs as $oExpression) - { - if ($oExpression instanceof IntervalExpression) - { - $sOperation .= $sVerb; - $sVerb = ''; - } - $sOperation .= $oExpression->Display($oSearch, $aArgs, $oAttDef, $aCtx); - } - - if (!empty($sVerb)) - { - $sOperation .= $sVerb; - } - return '('.$sOperation.')'; - } - - private function VerbToNaturalLanguage() - { - return Dict::S('Expression:Verb:'.$this->m_sVerb, " {$this->m_sVerb} "); - } - - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - $aCriteria = array(); - switch ($this->m_sVerb) - { - case 'ISNULL': - $aCriteria['operator'] = $this->m_sVerb; - foreach($this->m_aArgs as $oExpression) - { - $aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria); - } - $aCriteria['has_undefined'] = true; - $aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams); - break; - - case 'NOW': - $aCriteria = array('widget' => 'date_time'); - $aCriteria['is_relative'] = true; - $aCriteria['verb'] = $this->m_sVerb; - break; - - case 'DATE_ADD': - case 'DATE_SUB': - case 'DATE_FORMAT': - $aCriteria = array('widget' => 'date_time'); - foreach($this->m_aArgs as $oExpression) - { - $aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria); - } - $aCriteria['verb'] = $this->m_sVerb; - break; - - default: - return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - } - - return $aCriteria; - } -} - -class IntervalExpression extends Expression -{ - protected $m_oValue; // expression - protected $m_sUnit; - - public function __construct($oValue, $sUnit) - { - $this->m_oValue = $oValue; - $this->m_sUnit = $sUnit; - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return false; - } - - public function GetValue() - { - return $this->m_oValue; - } - - public function GetUnit() - { - return $this->m_sUnit; - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - return 'INTERVAL '.$this->m_oValue->RenderExpression($bForSQL, $aArgs, $bRetrofitParams).' '.$this->m_sUnit; - } - - public function Browse(Closure $callback) - { - $callback($this); - $this->m_oValue->Browse($callback); - } - - public function ApplyParameters($aArgs) - { - if ($this->m_oValue instanceof VariableExpression) - { - $this->m_oValue = $this->m_oValue->GetAsScalar($aArgs); - } - else - { - $this->m_oValue->ApplyParameters($aArgs); - } - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - $this->m_oValue->GetUnresolvedFields($sAlias, $aUnresolved); - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - return new IntervalExpression($this->m_oValue->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved), $this->m_sUnit); - } - - public function ListRequiredFields() - { - return array(); - } - - public function CollectUsedParents(&$aTable) - { - } - - public function ListConstantFields() - { - return array(); - } - - public function RenameParam($sOldName, $sNewName) - { - $this->m_oValue->RenameParam($sOldName, $sNewName); - } - - public function RenameAlias($sOldName, $sNewName) - { - $this->m_oValue->RenameAlias($sOldName, $sNewName); - } - - public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) - { - $aCriteria = $this->m_oValue->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); - $aCriteria['unit'] = $this->m_sUnit; - - return $aCriteria; - } - - public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) - { - return $this->m_oValue->RenderExpression(false, $aArgs).' '.Dict::S('Expression:Unit:Long:'.$this->m_sUnit, $this->m_sUnit); - } -} - -class CharConcatExpression extends Expression -{ - protected $m_aExpressions; - - public function __construct($aExpressions) - { - $this->m_aExpressions = $aExpressions; - } - - public function IsTrue() - { - // return true if we are certain that it will be true - return false; - } - - public function GetItems() - { - return $this->m_aExpressions; - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $sCol = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - // Concat will be globally NULL if one single argument is null ! - $aRes[] = "COALESCE($sCol, '')"; - } - return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)"; - } - - public function Browse(Closure $callback) - { - $callback($this); - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->Browse($callback); - } - } - - public function ApplyParameters($aArgs) - { - foreach ($this->m_aExpressions as $idx => $oExpr) - { - if ($oExpr instanceof VariableExpression) - { - $this->m_aExpressions[$idx] = $oExpr->GetAsScalar(); - } - else - { - $this->m_aExpressions->ApplyParameters($aArgs); - } - } - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); - } - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - return new CharConcatExpression($aRes); - } - - public function ListRequiredFields() - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); - } - return $aRes; - } - - public function CollectUsedParents(&$aTable) - { - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->CollectUsedParents($aTable); - } - } - - public function ListConstantFields() - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes = array_merge($aRes, $oExpr->ListConstantFields()); - } - return $aRes; - } - - public function RenameParam($sOldName, $sNewName) - { - foreach ($this->m_aExpressions as $key => $oExpr) - { - $this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName); - } - } - - public function RenameAlias($sOldName, $sNewName) - { - foreach ($this->m_aExpressions as $key => $oExpr) - { - $oExpr->RenameAlias($sOldName, $sNewName); - } - } -} - - -class CharConcatWSExpression extends CharConcatExpression -{ - protected $m_separator; - - public function __construct($separator, $aExpressions) - { - $this->m_separator = $separator; - parent::__construct($aExpressions); - } - - // recursive rendering - public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $sCol = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); - // Concat will be globally NULL if one single argument is null ! - $aRes[] = "COALESCE($sCol, '')"; - } - $sSep = CMDBSource::Quote($this->m_separator); - return "CAST(CONCAT_WS($sSep, ".implode(', ', $aRes).") AS CHAR)"; - } - - public function Browse(Closure $callback) - { - $callback($this); - foreach ($this->m_aExpressions as $oExpr) - { - $oExpr->Browse($callback); - } - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $aRes = array(); - foreach ($this->m_aExpressions as $oExpr) - { - $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - return new CharConcatWSExpression($this->m_separator, $aRes); - } -} - - -class QueryBuilderExpressions -{ - /** - * @var Expression - */ - protected $m_oConditionExpr; - /** - * @var Expression[] - */ - protected $m_aSelectExpr; - /** - * @var Expression[] - */ - protected $m_aGroupByExpr; - /** - * @var Expression[] - */ - protected $m_aJoinFields; - /** - * @var string[] - */ - protected $m_aClassIds; - - public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null, $aSelectExpr = null) - { - $this->m_oConditionExpr = $oSearch->GetCriteria(); - if (!$oSearch->GetShowObsoleteData()) - { - foreach ($oSearch->GetSelectedClasses() as $sAlias => $sClass) - { - if (MetaModel::IsObsoletable($sClass)) - { - $oNotObsolete = new BinaryExpression(new FieldExpression('obsolescence_flag', $sAlias), '=', new ScalarExpression(0)); - $this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oNotObsolete); - } - } - } - $this->m_aSelectExpr = is_null($aSelectExpr) ? array() : $aSelectExpr; - $this->m_aGroupByExpr = $aGroupByExpr; - $this->m_aJoinFields = array(); - - $this->m_aClassIds = array(); - foreach($oSearch->GetJoinedClasses() as $sClassAlias => $sClass) - { - $this->m_aClassIds[$sClassAlias] = new FieldExpression('id', $sClassAlias); - } - } - - public function GetSelect() - { - return $this->m_aSelectExpr; - } - - public function GetGroupBy() - { - return $this->m_aGroupByExpr; - } - - public function GetCondition() - { - return $this->m_oConditionExpr; - } - - /** - * @return Expression|mixed - */ - public function PopJoinField() - { - return array_pop($this->m_aJoinFields); - } - - /** - * @param string $sAttAlias - * @param Expression $oExpression - */ - public function AddSelect($sAttAlias, Expression $oExpression) - { - $this->m_aSelectExpr[$sAttAlias] = $oExpression; - } - - /** - * @param Expression $oExpression - */ - public function AddCondition(Expression $oExpression) - { - $this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oExpression); - } - - /** - * @param Expression $oExpression - */ - public function PushJoinField(Expression $oExpression) - { - array_push($this->m_aJoinFields, $oExpression); - } - - /** - * Get tables representing the queried objects - * Could be further optimized: when the first join is an outer join, then the rest can be omitted - * @param array $aTables - * @return array - */ - public function GetMandatoryTables(&$aTables = null) - { - if (is_null($aTables)) $aTables = array(); - - foreach($this->m_aClassIds as $sClass => $oExpression) - { - $oExpression->CollectUsedParents($aTables); - } - - return $aTables; - } - - public function GetUnresolvedFields($sAlias, &$aUnresolved) - { - $this->m_oConditionExpr->GetUnresolvedFields($sAlias, $aUnresolved); - foreach($this->m_aSelectExpr as $sColAlias => $oExpr) - { - $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); - } - if ($this->m_aGroupByExpr) - { - foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) - { - $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); - } - } - foreach($this->m_aJoinFields as $oExpression) - { - $oExpression->GetUnresolvedFields($sAlias, $aUnresolved); - } - } - - public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) - { - $this->m_oConditionExpr = $this->m_oConditionExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - foreach($this->m_aSelectExpr as $sColAlias => $oExpr) - { - $this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - if ($this->m_aGroupByExpr) - { - foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) - { - $this->m_aGroupByExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - } - foreach($this->m_aJoinFields as $index => $oExpression) - { - $this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - - foreach($this->m_aClassIds as $sClass => $oExpression) - { - $this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); - } - } - - public function RenameParam($sOldName, $sNewName) - { - $this->m_oConditionExpr->RenameParam($sOldName, $sNewName); - foreach($this->m_aSelectExpr as $sColAlias => $oExpr) - { - $this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName); - } - if ($this->m_aGroupByExpr) - { - foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) - { - $this->m_aGroupByExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName); - } - } - foreach($this->m_aJoinFields as $index => $oExpression) - { - $this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName); - } - } + +// + +class MissingQueryArgument extends CoreException +{ +} + + +abstract class Expression +{ + /** + * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects) + **/ + public function DeepClone() + { + return unserialize(serialize($this)); + } + + // recursive translation of identifiers + abstract public function GetUnresolvedFields($sAlias, &$aUnresolved); + + /** + * @param array $aTranslationData + * @param bool $bMatchAll + * @param bool $bMarkFieldsAsResolved + * + * @return Expression Translated expression + */ + abstract public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true); + + /** + * recursive rendering + * + * @deprecated use RenderExpression + * + * @param array $aArgs used as input by default, or used as output if bRetrofitParams set to True + * @param bool $bRetrofitParams + * + * @return array|string + * @throws \MissingQueryArgument + */ + public function Render(&$aArgs = null, $bRetrofitParams = false) + { + return $this->RenderExpression(false, $aArgs, $bRetrofitParams); + } + + /** + * recursive rendering + * + * @param bool $bForSQL generates code for OQL if false, for SQL otherwise + * @param array $aArgs used as input by default, or used as output if bRetrofitParams set to True + * @param bool $bRetrofitParams + * + * @return array|string + * @throws \MissingQueryArgument + */ + abstract public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false); + + /** + * @param DBObjectSearch $oSearch + * @param array $aArgs + * @param AttributeDefinition $oAttDef + * + * @param array $aCtx + * + * @return array parameters for the search form + */ + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + return $this->RenderExpression(false, $aArgs); + } + + public function GetAttDef($aClasses = array()) + { + return null; + } + + /** + * Recursively browse the expression tree + * @param Closure $callback + * @return mixed + */ + abstract public function Browse(Closure $callback); + + abstract public function ApplyParameters($aArgs); + + // recursively builds an array of class => fieldname + abstract public function ListRequiredFields(); + + // recursively list field parents ($aTable = array of sParent => dummy) + abstract public function CollectUsedParents(&$aTable); + + abstract public function IsTrue(); + + // recursively builds an array of [classAlias][fieldName] => value + abstract public function ListConstantFields(); + + public function RequiresField($sClass, $sFieldName) + { + // #@# todo - optimize : this is called quite often when building a single query ! + $aRequired = $this->ListRequiredFields(); + if (!in_array($sClass.'.'.$sFieldName, $aRequired)) return false; + return true; + } + + public function serialize() + { + return base64_encode($this->RenderExpression(false)); + } + + /** + * @param $sValue + * + * @return Expression + * @throws OQLException + */ + static public function unserialize($sValue) + { + return self::FromOQL(base64_decode($sValue)); + } + + /** + * @param $sConditionExpr + * @return Expression + */ + static public function FromOQL($sConditionExpr) + { + static $aCache = array(); + if (array_key_exists($sConditionExpr, $aCache)) + { + return unserialize($aCache[$sConditionExpr]); + } + $oOql = new OqlInterpreter($sConditionExpr); + $oExpression = $oOql->ParseExpression(); + $aCache[$sConditionExpr] = serialize($oExpression); + + return $oExpression; + } + + static public function FromSQL($sSQL) + { + $oSql = new SQLExpression($sSQL); + return $oSql; + } + + /** + * @param Expression $oExpr + * @return Expression + */ + public function LogAnd(Expression $oExpr) + { + if ($this->IsTrue()) return clone $oExpr; + if ($oExpr->IsTrue()) return clone $this; + return new BinaryExpression($this, 'AND', $oExpr); + } + + /** + * @param Expression $oExpr + * @return Expression + */ + public function LogOr(Expression $oExpr) + { + return new BinaryExpression($this, 'OR', $oExpr); + } + + abstract public function RenameParam($sOldName, $sNewName); + abstract public function RenameAlias($sOldName, $sNewName); + + /** + * Make the most relevant label, given the value of the expression + * + * @param DBSearch oFilter The context in which this expression has been used + * @param string sValue The value returned by the query, for this expression + * @param string sDefault The default value if no relevant label could be computed + * + * @return string label + */ + public function MakeValueLabel($oFilter, $sValue, $sDefault) + { + return $sDefault; + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + return array( + 'widget' => AttributeDefinition::SEARCH_WIDGET_TYPE_RAW, + 'oql' => $this->RenderExpression(false, $aArgs, $bRetrofitParams), + 'label' => $this->Display($oSearch, $aArgs, $oAttDef), + 'source' => get_class($this), + ); + } + + /** + * Split binary expression on given operator + * + * @param Expression $oExpr + * @param string $sOperator + * @param array $aAndExpr + * + * @return array of expressions + */ + public static function Split($oExpr, $sOperator = 'AND', &$aAndExpr = array()) + { + if (($oExpr instanceof BinaryExpression) && ($oExpr->GetOperator() == $sOperator)) + { + static::Split($oExpr->GetLeftExpr(), $sOperator, $aAndExpr); + static::Split($oExpr->GetRightExpr(), $sOperator, $aAndExpr); + } + else + { + $aAndExpr[] = $oExpr; + } + + return $aAndExpr; + } +} + +class SQLExpression extends Expression +{ + protected $m_sSQL; + + public function __construct($sSQL) + { + $this->m_sSQL = $sSQL; + } + + public function IsTrue() + { + return false; + } + + // recursive rendering + public function RenderExpression($bForSql = false, &$aArgs = null, $bRetrofitParams = false) + { + return $this->m_sSQL; + } + + public function Browse(Closure $callback) + { + $callback($this); + } + + public function ApplyParameters($aArgs) + { + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + return clone $this; + } + + public function ListRequiredFields() + { + return array(); + } + + public function CollectUsedParents(&$aTable) + { + } + + public function ListConstantFields() + { + return array(); + } + + public function RenameParam($sOldName, $sNewName) + { + // Do nothing, since there is nothing to rename + } + + public function RenameAlias($sOldName, $sNewName) + { + // Do nothing, since there is nothing to rename + } +} + + + +class BinaryExpression extends Expression +{ + protected $m_oLeftExpr; // filter code or an SQL expression (later?) + protected $m_oRightExpr; + protected $m_sOperator; + + /** + * @param \Expression $oLeftExpr + * @param string $sOperator + * @param \Expression $oRightExpr + * + * @throws \CoreException + */ + public function __construct($oLeftExpr, $sOperator, $oRightExpr) + { + $this->ValidateConstructorParams($oLeftExpr, $sOperator, $oRightExpr); + + $this->m_oLeftExpr = $oLeftExpr; + $this->m_oRightExpr = $oRightExpr; + $this->m_sOperator = $sOperator; + } + + /** + * @param $oLeftExpr + * @param $sOperator + * @param $oRightExpr + * + * @throws \CoreException if one of the parameter is invalid + */ + protected function ValidateConstructorParams($oLeftExpr, $sOperator, $oRightExpr) + { + if (!is_object($oLeftExpr)) + { + throw new CoreException('Expecting an Expression object on the left hand', array('found_type' => gettype($oLeftExpr))); + } + if (!is_object($oRightExpr)) + { + throw new CoreException('Expecting an Expression object on the right hand', array('found_type' => gettype($oRightExpr))); + } + if (!$oLeftExpr instanceof Expression) + { + throw new CoreException('Expecting an Expression object on the left hand', array('found_class' => get_class($oLeftExpr))); + } + if (!$oRightExpr instanceof Expression) + { + throw new CoreException('Expecting an Expression object on the right hand', array('found_class' => get_class($oRightExpr))); + } + if ((($sOperator == "IN") || ($sOperator == "NOT IN")) && !($oRightExpr instanceof ListExpression)) + { + throw new CoreException("Expecting a List Expression object on the right hand for operator $sOperator", + array('found_class' => get_class($oRightExpr))); + } + } + + public function IsTrue() + { + // return true if we are certain that it will be true + if ($this->m_sOperator == 'AND') + { + if ($this->m_oLeftExpr->IsTrue() && $this->m_oRightExpr->IsTrue()) return true; + } + elseif ($this->m_sOperator == 'OR') + { + if ($this->m_oLeftExpr->IsTrue() || $this->m_oRightExpr->IsTrue()) return true; + } + return false; + } + + public function GetLeftExpr() + { + return $this->m_oLeftExpr; + } + + public function GetRightExpr() + { + return $this->m_oRightExpr; + } + + public function GetOperator() + { + return $this->m_sOperator; + } + + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + $sOperator = $this->GetOperator(); + $sLeft = $this->GetLeftExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + $sRight = $this->GetRightExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + return "($sLeft $sOperator $sRight)"; + } + + public function Browse(Closure $callback) + { + $callback($this); + $this->m_oLeftExpr->Browse($callback); + $this->m_oRightExpr->Browse($callback); + } + + public function ApplyParameters($aArgs) + { + if ($this->m_oLeftExpr instanceof VariableExpression) + { + $this->m_oLeftExpr = $this->m_oLeftExpr->GetAsScalar($aArgs); + } + else //if ($this->m_oLeftExpr instanceof Expression) + { + $this->m_oLeftExpr->ApplyParameters($aArgs); + } + if ($this->m_oRightExpr instanceof VariableExpression) + { + $this->m_oRightExpr = $this->m_oRightExpr->GetAsScalar($aArgs); + } + else //if ($this->m_oRightExpr instanceof Expression) + { + $this->m_oRightExpr->ApplyParameters($aArgs); + } + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + $this->GetLeftExpr()->GetUnresolvedFields($sAlias, $aUnresolved); + $this->GetRightExpr()->GetUnresolvedFields($sAlias, $aUnresolved); + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + $oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + return new BinaryExpression($oLeft, $this->GetOperator(), $oRight); + } + + public function ListRequiredFields() + { + $aLeft = $this->GetLeftExpr()->ListRequiredFields(); + $aRight = $this->GetRightExpr()->ListRequiredFields(); + return array_merge($aLeft, $aRight); + } + + public function CollectUsedParents(&$aTable) + { + $this->GetLeftExpr()->CollectUsedParents($aTable); + $this->GetRightExpr()->CollectUsedParents($aTable); + } + + public function GetAttDef($aClasses = array()) + { + $oAttDef = $this->GetLeftExpr()->GetAttDef($aClasses); + if (!is_null($oAttDef)) return $oAttDef; + + return $this->GetRightExpr()->GetAttDef($aClasses); + } + + /** + * List all constant expression of the form = or = : + * Could be extended to support = + */ + public function ListConstantFields() + { + $aResult = array(); + if ($this->m_sOperator == '=') + { + if (($this->m_oLeftExpr instanceof FieldExpression) && ($this->m_oRightExpr instanceof ScalarExpression)) + { + $aResult[$this->m_oLeftExpr->GetParent()][$this->m_oLeftExpr->GetName()] = $this->m_oRightExpr; + } + else if (($this->m_oRightExpr instanceof FieldExpression) && ($this->m_oLeftExpr instanceof ScalarExpression)) + { + $aResult[$this->m_oRightExpr->GetParent()][$this->m_oRightExpr->GetName()] = $this->m_oLeftExpr; + } + else if (($this->m_oLeftExpr instanceof FieldExpression) && ($this->m_oRightExpr instanceof VariableExpression)) + { + $aResult[$this->m_oLeftExpr->GetParent()][$this->m_oLeftExpr->GetName()] = $this->m_oRightExpr; + } + else if (($this->m_oRightExpr instanceof FieldExpression) && ($this->m_oLeftExpr instanceof VariableExpression)) + { + $aResult[$this->m_oRightExpr->GetParent()][$this->m_oRightExpr->GetName()] = $this->m_oLeftExpr; + } + } + else if ($this->m_sOperator == 'AND') + { + // Strictly, this should be done only for the AND operator + $aResult = array_merge_recursive($this->m_oRightExpr->ListConstantFields(), $this->m_oLeftExpr->ListConstantFields()); + } + return $aResult; + } + + public function RenameParam($sOldName, $sNewName) + { + $this->GetLeftExpr()->RenameParam($sOldName, $sNewName); + $this->GetRightExpr()->RenameParam($sOldName, $sNewName); + } + + public function RenameAlias($sOldName, $sNewName) + { + $this->GetLeftExpr()->RenameAlias($sOldName, $sNewName); + $this->GetRightExpr()->RenameAlias($sOldName, $sNewName); + } + + // recursive rendering + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + $bReverseOperator = false; + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $aClasses = $oSearch->GetJoinedClasses(); + } + else + { + $aClasses = array($oSearch->GetClass()); + } + $oLeftExpr = $this->GetLeftExpr(); + if ($oLeftExpr instanceof FieldExpression) + { + $oAttDef = $oLeftExpr->GetAttDef($aClasses); + } + $oRightExpr = $this->GetRightExpr(); + if ($oRightExpr instanceof FieldExpression) + { + $oAttDef = $oRightExpr->GetAttDef($aClasses); + $bReverseOperator = true; + } + + + if ($bReverseOperator) + { + $sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); + $sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); + + // switch left and right expressions so reverse the operator + // Note that the operation is the same so < becomes > and not >= + switch ($this->GetOperator()) + { + case '>': + $sOperator = '<'; + break; + case '<': + $sOperator = '>'; + break; + case '>=': + $sOperator = '<='; + break; + case '<=': + $sOperator = '>='; + break; + default: + $sOperator = $this->GetOperator(); + break; + } + $sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef); + + return "({$sRight}{$sOperator}{$sLeft})"; + } + + $sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); + $sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef, $aCtx); + + $sOperator = $this->GetOperator(); + $sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef); + + return "({$sLeft}{$sOperator}{$sRight})"; + } + + private function OperatorToNaturalLanguage($sOperator, $oAttDef) + { + if ($oAttDef instanceof AttributeDateTime) + { + return Dict::S('Expression:Operator:Date:'.$sOperator, " $sOperator "); + } + + return Dict::S('Expression:Operator:'.$sOperator, " $sOperator "); + } + + /** + * @param DBSearch $oSearch + * @param null $aArgs + * @param bool $bRetrofitParams + * @param null $oAttDef + * + * @return array + */ + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $bReverseOperator = false; + $oLeftExpr = $this->GetLeftExpr(); + $oRightExpr = $this->GetRightExpr(); + + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $aClasses = $oSearch->GetJoinedClasses(); + } + else + { + $aClasses = array($oSearch->GetClass()); + } + + $oAttDef = $oLeftExpr->GetAttDef($aClasses); + if (is_null($oAttDef)) + { + $oAttDef = $oRightExpr->GetAttDef($aClasses); + $bReverseOperator = true; + } + + if (is_null($oAttDef)) + { + return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + } + + + if ($bReverseOperator) + { + $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + // $oAttDef can be different now + $oAttDef = $oRightExpr->GetAttDef($aClasses); + $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + + // switch left and right expressions so reverse the operator + // Note that the operation is the same so < becomes > and not >= + switch ($this->GetOperator()) + { + case '>': + $sOperator = '<'; + break; + case '<': + $sOperator = '>'; + break; + case '>=': + $sOperator = '<='; + break; + case '<=': + $sOperator = '>='; + break; + default: + $sOperator = $this->GetOperator(); + break; + } + $aCriteria = self::MergeCriteria($aCriteriaRight, $aCriteriaLeft, $sOperator); + } + else + { + $aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + // $oAttDef can be different now + $oAttDef = $oLeftExpr->GetAttDef($aClasses); + $aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + + $aCriteria = self::MergeCriteria($aCriteriaLeft, $aCriteriaRight, $this->GetOperator()); + } + $aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams); + $aCriteria['label'] = $this->Display($oSearch, $aArgs, $oAttDef); + + if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] != $aCriteriaRight['ref'])) + { + // Only one Field is supported in the expressions + $aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW; + } + + return $aCriteria; + } + + protected static function MergeCriteria($aCriteriaLeft, $aCriteriaRight, $sOperator) + { + $aCriteriaOverride = array(); + $aCriteriaOverride['operator'] = $sOperator; + if ($sOperator == 'OR') + { + if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] == $aCriteriaRight['ref'])) + { + if (isset($aCriteriaLeft['widget']) && isset($aCriteriaRight['widget']) && ($aCriteriaLeft['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY) && ($aCriteriaRight['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY)) + { + $aCriteriaOverride['operator'] = 'IN'; + $aCriteriaOverride['is_hierarchical'] = true; + + if (isset($aCriteriaLeft['values']) && isset($aCriteriaRight['values'])) + { + $aCriteriaOverride['values'] = array_merge($aCriteriaLeft['values'], $aCriteriaRight['values']); + } + } + } + } + + return array_merge($aCriteriaLeft, $aCriteriaRight, $aCriteriaOverride); + } +} + + +/** + * @since 2.6 N°931 tag fields + */ +class MatchExpression extends BinaryExpression +{ + /** @var \FieldExpression */ + protected $m_oLeftExpr; + /** @var \ScalarExpression */ + protected $m_oRightExpr; + + /** + * MatchExpression constructor. + * + * @param \FieldExpression $oLeftExpr + * @param \ScalarExpression $oRightExpr + * + * @throws \CoreException + */ + public function __construct(FieldExpression $oLeftExpr, ScalarExpression $oRightExpr) + { + parent::__construct($oLeftExpr, 'MATCHES', $oRightExpr); + } + + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + $sLeft = $this->GetLeftExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + $sRight = $this->GetRightExpr()->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + + if ($bForSQL) + { + $sRet = "MATCH ($sLeft) AGAINST ($sRight IN BOOLEAN MODE)"; + } + else + { + $sRet = "$sLeft MATCHES $sRight"; + } + + return $sRet; + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $oLeft = $this->GetLeftExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + $oRight = $this->GetRightExpr()->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + + return new static($oLeft, $oRight); + } +} + + +class UnaryExpression extends Expression +{ + protected $m_value; + + public function __construct($value) + { + $this->m_value = $value; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return ($this->m_value == 1); + } + + public function GetValue() + { + return $this->m_value; + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + return CMDBSource::Quote($this->m_value); + } + + public function Browse(Closure $callback) + { + $callback($this); + } + + public function ApplyParameters($aArgs) + { + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + return clone $this; + } + + public function ListRequiredFields() + { + return array(); + } + + public function CollectUsedParents(&$aTable) + { + } + + public function ListConstantFields() + { + return array(); + } + + public function RenameParam($sOldName, $sNewName) + { + // Do nothing + // really ? what about :param{$iParamIndex} ?? + } + + public function RenameAlias($sOldName, $sNewName) + { + // Do nothing + } +} + +class ScalarExpression extends UnaryExpression +{ + public function __construct($value) + { + if (!is_scalar($value) && !is_null($value) && (!$value instanceof OqlHexValue)) + { + throw new CoreException('Attempt to create a scalar expression from a non scalar', array('var_type'=>gettype($value))); + } + parent::__construct($value); + } + + /** + * @param array $oSearch + * @param array $aArgs + * @param AttributeDefinition $oAttDef + * + * @param array $aCtx + * + * @return array|string + * @throws \Exception + */ + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + if (!is_null($oAttDef)) + { + if ($oAttDef->IsExternalKey()) + { + try + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $this->m_value, false); + if (empty($oObj)) + { + return Dict::S('Enum:Undefined'); + } + + return $oObj->Get("friendlyname"); + } catch (CoreException $e) + { + } + } + + if (!($oAttDef instanceof AttributeDateTime)) + { + return $oAttDef->GetAsPlainText($this->m_value); + } + } + + if (strpos($this->m_value, '%') === 0) + { + return ''; + } + + if (isset($aCtx['date_display'])) + { + return $aCtx['date_display']->MakeValueLabel($oSearch, $this->m_value, $this->m_value); + } + + return $this->RenderExpression(false, $aArgs); + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + if (is_null($this->m_value)) + { + $sRet = 'NULL'; + } + else + { + $sRet = CMDBSource::Quote($this->m_value); + } + return $sRet; + } + + public function GetAsScalar($aArgs) + { + return clone $this; + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = array(); + switch ((string)($this->m_value)) + { + case '%Y-%m-%d': + $aCriteria['unit'] = 'DAY'; + break; + case '%Y-%m': + $aCriteria['unit'] = 'MONTH'; + break; + case '%w': + $aCriteria['unit'] = 'WEEKDAY'; + break; + case '%H': + $aCriteria['unit'] = 'HOUR'; + break; + default: + $aValue = array(); + if (!is_null($oAttDef)) + { + switch (true) + { + case ($oAttDef instanceof AttributeExternalField): + try + { + if ($this->GetValue() != 0) + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetFinalAttDef()->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $this->GetValue()); + + $aValue['label'] = $oObj->Get("friendlyname"); + } + } + catch (Exception $e) + { + IssueLog::Error($e->getMessage()); + } + break; + case $oAttDef->IsExternalKey(): + try + { + if ($this->GetValue() != 0) + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $this->GetValue(), true, true); + $aValue['label'] = $oObj->Get("friendlyname"); + } + } + catch (Exception $e) + { + // This object cannot be seen... ignore + } + break; + default: + try + { + $aValue['label'] = $oAttDef->GetAsPlainText($this->GetValue()); + } catch (Exception $e) + { + $aValue['label'] = $this->GetValue(); + } + break; + } + } + if (!empty($aValue)) + { + // only if a label is found + $aValue['value'] = $this->GetValue(); + $aCriteria['values'] = array($aValue); + } + break; + } + $aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams); + return $aCriteria; + } + +} + +class TrueExpression extends ScalarExpression +{ + public function __construct() + { + parent::__construct(1); + } + + public function IsTrue() + { + return true; + } +} + +class FalseExpression extends ScalarExpression +{ + public function __construct() + { + parent::__construct(0); + } + + public function IsTrue() + { + return false; + } +} + +class FieldExpression extends UnaryExpression +{ + protected $m_sParent; + protected $m_sName; + + public function __construct($sName, $sParent = '') + { + parent::__construct("$sParent.$sName"); + + $this->m_sParent = $sParent; + $this->m_sName = $sName; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetParent() {return $this->m_sParent;} + public function GetName() {return $this->m_sName;} + + public function SetParent($sParent) + { + $this->m_sParent = $sParent; + $this->m_value = $sParent.'.'.$this->m_sName; + } + + private function GetClassName($aClasses = array()) + { + if (isset($aClasses[$this->m_sParent])) + { + return $aClasses[$this->m_sParent]; + } + else + { + return $this->m_sParent; + } + } + + /** + * @param DBObjectSearch $oSearch + * @param array $aArgs + * @param AttributeDefinition $oAttDef + * + * @param array $aCtx + * + * @return array|string + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + if (empty($this->m_sParent)) + { + return "`{$this->m_sName}`"; + } + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $aClasses = $oSearch->GetJoinedClasses(); + } + else + { + $aClasses = array($oSearch->GetClass()); + } + $sClass = $this->GetClassName($aClasses); + $sAttName = MetaModel::GetLabel($sClass, $this->m_sName); + if ($sClass != $oSearch->GetClass()) + { + $sAttName = MetaModel::GetName($sClass).':'.$sAttName; + } + + return $sAttName; + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + if (empty($this->m_sParent)) + { + return "`{$this->m_sName}`"; + } + return "`{$this->m_sParent}`.`{$this->m_sName}`"; + } + + public function GetAttDef($aClasses = array()) + { + if (!empty($this->m_sParent)) + { + $sClass = $this->GetClassName($aClasses); + $aAttDefs = MetaModel::ListAttributeDefs($sClass); + if (isset($aAttDefs[$this->m_sName])) + { + return $aAttDefs[$this->m_sName]; + } + else + { + if ($this->m_sName == 'id') + { + $aParams = array( + 'default_value' => 0, + 'is_null_allowed' => false, + 'allowed_values' => null, + 'depends_on' => null, + 'sql' => 'id', + ); + + return new AttributeInteger($this->m_sName, $aParams); + } + } + } + + return null; + } + + + public function ListRequiredFields() + { + return array($this->m_sParent.'.'.$this->m_sName); + } + + public function CollectUsedParents(&$aTable) + { + $aTable[$this->m_sParent] = true; + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + if ($this->m_sParent == $sAlias) + { + // Add a reference to the field + $aUnresolved[$this->m_sName] = $this; + } + elseif ($sAlias == '') + { + // An empty alias means "any alias" + // In such a case, the results are indexed differently + $aUnresolved[$this->m_sParent][$this->m_sName] = $this; + } + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + if (!array_key_exists($this->m_sParent, $aTranslationData)) + { + if ($bMatchAll) throw new CoreException('Unknown parent id in translation table', array('parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData))); + + return clone $this; + } + if (!array_key_exists($this->m_sName, $aTranslationData[$this->m_sParent])) + { + if (!array_key_exists('*', $aTranslationData[$this->m_sParent])) + { + // #@# debug - if ($bMatchAll) MyHelpers::var_dump_html($aTranslationData, true); + if ($bMatchAll) throw new CoreException('Unknown name in translation table', array('name' => $this->m_sName, 'parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData[$this->m_sParent]))); + return clone $this; + } + $sNewParent = $aTranslationData[$this->m_sParent]['*']; + $sNewName = $this->m_sName; + if ($bMarkFieldsAsResolved) + { + $oRet = new FieldExpressionResolved($sNewName, $sNewParent); + } + else + { + $oRet = new FieldExpression($sNewName, $sNewParent); + } + } + else + { + $oRet = $aTranslationData[$this->m_sParent][$this->m_sName]; + } + return $oRet; + } + + /** + * Make the most relevant label, given the value of the expression + * + * @param DBSearch oFilter The context in which this expression has been used + * @param string sValue The value returned by the query, for this expression + * @param string sDefault The default value if no relevant label could be computed + * + * @return string label + * @throws \CoreException + */ + public function MakeValueLabel($oFilter, $sValue, $sDefault) + { + $sAttCode = $this->GetName(); + $sParentAlias = $this->GetParent(); + + $aSelectedClasses = $oFilter->GetSelectedClasses(); + $sClass = $aSelectedClasses[$sParentAlias]; + + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + // Set a default value for the general case + $sRes = $oAttDef->GetAsHtml($sValue); + + // Exceptions... + if ($oAttDef->IsExternalKey()) + { + /** @var AttributeExternalKey $oAttDef */ + $sObjClass = $oAttDef->GetTargetClass(); + $iObjKey = (int)$sValue; + if ($iObjKey > 0) + { + $oObject = MetaModel::GetObjectWithArchive($sObjClass, $iObjKey, true, true); + $sRes = $oObject->GetHyperlink(); + } + else + { + // Undefined + $sRes = DBObject::MakeHyperLink($sObjClass, 0); + } + } + elseif ($oAttDef->IsExternalField()) + { + if (is_null($sValue)) + { + $sRes = Dict::S('UI:UndefinedObject'); + } + } + return $sRes; + } + + public function RenameAlias($sOldName, $sNewName) + { + if ($this->m_sParent == $sOldName) + { + $this->m_sParent = $sNewName; + } + } + + private function GetJoinedFilters($oSearch, $iOperatorCodeTarget) + { + $aFilters = array(); + $aPointingToByKey = $oSearch->GetCriteria_PointingTo(); + foreach ($aPointingToByKey as $sExtKey => $aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + if ($iOperatorCode == $iOperatorCodeTarget) + { + foreach($aFilter as $oExtFilter) + { + $aFilters[$sExtKey] = $oExtFilter; + } + } + } + } + return $aFilters; + } + + /** + * @param DBObjectSearch $oSearch + * @param null $aArgs + * @param bool $bRetrofitParams + * @param AttributeDefinition $oAttDef + * + * @return array + */ + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = array(); + $aCriteria['is_hierarchical'] = false; + // Replace BELOW joins by the corresponding external key for the search + // Try to detect hierarchical links + if ($this->m_sName == 'id') + { + if (method_exists($oSearch, 'GetCriteria_PointingTo')) + { + $aFilters = $this->GetJoinedFilters($oSearch, TREE_OPERATOR_EQUALS); + if (!empty($aFilters)) + { + foreach($aFilters as $sExtKey => $oFilter) + { + $aSubFilters = $this->GetJoinedFilters($oFilter, TREE_OPERATOR_BELOW); + foreach($aSubFilters as $oSubFilter) + { + /** @var \DBObjectSearch $oSubFilter */ + $sClassAlias = $oSubFilter->GetClassAlias(); + if ($sClassAlias == $this->m_sParent) + { + // Hierarchical link detected + // replace current field with the corresponding external key + $this->m_sName = $sExtKey; + $this->m_sParent = $oSearch->GetClassAlias(); + $aCriteria['is_hierarchical'] = true; + } + } + } + } + } + } + + if (method_exists($oSearch, 'GetJoinedClasses')) + { + $oAttDef = $this->GetAttDef($oSearch->GetJoinedClasses()); + } + else + { + $oAttDef = $this->GetAttDef($oSearch->GetSelectedClasses()); + } + if (!is_null($oAttDef)) + { + $sSearchType = $oAttDef->GetSearchType(); + try + { + if ($sSearchType == AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY) + { + if (MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass())) + { + $sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; + } + } + } + catch (CoreException $e) + { + } + } + else + { + $sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE; + } + + $aCriteria['widget'] = $sSearchType; + $aCriteria['ref'] = $this->GetParent().'.'.$this->GetName(); + $aCriteria['class_alias'] = $this->GetParent(); + + return $aCriteria; + } +} + +// Has been resolved into an SQL expression +class FieldExpressionResolved extends FieldExpression +{ + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + return clone $this; + } +} + +class VariableExpression extends UnaryExpression +{ + protected $m_sName; + + public function __construct($sName) + { + parent::__construct($sName); + + $this->m_sName = $sName; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetName() + { + return $this->m_sName; + } + + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + $sValue = $this->m_value; + if (!is_null($aArgs) && (array_key_exists($this->m_sName, $aArgs))) + { + $sValue = $aArgs[$this->m_sName]; + } + elseif (($iPos = strpos($this->m_sName, '->')) !== false) + { + $sParamName = substr($this->m_sName, 0, $iPos); + $oObj = null; + $sAttCode = 'id'; + if (array_key_exists($sParamName.'->object()', $aArgs)) + { + $sAttCode = substr($this->m_sName, $iPos + 2); + $oObj = $aArgs[$sParamName.'->object()']; + } + elseif (array_key_exists($sParamName, $aArgs)) + { + $sAttCode = substr($this->m_sName, $iPos + 2); + $oObj = $aArgs[$sParamName]; + } + if (!is_null($oObj)) + { + if ($sAttCode == 'id') + { + $sValue = $oObj->Get("friendlyname"); + } + else + { + $sValue = $oObj->Get($sAttCode); + } + + return $sValue; + } + } + if (!is_null($oAttDef)) + { + if ($oAttDef->IsExternalKey()) + { + try + { + /** @var AttributeExternalKey $oAttDef */ + $sTarget = $oAttDef->GetTargetClass(); + $oObj = MetaModel::GetObject($sTarget, $sValue); + + return $oObj->Get("friendlyname"); + } catch (CoreException $e) + { + } + } + + return $oAttDef->GetAsPlainText($sValue); + } + + return $this->RenderExpression(false, $aArgs); + } + + /** + * @param bool $bForSQL + * @param array $aArgs + * @param bool $bRetrofitParams + * + * @return array|string + * @throws \MissingQueryArgument + */ + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + if (is_null($aArgs)) + { + return ':'.$this->m_sName; + } + elseif (array_key_exists($this->m_sName, $aArgs)) + { + $res = CMDBSource::Quote($aArgs[$this->m_sName]); + if (is_array($res)) + { + $res = implode(', ', $res); + } + return $res; + } + elseif (($iPos = strpos($this->m_sName, '->')) !== false) + { + $sParamName = substr($this->m_sName, 0, $iPos); + if (array_key_exists($sParamName.'->object()', $aArgs)) + { + $sAttCode = substr($this->m_sName, $iPos + 2); + $oObj = $aArgs[$sParamName.'->object()']; + if ($sAttCode == 'id') + { + return CMDBSource::Quote($oObj->GetKey()); + } + return CMDBSource::Quote($oObj->Get($sAttCode)); + } + } + + if ($bRetrofitParams) + { + $aArgs[$this->m_sName] = null; + return ':'.$this->m_sName; + } + else + { + throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs))); + } + } + + public function RenameParam($sOldName, $sNewName) + { + if ($this->m_sName == $sOldName) + { + $this->m_sName = $sNewName; + } + } + + public function GetAsScalar($aArgs) + { + $oRet = null; + if (array_key_exists($this->m_sName, $aArgs)) + { + $oRet = new ScalarExpression($aArgs[$this->m_sName]); + } + elseif (($iPos = strpos($this->m_sName, '->')) !== false) + { + $sParamName = substr($this->m_sName, 0, $iPos); + if (array_key_exists($sParamName.'->object()', $aArgs)) + { + $sAttCode = substr($this->m_sName, $iPos + 2); + $oObj = $aArgs[$sParamName.'->object()']; + if ($sAttCode == 'id') + { + $oRet = new ScalarExpression($oObj->GetKey()); + } + elseif (MetaModel::IsValidAttCode(get_class($oObj), $sAttCode)) + { + $oRet = new ScalarExpression($oObj->Get($sAttCode)); + } + else + { + throw new CoreException("Query argument {$this->m_sName} not matching any attribute of class ".get_class($oObj)); + } + } + } + if (is_null($oRet)) + { + throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs))); + } + return $oRet; + } +} + +// Temporary, until we implement functions and expression casting! +// ... or until we implement a real full text search based in the MATCH() expression +class ListExpression extends Expression +{ + protected $m_aExpressions; + + public function __construct($aExpressions) + { + $this->m_aExpressions = $aExpressions; + } + + public static function FromScalars($aScalars) + { + $aExpressions = array(); + foreach($aScalars as $value) + { + $aExpressions[] = new ScalarExpression($value); + } + return new ListExpression($aExpressions); + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetItems() + { + return $this->m_aExpressions; + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + } + return '('.implode(', ', $aRes).')'; + } + + public function Browse(Closure $callback) + { + $callback($this); + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->Browse($callback); + } + } + + public function ApplyParameters($aArgs) + { + foreach ($this->m_aExpressions as $idx => $oExpr) + { + if ($oExpr instanceof VariableExpression) + { + $this->m_aExpressions[$idx] = $oExpr->GetAsScalar(); + } + else + { + $oExpr->ApplyParameters($aArgs); + } + } + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); + } + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + return new ListExpression($aRes); + } + + public function ListRequiredFields() + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); + } + return $aRes; + } + + public function CollectUsedParents(&$aTable) + { + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->CollectUsedParents($aTable); + } + } + + public function ListConstantFields() + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListConstantFields()); + } + return $aRes; + } + + public function RenameParam($sOldName, $sNewName) + { + foreach ($this->m_aExpressions as $key => $oExpr) + { + $this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName); + } + } + + public function RenameAlias($sOldName, $sNewName) + { + foreach ($this->m_aExpressions as $key => $oExpr) + { + $oExpr->RenameAlias($sOldName, $sNewName); + } + } + + public function GetAttDef($aClasses = array()) + { + foreach($this->m_aExpressions as $oExpression) + { + $oAttDef = $oExpression->GetAttDef($aClasses); + if (!is_null($oAttDef)) return $oAttDef; + } + + return null; + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aValues = array(); + + foreach($this->m_aExpressions as $oExpression) + { + $aCrit = $oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + if (array_key_exists('values', $aCrit)) + { + $aValues = array_merge($aValues, $aCrit['values']); + } + } + + return array('values' => $aValues); + } +} + + +class FunctionExpression extends Expression +{ + protected $m_sVerb; + protected $m_aArgs; // array of expressions + + public function __construct($sVerb, $aArgExpressions) + { + $this->m_sVerb = $sVerb; + $this->m_aArgs = $aArgExpressions; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetVerb() + { + return $this->m_sVerb; + } + + public function GetArgs() + { + return $this->m_aArgs; + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aArgs as $iPos => $oExpr) + { + $aRes[] = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + } + return $this->m_sVerb.'('.implode(', ', $aRes).')'; + } + + public function Browse(Closure $callback) + { + $callback($this); + foreach ($this->m_aArgs as $iPos => $oExpr) + { + $oExpr->Browse($callback); + } + } + + public function ApplyParameters($aArgs) + { + foreach ($this->m_aArgs as $idx => $oExpr) + { + if ($oExpr instanceof VariableExpression) + { + $this->m_aArgs[$idx] = $oExpr->GetAsScalar($aArgs); + } + else + { + $oExpr->ApplyParameters($aArgs); + } + } + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + foreach ($this->m_aArgs as $oExpr) + { + $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); + } + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $aRes = array(); + foreach ($this->m_aArgs as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + return new FunctionExpression($this->m_sVerb, $aRes); + } + + public function ListRequiredFields() + { + $aRes = array(); + foreach ($this->m_aArgs as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); + } + return $aRes; + } + + public function CollectUsedParents(&$aTable) + { + foreach ($this->m_aArgs as $oExpr) + { + $oExpr->CollectUsedParents($aTable); + } + } + + public function ListConstantFields() + { + $aRes = array(); + foreach ($this->m_aArgs as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListConstantFields()); + } + return $aRes; + } + + public function RenameParam($sOldName, $sNewName) + { + foreach ($this->m_aArgs as $key => $oExpr) + { + $this->m_aArgs[$key] = $oExpr->RenameParam($sOldName, $sNewName); + } + } + + public function RenameAlias($sOldName, $sNewName) + { + foreach ($this->m_aArgs as $key => $oExpr) + { + $oExpr->RenameAlias($sOldName, $sNewName); + } + } + + public function GetAttDef($aClasses = array()) + { + foreach($this->m_aArgs as $oExpression) + { + $oAttDef = $oExpression->GetAttDef($aClasses); + if (!is_null($oAttDef)) return $oAttDef; + } + + return null; + } + + /** + * Make the most relevant label, given the value of the expression + * + * @param DBSearch oFilter The context in which this expression has been used + * @param string sValue The value returned by the query, for this expression + * @param string sDefault The default value if no relevant label could be computed + * + * @return string label + */ + public function MakeValueLabel($oFilter, $sValue, $sDefault) + { + static $aWeekDayToString = null; + if (is_null($aWeekDayToString)) + { + // Init the correspondance table + $aWeekDayToString = array( + 0 => Dict::S('DayOfWeek-Sunday'), + 1 => Dict::S('DayOfWeek-Monday'), + 2 => Dict::S('DayOfWeek-Tuesday'), + 3 => Dict::S('DayOfWeek-Wednesday'), + 4 => Dict::S('DayOfWeek-Thursday'), + 5 => Dict::S('DayOfWeek-Friday'), + 6 => Dict::S('DayOfWeek-Saturday') + ); + } + static $aMonthToString = null; + if (is_null($aMonthToString)) + { + // Init the correspondance table + $aMonthToString = array( + 1 => Dict::S('Month-01'), + 2 => Dict::S('Month-02'), + 3 => Dict::S('Month-03'), + 4 => Dict::S('Month-04'), + 5 => Dict::S('Month-05'), + 6 => Dict::S('Month-06'), + 7 => Dict::S('Month-07'), + 8 => Dict::S('Month-08'), + 9 => Dict::S('Month-09'), + 10 => Dict::S('Month-10'), + 11 => Dict::S('Month-11'), + 12 => Dict::S('Month-12'), + ); + } + + $sRes = $sDefault; + if (strtolower($this->m_sVerb) == 'date_format') + { + $oFormatExpr = $this->m_aArgs[1]; + if ($oFormatExpr->Render() == "'%w'") + { + if (isset($aWeekDayToString[(int)$sValue])) + { + $sRes = $aWeekDayToString[(int)$sValue]; + } + } + elseif ($oFormatExpr->Render() == "'%Y-%m'") + { + // yyyy-mm => "yyyy month" + $iMonth = (int) substr($sValue, -2); // the two last chars + $sRes = substr($sValue, 0, 4).' '.$aMonthToString[$iMonth]; + } + elseif ($oFormatExpr->Render() == "'%Y-%m-%d'") + { + // yyyy-mm-dd => "month d" + $iMonth = (int) substr($sValue, 5, 2); + $sRes = $aMonthToString[$iMonth].' '.(int)substr($sValue, -2); + } + elseif ($oFormatExpr->Render() == "'%H'") + { + // H => "H Hour(s)" + $sRes = $sValue.':00'; + } + } + return $sRes; + } + + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + $sOperation = ''; + $sVerb = ''; + switch ($this->m_sVerb) + { + case 'ISNULL': + case 'NOW': + $sVerb = $this->VerbToNaturalLanguage(); + break; + case 'DATE_SUB': + $sVerb = ' -'; + break; + case 'DATE_ADD': + $sVerb = ' +'; + break; + case 'DATE_FORMAT': + $aCtx['date_display'] = $this; + break; + default: + return $this->RenderExpression(false, $aArgs); + } + + foreach($this->m_aArgs as $oExpression) + { + if ($oExpression instanceof IntervalExpression) + { + $sOperation .= $sVerb; + $sVerb = ''; + } + $sOperation .= $oExpression->Display($oSearch, $aArgs, $oAttDef, $aCtx); + } + + if (!empty($sVerb)) + { + $sOperation .= $sVerb; + } + return '('.$sOperation.')'; + } + + private function VerbToNaturalLanguage() + { + return Dict::S('Expression:Verb:'.$this->m_sVerb, " {$this->m_sVerb} "); + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = array(); + switch ($this->m_sVerb) + { + case 'ISNULL': + $aCriteria['operator'] = $this->m_sVerb; + foreach($this->m_aArgs as $oExpression) + { + $aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria); + } + $aCriteria['has_undefined'] = true; + $aCriteria['oql'] = $this->RenderExpression(false, $aArgs, $bRetrofitParams); + break; + + case 'NOW': + $aCriteria = array('widget' => 'date_time'); + $aCriteria['is_relative'] = true; + $aCriteria['verb'] = $this->m_sVerb; + break; + + case 'DATE_ADD': + case 'DATE_SUB': + case 'DATE_FORMAT': + $aCriteria = array('widget' => 'date_time'); + foreach($this->m_aArgs as $oExpression) + { + $aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria); + } + $aCriteria['verb'] = $this->m_sVerb; + break; + + default: + return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + } + + return $aCriteria; + } +} + +class IntervalExpression extends Expression +{ + protected $m_oValue; // expression + protected $m_sUnit; + + public function __construct($oValue, $sUnit) + { + $this->m_oValue = $oValue; + $this->m_sUnit = $sUnit; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetValue() + { + return $this->m_oValue; + } + + public function GetUnit() + { + return $this->m_sUnit; + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + return 'INTERVAL '.$this->m_oValue->RenderExpression($bForSQL, $aArgs, $bRetrofitParams).' '.$this->m_sUnit; + } + + public function Browse(Closure $callback) + { + $callback($this); + $this->m_oValue->Browse($callback); + } + + public function ApplyParameters($aArgs) + { + if ($this->m_oValue instanceof VariableExpression) + { + $this->m_oValue = $this->m_oValue->GetAsScalar($aArgs); + } + else + { + $this->m_oValue->ApplyParameters($aArgs); + } + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + $this->m_oValue->GetUnresolvedFields($sAlias, $aUnresolved); + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + return new IntervalExpression($this->m_oValue->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved), $this->m_sUnit); + } + + public function ListRequiredFields() + { + return array(); + } + + public function CollectUsedParents(&$aTable) + { + } + + public function ListConstantFields() + { + return array(); + } + + public function RenameParam($sOldName, $sNewName) + { + $this->m_oValue->RenameParam($sOldName, $sNewName); + } + + public function RenameAlias($sOldName, $sNewName) + { + $this->m_oValue->RenameAlias($sOldName, $sNewName); + } + + public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null) + { + $aCriteria = $this->m_oValue->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef); + $aCriteria['unit'] = $this->m_sUnit; + + return $aCriteria; + } + + public function Display($oSearch, &$aArgs = null, $oAttDef = null, &$aCtx = array()) + { + return $this->m_oValue->RenderExpression(false, $aArgs).' '.Dict::S('Expression:Unit:Long:'.$this->m_sUnit, $this->m_sUnit); + } +} + +class CharConcatExpression extends Expression +{ + protected $m_aExpressions; + + public function __construct($aExpressions) + { + $this->m_aExpressions = $aExpressions; + } + + public function IsTrue() + { + // return true if we are certain that it will be true + return false; + } + + public function GetItems() + { + return $this->m_aExpressions; + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $sCol = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + // Concat will be globally NULL if one single argument is null ! + $aRes[] = "COALESCE($sCol, '')"; + } + return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)"; + } + + public function Browse(Closure $callback) + { + $callback($this); + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->Browse($callback); + } + } + + public function ApplyParameters($aArgs) + { + foreach ($this->m_aExpressions as $idx => $oExpr) + { + if ($oExpr instanceof VariableExpression) + { + $this->m_aExpressions[$idx] = $oExpr->GetAsScalar(); + } + else + { + $this->m_aExpressions->ApplyParameters($aArgs); + } + } + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); + } + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + return new CharConcatExpression($aRes); + } + + public function ListRequiredFields() + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListRequiredFields()); + } + return $aRes; + } + + public function CollectUsedParents(&$aTable) + { + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->CollectUsedParents($aTable); + } + } + + public function ListConstantFields() + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes = array_merge($aRes, $oExpr->ListConstantFields()); + } + return $aRes; + } + + public function RenameParam($sOldName, $sNewName) + { + foreach ($this->m_aExpressions as $key => $oExpr) + { + $this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName); + } + } + + public function RenameAlias($sOldName, $sNewName) + { + foreach ($this->m_aExpressions as $key => $oExpr) + { + $oExpr->RenameAlias($sOldName, $sNewName); + } + } +} + + +class CharConcatWSExpression extends CharConcatExpression +{ + protected $m_separator; + + public function __construct($separator, $aExpressions) + { + $this->m_separator = $separator; + parent::__construct($aExpressions); + } + + // recursive rendering + public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $sCol = $oExpr->RenderExpression($bForSQL, $aArgs, $bRetrofitParams); + // Concat will be globally NULL if one single argument is null ! + $aRes[] = "COALESCE($sCol, '')"; + } + $sSep = CMDBSource::Quote($this->m_separator); + return "CAST(CONCAT_WS($sSep, ".implode(', ', $aRes).") AS CHAR)"; + } + + public function Browse(Closure $callback) + { + $callback($this); + foreach ($this->m_aExpressions as $oExpr) + { + $oExpr->Browse($callback); + } + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $aRes = array(); + foreach ($this->m_aExpressions as $oExpr) + { + $aRes[] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + return new CharConcatWSExpression($this->m_separator, $aRes); + } +} + + +class QueryBuilderExpressions +{ + /** + * @var Expression + */ + protected $m_oConditionExpr; + /** + * @var Expression[] + */ + protected $m_aSelectExpr; + /** + * @var Expression[] + */ + protected $m_aGroupByExpr; + /** + * @var Expression[] + */ + protected $m_aJoinFields; + /** + * @var string[] + */ + protected $m_aClassIds; + + public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null, $aSelectExpr = null) + { + $this->m_oConditionExpr = $oSearch->GetCriteria(); + if (!$oSearch->GetShowObsoleteData()) + { + foreach ($oSearch->GetSelectedClasses() as $sAlias => $sClass) + { + if (MetaModel::IsObsoletable($sClass)) + { + $oNotObsolete = new BinaryExpression(new FieldExpression('obsolescence_flag', $sAlias), '=', new ScalarExpression(0)); + $this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oNotObsolete); + } + } + } + $this->m_aSelectExpr = is_null($aSelectExpr) ? array() : $aSelectExpr; + $this->m_aGroupByExpr = $aGroupByExpr; + $this->m_aJoinFields = array(); + + $this->m_aClassIds = array(); + foreach($oSearch->GetJoinedClasses() as $sClassAlias => $sClass) + { + $this->m_aClassIds[$sClassAlias] = new FieldExpression('id', $sClassAlias); + } + } + + public function GetSelect() + { + return $this->m_aSelectExpr; + } + + public function GetGroupBy() + { + return $this->m_aGroupByExpr; + } + + public function GetCondition() + { + return $this->m_oConditionExpr; + } + + /** + * @return Expression|mixed + */ + public function PopJoinField() + { + return array_pop($this->m_aJoinFields); + } + + /** + * @param string $sAttAlias + * @param Expression $oExpression + */ + public function AddSelect($sAttAlias, Expression $oExpression) + { + $this->m_aSelectExpr[$sAttAlias] = $oExpression; + } + + /** + * @param Expression $oExpression + */ + public function AddCondition(Expression $oExpression) + { + $this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oExpression); + } + + /** + * @param Expression $oExpression + */ + public function PushJoinField(Expression $oExpression) + { + array_push($this->m_aJoinFields, $oExpression); + } + + /** + * Get tables representing the queried objects + * Could be further optimized: when the first join is an outer join, then the rest can be omitted + * @param array $aTables + * @return array + */ + public function GetMandatoryTables(&$aTables = null) + { + if (is_null($aTables)) $aTables = array(); + + foreach($this->m_aClassIds as $sClass => $oExpression) + { + $oExpression->CollectUsedParents($aTables); + } + + return $aTables; + } + + public function GetUnresolvedFields($sAlias, &$aUnresolved) + { + $this->m_oConditionExpr->GetUnresolvedFields($sAlias, $aUnresolved); + foreach($this->m_aSelectExpr as $sColAlias => $oExpr) + { + $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); + } + if ($this->m_aGroupByExpr) + { + foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) + { + $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); + } + } + foreach($this->m_aJoinFields as $oExpression) + { + $oExpression->GetUnresolvedFields($sAlias, $aUnresolved); + } + } + + public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true) + { + $this->m_oConditionExpr = $this->m_oConditionExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + foreach($this->m_aSelectExpr as $sColAlias => $oExpr) + { + $this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + if ($this->m_aGroupByExpr) + { + foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) + { + $this->m_aGroupByExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + } + foreach($this->m_aJoinFields as $index => $oExpression) + { + $this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + + foreach($this->m_aClassIds as $sClass => $oExpression) + { + $this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + } + + public function RenameParam($sOldName, $sNewName) + { + $this->m_oConditionExpr->RenameParam($sOldName, $sNewName); + foreach($this->m_aSelectExpr as $sColAlias => $oExpr) + { + $this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName); + } + if ($this->m_aGroupByExpr) + { + foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) + { + $this->m_aGroupByExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName); + } + } + foreach($this->m_aJoinFields as $index => $oExpression) + { + $this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName); + } + } } \ No newline at end of file diff --git a/core/oql/oql-lexer.plex b/core/oql/oql-lexer.plex index fc0101b42..ebd811a2f 100644 --- a/core/oql/oql-lexer.plex +++ b/core/oql/oql-lexer.plex @@ -1,444 +1,444 @@ - - - -/** - * OQL syntax analyzer, to be used prior to run the lexical analyzer - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -// Notes (from the source file: oql-lexer.plex) - Romain -// -// The strval rule is a little bit cryptic. -// This is due to both a bug in the lexer generator and the complexity of our need -// The rule means: either a quoted string with ", or a quoted string with ' -// literal " (resp. ') must be escaped by a \ -// \ must be escaped by an additional \ -// -// Here are the issues and limitation found in the lexer generator: -// * Matching simple quotes is an issue, because regexp are not correctly escaped (and the ESC code is escaped itself) -// Workaround: insert '.chr(39).' which will be a real ' in the end -// * Matching an alternate regexp is an issue because you must specify "|^...." -// and the regexp parser will not accept that syntax -// Workaround: insert '.chr(94).' which will be a real ^ -// -// Let's analyze an overview of the regexp, we have -// 1) The strval rule in the lexer definition -// /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/ -// 2) Becomes the php expression in the lexer -// (note the escaped double quotes, hopefully having no effect, but showing where the issue is!) -// $myRegexp = '/^\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/'; -// -// To be fixed in LexerGenerator/Parser.y, in doLongestMatch (doFirstMatch is ok) -// -// -// Now, let's explain how the regexp has been designed. -// Here is a simplified version, dealing with simple quotes, and based on the assumption that the lexer generator has been fixed! -// The strval rule in the lexer definition -// /'([^\\']*(\\')*(\\\\)*)*'/ -// This means anything containing \\ or \' or any other char but a standalone ' or \ -// This means ' or \ could not be found without a preceding \ -// -class OQLLexerRaw -{ - protected $data; // input string - public $token; // token id - public $value; // token string representation - protected $line; // current line - protected $count; // current column - - function __construct($data) - { - $this->data = $data; - $this->count = 0; - $this->line = 1; - } - -/*!lex2php -%input $this->data -%counter $this->count -%token $this->token -%value $this->value -%line $this->line -%matchlongest 1 -whitespace = /[ \t\n\r]+/ -union = "UNION" -select = "SELECT" -from = "FROM" -as_alias = "AS" -where = "WHERE" -join = "JOIN" -on = "ON" -coma = "," -par_open = "(" -par_close = ")" -math_div = "/" -math_mult = "*" -math_plus = "+" -math_minus = "-" -log_and = "AND" -log_or = "OR" -bitwise_and = "&" -bitwise_or = "|" -bitwise_xor = "^" -bitwise_leftshift = "<<" -bitwise_rightshift = ">>" -regexp = "REGEXP" -eq = "=" -not_eq = "!=" -gt = ">" -lt = "<" -ge = ">=" -le = "<=" -like = "LIKE" -not_like = "NOT LIKE" -in = "IN" -not_in = "NOT IN" -interval = "INTERVAL" -f_if = "IF" -f_elt = "ELT" -f_coalesce = "COALESCE" -f_isnull = "ISNULL" -f_concat = "CONCAT" -f_substr = "SUBSTR" -f_trim = "TRIM" -f_date = "DATE" -f_date_format = "DATE_FORMAT" -f_current_date = "CURRENT_DATE" -f_now = "NOW" -f_time = "TIME" -f_to_days = "TO_DAYS" -f_from_days = "FROM_DAYS" -f_year = "YEAR" -f_month = "MONTH" -f_day = "DAY" -f_hour = "HOUR" -f_minute = "MINUTE" -f_second = "SECOND" -f_date_add = "DATE_ADD" -f_date_sub = "DATE_SUB" -f_round = "ROUND" -f_floor = "FLOOR" -f_inet_aton = "INET_ATON" -f_inet_ntoa = "INET_NTOA" -below = "BELOW" -below_strict = "BELOW STRICT" -not_below = "NOT BELOW" -not_below_strict = "NOT BELOW STRICT" -above = "ABOVE" -above_strict = "ABOVE STRICT" -not_above = "NOT ABOVE" -not_above_strict = "NOT ABOVE STRICT" -// -// WARNING: there seems to be a bug in the Lexer about matching the longest pattern -// when there are alternates in the regexp. -// -// For instance: -// numval = /[0-9]+|0x[0-9a-fA-F]+/ -// Does not work: SELECT Toto WHERE name = 'Text0xCTest' => Fails because 0xC is recongnized as a numval (inside the string) instead of a strval !! -// -// Inserting a ^ after the alternate (see comment at the top of this file) does not work either -// numval = /[0-9]+|'.chr(94).'0x[0-9a-fA-F]+/ -// SELECT Toto WHERE name = 'Text0xCTest' => works but -// SELECT Toto WHERE id = 0xC => does not work, 'xC' is found as a name (apparently 0 is recognized as a numval and the remaining is a name !) -// -// numval = /([0-9]+|0x[0-9a-fA-F]+)/ -// Does not work either, the hexadecimal numbers are not matched properly -// Anyhow let's distinguish the hexadecimal values from decimal integers, hex numbers will be stored as strings -// and passed as-is to MySQL which enables us to pass 64-bit values without messing with them in PHP -// -hexval = /(0x[0-9a-fA-F]+)/ -numval = /([0-9]+)/ -strval = /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/ -name = /([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/ -varname = /:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/ -dot = "." -*/ - -/*!lex2php -whitespace { - return false; -} -union { - $this->token = OQLParser::UNION; -} -select { - $this->token = OQLParser::SELECT; -} -from { - $this->token = OQLParser::FROM; -} -as_alias { - $this->token = OQLParser::AS_ALIAS; -} -where { - $this->token = OQLParser::WHERE; -} -join { - $this->token = OQLParser::JOIN; -} -on { - $this->token = OQLParser::ON; -} -math_div { - $this->token = OQLParser::MATH_DIV; -} -math_mult { - $this->token = OQLParser::MATH_MULT; -} -math_plus { - $this->token = OQLParser::MATH_PLUS; -} -math_minus { - $this->token = OQLParser::MATH_MINUS; -} -log_and { - $this->token = OQLParser::LOG_AND; -} -log_or { - $this->token = OQLParser::LOG_OR; -} -bitwise_or { - $this->token = OQLParser::BITWISE_OR; -} -bitwise_and { - $this->token = OQLParser::BITWISE_AND; -} -bitwise_xor { - $this->token = OQLParser::BITWISE_XOR; -} -bitwise_leftshift { - $this->token = OQLParser::BITWISE_LEFT_SHIFT; -} -bitwise_rightshift { - $this->token = OQLParser::BITWISE_RIGHT_SHIFT; -} -coma { - $this->token = OQLParser::COMA; -} -par_open { - $this->token = OQLParser::PAR_OPEN; -} -par_close { - $this->token = OQLParser::PAR_CLOSE; -} -regexp { - $this->token = OQLParser::REGEXP; -} -eq { - $this->token = OQLParser::EQ; -} -not_eq { - $this->token = OQLParser::NOT_EQ; -} -gt { - $this->token = OQLParser::GT; -} -lt { - $this->token = OQLParser::LT; -} -ge { - $this->token = OQLParser::GE; -} -le { - $this->token = OQLParser::LE; -} -like { - $this->token = OQLParser::LIKE; -} -not_like { - $this->token = OQLParser::NOT_LIKE; -} -in { - $this->token = OQLParser::IN; -} -not_in { - $this->token = OQLParser::NOT_IN; -} -interval { - $this->token = OQLParser::INTERVAL; -} -f_if { - $this->token = OQLParser::F_IF; -} -f_elt { - $this->token = OQLParser::F_ELT; -} -f_coalesce { - $this->token = OQLParser::F_COALESCE; -} -f_isnull { - $this->token = OQLParser::F_ISNULL; -} -f_concat { - $this->token = OQLParser::F_CONCAT; -} -f_substr { - $this->token = OQLParser::F_SUBSTR; -} -f_trim { - $this->token = OQLParser::F_TRIM; -} -f_date { - $this->token = OQLParser::F_DATE; -} -f_date_format { - $this->token = OQLParser::F_DATE_FORMAT; -} -f_current_date { - $this->token = OQLParser::F_CURRENT_DATE; -} -f_now { - $this->token = OQLParser::F_NOW; -} -f_time { - $this->token = OQLParser::F_TIME; -} -f_to_days { - $this->token = OQLParser::F_TO_DAYS; -} -f_from_days { - $this->token = OQLParser::F_FROM_DAYS; -} -f_year { - $this->token = OQLParser::F_YEAR; -} -f_month { - $this->token = OQLParser::F_MONTH; -} -f_day { - $this->token = OQLParser::F_DAY; -} -f_hour { - $this->token = OQLParser::F_HOUR; -} -f_minute { - $this->token = OQLParser::F_MINUTE; -} -f_second { - $this->token = OQLParser::F_SECOND; -} -f_date_add { - $this->token = OQLParser::F_DATE_ADD; -} -f_date_sub { - $this->token = OQLParser::F_DATE_SUB; -} -f_round { - $this->token = OQLParser::F_ROUND; -} -f_floor { - $this->token = OQLParser::F_FLOOR; -} -f_inet_aton { - $this->token = OQLParser::F_INET_ATON; -} -f_inet_ntoa { - $this->token = OQLParser::F_INET_NTOA; -} -below { - $this->token = OQLParser::BELOW; -} -below_strict { - $this->token = OQLParser::BELOW_STRICT; -} -not_below { - $this->token = OQLParser::NOT_BELOW; -} -not_below_strict { - $this->token = OQLParser::NOT_BELOW_STRICT; -} -above { - $this->token = OQLParser::ABOVE; -} -above_strict { - $this->token = OQLParser::ABOVE_STRICT; -} -not_above { - $this->token = OQLParser::NOT_ABOVE; -} -not_above_strict { - $this->token = OQLParser::NOT_ABOVE_STRICT; -} -hexval { - $this->token = OQLParser::HEXVAL; -} -numval { - $this->token = OQLParser::NUMVAL; -} -strval { - $this->token = OQLParser::STRVAL; -} -name { - $this->token = OQLParser::NAME; -} -varname { - $this->token = OQLParser::VARNAME; -} -dot { - $this->token = OQLParser::DOT; -} -*/ - -} - -define('UNEXPECTED_INPUT_AT_LINE', 'Unexpected input at line'); - -class OQLLexerException extends OQLException -{ - public function __construct($sInput, $iLine, $iCol, $sUnexpected) - { - parent::__construct("Syntax error", $sInput, $iLine, $iCol, $sUnexpected); - } -} - -class OQLLexer extends OQLLexerRaw -{ - public function getTokenPos() - { - return max(0, $this->count - strlen($this->value)); - } - - function yylex() - { - try - { - return parent::yylex(); - } - catch (Exception $e) - { - $sMessage = $e->getMessage(); - if (substr($sMessage, 0, strlen(UNEXPECTED_INPUT_AT_LINE)) == UNEXPECTED_INPUT_AT_LINE) - { - $sLineAndChar = substr($sMessage, strlen(UNEXPECTED_INPUT_AT_LINE)); - if (preg_match('#^([0-9]+): (.+)$#', $sLineAndChar, $aMatches)) - { - $iLine = $aMatches[1]; - $sUnexpected = $aMatches[2]; - throw new OQLLexerException($this->data, $iLine, $this->count, $sUnexpected); - } - } - // Default: forward the exception - throw $e; - } - } -} -?> + + + +/** + * OQL syntax analyzer, to be used prior to run the lexical analyzer + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +// Notes (from the source file: oql-lexer.plex) - Romain +// +// The strval rule is a little bit cryptic. +// This is due to both a bug in the lexer generator and the complexity of our need +// The rule means: either a quoted string with ", or a quoted string with ' +// literal " (resp. ') must be escaped by a \ +// \ must be escaped by an additional \ +// +// Here are the issues and limitation found in the lexer generator: +// * Matching simple quotes is an issue, because regexp are not correctly escaped (and the ESC code is escaped itself) +// Workaround: insert '.chr(39).' which will be a real ' in the end +// * Matching an alternate regexp is an issue because you must specify "|^...." +// and the regexp parser will not accept that syntax +// Workaround: insert '.chr(94).' which will be a real ^ +// +// Let's analyze an overview of the regexp, we have +// 1) The strval rule in the lexer definition +// /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/ +// 2) Becomes the php expression in the lexer +// (note the escaped double quotes, hopefully having no effect, but showing where the issue is!) +// $myRegexp = '/^\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/'; +// +// To be fixed in LexerGenerator/Parser.y, in doLongestMatch (doFirstMatch is ok) +// +// +// Now, let's explain how the regexp has been designed. +// Here is a simplified version, dealing with simple quotes, and based on the assumption that the lexer generator has been fixed! +// The strval rule in the lexer definition +// /'([^\\']*(\\')*(\\\\)*)*'/ +// This means anything containing \\ or \' or any other char but a standalone ' or \ +// This means ' or \ could not be found without a preceding \ +// +class OQLLexerRaw +{ + protected $data; // input string + public $token; // token id + public $value; // token string representation + protected $line; // current line + protected $count; // current column + + function __construct($data) + { + $this->data = $data; + $this->count = 0; + $this->line = 1; + } + +/*!lex2php +%input $this->data +%counter $this->count +%token $this->token +%value $this->value +%line $this->line +%matchlongest 1 +whitespace = /[ \t\n\r]+/ +union = "UNION" +select = "SELECT" +from = "FROM" +as_alias = "AS" +where = "WHERE" +join = "JOIN" +on = "ON" +coma = "," +par_open = "(" +par_close = ")" +math_div = "/" +math_mult = "*" +math_plus = "+" +math_minus = "-" +log_and = "AND" +log_or = "OR" +bitwise_and = "&" +bitwise_or = "|" +bitwise_xor = "^" +bitwise_leftshift = "<<" +bitwise_rightshift = ">>" +regexp = "REGEXP" +eq = "=" +not_eq = "!=" +gt = ">" +lt = "<" +ge = ">=" +le = "<=" +like = "LIKE" +not_like = "NOT LIKE" +in = "IN" +not_in = "NOT IN" +interval = "INTERVAL" +f_if = "IF" +f_elt = "ELT" +f_coalesce = "COALESCE" +f_isnull = "ISNULL" +f_concat = "CONCAT" +f_substr = "SUBSTR" +f_trim = "TRIM" +f_date = "DATE" +f_date_format = "DATE_FORMAT" +f_current_date = "CURRENT_DATE" +f_now = "NOW" +f_time = "TIME" +f_to_days = "TO_DAYS" +f_from_days = "FROM_DAYS" +f_year = "YEAR" +f_month = "MONTH" +f_day = "DAY" +f_hour = "HOUR" +f_minute = "MINUTE" +f_second = "SECOND" +f_date_add = "DATE_ADD" +f_date_sub = "DATE_SUB" +f_round = "ROUND" +f_floor = "FLOOR" +f_inet_aton = "INET_ATON" +f_inet_ntoa = "INET_NTOA" +below = "BELOW" +below_strict = "BELOW STRICT" +not_below = "NOT BELOW" +not_below_strict = "NOT BELOW STRICT" +above = "ABOVE" +above_strict = "ABOVE STRICT" +not_above = "NOT ABOVE" +not_above_strict = "NOT ABOVE STRICT" +// +// WARNING: there seems to be a bug in the Lexer about matching the longest pattern +// when there are alternates in the regexp. +// +// For instance: +// numval = /[0-9]+|0x[0-9a-fA-F]+/ +// Does not work: SELECT Toto WHERE name = 'Text0xCTest' => Fails because 0xC is recongnized as a numval (inside the string) instead of a strval !! +// +// Inserting a ^ after the alternate (see comment at the top of this file) does not work either +// numval = /[0-9]+|'.chr(94).'0x[0-9a-fA-F]+/ +// SELECT Toto WHERE name = 'Text0xCTest' => works but +// SELECT Toto WHERE id = 0xC => does not work, 'xC' is found as a name (apparently 0 is recognized as a numval and the remaining is a name !) +// +// numval = /([0-9]+|0x[0-9a-fA-F]+)/ +// Does not work either, the hexadecimal numbers are not matched properly +// Anyhow let's distinguish the hexadecimal values from decimal integers, hex numbers will be stored as strings +// and passed as-is to MySQL which enables us to pass 64-bit values without messing with them in PHP +// +hexval = /(0x[0-9a-fA-F]+)/ +numval = /([0-9]+)/ +strval = /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/ +name = /([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/ +varname = /:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/ +dot = "." +*/ + +/*!lex2php +whitespace { + return false; +} +union { + $this->token = OQLParser::UNION; +} +select { + $this->token = OQLParser::SELECT; +} +from { + $this->token = OQLParser::FROM; +} +as_alias { + $this->token = OQLParser::AS_ALIAS; +} +where { + $this->token = OQLParser::WHERE; +} +join { + $this->token = OQLParser::JOIN; +} +on { + $this->token = OQLParser::ON; +} +math_div { + $this->token = OQLParser::MATH_DIV; +} +math_mult { + $this->token = OQLParser::MATH_MULT; +} +math_plus { + $this->token = OQLParser::MATH_PLUS; +} +math_minus { + $this->token = OQLParser::MATH_MINUS; +} +log_and { + $this->token = OQLParser::LOG_AND; +} +log_or { + $this->token = OQLParser::LOG_OR; +} +bitwise_or { + $this->token = OQLParser::BITWISE_OR; +} +bitwise_and { + $this->token = OQLParser::BITWISE_AND; +} +bitwise_xor { + $this->token = OQLParser::BITWISE_XOR; +} +bitwise_leftshift { + $this->token = OQLParser::BITWISE_LEFT_SHIFT; +} +bitwise_rightshift { + $this->token = OQLParser::BITWISE_RIGHT_SHIFT; +} +coma { + $this->token = OQLParser::COMA; +} +par_open { + $this->token = OQLParser::PAR_OPEN; +} +par_close { + $this->token = OQLParser::PAR_CLOSE; +} +regexp { + $this->token = OQLParser::REGEXP; +} +eq { + $this->token = OQLParser::EQ; +} +not_eq { + $this->token = OQLParser::NOT_EQ; +} +gt { + $this->token = OQLParser::GT; +} +lt { + $this->token = OQLParser::LT; +} +ge { + $this->token = OQLParser::GE; +} +le { + $this->token = OQLParser::LE; +} +like { + $this->token = OQLParser::LIKE; +} +not_like { + $this->token = OQLParser::NOT_LIKE; +} +in { + $this->token = OQLParser::IN; +} +not_in { + $this->token = OQLParser::NOT_IN; +} +interval { + $this->token = OQLParser::INTERVAL; +} +f_if { + $this->token = OQLParser::F_IF; +} +f_elt { + $this->token = OQLParser::F_ELT; +} +f_coalesce { + $this->token = OQLParser::F_COALESCE; +} +f_isnull { + $this->token = OQLParser::F_ISNULL; +} +f_concat { + $this->token = OQLParser::F_CONCAT; +} +f_substr { + $this->token = OQLParser::F_SUBSTR; +} +f_trim { + $this->token = OQLParser::F_TRIM; +} +f_date { + $this->token = OQLParser::F_DATE; +} +f_date_format { + $this->token = OQLParser::F_DATE_FORMAT; +} +f_current_date { + $this->token = OQLParser::F_CURRENT_DATE; +} +f_now { + $this->token = OQLParser::F_NOW; +} +f_time { + $this->token = OQLParser::F_TIME; +} +f_to_days { + $this->token = OQLParser::F_TO_DAYS; +} +f_from_days { + $this->token = OQLParser::F_FROM_DAYS; +} +f_year { + $this->token = OQLParser::F_YEAR; +} +f_month { + $this->token = OQLParser::F_MONTH; +} +f_day { + $this->token = OQLParser::F_DAY; +} +f_hour { + $this->token = OQLParser::F_HOUR; +} +f_minute { + $this->token = OQLParser::F_MINUTE; +} +f_second { + $this->token = OQLParser::F_SECOND; +} +f_date_add { + $this->token = OQLParser::F_DATE_ADD; +} +f_date_sub { + $this->token = OQLParser::F_DATE_SUB; +} +f_round { + $this->token = OQLParser::F_ROUND; +} +f_floor { + $this->token = OQLParser::F_FLOOR; +} +f_inet_aton { + $this->token = OQLParser::F_INET_ATON; +} +f_inet_ntoa { + $this->token = OQLParser::F_INET_NTOA; +} +below { + $this->token = OQLParser::BELOW; +} +below_strict { + $this->token = OQLParser::BELOW_STRICT; +} +not_below { + $this->token = OQLParser::NOT_BELOW; +} +not_below_strict { + $this->token = OQLParser::NOT_BELOW_STRICT; +} +above { + $this->token = OQLParser::ABOVE; +} +above_strict { + $this->token = OQLParser::ABOVE_STRICT; +} +not_above { + $this->token = OQLParser::NOT_ABOVE; +} +not_above_strict { + $this->token = OQLParser::NOT_ABOVE_STRICT; +} +hexval { + $this->token = OQLParser::HEXVAL; +} +numval { + $this->token = OQLParser::NUMVAL; +} +strval { + $this->token = OQLParser::STRVAL; +} +name { + $this->token = OQLParser::NAME; +} +varname { + $this->token = OQLParser::VARNAME; +} +dot { + $this->token = OQLParser::DOT; +} +*/ + +} + +define('UNEXPECTED_INPUT_AT_LINE', 'Unexpected input at line'); + +class OQLLexerException extends OQLException +{ + public function __construct($sInput, $iLine, $iCol, $sUnexpected) + { + parent::__construct("Syntax error", $sInput, $iLine, $iCol, $sUnexpected); + } +} + +class OQLLexer extends OQLLexerRaw +{ + public function getTokenPos() + { + return max(0, $this->count - strlen($this->value)); + } + + function yylex() + { + try + { + return parent::yylex(); + } + catch (Exception $e) + { + $sMessage = $e->getMessage(); + if (substr($sMessage, 0, strlen(UNEXPECTED_INPUT_AT_LINE)) == UNEXPECTED_INPUT_AT_LINE) + { + $sLineAndChar = substr($sMessage, strlen(UNEXPECTED_INPUT_AT_LINE)); + if (preg_match('#^([0-9]+): (.+)$#', $sLineAndChar, $aMatches)) + { + $iLine = $aMatches[1]; + $sUnexpected = $aMatches[2]; + throw new OQLLexerException($this->data, $iLine, $this->count, $sUnexpected); + } + } + // Default: forward the exception + throw $e; + } + } +} +?> diff --git a/core/oql/oql-parser.y b/core/oql/oql-parser.y index 03bb3fe52..79dfb3139 100644 --- a/core/oql/oql-parser.y +++ b/core/oql/oql-parser.y @@ -1,303 +1,303 @@ - -/* - -This is a LALR(1) grammar -(seek for Lemon grammar to get some documentation from the Net) -That doc was helpful: http://www.hwaci.com/sw/lemon/lemon.html - -To handle operators precedence we could have used the %left directive -(we took another option, because that one was discovered right after... -which option is the best for us?) -Example: -%left LOG_AND. -%left LOG_OR. -%nonassoc EQ NE GT GE LT LE. -%left PLUS MINUS. -%left TIMES DIVIDE MOD. -%right EXP NOT. - -TODO : solve the 2 remaining shift-reduce conflicts (JOIN) - -*/ - -%name OQLParser_ -%declare_class {class OQLParserRaw} -%syntax_error { -throw new OQLParserException($this->m_sSourceQuery, $this->m_iLine, $this->m_iCol, $this->tokenName($yymajor), $TOKEN); -} - -result ::= union(X). { $this->my_result = X; } -result ::= query(X). { $this->my_result = X; } -result ::= condition(X). { $this->my_result = X; } - -union(A) ::= query(X) UNION query(Y). { - A = new OqlUnionQuery(X, Y); -} -union(A) ::= query(X) UNION union(Y). { - A = new OqlUnionQuery(X, Y); -} - -query(A) ::= SELECT class_name(X) join_statement(J) where_statement(W). { - A = new OqlObjectQuery(X, X, W, J, array(X)); -} -query(A) ::= SELECT class_name(X) AS_ALIAS class_name(Y) join_statement(J) where_statement(W). { - A = new OqlObjectQuery(X, Y, W, J, array(Y)); -} - -query(A) ::= SELECT class_list(E) FROM class_name(X) join_statement(J) where_statement(W). { - A = new OqlObjectQuery(X, X, W, J, E); -} -query(A) ::= SELECT class_list(E) FROM class_name(X) AS_ALIAS class_name(Y) join_statement(J) where_statement(W). { - A = new OqlObjectQuery(X, Y, W, J, E); -} - - -class_list(A) ::= class_name(X). { - A = array(X); -} -class_list(A) ::= class_list(L) COMA class_name(X). { - array_push(L, X); - A = L; -} - -where_statement(A) ::= WHERE condition(C). { A = C;} -where_statement(A) ::= . { A = null;} - -join_statement(A) ::= join_item(J) join_statement(S). { - // insert the join statement on top of the existing list - array_unshift(S, J); - // and return the updated array - A = S; -} -join_statement(A) ::= join_item(J). { - A = Array(J); -} -join_statement(A) ::= . { A = null;} - -join_item(A) ::= JOIN class_name(X) AS_ALIAS class_name(Y) ON join_condition(C). -{ - // create an array with one single item - A = new OqlJoinSpec(X, Y, C); -} -join_item(A) ::= JOIN class_name(X) ON join_condition(C). -{ - // create an array with one single item - A = new OqlJoinSpec(X, X, C); -} - -join_condition(A) ::= field_id(X) EQ field_id(Y). { A = new BinaryOqlExpression(X, '=', Y); } -join_condition(A) ::= field_id(X) BELOW field_id(Y). { A = new BinaryOqlExpression(X, 'BELOW', Y); } -join_condition(A) ::= field_id(X) BELOW_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'BELOW_STRICT', Y); } -join_condition(A) ::= field_id(X) NOT_BELOW field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_BELOW', Y); } -join_condition(A) ::= field_id(X) NOT_BELOW_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_BELOW_STRICT', Y); } -join_condition(A) ::= field_id(X) ABOVE field_id(Y). { A = new BinaryOqlExpression(X, 'ABOVE', Y); } -join_condition(A) ::= field_id(X) ABOVE_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'ABOVE_STRICT', Y); } -join_condition(A) ::= field_id(X) NOT_ABOVE field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_ABOVE', Y); } -join_condition(A) ::= field_id(X) NOT_ABOVE_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_ABOVE_STRICT', Y); } - -condition(A) ::= expression_prio4(X). { A = X; } - -expression_basic(A) ::= scalar(X). { A = X; } -expression_basic(A) ::= field_id(X). { A = X; } -expression_basic(A) ::= var_name(X). { A = X; } -expression_basic(A) ::= func_name(X) PAR_OPEN arg_list(Y) PAR_CLOSE. { A = new FunctionOqlExpression(X, Y); } -expression_basic(A) ::= PAR_OPEN expression_prio4(X) PAR_CLOSE. { A = X; } -expression_basic(A) ::= expression_basic(X) list_operator(Y) list(Z). { A = new BinaryOqlExpression(X, Y, Z); } - -expression_prio1(A) ::= expression_basic(X). { A = X; } -expression_prio1(A) ::= expression_prio1(X) operator1(Y) expression_basic(Z). { A = new BinaryOqlExpression(X, Y, Z); } - -expression_prio2(A) ::= expression_prio1(X). { A = X; } -expression_prio2(A) ::= expression_prio2(X) operator2(Y) expression_prio1(Z). { A = new BinaryOqlExpression(X, Y, Z); } - -expression_prio3(A) ::= expression_prio2(X). { A = X; } -expression_prio3(A) ::= expression_prio3(X) operator3(Y) expression_prio2(Z). { A = new BinaryOqlExpression(X, Y, Z); } - -expression_prio4(A) ::= expression_prio3(X). { A = X; } -expression_prio4(A) ::= expression_prio4(X) operator4(Y) expression_prio3(Z). { A = new BinaryOqlExpression(X, Y, Z); } - - -list(A) ::= PAR_OPEN list_items(X) PAR_CLOSE. { - A = new ListOqlExpression(X); -} -list_items(A) ::= expression_prio4(X). { - A = array(X); -} -list_items(A) ::= list_items(L) COMA expression_prio4(X). { - array_push(L, X); - A = L; -} - -arg_list(A) ::= . { - A = array(); -} -arg_list(A) ::= argument(X). { - A = array(X); -} -arg_list(A) ::= arg_list(L) COMA argument(X). { - array_push(L, X); - A = L; -} -argument(A) ::= expression_prio4(X). { A = X; } -argument(A) ::= INTERVAL expression_prio4(X) interval_unit(Y). { A = new IntervalOqlExpression(X, Y); } - -interval_unit(A) ::= F_SECOND(X). { A = X; } -interval_unit(A) ::= F_MINUTE(X). { A = X; } -interval_unit(A) ::= F_HOUR(X). { A = X; } -interval_unit(A) ::= F_DAY(X). { A = X; } -interval_unit(A) ::= F_MONTH(X). { A = X; } -interval_unit(A) ::= F_YEAR(X). { A = X; } - -scalar(A) ::= num_scalar(X). { A = X; } -scalar(A) ::= str_scalar(X). { A = X; } - -num_scalar(A) ::= num_value(X). { A = new ScalarOqlExpression(X); } -str_scalar(A) ::= str_value(X). { A = new ScalarOqlExpression(X); } - -field_id(A) ::= name(X). { A = new FieldOqlExpression(X); } -field_id(A) ::= class_name(X) DOT name(Y). { A = new FieldOqlExpression(Y, X); } -class_name(A) ::= name(X). { A=X; } - - -var_name(A) ::= VARNAME(X). { A = new VariableOqlExpression(substr(X, 1)); } - -name(A) ::= NAME(X). { - if (X[0] == '`') - { - $name = substr(X, 1, strlen(X) - 2); - } - else - { - $name = X; - } - A = new OqlName($name, $this->m_iColPrev); -} -num_value(A) ::= NUMVAL(X). {A=(int)X;} -num_value(A) ::= MATH_MINUS NUMVAL(X). {A=(int)-X;} -num_value(A) ::= HEXVAL(X). {A=new OqlHexValue(X);} -str_value(A) ::= STRVAL(X). {A=stripslashes(substr(X, 1, strlen(X) - 2));} - - -operator1(A) ::= num_operator1(X). {A=X;} -operator1(A) ::= bitwise_operator1(X). {A=X;} -operator2(A) ::= num_operator2(X). {A=X;} -operator2(A) ::= str_operator(X). {A=X;} -operator2(A) ::= REGEXP(X). {A=X;} -operator2(A) ::= EQ(X). {A=X;} -operator2(A) ::= NOT_EQ(X). {A=X;} -operator3(A) ::= LOG_AND(X). {A=X;} -operator3(A) ::= bitwise_operator3(X). {A=X;} -operator4(A) ::= LOG_OR(X). {A=X;} -operator4(A) ::= bitwise_operator4(X). {A=X;} - -num_operator1(A) ::= MATH_DIV(X). {A=X;} -num_operator1(A) ::= MATH_MULT(X). {A=X;} -num_operator2(A) ::= MATH_PLUS(X). {A=X;} -num_operator2(A) ::= MATH_MINUS(X). {A=X;} -num_operator2(A) ::= GT(X). {A=X;} -num_operator2(A) ::= LT(X). {A=X;} -num_operator2(A) ::= GE(X). {A=X;} -num_operator2(A) ::= LE(X). {A=X;} - -str_operator(A) ::= LIKE(X). {A=X;} -str_operator(A) ::= NOT_LIKE(X). {A=X;} - -bitwise_operator1(A) ::= BITWISE_LEFT_SHIFT(X). {A=X;} -bitwise_operator1(A) ::= BITWISE_RIGHT_SHIFT(X). {A=X;} -bitwise_operator3(A) ::= BITWISE_AND(X). {A=X;} -bitwise_operator4(A) ::= BITWISE_OR(X). {A=X;} -bitwise_operator4(A) ::= BITWISE_XOR(X). {A=X;} - -list_operator(A) ::= IN(X). {A=X;} -list_operator(A) ::= NOT_IN(X). {A=X;} - -func_name(A) ::= F_IF(X). { A=X; } -func_name(A) ::= F_ELT(X). { A=X; } -func_name(A) ::= F_COALESCE(X). { A=X; } -func_name(A) ::= F_ISNULL(X). { A=X; } -func_name(A) ::= F_CONCAT(X). { A=X; } -func_name(A) ::= F_SUBSTR(X). { A=X; } -func_name(A) ::= F_TRIM(X). { A=X; } -func_name(A) ::= F_DATE(X). { A=X; } -func_name(A) ::= F_DATE_FORMAT(X). { A=X; } -func_name(A) ::= F_CURRENT_DATE(X). { A=X; } -func_name(A) ::= F_NOW(X). { A=X; } -func_name(A) ::= F_TIME(X). { A=X; } -func_name(A) ::= F_TO_DAYS(X). { A=X; } -func_name(A) ::= F_FROM_DAYS(X). { A=X; } -func_name(A) ::= F_YEAR(X). { A=X; } -func_name(A) ::= F_MONTH(X). { A=X; } -func_name(A) ::= F_DAY(X). { A=X; } -func_name(A) ::= F_DATE_ADD(X). { A=X; } -func_name(A) ::= F_DATE_SUB(X). { A=X; } -func_name(A) ::= F_ROUND(X). { A=X; } -func_name(A) ::= F_FLOOR(X). { A=X; } -func_name(A) ::= F_INET_ATON(X). { A=X; } -func_name(A) ::= F_INET_NTOA(X). { A=X; } - - -%code { - -class OQLParserException extends OQLException -{ - public function __construct($sInput, $iLine, $iCol, $sTokenName, $sTokenValue) - { - $sIssue = "Unexpected token $sTokenName"; - - parent::__construct($sIssue, $sInput, $iLine, $iCol, $sTokenValue); - } -} - -class OQLParser extends OQLParserRaw -{ - // dirty, but working for us (no other mean to get the final result :-( - protected $my_result; - - public function GetResult() - { - return $this->my_result; - } - - // More info on the source query and the current position while parsing it - // Data used when an exception is raised - protected $m_iLine; // still not used - protected $m_iCol; - protected $m_iColPrev; // this is the interesting one, because the parser will reduce on the next token - protected $m_sSourceQuery; - - public function __construct($sQuery) - { - $this->m_iLine = 0; - $this->m_iCol = 0; - $this->m_iColPrev = 0; - $this->m_sSourceQuery = $sQuery; - // no constructor - parent::__construct(); - } - - public function doParse($token, $value, $iCurrPosition = 0) - { - $this->m_iColPrev = $this->m_iCol; - $this->m_iCol = $iCurrPosition; - - return parent::DoParse($token, $value); - } - - public function doFinish() - { - $this->doParse(0, 0); - return $this->my_result; - } - - public function __destruct() - { - // Bug in the original destructor, causing an infinite loop ! - // This is a real issue when a fatal error occurs on the first token (the error could not be seen) - if (is_null($this->yyidx)) - { - $this->yyidx = -1; - } - parent::__destruct(); - } -} - -} + +/* + +This is a LALR(1) grammar +(seek for Lemon grammar to get some documentation from the Net) +That doc was helpful: http://www.hwaci.com/sw/lemon/lemon.html + +To handle operators precedence we could have used the %left directive +(we took another option, because that one was discovered right after... +which option is the best for us?) +Example: +%left LOG_AND. +%left LOG_OR. +%nonassoc EQ NE GT GE LT LE. +%left PLUS MINUS. +%left TIMES DIVIDE MOD. +%right EXP NOT. + +TODO : solve the 2 remaining shift-reduce conflicts (JOIN) + +*/ + +%name OQLParser_ +%declare_class {class OQLParserRaw} +%syntax_error { +throw new OQLParserException($this->m_sSourceQuery, $this->m_iLine, $this->m_iCol, $this->tokenName($yymajor), $TOKEN); +} + +result ::= union(X). { $this->my_result = X; } +result ::= query(X). { $this->my_result = X; } +result ::= condition(X). { $this->my_result = X; } + +union(A) ::= query(X) UNION query(Y). { + A = new OqlUnionQuery(X, Y); +} +union(A) ::= query(X) UNION union(Y). { + A = new OqlUnionQuery(X, Y); +} + +query(A) ::= SELECT class_name(X) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, X, W, J, array(X)); +} +query(A) ::= SELECT class_name(X) AS_ALIAS class_name(Y) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, Y, W, J, array(Y)); +} + +query(A) ::= SELECT class_list(E) FROM class_name(X) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, X, W, J, E); +} +query(A) ::= SELECT class_list(E) FROM class_name(X) AS_ALIAS class_name(Y) join_statement(J) where_statement(W). { + A = new OqlObjectQuery(X, Y, W, J, E); +} + + +class_list(A) ::= class_name(X). { + A = array(X); +} +class_list(A) ::= class_list(L) COMA class_name(X). { + array_push(L, X); + A = L; +} + +where_statement(A) ::= WHERE condition(C). { A = C;} +where_statement(A) ::= . { A = null;} + +join_statement(A) ::= join_item(J) join_statement(S). { + // insert the join statement on top of the existing list + array_unshift(S, J); + // and return the updated array + A = S; +} +join_statement(A) ::= join_item(J). { + A = Array(J); +} +join_statement(A) ::= . { A = null;} + +join_item(A) ::= JOIN class_name(X) AS_ALIAS class_name(Y) ON join_condition(C). +{ + // create an array with one single item + A = new OqlJoinSpec(X, Y, C); +} +join_item(A) ::= JOIN class_name(X) ON join_condition(C). +{ + // create an array with one single item + A = new OqlJoinSpec(X, X, C); +} + +join_condition(A) ::= field_id(X) EQ field_id(Y). { A = new BinaryOqlExpression(X, '=', Y); } +join_condition(A) ::= field_id(X) BELOW field_id(Y). { A = new BinaryOqlExpression(X, 'BELOW', Y); } +join_condition(A) ::= field_id(X) BELOW_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'BELOW_STRICT', Y); } +join_condition(A) ::= field_id(X) NOT_BELOW field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_BELOW', Y); } +join_condition(A) ::= field_id(X) NOT_BELOW_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_BELOW_STRICT', Y); } +join_condition(A) ::= field_id(X) ABOVE field_id(Y). { A = new BinaryOqlExpression(X, 'ABOVE', Y); } +join_condition(A) ::= field_id(X) ABOVE_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'ABOVE_STRICT', Y); } +join_condition(A) ::= field_id(X) NOT_ABOVE field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_ABOVE', Y); } +join_condition(A) ::= field_id(X) NOT_ABOVE_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_ABOVE_STRICT', Y); } + +condition(A) ::= expression_prio4(X). { A = X; } + +expression_basic(A) ::= scalar(X). { A = X; } +expression_basic(A) ::= field_id(X). { A = X; } +expression_basic(A) ::= var_name(X). { A = X; } +expression_basic(A) ::= func_name(X) PAR_OPEN arg_list(Y) PAR_CLOSE. { A = new FunctionOqlExpression(X, Y); } +expression_basic(A) ::= PAR_OPEN expression_prio4(X) PAR_CLOSE. { A = X; } +expression_basic(A) ::= expression_basic(X) list_operator(Y) list(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio1(A) ::= expression_basic(X). { A = X; } +expression_prio1(A) ::= expression_prio1(X) operator1(Y) expression_basic(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio2(A) ::= expression_prio1(X). { A = X; } +expression_prio2(A) ::= expression_prio2(X) operator2(Y) expression_prio1(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio3(A) ::= expression_prio2(X). { A = X; } +expression_prio3(A) ::= expression_prio3(X) operator3(Y) expression_prio2(Z). { A = new BinaryOqlExpression(X, Y, Z); } + +expression_prio4(A) ::= expression_prio3(X). { A = X; } +expression_prio4(A) ::= expression_prio4(X) operator4(Y) expression_prio3(Z). { A = new BinaryOqlExpression(X, Y, Z); } + + +list(A) ::= PAR_OPEN list_items(X) PAR_CLOSE. { + A = new ListOqlExpression(X); +} +list_items(A) ::= expression_prio4(X). { + A = array(X); +} +list_items(A) ::= list_items(L) COMA expression_prio4(X). { + array_push(L, X); + A = L; +} + +arg_list(A) ::= . { + A = array(); +} +arg_list(A) ::= argument(X). { + A = array(X); +} +arg_list(A) ::= arg_list(L) COMA argument(X). { + array_push(L, X); + A = L; +} +argument(A) ::= expression_prio4(X). { A = X; } +argument(A) ::= INTERVAL expression_prio4(X) interval_unit(Y). { A = new IntervalOqlExpression(X, Y); } + +interval_unit(A) ::= F_SECOND(X). { A = X; } +interval_unit(A) ::= F_MINUTE(X). { A = X; } +interval_unit(A) ::= F_HOUR(X). { A = X; } +interval_unit(A) ::= F_DAY(X). { A = X; } +interval_unit(A) ::= F_MONTH(X). { A = X; } +interval_unit(A) ::= F_YEAR(X). { A = X; } + +scalar(A) ::= num_scalar(X). { A = X; } +scalar(A) ::= str_scalar(X). { A = X; } + +num_scalar(A) ::= num_value(X). { A = new ScalarOqlExpression(X); } +str_scalar(A) ::= str_value(X). { A = new ScalarOqlExpression(X); } + +field_id(A) ::= name(X). { A = new FieldOqlExpression(X); } +field_id(A) ::= class_name(X) DOT name(Y). { A = new FieldOqlExpression(Y, X); } +class_name(A) ::= name(X). { A=X; } + + +var_name(A) ::= VARNAME(X). { A = new VariableOqlExpression(substr(X, 1)); } + +name(A) ::= NAME(X). { + if (X[0] == '`') + { + $name = substr(X, 1, strlen(X) - 2); + } + else + { + $name = X; + } + A = new OqlName($name, $this->m_iColPrev); +} +num_value(A) ::= NUMVAL(X). {A=(int)X;} +num_value(A) ::= MATH_MINUS NUMVAL(X). {A=(int)-X;} +num_value(A) ::= HEXVAL(X). {A=new OqlHexValue(X);} +str_value(A) ::= STRVAL(X). {A=stripslashes(substr(X, 1, strlen(X) - 2));} + + +operator1(A) ::= num_operator1(X). {A=X;} +operator1(A) ::= bitwise_operator1(X). {A=X;} +operator2(A) ::= num_operator2(X). {A=X;} +operator2(A) ::= str_operator(X). {A=X;} +operator2(A) ::= REGEXP(X). {A=X;} +operator2(A) ::= EQ(X). {A=X;} +operator2(A) ::= NOT_EQ(X). {A=X;} +operator3(A) ::= LOG_AND(X). {A=X;} +operator3(A) ::= bitwise_operator3(X). {A=X;} +operator4(A) ::= LOG_OR(X). {A=X;} +operator4(A) ::= bitwise_operator4(X). {A=X;} + +num_operator1(A) ::= MATH_DIV(X). {A=X;} +num_operator1(A) ::= MATH_MULT(X). {A=X;} +num_operator2(A) ::= MATH_PLUS(X). {A=X;} +num_operator2(A) ::= MATH_MINUS(X). {A=X;} +num_operator2(A) ::= GT(X). {A=X;} +num_operator2(A) ::= LT(X). {A=X;} +num_operator2(A) ::= GE(X). {A=X;} +num_operator2(A) ::= LE(X). {A=X;} + +str_operator(A) ::= LIKE(X). {A=X;} +str_operator(A) ::= NOT_LIKE(X). {A=X;} + +bitwise_operator1(A) ::= BITWISE_LEFT_SHIFT(X). {A=X;} +bitwise_operator1(A) ::= BITWISE_RIGHT_SHIFT(X). {A=X;} +bitwise_operator3(A) ::= BITWISE_AND(X). {A=X;} +bitwise_operator4(A) ::= BITWISE_OR(X). {A=X;} +bitwise_operator4(A) ::= BITWISE_XOR(X). {A=X;} + +list_operator(A) ::= IN(X). {A=X;} +list_operator(A) ::= NOT_IN(X). {A=X;} + +func_name(A) ::= F_IF(X). { A=X; } +func_name(A) ::= F_ELT(X). { A=X; } +func_name(A) ::= F_COALESCE(X). { A=X; } +func_name(A) ::= F_ISNULL(X). { A=X; } +func_name(A) ::= F_CONCAT(X). { A=X; } +func_name(A) ::= F_SUBSTR(X). { A=X; } +func_name(A) ::= F_TRIM(X). { A=X; } +func_name(A) ::= F_DATE(X). { A=X; } +func_name(A) ::= F_DATE_FORMAT(X). { A=X; } +func_name(A) ::= F_CURRENT_DATE(X). { A=X; } +func_name(A) ::= F_NOW(X). { A=X; } +func_name(A) ::= F_TIME(X). { A=X; } +func_name(A) ::= F_TO_DAYS(X). { A=X; } +func_name(A) ::= F_FROM_DAYS(X). { A=X; } +func_name(A) ::= F_YEAR(X). { A=X; } +func_name(A) ::= F_MONTH(X). { A=X; } +func_name(A) ::= F_DAY(X). { A=X; } +func_name(A) ::= F_DATE_ADD(X). { A=X; } +func_name(A) ::= F_DATE_SUB(X). { A=X; } +func_name(A) ::= F_ROUND(X). { A=X; } +func_name(A) ::= F_FLOOR(X). { A=X; } +func_name(A) ::= F_INET_ATON(X). { A=X; } +func_name(A) ::= F_INET_NTOA(X). { A=X; } + + +%code { + +class OQLParserException extends OQLException +{ + public function __construct($sInput, $iLine, $iCol, $sTokenName, $sTokenValue) + { + $sIssue = "Unexpected token $sTokenName"; + + parent::__construct($sIssue, $sInput, $iLine, $iCol, $sTokenValue); + } +} + +class OQLParser extends OQLParserRaw +{ + // dirty, but working for us (no other mean to get the final result :-( + protected $my_result; + + public function GetResult() + { + return $this->my_result; + } + + // More info on the source query and the current position while parsing it + // Data used when an exception is raised + protected $m_iLine; // still not used + protected $m_iCol; + protected $m_iColPrev; // this is the interesting one, because the parser will reduce on the next token + protected $m_sSourceQuery; + + public function __construct($sQuery) + { + $this->m_iLine = 0; + $this->m_iCol = 0; + $this->m_iColPrev = 0; + $this->m_sSourceQuery = $sQuery; + // no constructor - parent::__construct(); + } + + public function doParse($token, $value, $iCurrPosition = 0) + { + $this->m_iColPrev = $this->m_iCol; + $this->m_iCol = $iCurrPosition; + + return parent::DoParse($token, $value); + } + + public function doFinish() + { + $this->doParse(0, 0); + return $this->my_result; + } + + public function __destruct() + { + // Bug in the original destructor, causing an infinite loop ! + // This is a real issue when a fatal error occurs on the first token (the error could not be seen) + if (is_null($this->yyidx)) + { + $this->yyidx = -1; + } + parent::__destruct(); + } +} + +} diff --git a/core/oql/oqlinterpreter.class.inc.php b/core/oql/oqlinterpreter.class.inc.php index d59c4bb50..342dd8cd1 100644 --- a/core/oql/oqlinterpreter.class.inc.php +++ b/core/oql/oqlinterpreter.class.inc.php @@ -1,112 +1,112 @@ - - - -/** - * Wrapper to execute the parser, lexical analyzer and normalization of an OQL query - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class OqlNormalizeException extends OQLException -{ - public function __construct($sIssue, $sInput, OqlName $oName, $aExpecting = null) - { - parent::__construct($sIssue, $sInput, 0, $oName->GetPos(), $oName->GetValue(), $aExpecting); - } -} -class UnknownClassOqlException extends OqlNormalizeException -{ - public function __construct($sInput, OqlName $oName, $aExpecting = null) - { - parent::__construct('Unknown class', $sInput, $oName, $aExpecting); - } - - public function GetUserFriendlyDescription() - { - $sWrongClass = $this->GetWrongWord(); - $sSuggest = self::FindClosestString($sWrongClass, $this->GetSuggestions()); - - if ($sSuggest != '') - { - return Dict::Format('UI:OQL:UnknownClassAndFix', $sWrongClass, $sSuggest); - } - else - { - return Dict::Format('UI:OQL:UnknownClassNoFix', $sWrongClass); - } - } -} - -class OqlInterpreterException extends OQLException -{ -} - - -class OqlInterpreter -{ - public $m_sQuery; - - public function __construct($sQuery) - { - $this->m_sQuery = $sQuery; - } - - // Note: this function is left public for unit test purposes - public function Parse() - { - $oLexer = new OQLLexer($this->m_sQuery); - $oParser = new OQLParser($this->m_sQuery); - - while($oLexer->yylex()) - { - $oParser->doParse($oLexer->token, $oLexer->value, $oLexer->getTokenPos()); - } - $res = $oParser->doFinish(); - return $res; - } - - /** - * @return OqlQuery - * @throws \OQLException - */ - public function ParseQuery() - { - $oRes = $this->Parse(); - if (!$oRes instanceof OqlQuery) - { - throw new OQLException('Expecting an OQL query', $this->m_sQuery, 0, 0, get_class($oRes)); - } - return $oRes; - } - - /** - * @return Expression - */ - public function ParseExpression() - { - $oRes = $this->Parse(); - if (!$oRes instanceof Expression) - { - throw new OQLException('Expecting an OQL expression', $this->m_sQuery, 0, 0, get_class($oRes), array('Expression')); - } - return $oRes; - } -} + + + +/** + * Wrapper to execute the parser, lexical analyzer and normalization of an OQL query + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +class OqlNormalizeException extends OQLException +{ + public function __construct($sIssue, $sInput, OqlName $oName, $aExpecting = null) + { + parent::__construct($sIssue, $sInput, 0, $oName->GetPos(), $oName->GetValue(), $aExpecting); + } +} +class UnknownClassOqlException extends OqlNormalizeException +{ + public function __construct($sInput, OqlName $oName, $aExpecting = null) + { + parent::__construct('Unknown class', $sInput, $oName, $aExpecting); + } + + public function GetUserFriendlyDescription() + { + $sWrongClass = $this->GetWrongWord(); + $sSuggest = self::FindClosestString($sWrongClass, $this->GetSuggestions()); + + if ($sSuggest != '') + { + return Dict::Format('UI:OQL:UnknownClassAndFix', $sWrongClass, $sSuggest); + } + else + { + return Dict::Format('UI:OQL:UnknownClassNoFix', $sWrongClass); + } + } +} + +class OqlInterpreterException extends OQLException +{ +} + + +class OqlInterpreter +{ + public $m_sQuery; + + public function __construct($sQuery) + { + $this->m_sQuery = $sQuery; + } + + // Note: this function is left public for unit test purposes + public function Parse() + { + $oLexer = new OQLLexer($this->m_sQuery); + $oParser = new OQLParser($this->m_sQuery); + + while($oLexer->yylex()) + { + $oParser->doParse($oLexer->token, $oLexer->value, $oLexer->getTokenPos()); + } + $res = $oParser->doFinish(); + return $res; + } + + /** + * @return OqlQuery + * @throws \OQLException + */ + public function ParseQuery() + { + $oRes = $this->Parse(); + if (!$oRes instanceof OqlQuery) + { + throw new OQLException('Expecting an OQL query', $this->m_sQuery, 0, 0, get_class($oRes)); + } + return $oRes; + } + + /** + * @return Expression + */ + public function ParseExpression() + { + $oRes = $this->Parse(); + if (!$oRes instanceof Expression) + { + throw new OQLException('Expecting an OQL expression', $this->m_sQuery, 0, 0, get_class($oRes), array('Expression')); + } + return $oRes; + } +} diff --git a/core/oql/oqlquery.class.inc.php b/core/oql/oqlquery.class.inc.php index b0bff7db6..5fbcc8aed 100644 --- a/core/oql/oqlquery.class.inc.php +++ b/core/oql/oqlquery.class.inc.php @@ -1,747 +1,747 @@ - - - -/** - * Classes defined for lexical analyze (see oql-parser.y) - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -define('TREE_OPERATOR_EQUALS', 0); -define('TREE_OPERATOR_BELOW', 1); -define('TREE_OPERATOR_BELOW_STRICT', 2); -define('TREE_OPERATOR_NOT_BELOW', 3); -define('TREE_OPERATOR_NOT_BELOW_STRICT', 4); -define('TREE_OPERATOR_ABOVE', 5); -define('TREE_OPERATOR_ABOVE_STRICT', 6); -define('TREE_OPERATOR_NOT_ABOVE', 7); -define('TREE_OPERATOR_NOT_ABOVE_STRICT', 8); - -// Position a string within an OQL query -// This is a must if we want to be able to pinpoint an error at any stage of the query interpretation -// In particular, the normalization phase requires this -class OqlName -{ - protected $m_sValue; - protected $m_iPos; - - public function __construct($sValue, $iPos) - { - $this->m_iPos = $iPos; - $this->m_sValue = $sValue; - } - - public function GetValue() - { - return $this->m_sValue; - } - - public function GetPos() - { - return $this->m_iPos; - } - - public function __toString() - { - return $this->m_sValue; - } -} - -/** - * - * Store hexadecimal values as strings so that we can support 64-bit values - * - */ -class OqlHexValue -{ - protected $m_sValue; - - public function __construct($sValue) - { - $this->m_sValue = $sValue; - } - - public function __toString() - { - return $this->m_sValue; - } - -} - -class OqlJoinSpec -{ - protected $m_oClass; - protected $m_oClassAlias; - protected $m_oLeftField; - protected $m_oRightField; - protected $m_sOperator; - - protected $m_oNextJoinspec; - - public function __construct($oClass, $oClassAlias, BinaryExpression $oExpression) - { - $this->m_oClass = $oClass; - $this->m_oClassAlias = $oClassAlias; - $this->m_oLeftField = $oExpression->GetLeftExpr(); - $this->m_oRightField = $oExpression->GetRightExpr(); - $this->m_oRightField = $oExpression->GetRightExpr(); - $this->m_sOperator = $oExpression->GetOperator(); - } - - public function GetClass() - { - return $this->m_oClass->GetValue(); - } - public function GetClassAlias() - { - return $this->m_oClassAlias->GetValue(); - } - - public function GetClassDetails() - { - return $this->m_oClass; - } - public function GetClassAliasDetails() - { - return $this->m_oClassAlias; - } - - public function GetLeftField() - { - return $this->m_oLeftField; - } - public function GetRightField() - { - return $this->m_oRightField; - } - public function GetOperator() - { - return $this->m_sOperator; - } -} - -interface CheckableExpression -{ - /** - * Check the validity of the expression with regard to the data model - * and the query in which it is used - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @param array $aAliases Aliases to class names (for the current query) - * @param string $sSourceQuery For the reporting - * @throws OqlNormalizeException - */ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery); -} - -class BinaryOqlExpression extends BinaryExpression implements CheckableExpression -{ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - $this->m_oLeftExpr->Check($oModelReflection, $aAliases, $sSourceQuery); - $this->m_oRightExpr->Check($oModelReflection, $aAliases, $sSourceQuery); - } -} - -class ScalarOqlExpression extends ScalarExpression implements CheckableExpression -{ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - // a scalar is always fine - } -} - -class FieldOqlExpression extends FieldExpression implements CheckableExpression -{ - protected $m_oParent; - protected $m_oName; - - public function __construct($oName, $oParent = null) - { - if (is_null($oParent)) - { - $oParent = new OqlName('', 0); - } - $this->m_oParent = $oParent; - $this->m_oName = $oName; - - parent::__construct($oName->GetValue(), $oParent->GetValue()); - } - - public function GetParentDetails() - { - return $this->m_oParent; - } - - public function GetNameDetails() - { - return $this->m_oName; - } - - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - $sClassAlias = $this->GetParent(); - $sFltCode = $this->GetName(); - if (empty($sClassAlias)) - { - // Try to find an alias - // Build an array of field => array of aliases - $aFieldClasses = array(); - foreach($aAliases as $sAlias => $sReal) - { - foreach($oModelReflection->GetFiltersList($sReal) as $sAnFltCode) - { - $aFieldClasses[$sAnFltCode][] = $sAlias; - } - } - if (!array_key_exists($sFltCode, $aFieldClasses)) - { - throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), array_keys($aFieldClasses)); - } - if (count($aFieldClasses[$sFltCode]) > 1) - { - throw new OqlNormalizeException('Ambiguous filter code', $sSourceQuery, $this->GetNameDetails()); - } - $sClassAlias = $aFieldClasses[$sFltCode][0]; - } - else - { - if (!array_key_exists($sClassAlias, $aAliases)) - { - throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $this->GetParentDetails(), array_keys($aAliases)); - } - $sClass = $aAliases[$sClassAlias]; - if (!$oModelReflection->IsValidFilterCode($sClass, $sFltCode)) - { - throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), $oModelReflection->GetFiltersList($sClass)); - } - } - } -} - -class VariableOqlExpression extends VariableExpression implements CheckableExpression -{ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - // a scalar is always fine - } -} - -class ListOqlExpression extends ListExpression implements CheckableExpression -{ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - foreach ($this->GetItems() as $oItemExpression) - { - $oItemExpression->Check($oModelReflection, $aAliases, $sSourceQuery); - } - } -} - -class FunctionOqlExpression extends FunctionExpression implements CheckableExpression -{ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - foreach ($this->GetArgs() as $oArgExpression) - { - $oArgExpression->Check($oModelReflection, $aAliases, $sSourceQuery); - } - } -} - -class IntervalOqlExpression extends IntervalExpression implements CheckableExpression -{ - public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) - { - // an interval is always fine (made of a scalar and unit) - } -} - -abstract class OqlQuery -{ - public function __construct() - { - } - - /** - * Check the validity of the expression with regard to the data model - * and the query in which it is used - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @param string $sSourceQuery - */ - abstract public function Check(ModelReflection $oModelReflection, $sSourceQuery); - - /** - * Determine the class - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @return string - * @throws Exception - */ - abstract public function GetClass(ModelReflection $oModelReflection); - - /** - * Determine the class alias - * - * @return string - * @throws Exception - */ - abstract public function GetClassAlias(); -} - -class OqlObjectQuery extends OqlQuery -{ - protected $m_aSelect; // array of selected classes - protected $m_oClass; - protected $m_oClassAlias; - protected $m_aJoins; // array of OqlJoinSpec - protected $m_oCondition; // condition tree (expressions) - - public function __construct($oClass, $oClassAlias, $oCondition = null, $aJoins = null, $aSelect = null) - { - $this->m_aSelect = $aSelect; - $this->m_oClass = $oClass; - $this->m_oClassAlias = $oClassAlias; - $this->m_aJoins = $aJoins; - $this->m_oCondition = $oCondition; - - parent::__construct(); - } - - public function GetSelectedClasses() - { - return $this->m_aSelect; - } - - /** - * Determine the class - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @return string - * @throws Exception - */ - public function GetClass(ModelReflection $oModelReflection) - { - return $this->m_oClass->GetValue(); - } - - /** - * Determine the class alias - * - * @return string - * @throws Exception - */ - public function GetClassAlias() - { - return $this->m_oClassAlias->GetValue(); - } - - public function GetClassDetails() - { - return $this->m_oClass; - } - public function GetClassAliasDetails() - { - return $this->m_oClassAlias; - } - - public function GetJoins() - { - return $this->m_aJoins; - } - public function GetCondition() - { - return $this->m_oCondition; - } - - /** - * Recursively check the validity of the expression with regard to the data model - * and the query in which it is used - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @throws OqlNormalizeException - */ - public function Check(ModelReflection $oModelReflection, $sSourceQuery) - { - $sClass = $this->GetClass($oModelReflection); - $sClassAlias = $this->GetClassAlias(); - - if (!$oModelReflection->IsValidClass($sClass)) - { - throw new UnknownClassOqlException($sSourceQuery, $this->GetClassDetails(), $oModelReflection->GetClasses()); - } - - $aAliases = array($sClassAlias => $sClass); - - $aJoinSpecs = $this->GetJoins(); - if (is_array($aJoinSpecs)) - { - foreach ($aJoinSpecs as $oJoinSpec) - { - $sJoinClass = $oJoinSpec->GetClass(); - $sJoinClassAlias = $oJoinSpec->GetClassAlias(); - if (!$oModelReflection->IsValidClass($sJoinClass)) - { - throw new UnknownClassOqlException($sSourceQuery, $oJoinSpec->GetClassDetails(), $oModelReflection->GetClasses()); - } - if (array_key_exists($sJoinClassAlias, $aAliases)) - { - if ($sJoinClassAlias != $sJoinClass) - { - throw new OqlNormalizeException('Duplicate class alias', $sSourceQuery, $oJoinSpec->GetClassAliasDetails()); - } - else - { - throw new OqlNormalizeException('Duplicate class name', $sSourceQuery, $oJoinSpec->GetClassDetails()); - } - } - - // Assumption: ext key on the left only !!! - // normalization should take care of this - $oLeftField = $oJoinSpec->GetLeftField(); - $sFromClass = $oLeftField->GetParent(); - $sExtKeyAttCode = $oLeftField->GetName(); - - $oRightField = $oJoinSpec->GetRightField(); - $sToClass = $oRightField->GetParent(); - $sPKeyDescriptor = $oRightField->GetName(); - if ($sPKeyDescriptor != 'id') - { - throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sSourceQuery, $oRightField->GetNameDetails(), array('id')); - } - - $aAliases[$sJoinClassAlias] = $sJoinClass; - - if (!array_key_exists($sFromClass, $aAliases)) - { - throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sSourceQuery, $oLeftField->GetParentDetails(), array_keys($aAliases)); - } - if (!array_key_exists($sToClass, $aAliases)) - { - throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sSourceQuery, $oRightField->GetParentDetails(), array_keys($aAliases)); - } - $aExtKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], 'AttributeExternalKey'); - $aObjKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], 'AttributeObjectKey'); - $aAllKeys = array_merge($aExtKeys, $aObjKeys); - if (!array_key_exists($sExtKeyAttCode, $aAllKeys)) - { - throw new OqlNormalizeException('Unknown key in join condition (left expression)', $sSourceQuery, $oLeftField->GetNameDetails(), array_keys($aAllKeys)); - } - - if ($sFromClass == $sJoinClassAlias) - { - if (array_key_exists($sExtKeyAttCode, $aExtKeys)) // Skip that check for object keys - { - $sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass'); - if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass)) - { - throw new OqlNormalizeException("The joined class ($aAliases[$sFromClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails()); - } - } - } - else - { - $sOperator = $oJoinSpec->GetOperator(); - switch($sOperator) - { - case '=': - $iOperatorCode = TREE_OPERATOR_EQUALS; - break; - case 'BELOW': - $iOperatorCode = TREE_OPERATOR_BELOW; - break; - case 'BELOW_STRICT': - $iOperatorCode = TREE_OPERATOR_BELOW_STRICT; - break; - case 'NOT_BELOW': - $iOperatorCode = TREE_OPERATOR_NOT_BELOW; - break; - case 'NOT_BELOW_STRICT': - $iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT; - break; - case 'ABOVE': - $iOperatorCode = TREE_OPERATOR_ABOVE; - break; - case 'ABOVE_STRICT': - $iOperatorCode = TREE_OPERATOR_ABOVE_STRICT; - break; - case 'NOT_ABOVE': - $iOperatorCode = TREE_OPERATOR_NOT_ABOVE; - break; - case 'NOT_ABOVE_STRICT': - $iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT; - break; - } - if (array_key_exists($sExtKeyAttCode, $aExtKeys)) // Skip that check for object keys - { - $sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass'); - if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass)) - { - throw new OqlNormalizeException("The joined class ($aAliases[$sToClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails()); - } - } - $aAttList = $oModelReflection->ListAttributes($aAliases[$sFromClass]); - $sAttType = $aAttList[$sExtKeyAttCode]; - if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !is_subclass_of($sAttType, 'AttributeHierarchicalKey') && ($sAttType != 'AttributeHierarchicalKey')) - { - throw new OqlNormalizeException("The specified tree operator $sOperator is not applicable to the key", $sSourceQuery, $oLeftField->GetNameDetails()); - } - } - } - } - - // Check the select information - // - foreach ($this->GetSelectedClasses() as $oClassDetails) - { - $sClassToSelect = $oClassDetails->GetValue(); - if (!array_key_exists($sClassToSelect, $aAliases)) - { - throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases)); - } - } - - // Check the condition tree - // - if ($this->m_oCondition instanceof Expression) - { - $this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery); - } - } - - /** - * Make the relevant DBSearch instance (FromOQL) - */ - public function ToDBSearch($sQuery) - { - $sClass = $this->GetClass(new ModelReflectionRuntime()); - $sClassAlias = $this->GetClassAlias(); - - $oSearch = new DBObjectSearch($sClass, $sClassAlias); - $oSearch->InitFromOqlQuery($this, $sQuery); - return $oSearch; - } -} - -class OqlUnionQuery extends OqlQuery -{ - protected $aQueries; - - public function __construct(OqlObjectQuery $oLeftQuery, OqlQuery $oRightQueryOrUnion) - { - $this->aQueries[] = $oLeftQuery; - if ($oRightQueryOrUnion instanceof OqlUnionQuery) - { - foreach ($oRightQueryOrUnion->GetQueries() as $oSingleQuery) - { - $this->aQueries[] = $oSingleQuery; - } - } - else - { - $this->aQueries[] = $oRightQueryOrUnion; - } - } - - public function GetQueries() - { - return $this->aQueries; - } - - /** - * Check the validity of the expression with regard to the data model - * and the query in which it is used - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @throws OqlNormalizeException - */ - public function Check(ModelReflection $oModelReflection, $sSourceQuery) - { - $aColumnToClasses = array(); - foreach ($this->aQueries as $iQuery => $oQuery) - { - $oQuery->Check($oModelReflection, $sSourceQuery); - - $aAliasToClass = array($oQuery->GetClassAlias() => $oQuery->GetClass($oModelReflection)); - $aJoinSpecs = $oQuery->GetJoins(); - if (is_array($aJoinSpecs)) - { - foreach ($aJoinSpecs as $oJoinSpec) - { - $aAliasToClass[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass(); - } - } - - $aSelectedClasses = $oQuery->GetSelectedClasses(); - if ($iQuery != 0) - { - if (count($aSelectedClasses) < count($aColumnToClasses)) - { - $oLastClass = end($aSelectedClasses); - throw new OqlNormalizeException('Too few selected classes in the subquery', $sSourceQuery, $oLastClass); - } - if (count($aSelectedClasses) > count($aColumnToClasses)) - { - $oLastClass = end($aSelectedClasses); - throw new OqlNormalizeException('Too many selected classes in the subquery', $sSourceQuery, $oLastClass); - } - } - foreach ($aSelectedClasses as $iColumn => $oClassDetails) - { - $sAlias = $oClassDetails->GetValue(); - $sClass = $aAliasToClass[$sAlias]; - $aColumnToClasses[$iColumn][] = array( - 'alias' => $sAlias, - 'class' => $sClass, - 'class_name' => $oClassDetails, - ); - } - } - foreach ($aColumnToClasses as $iColumn => $aClasses) - { - foreach ($aClasses as $iQuery => $aData) - { - if ($iQuery == 0) - { - // Establish the reference - $sRootClass = $oModelReflection->GetRootClass($aData['class']); - } - else - { - if ($oModelReflection->GetRootClass($aData['class']) != $sRootClass) - { - $aSubclasses = $oModelReflection->EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL); - throw new OqlNormalizeException('Incompatible classes: could not find a common ancestor', $sSourceQuery, $aData['class_name'], $aSubclasses); - } - } - } - } - } - - /** - * Determine the class - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @return string - * @throws Exception - */ - public function GetClass(ModelReflection $oModelReflection) - { - $aFirstColClasses = array(); - foreach ($this->aQueries as $iQuery => $oQuery) - { - $aFirstColClasses[] = $oQuery->GetClass($oModelReflection); - } - $sClass = self::GetLowestCommonAncestor($oModelReflection, $aFirstColClasses); - if (is_null($sClass)) - { - throw new Exception('Could not determine the class of the union query. This issue should have been detected earlier by calling OqlQuery::Check()'); - } - return $sClass; - } - - /** - * Determine the class alias - * - * @return string - * @throws Exception - */ - public function GetClassAlias() - { - $sAlias = $this->aQueries[0]->GetClassAlias(); - return $sAlias; - } - - /** - * Check the validity of the expression with regard to the data model - * and the query in which it is used - * - * @param ModelReflection $oModelReflection MetaModel to consider - * @param array $aClasses Flat list of classes - * @return string the lowest common ancestor amongst classes, null if none has been found - * @throws Exception - */ - public static function GetLowestCommonAncestor(ModelReflection $oModelReflection, $aClasses) - { - $sAncestor = null; - foreach($aClasses as $sClass) - { - if (is_null($sAncestor)) - { - // first loop - $sAncestor = $sClass; - } - elseif ($sClass == $sAncestor) - { - // remains the same - } - elseif ($oModelReflection->GetRootClass($sClass) != $oModelReflection->GetRootClass($sAncestor)) - { - $sAncestor = null; - break; - } - else - { - $sAncestor = self::LowestCommonAncestor($oModelReflection, $sAncestor, $sClass); - } - } - return $sAncestor; - } - - /** - * Note: assumes that class A and B have a common ancestor - */ - protected static function LowestCommonAncestor(ModelReflection $oModelReflection, $sClassA, $sClassB) - { - if ($sClassA == $sClassB) - { - $sRet = $sClassA; - } - elseif (in_array($sClassA, $oModelReflection->EnumChildClasses($sClassB))) - { - $sRet = $sClassB; - } - elseif (in_array($sClassB, $oModelReflection->EnumChildClasses($sClassA))) - { - $sRet = $sClassA; - } - else - { - // Recurse - $sRet = self::LowestCommonAncestor($oModelReflection, $sClassA, $oModelReflection->GetParentClass($sClassB)); - } - return $sRet; - } - /** - * Make the relevant DBSearch instance (FromOQL) - */ - public function ToDBSearch($sQuery) - { - $aSearches = array(); - foreach ($this->aQueries as $oQuery) - { - $aSearches[] = $oQuery->ToDBSearch($sQuery); - } - - $oSearch = new DBUnionSearch($aSearches); - return $oSearch; - } + + + +/** + * Classes defined for lexical analyze (see oql-parser.y) + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +define('TREE_OPERATOR_EQUALS', 0); +define('TREE_OPERATOR_BELOW', 1); +define('TREE_OPERATOR_BELOW_STRICT', 2); +define('TREE_OPERATOR_NOT_BELOW', 3); +define('TREE_OPERATOR_NOT_BELOW_STRICT', 4); +define('TREE_OPERATOR_ABOVE', 5); +define('TREE_OPERATOR_ABOVE_STRICT', 6); +define('TREE_OPERATOR_NOT_ABOVE', 7); +define('TREE_OPERATOR_NOT_ABOVE_STRICT', 8); + +// Position a string within an OQL query +// This is a must if we want to be able to pinpoint an error at any stage of the query interpretation +// In particular, the normalization phase requires this +class OqlName +{ + protected $m_sValue; + protected $m_iPos; + + public function __construct($sValue, $iPos) + { + $this->m_iPos = $iPos; + $this->m_sValue = $sValue; + } + + public function GetValue() + { + return $this->m_sValue; + } + + public function GetPos() + { + return $this->m_iPos; + } + + public function __toString() + { + return $this->m_sValue; + } +} + +/** + * + * Store hexadecimal values as strings so that we can support 64-bit values + * + */ +class OqlHexValue +{ + protected $m_sValue; + + public function __construct($sValue) + { + $this->m_sValue = $sValue; + } + + public function __toString() + { + return $this->m_sValue; + } + +} + +class OqlJoinSpec +{ + protected $m_oClass; + protected $m_oClassAlias; + protected $m_oLeftField; + protected $m_oRightField; + protected $m_sOperator; + + protected $m_oNextJoinspec; + + public function __construct($oClass, $oClassAlias, BinaryExpression $oExpression) + { + $this->m_oClass = $oClass; + $this->m_oClassAlias = $oClassAlias; + $this->m_oLeftField = $oExpression->GetLeftExpr(); + $this->m_oRightField = $oExpression->GetRightExpr(); + $this->m_oRightField = $oExpression->GetRightExpr(); + $this->m_sOperator = $oExpression->GetOperator(); + } + + public function GetClass() + { + return $this->m_oClass->GetValue(); + } + public function GetClassAlias() + { + return $this->m_oClassAlias->GetValue(); + } + + public function GetClassDetails() + { + return $this->m_oClass; + } + public function GetClassAliasDetails() + { + return $this->m_oClassAlias; + } + + public function GetLeftField() + { + return $this->m_oLeftField; + } + public function GetRightField() + { + return $this->m_oRightField; + } + public function GetOperator() + { + return $this->m_sOperator; + } +} + +interface CheckableExpression +{ + /** + * Check the validity of the expression with regard to the data model + * and the query in which it is used + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @param array $aAliases Aliases to class names (for the current query) + * @param string $sSourceQuery For the reporting + * @throws OqlNormalizeException + */ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery); +} + +class BinaryOqlExpression extends BinaryExpression implements CheckableExpression +{ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + $this->m_oLeftExpr->Check($oModelReflection, $aAliases, $sSourceQuery); + $this->m_oRightExpr->Check($oModelReflection, $aAliases, $sSourceQuery); + } +} + +class ScalarOqlExpression extends ScalarExpression implements CheckableExpression +{ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + // a scalar is always fine + } +} + +class FieldOqlExpression extends FieldExpression implements CheckableExpression +{ + protected $m_oParent; + protected $m_oName; + + public function __construct($oName, $oParent = null) + { + if (is_null($oParent)) + { + $oParent = new OqlName('', 0); + } + $this->m_oParent = $oParent; + $this->m_oName = $oName; + + parent::__construct($oName->GetValue(), $oParent->GetValue()); + } + + public function GetParentDetails() + { + return $this->m_oParent; + } + + public function GetNameDetails() + { + return $this->m_oName; + } + + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + $sClassAlias = $this->GetParent(); + $sFltCode = $this->GetName(); + if (empty($sClassAlias)) + { + // Try to find an alias + // Build an array of field => array of aliases + $aFieldClasses = array(); + foreach($aAliases as $sAlias => $sReal) + { + foreach($oModelReflection->GetFiltersList($sReal) as $sAnFltCode) + { + $aFieldClasses[$sAnFltCode][] = $sAlias; + } + } + if (!array_key_exists($sFltCode, $aFieldClasses)) + { + throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), array_keys($aFieldClasses)); + } + if (count($aFieldClasses[$sFltCode]) > 1) + { + throw new OqlNormalizeException('Ambiguous filter code', $sSourceQuery, $this->GetNameDetails()); + } + $sClassAlias = $aFieldClasses[$sFltCode][0]; + } + else + { + if (!array_key_exists($sClassAlias, $aAliases)) + { + throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $this->GetParentDetails(), array_keys($aAliases)); + } + $sClass = $aAliases[$sClassAlias]; + if (!$oModelReflection->IsValidFilterCode($sClass, $sFltCode)) + { + throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), $oModelReflection->GetFiltersList($sClass)); + } + } + } +} + +class VariableOqlExpression extends VariableExpression implements CheckableExpression +{ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + // a scalar is always fine + } +} + +class ListOqlExpression extends ListExpression implements CheckableExpression +{ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + foreach ($this->GetItems() as $oItemExpression) + { + $oItemExpression->Check($oModelReflection, $aAliases, $sSourceQuery); + } + } +} + +class FunctionOqlExpression extends FunctionExpression implements CheckableExpression +{ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + foreach ($this->GetArgs() as $oArgExpression) + { + $oArgExpression->Check($oModelReflection, $aAliases, $sSourceQuery); + } + } +} + +class IntervalOqlExpression extends IntervalExpression implements CheckableExpression +{ + public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery) + { + // an interval is always fine (made of a scalar and unit) + } +} + +abstract class OqlQuery +{ + public function __construct() + { + } + + /** + * Check the validity of the expression with regard to the data model + * and the query in which it is used + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @param string $sSourceQuery + */ + abstract public function Check(ModelReflection $oModelReflection, $sSourceQuery); + + /** + * Determine the class + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @return string + * @throws Exception + */ + abstract public function GetClass(ModelReflection $oModelReflection); + + /** + * Determine the class alias + * + * @return string + * @throws Exception + */ + abstract public function GetClassAlias(); +} + +class OqlObjectQuery extends OqlQuery +{ + protected $m_aSelect; // array of selected classes + protected $m_oClass; + protected $m_oClassAlias; + protected $m_aJoins; // array of OqlJoinSpec + protected $m_oCondition; // condition tree (expressions) + + public function __construct($oClass, $oClassAlias, $oCondition = null, $aJoins = null, $aSelect = null) + { + $this->m_aSelect = $aSelect; + $this->m_oClass = $oClass; + $this->m_oClassAlias = $oClassAlias; + $this->m_aJoins = $aJoins; + $this->m_oCondition = $oCondition; + + parent::__construct(); + } + + public function GetSelectedClasses() + { + return $this->m_aSelect; + } + + /** + * Determine the class + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @return string + * @throws Exception + */ + public function GetClass(ModelReflection $oModelReflection) + { + return $this->m_oClass->GetValue(); + } + + /** + * Determine the class alias + * + * @return string + * @throws Exception + */ + public function GetClassAlias() + { + return $this->m_oClassAlias->GetValue(); + } + + public function GetClassDetails() + { + return $this->m_oClass; + } + public function GetClassAliasDetails() + { + return $this->m_oClassAlias; + } + + public function GetJoins() + { + return $this->m_aJoins; + } + public function GetCondition() + { + return $this->m_oCondition; + } + + /** + * Recursively check the validity of the expression with regard to the data model + * and the query in which it is used + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @throws OqlNormalizeException + */ + public function Check(ModelReflection $oModelReflection, $sSourceQuery) + { + $sClass = $this->GetClass($oModelReflection); + $sClassAlias = $this->GetClassAlias(); + + if (!$oModelReflection->IsValidClass($sClass)) + { + throw new UnknownClassOqlException($sSourceQuery, $this->GetClassDetails(), $oModelReflection->GetClasses()); + } + + $aAliases = array($sClassAlias => $sClass); + + $aJoinSpecs = $this->GetJoins(); + if (is_array($aJoinSpecs)) + { + foreach ($aJoinSpecs as $oJoinSpec) + { + $sJoinClass = $oJoinSpec->GetClass(); + $sJoinClassAlias = $oJoinSpec->GetClassAlias(); + if (!$oModelReflection->IsValidClass($sJoinClass)) + { + throw new UnknownClassOqlException($sSourceQuery, $oJoinSpec->GetClassDetails(), $oModelReflection->GetClasses()); + } + if (array_key_exists($sJoinClassAlias, $aAliases)) + { + if ($sJoinClassAlias != $sJoinClass) + { + throw new OqlNormalizeException('Duplicate class alias', $sSourceQuery, $oJoinSpec->GetClassAliasDetails()); + } + else + { + throw new OqlNormalizeException('Duplicate class name', $sSourceQuery, $oJoinSpec->GetClassDetails()); + } + } + + // Assumption: ext key on the left only !!! + // normalization should take care of this + $oLeftField = $oJoinSpec->GetLeftField(); + $sFromClass = $oLeftField->GetParent(); + $sExtKeyAttCode = $oLeftField->GetName(); + + $oRightField = $oJoinSpec->GetRightField(); + $sToClass = $oRightField->GetParent(); + $sPKeyDescriptor = $oRightField->GetName(); + if ($sPKeyDescriptor != 'id') + { + throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sSourceQuery, $oRightField->GetNameDetails(), array('id')); + } + + $aAliases[$sJoinClassAlias] = $sJoinClass; + + if (!array_key_exists($sFromClass, $aAliases)) + { + throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sSourceQuery, $oLeftField->GetParentDetails(), array_keys($aAliases)); + } + if (!array_key_exists($sToClass, $aAliases)) + { + throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sSourceQuery, $oRightField->GetParentDetails(), array_keys($aAliases)); + } + $aExtKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], 'AttributeExternalKey'); + $aObjKeys = $oModelReflection->ListAttributes($aAliases[$sFromClass], 'AttributeObjectKey'); + $aAllKeys = array_merge($aExtKeys, $aObjKeys); + if (!array_key_exists($sExtKeyAttCode, $aAllKeys)) + { + throw new OqlNormalizeException('Unknown key in join condition (left expression)', $sSourceQuery, $oLeftField->GetNameDetails(), array_keys($aAllKeys)); + } + + if ($sFromClass == $sJoinClassAlias) + { + if (array_key_exists($sExtKeyAttCode, $aExtKeys)) // Skip that check for object keys + { + $sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass'); + if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass)) + { + throw new OqlNormalizeException("The joined class ($aAliases[$sFromClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails()); + } + } + } + else + { + $sOperator = $oJoinSpec->GetOperator(); + switch($sOperator) + { + case '=': + $iOperatorCode = TREE_OPERATOR_EQUALS; + break; + case 'BELOW': + $iOperatorCode = TREE_OPERATOR_BELOW; + break; + case 'BELOW_STRICT': + $iOperatorCode = TREE_OPERATOR_BELOW_STRICT; + break; + case 'NOT_BELOW': + $iOperatorCode = TREE_OPERATOR_NOT_BELOW; + break; + case 'NOT_BELOW_STRICT': + $iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT; + break; + case 'ABOVE': + $iOperatorCode = TREE_OPERATOR_ABOVE; + break; + case 'ABOVE_STRICT': + $iOperatorCode = TREE_OPERATOR_ABOVE_STRICT; + break; + case 'NOT_ABOVE': + $iOperatorCode = TREE_OPERATOR_NOT_ABOVE; + break; + case 'NOT_ABOVE_STRICT': + $iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT; + break; + } + if (array_key_exists($sExtKeyAttCode, $aExtKeys)) // Skip that check for object keys + { + $sTargetClass = $oModelReflection->GetAttributeProperty($aAliases[$sFromClass], $sExtKeyAttCode, 'targetclass'); + if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $sTargetClass)) + { + throw new OqlNormalizeException("The joined class ($aAliases[$sToClass]) is not compatible with the external key, which is pointing to $sTargetClass", $sSourceQuery, $oLeftField->GetNameDetails()); + } + } + $aAttList = $oModelReflection->ListAttributes($aAliases[$sFromClass]); + $sAttType = $aAttList[$sExtKeyAttCode]; + if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !is_subclass_of($sAttType, 'AttributeHierarchicalKey') && ($sAttType != 'AttributeHierarchicalKey')) + { + throw new OqlNormalizeException("The specified tree operator $sOperator is not applicable to the key", $sSourceQuery, $oLeftField->GetNameDetails()); + } + } + } + } + + // Check the select information + // + foreach ($this->GetSelectedClasses() as $oClassDetails) + { + $sClassToSelect = $oClassDetails->GetValue(); + if (!array_key_exists($sClassToSelect, $aAliases)) + { + throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases)); + } + } + + // Check the condition tree + // + if ($this->m_oCondition instanceof Expression) + { + $this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery); + } + } + + /** + * Make the relevant DBSearch instance (FromOQL) + */ + public function ToDBSearch($sQuery) + { + $sClass = $this->GetClass(new ModelReflectionRuntime()); + $sClassAlias = $this->GetClassAlias(); + + $oSearch = new DBObjectSearch($sClass, $sClassAlias); + $oSearch->InitFromOqlQuery($this, $sQuery); + return $oSearch; + } +} + +class OqlUnionQuery extends OqlQuery +{ + protected $aQueries; + + public function __construct(OqlObjectQuery $oLeftQuery, OqlQuery $oRightQueryOrUnion) + { + $this->aQueries[] = $oLeftQuery; + if ($oRightQueryOrUnion instanceof OqlUnionQuery) + { + foreach ($oRightQueryOrUnion->GetQueries() as $oSingleQuery) + { + $this->aQueries[] = $oSingleQuery; + } + } + else + { + $this->aQueries[] = $oRightQueryOrUnion; + } + } + + public function GetQueries() + { + return $this->aQueries; + } + + /** + * Check the validity of the expression with regard to the data model + * and the query in which it is used + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @throws OqlNormalizeException + */ + public function Check(ModelReflection $oModelReflection, $sSourceQuery) + { + $aColumnToClasses = array(); + foreach ($this->aQueries as $iQuery => $oQuery) + { + $oQuery->Check($oModelReflection, $sSourceQuery); + + $aAliasToClass = array($oQuery->GetClassAlias() => $oQuery->GetClass($oModelReflection)); + $aJoinSpecs = $oQuery->GetJoins(); + if (is_array($aJoinSpecs)) + { + foreach ($aJoinSpecs as $oJoinSpec) + { + $aAliasToClass[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass(); + } + } + + $aSelectedClasses = $oQuery->GetSelectedClasses(); + if ($iQuery != 0) + { + if (count($aSelectedClasses) < count($aColumnToClasses)) + { + $oLastClass = end($aSelectedClasses); + throw new OqlNormalizeException('Too few selected classes in the subquery', $sSourceQuery, $oLastClass); + } + if (count($aSelectedClasses) > count($aColumnToClasses)) + { + $oLastClass = end($aSelectedClasses); + throw new OqlNormalizeException('Too many selected classes in the subquery', $sSourceQuery, $oLastClass); + } + } + foreach ($aSelectedClasses as $iColumn => $oClassDetails) + { + $sAlias = $oClassDetails->GetValue(); + $sClass = $aAliasToClass[$sAlias]; + $aColumnToClasses[$iColumn][] = array( + 'alias' => $sAlias, + 'class' => $sClass, + 'class_name' => $oClassDetails, + ); + } + } + foreach ($aColumnToClasses as $iColumn => $aClasses) + { + foreach ($aClasses as $iQuery => $aData) + { + if ($iQuery == 0) + { + // Establish the reference + $sRootClass = $oModelReflection->GetRootClass($aData['class']); + } + else + { + if ($oModelReflection->GetRootClass($aData['class']) != $sRootClass) + { + $aSubclasses = $oModelReflection->EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL); + throw new OqlNormalizeException('Incompatible classes: could not find a common ancestor', $sSourceQuery, $aData['class_name'], $aSubclasses); + } + } + } + } + } + + /** + * Determine the class + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @return string + * @throws Exception + */ + public function GetClass(ModelReflection $oModelReflection) + { + $aFirstColClasses = array(); + foreach ($this->aQueries as $iQuery => $oQuery) + { + $aFirstColClasses[] = $oQuery->GetClass($oModelReflection); + } + $sClass = self::GetLowestCommonAncestor($oModelReflection, $aFirstColClasses); + if (is_null($sClass)) + { + throw new Exception('Could not determine the class of the union query. This issue should have been detected earlier by calling OqlQuery::Check()'); + } + return $sClass; + } + + /** + * Determine the class alias + * + * @return string + * @throws Exception + */ + public function GetClassAlias() + { + $sAlias = $this->aQueries[0]->GetClassAlias(); + return $sAlias; + } + + /** + * Check the validity of the expression with regard to the data model + * and the query in which it is used + * + * @param ModelReflection $oModelReflection MetaModel to consider + * @param array $aClasses Flat list of classes + * @return string the lowest common ancestor amongst classes, null if none has been found + * @throws Exception + */ + public static function GetLowestCommonAncestor(ModelReflection $oModelReflection, $aClasses) + { + $sAncestor = null; + foreach($aClasses as $sClass) + { + if (is_null($sAncestor)) + { + // first loop + $sAncestor = $sClass; + } + elseif ($sClass == $sAncestor) + { + // remains the same + } + elseif ($oModelReflection->GetRootClass($sClass) != $oModelReflection->GetRootClass($sAncestor)) + { + $sAncestor = null; + break; + } + else + { + $sAncestor = self::LowestCommonAncestor($oModelReflection, $sAncestor, $sClass); + } + } + return $sAncestor; + } + + /** + * Note: assumes that class A and B have a common ancestor + */ + protected static function LowestCommonAncestor(ModelReflection $oModelReflection, $sClassA, $sClassB) + { + if ($sClassA == $sClassB) + { + $sRet = $sClassA; + } + elseif (in_array($sClassA, $oModelReflection->EnumChildClasses($sClassB))) + { + $sRet = $sClassB; + } + elseif (in_array($sClassB, $oModelReflection->EnumChildClasses($sClassA))) + { + $sRet = $sClassA; + } + else + { + // Recurse + $sRet = self::LowestCommonAncestor($oModelReflection, $sClassA, $oModelReflection->GetParentClass($sClassB)); + } + return $sRet; + } + /** + * Make the relevant DBSearch instance (FromOQL) + */ + public function ToDBSearch($sQuery) + { + $aSearches = array(); + foreach ($this->aQueries as $oQuery) + { + $aSearches[] = $oQuery->ToDBSearch($sQuery); + } + + $oSearch = new DBUnionSearch($aSearches); + return $oSearch; + } } \ No newline at end of file diff --git a/core/ormcustomfieldsvalue.class.inc.php b/core/ormcustomfieldsvalue.class.inc.php index 0b896999d..f345ac53c 100644 --- a/core/ormcustomfieldsvalue.class.inc.php +++ b/core/ormcustomfieldsvalue.class.inc.php @@ -1,103 +1,103 @@ - - - -/** - * Base class to hold the value managed by CustomFieldsHandler - * - * @copyright Copyright (C) 2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class ormCustomFieldsValue -{ - protected $oHostObject; - protected $sAttCode; - protected $aCurrentValues; - - /** - * @param DBObject $oHostObject - * @param $sAttCode - */ - public function __construct(DBObject $oHostObject, $sAttCode, $aCurrentValues = null) - { - $this->oHostObject = $oHostObject; - $this->sAttCode = $sAttCode; - $this->aCurrentValues = $aCurrentValues; - } - - public function GetValues() - { - return $this->aCurrentValues; - } - - /** - * Wrapper used when the only thing you have is the value... - * @return \Combodo\iTop\Form\Form - */ - public function GetForm() - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - return $oAttDef->GetForm($this->oHostObject); - } - - public function GetAsHTML($bLocalize = true) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->GetValues()); - return $oHandler->GetAsHTML($this->aCurrentValues, $bLocalize); - } - - public function GetAsXML($bLocalize = true) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->GetValues()); - return $oHandler->GetAsXML($this->aCurrentValues, $bLocalize); - } - - public function GetAsCSV($sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->GetValues()); - return $oHandler->GetAsCSV($this->aCurrentValues, $sSeparator, $sTextQualifier, $bLocalize); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * @param $value mixed The current value of the field - * @param $sVerb string The verb specifying the representation of the value - * @param $bLocalize bool Whether or not to localize the value - */ - public function GetForTemplate($sVerb, $bLocalize = true) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->GetValues()); - return $oHandler->GetForTemplate($this->aCurrentValues, $sVerb, $bLocalize); - } - - /** - * @param ormCustomFieldsValue $fellow - * @return bool - */ - public function Equals(ormCustomFieldsValue $oReference) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); - $oHandler = $oAttDef->GetHandler($this->GetValues()); - return $oHandler->CompareValues($this->aCurrentValues, $oReference->aCurrentValues); - } -} + + + +/** + * Base class to hold the value managed by CustomFieldsHandler + * + * @copyright Copyright (C) 2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class ormCustomFieldsValue +{ + protected $oHostObject; + protected $sAttCode; + protected $aCurrentValues; + + /** + * @param DBObject $oHostObject + * @param $sAttCode + */ + public function __construct(DBObject $oHostObject, $sAttCode, $aCurrentValues = null) + { + $this->oHostObject = $oHostObject; + $this->sAttCode = $sAttCode; + $this->aCurrentValues = $aCurrentValues; + } + + public function GetValues() + { + return $this->aCurrentValues; + } + + /** + * Wrapper used when the only thing you have is the value... + * @return \Combodo\iTop\Form\Form + */ + public function GetForm() + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + return $oAttDef->GetForm($this->oHostObject); + } + + public function GetAsHTML($bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->GetValues()); + return $oHandler->GetAsHTML($this->aCurrentValues, $bLocalize); + } + + public function GetAsXML($bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->GetValues()); + return $oHandler->GetAsXML($this->aCurrentValues, $bLocalize); + } + + public function GetAsCSV($sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->GetValues()); + return $oHandler->GetAsCSV($this->aCurrentValues, $sSeparator, $sTextQualifier, $bLocalize); + } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $bLocalize bool Whether or not to localize the value + */ + public function GetForTemplate($sVerb, $bLocalize = true) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->GetValues()); + return $oHandler->GetForTemplate($this->aCurrentValues, $sVerb, $bLocalize); + } + + /** + * @param ormCustomFieldsValue $fellow + * @return bool + */ + public function Equals(ormCustomFieldsValue $oReference) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oHostObject), $this->sAttCode); + $oHandler = $oAttDef->GetHandler($this->GetValues()); + return $oHandler->CompareValues($this->aCurrentValues, $oReference->aCurrentValues); + } +} diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php index 585c4a3d2..3ff796f8c 100644 --- a/core/ormdocument.class.inc.php +++ b/core/ormdocument.class.inc.php @@ -1,198 +1,198 @@ - - - -/** - * ormDocument - * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob - * - * @copyright Copyright (C) 2010-2016 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * ormDocument - * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob - * - * @package itopORM - */ - -class ormDocument -{ - protected $m_data; - protected $m_sMimeType; - protected $m_sFileName; - - /** - * Constructor - */ - public function __construct($data = null, $sMimeType = 'text/plain', $sFileName = '') - { - $this->m_data = $data; - $this->m_sMimeType = $sMimeType; - $this->m_sFileName = $sFileName; - } - - public function __toString() - { - if($this->IsEmpty()) return ''; - - return MyHelpers::beautifulstr($this->m_data, 100, true); - } - - public function IsEmpty() - { - return ($this->m_data == null); - } - - public function GetMimeType() - { - return $this->m_sMimeType; - } - public function GetMainMimeType() - { - $iSeparatorPos = strpos($this->m_sMimeType, '/'); - if ($iSeparatorPos > 0) - { - return substr($this->m_sMimeType, 0, $iSeparatorPos); - } - return $this->m_sMimeType; - } - - public function GetData() - { - return $this->m_data; - } - - public function GetFileName() - { - return $this->m_sFileName; - } - - public function GetAsHTML() - { - $sResult = ''; - if ($this->IsEmpty()) - { - // If the filename is not empty, display it, this is used - // by the creation wizard while the file has not yet been uploaded - $sResult = htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8'); - } - else - { - $data = $this->GetData(); - $sResult = htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8').' [ '.$this->GetMimeType().', size: '.strlen($data).' byte(s) ]
    '; - } - return $sResult; - } - - /** - * Returns an hyperlink to display the document *inline* - * @return string - */ - public function GetDisplayLink($sClass, $Id, $sAttCode) - { - return "".htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8')."\n"; - } - - /** - * Returns an hyperlink to download the document (content-disposition: attachment) - * @return string - */ - public function GetDownloadLink($sClass, $Id, $sAttCode) - { - return "".htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8')."\n"; - } - - /** - * Returns an URL to display a document like an image - * @return string - */ - public function GetDisplayURL($sClass, $Id, $sAttCode) - { - return utils::GetAbsoluteUrlAppRoot() . "pages/ajax.render.php?operation=display_document&class=$sClass&id=$Id&field=$sAttCode"; - } - - /** - * Returns an URL to download a document like an image (uses HTTP caching) - * @return string - */ - public function GetDownloadURL($sClass, $Id, $sAttCode) - { - // Compute a signature to reset the cache anytime the data changes (this is acceptable if used only with icon files) - $sSignature = md5($this->GetData()); - return utils::GetAbsoluteUrlAppRoot() . "pages/ajax.document.php?operation=download_document&class=$sClass&id=$Id&field=$sAttCode&s=$sSignature&cache=86400"; - } - - public function IsPreviewAvailable() - { - $bRet = false; - switch($this->GetMimeType()) - { - case 'image/png': - case 'image/jpg': - case 'image/jpeg': - case 'image/gif': - $bRet = true; - break; - } - return $bRet; - } - - /** - * Downloads a document to the browser, either as 'inline' or 'attachment' - * - * @param WebPage $oPage The web page for the output - * @param string $sClass Class name of the object - * @param mixed $id Identifier of the object - * @param string $sAttCode Name of the attribute containing the document to download - * @param string $sContentDisposition Either 'inline' or 'attachment' - * @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file - * @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField - * @return none - */ - public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) - { - try - { - $oObj = MetaModel::GetObject($sClass, $id, false, false); - if (!is_object($oObj)) - { - throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it"); - } - if (($sSecretField != null) && ($oObj->Get($sSecretField) != $sSecretValue)) - { - usleep(200); - throw new Exception("Invalid secret for class '$sClass' - the object does not exist or you are not allowed to view it"); - } - $oDocument = $oObj->Get($sAttCode); - if (is_object($oDocument)) - { - $oPage->TrashUnexpectedOutput(); - $oPage->SetContentType($oDocument->GetMimeType()); - $oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName()); - $oPage->add($oDocument->GetData()); - } - } - catch(Exception $e) - { - $oPage->p($e->getMessage()); - } - } -} + + + +/** + * ormDocument + * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * ormDocument + * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob + * + * @package itopORM + */ + +class ormDocument +{ + protected $m_data; + protected $m_sMimeType; + protected $m_sFileName; + + /** + * Constructor + */ + public function __construct($data = null, $sMimeType = 'text/plain', $sFileName = '') + { + $this->m_data = $data; + $this->m_sMimeType = $sMimeType; + $this->m_sFileName = $sFileName; + } + + public function __toString() + { + if($this->IsEmpty()) return ''; + + return MyHelpers::beautifulstr($this->m_data, 100, true); + } + + public function IsEmpty() + { + return ($this->m_data == null); + } + + public function GetMimeType() + { + return $this->m_sMimeType; + } + public function GetMainMimeType() + { + $iSeparatorPos = strpos($this->m_sMimeType, '/'); + if ($iSeparatorPos > 0) + { + return substr($this->m_sMimeType, 0, $iSeparatorPos); + } + return $this->m_sMimeType; + } + + public function GetData() + { + return $this->m_data; + } + + public function GetFileName() + { + return $this->m_sFileName; + } + + public function GetAsHTML() + { + $sResult = ''; + if ($this->IsEmpty()) + { + // If the filename is not empty, display it, this is used + // by the creation wizard while the file has not yet been uploaded + $sResult = htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8'); + } + else + { + $data = $this->GetData(); + $sResult = htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8').' [ '.$this->GetMimeType().', size: '.strlen($data).' byte(s) ]
    '; + } + return $sResult; + } + + /** + * Returns an hyperlink to display the document *inline* + * @return string + */ + public function GetDisplayLink($sClass, $Id, $sAttCode) + { + return "".htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8')."\n"; + } + + /** + * Returns an hyperlink to download the document (content-disposition: attachment) + * @return string + */ + public function GetDownloadLink($sClass, $Id, $sAttCode) + { + return "".htmlentities($this->GetFileName(), ENT_QUOTES, 'UTF-8')."\n"; + } + + /** + * Returns an URL to display a document like an image + * @return string + */ + public function GetDisplayURL($sClass, $Id, $sAttCode) + { + return utils::GetAbsoluteUrlAppRoot() . "pages/ajax.render.php?operation=display_document&class=$sClass&id=$Id&field=$sAttCode"; + } + + /** + * Returns an URL to download a document like an image (uses HTTP caching) + * @return string + */ + public function GetDownloadURL($sClass, $Id, $sAttCode) + { + // Compute a signature to reset the cache anytime the data changes (this is acceptable if used only with icon files) + $sSignature = md5($this->GetData()); + return utils::GetAbsoluteUrlAppRoot() . "pages/ajax.document.php?operation=download_document&class=$sClass&id=$Id&field=$sAttCode&s=$sSignature&cache=86400"; + } + + public function IsPreviewAvailable() + { + $bRet = false; + switch($this->GetMimeType()) + { + case 'image/png': + case 'image/jpg': + case 'image/jpeg': + case 'image/gif': + $bRet = true; + break; + } + return $bRet; + } + + /** + * Downloads a document to the browser, either as 'inline' or 'attachment' + * + * @param WebPage $oPage The web page for the output + * @param string $sClass Class name of the object + * @param mixed $id Identifier of the object + * @param string $sAttCode Name of the attribute containing the document to download + * @param string $sContentDisposition Either 'inline' or 'attachment' + * @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file + * @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField + * @return none + */ + public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) + { + try + { + $oObj = MetaModel::GetObject($sClass, $id, false, false); + if (!is_object($oObj)) + { + throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it"); + } + if (($sSecretField != null) && ($oObj->Get($sSecretField) != $sSecretValue)) + { + usleep(200); + throw new Exception("Invalid secret for class '$sClass' - the object does not exist or you are not allowed to view it"); + } + $oDocument = $oObj->Get($sAttCode); + if (is_object($oDocument)) + { + $oPage->TrashUnexpectedOutput(); + $oPage->SetContentType($oDocument->GetMimeType()); + $oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName()); + $oPage->add($oDocument->GetData()); + } + } + catch(Exception $e) + { + $oPage->p($e->getMessage()); + } + } +} diff --git a/core/ormlinkset.class.inc.php b/core/ormlinkset.class.inc.php index 72fda32b5..ecd610124 100644 --- a/core/ormlinkset.class.inc.php +++ b/core/ormlinkset.class.inc.php @@ -1,750 +1,750 @@ - - -require_once('dbobjectiterator.php'); - - -/** - * The value for an attribute representing a set of links between the host object and "remote" objects - * - * @package iTopORM - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator -{ - protected $sHostClass; // subclass of DBObject - protected $sAttCode; // xxxxxx_list - protected $sClass; // class of the links - - /** - * @var DBObjectSet - */ - protected $oOriginalSet; - - /** - * @var DBObject[] array of iObjectId => DBObject - */ - protected $aOriginalObjects = null; - - /** - * @var bool - */ - protected $bHasDelta = false; - - /** - * Object from the original set, minus the removed objects - * @var DBObject[] array of iObjectId => DBObject - */ - protected $aPreserved = array(); - - /** - * @var DBObject[] New items - */ - protected $aAdded = array(); - - /** - * @var DBObject[] Modified items (could also be found in aPreserved) - */ - protected $aModified = array(); - - /** - * @var int[] Removed items - */ - protected $aRemoved = array(); - - /** - * @var int Position in the collection - */ - protected $iCursor = 0; - - /** - * __toString magical function overload. - */ - public function __toString() - { - return ''; - } - - /** - * ormLinkSet constructor. - * @param $sHostClass - * @param $sAttCode - * @param DBObjectSet|null $oOriginalSet - * @throws Exception - */ - public function __construct($sHostClass, $sAttCode, DBObjectSet $oOriginalSet = null) - { - $this->sHostClass = $sHostClass; - $this->sAttCode = $sAttCode; - $this->oOriginalSet = $oOriginalSet ? clone $oOriginalSet : null; - - $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sAttCode); - if (!$oAttDef instanceof AttributeLinkedSet) - { - throw new Exception("ormLinkSet: $sAttCode is not a link set"); - } - $this->sClass = $oAttDef->GetLinkedClass(); - if ($oOriginalSet && ($oOriginalSet->GetClass() != $this->sClass)) - { - throw new Exception("ormLinkSet: wrong class for the original set, found {$oOriginalSet->GetClass()} while expecting {$oAttDef->GetLinkedClass()}"); - } - } - - public function GetFilter() - { - return clone $this->oOriginalSet->GetFilter(); - } - - /** - * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB - * - * @param hash $aAttToLoad Format: alias => array of attribute_codes - * - * @return void - */ - public function OptimizeColumnLoad($aAttToLoad) - { - $this->oOriginalSet->OptimizeColumnLoad($aAttToLoad); - } - - /** - * @param DBObject $oLink - */ - public function AddItem(DBObject $oLink) - { - assert($oLink instanceof $this->sClass); - // No impact on the iteration algorithm - $iObjectId = $oLink->GetKey(); - $this->aAdded[$iObjectId] = $oLink; - $this->bHasDelta = true; - } - - /** - * @param DBObject $oObject - * @param string $sClassAlias - * @deprecated Since iTop 2.4, use ormLinkset->AddItem() instead. - */ - public function AddObject(DBObject $oObject, $sClassAlias = '') - { - $this->AddItem($oObject); - } - - /** - * @param $iObjectId - */ - public function RemoveItem($iObjectId) - { - if (array_key_exists($iObjectId, $this->aPreserved)) - { - unset($this->aPreserved[$iObjectId]); - $this->aRemoved[$iObjectId] = $iObjectId; - $this->bHasDelta = true; - } - else - { - if (array_key_exists($iObjectId, $this->aAdded)) - { - unset($this->aAdded[$iObjectId]); - } - } - } - - /** - * @param DBObject $oLink - */ - public function ModifyItem(DBObject $oLink) - { - assert($oLink instanceof $this->sClass); - - $iObjectId = $oLink->GetKey(); - if (array_key_exists($iObjectId, $this->aPreserved)) - { - unset($this->aPreserved[$iObjectId]); - $this->aModified[$iObjectId] = $oLink; - $this->bHasDelta = true; - } - } - - protected function LoadOriginalIds() - { - if ($this->aOriginalObjects === null) - { - if ($this->oOriginalSet) - { - $this->aOriginalObjects = $this->GetArrayOfIndex(); - $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified) - foreach ($this->aRemoved as $iObjectId) - { - if (array_key_exists($iObjectId, $this->aPreserved)) - { - unset($this->aPreserved[$iObjectId]); - } - } - foreach ($this->aModified as $iObjectId => $oLink) - { - if (array_key_exists($iObjectId, $this->aPreserved)) - { - unset($this->aPreserved[$iObjectId]); - } - } - } - else - { - - // Nothing to load - $this->aOriginalObjects = array(); - $this->aPreserved = array(); - } - } - } - - /** - * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. - * @return array - */ - protected function GetArrayOfIndex() - { - $aRet = array(); - $this->oOriginalSet->Rewind(); - $iRow = 0; - while ($oObject = $this->oOriginalSet->Fetch()) - { - $aRet[$oObject->GetKey()] = $iRow++; - } - return $aRet; - } - - /** - * @param bool $bWithId - * @return array - * @deprecated Since iTop 2.4, use foreach($this as $oItem){} instead - */ - public function ToArray($bWithId = true) - { - $aRet = array(); - foreach($this as $oItem) - { - if ($bWithId) - { - $aRet[$oItem->GetKey()] = $oItem; - } - else - { - $aRet[] = $oItem; - } - } - return $aRet; - } - - /** - * @param string $sAttCode - * @param bool $bWithId - * @return array - */ - public function GetColumnAsArray($sAttCode, $bWithId = true) - { - $aRet = array(); - foreach($this as $oItem) - { - if ($bWithId) - { - $aRet[$oItem->GetKey()] = $oItem->Get($sAttCode); - } - else - { - $aRet[] = $oItem->Get($sAttCode); - } - } - return $aRet; - } - - /** - * The class of the objects of the collection (at least a common ancestor) - * - * @return string - */ - public function GetClass() - { - return $this->sClass; - } - - /** - * The total number of objects in the collection - * - * @return int - */ - public function Count() - { - $this->LoadOriginalIds(); - $iRet = count($this->aPreserved) + count($this->aAdded) + count($this->aModified); - return $iRet; - } - - /** - * Position the cursor to the given 0-based position - * - * @param $iPosition - * @throws Exception - * @internal param int $iRow - */ - public function Seek($iPosition) - { - $this->LoadOriginalIds(); - - $iCount = $this->Count(); - if ($iPosition >= $iCount) - { - throw new Exception("Invalid position $iPosition: the link set is made of $iCount items."); - } - $this->rewind(); - for($iPos = 0 ; $iPos < $iPosition ; $iPos++) - { - $this->next(); - } - } - - /** - * Fetch the object at the current position in the collection and move the cursor to the next position. - * - * @return DBObject|null The fetched object or null when at the end - */ - public function Fetch() - { - $this->LoadOriginalIds(); - - $ret = $this->current(); - if ($ret === false) - { - $ret = null; - } - $this->next(); - return $ret; - } - - /** - * Return the current element - * @link http://php.net/manual/en/iterator.current.php - * @return mixed Can return any type. - */ - public function current() - { - $this->LoadOriginalIds(); - - $iPreservedCount = count($this->aPreserved); - if ($this->iCursor < $iPreservedCount) - { - $iRet = current($this->aPreserved); - $this->oOriginalSet->Seek($iRet); - $oRet = $this->oOriginalSet->Fetch(); - } - else - { - $iModifiedCount = count($this->aModified); - if($this->iCursor < $iPreservedCount + $iModifiedCount) - { - $oRet = current($this->aModified); - } - else - { - $oRet = current($this->aAdded); - } - } - return $oRet; - } - - /** - * Move forward to next element - * @link http://php.net/manual/en/iterator.next.php - * @return void Any returned value is ignored. - */ - public function next() - { - $this->LoadOriginalIds(); - - $iPreservedCount = count($this->aPreserved); - if ($this->iCursor < $iPreservedCount) - { - next($this->aPreserved); - } - else - { - $iModifiedCount = count($this->aModified); - if($this->iCursor < $iPreservedCount + $iModifiedCount) - { - next($this->aModified); - } - else - { - next($this->aAdded); - } - } - // Increment AFTER moving the internal cursors because when starting aModified / aAdded, we must leave it intact - $this->iCursor++; - } - - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * @return mixed scalar on success, or null on failure. - */ - public function key() - { - return $this->iCursor; - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * @return boolean The return value will be casted to boolean and then evaluated. - * Returns true on success or false on failure. - */ - public function valid() - { - $this->LoadOriginalIds(); - - $iCount = $this->Count(); - $bRet = ($this->iCursor < $iCount); - return $bRet; - } - - /** - * Rewind the Iterator to the first element - * @link http://php.net/manual/en/iterator.rewind.php - * @return void Any returned value is ignored. - */ - public function rewind() - { - $this->LoadOriginalIds(); - - $this->iCursor = 0; - reset($this->aPreserved); - reset($this->aAdded); - reset($this->aModified); - } - - public function HasDelta() - { - return $this->bHasDelta; - } - - /** - * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this. - * @param ormLinkSet $oFellow - * @return bool|null - * @throws Exception - */ - public function Equals(ormLinkSet $oFellow) - { - $bRet = null; - if ($this === $oFellow) - { - $bRet = true; - } - else - { - if ( ($this->oOriginalSet !== $oFellow->oOriginalSet) - && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) - { - throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope'); - } - if ($this->HasDelta()) - { - throw new Exception('ormLinkSet::Equals assumes that left link set had no delta'); - } - $bRet = !$oFellow->HasDelta(); - } - return $bRet; - } - - public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow) - { - if ($oFellow === $this) - { - throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one'); - } - $bUpdateFromDelta = false; - if ($oFellow instanceof ormLinkSet) - { - if ( ($this->oOriginalSet === $oFellow->oOriginalSet) - || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) - { - $bUpdateFromDelta = true; - } - } - - if ($bUpdateFromDelta) - { - // Same original set -> simply update the delta - $this->iCursor = 0; - $this->aAdded = $oFellow->aAdded; - $this->aRemoved = $oFellow->aRemoved; - $this->aModified = $oFellow->aModified; - $this->aPreserved = $oFellow->aPreserved; - $this->bHasDelta = $oFellow->bHasDelta; - } - else - { - // For backward compatibility reasons, let's rebuild a delta... - - // Reset the delta - $this->iCursor = 0; - $this->aAdded = array(); - $this->aRemoved = array(); - $this->aModified = array(); - $this->aPreserved = ($this->aOriginalObjects === null) ? array() : $this->aOriginalObjects; - $this->bHasDelta = false; - - /** @var AttributeLinkedSet $oAttDef */ - $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); - $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); - $sAdditionalKey = null; - if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) - { - $sAdditionalKey = $oAttDef->GetExtKeyToRemote(); - } - // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference) - $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey); - $aChanges = $oComparator->GetDifferences(); - foreach ($aChanges['added'] as $oLink) - { - $this->AddItem($oLink); - } - - foreach ($aChanges['modified'] as $oLink) - { - $this->ModifyItem($oLink); - } - - foreach ($aChanges['removed'] as $oLink) - { - $this->RemoveItem($oLink->GetKey()); - } - } - } - - /** - * Get the list of all modified (added, modified and removed) links - * - * @return array of link objects - * @throws \Exception - */ - public function ListModifiedLinks() - { - $aAdded = $this->aAdded; - $aModified = $this->aModified; - $aRemoved = array(); - if (count($this->aRemoved) > 0) - { - $oSearch = new DBObjectSearch($this->sClass); - $oSearch->AddCondition('id', $this->aRemoved, 'IN'); - $oSet = new DBObjectSet($oSearch); - $aRemoved = $oSet->ToArray(); - } - return array_merge($aAdded, $aModified, $aRemoved); - } - - /** - * @param DBObject $oHostObject - */ - public function DBWrite(DBObject $oHostObject) - { - /** @var AttributeLinkedSet $oAttDef */ - $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode); - $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); - $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a'; - - $aCheckLinks = array(); - $aCheckRemote = array(); - foreach ($this->aAdded as $oLink) - { - if ($oLink->IsNew()) - { - if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) - { - //todo: faire un test qui passe dans cette branche ! - $aCheckRemote[] = $oLink->Get($sExtKeyToRemote); - } - } - else - { - //todo: faire un test qui passe dans cette branche ! - $aCheckLinks[] = $oLink->GetKey(); - } - } - foreach ($this->aRemoved as $iLinkId) - { - $aCheckLinks[] = $iLinkId; - } - foreach ($this->aModified as $iLinkId => $oLink) - { - $aCheckLinks[] = $oLink->GetKey(); - } - - // Critical section : serialize any write access to these links - // - $oMtx = new iTopMutex('Write-'.$this->sClass); - $oMtx->Lock(); - - // Check for the existing links - // - /** @var DBObject[] $aExistingLinks */ - $aExistingLinks = array(); - /** @var Int[] $aExistingRemote */ - $aExistingRemote = array(); - if (count($aCheckLinks) > 0) - { - $oSearch = new DBObjectSearch($this->sClass); - $oSearch->AddCondition('id', $aCheckLinks, 'IN'); - $oSet = new DBObjectSet($oSearch); - $aExistingLinks = $oSet->ToArray(); - } - - // Check for the existing remote objects - // - if (count($aCheckRemote) > 0) - { - $oSearch = new DBObjectSearch($this->sClass); - $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '='); - $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN'); - $oSet = new DBObjectSet($oSearch); - $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote, true); - } - - // Write the links according to the existing links - // - foreach ($this->aAdded as $oLink) - { - // Make sure that the objects in the set point to "this" - $oLink->Set($sExtKeyToMe, $oHostObject->GetKey()); - - if ($oLink->IsNew()) - { - if (count($aCheckRemote) > 0) - { - $bIsDuplicate = false; - foreach($aExistingRemote as $sLinkKey => $sExtKey) - { - if ($sExtKey == $oLink->Get($sExtKeyToRemote)) - { - // Do not create a duplicate - // + In the case of a remove action followed by an add action - // of an existing link, - // the final state to consider is add action, - // so suppress the entry in the removed list. - if (array_key_exists($sLinkKey, $this->aRemoved)) - { - unset($this->aRemoved[$sLinkKey]); - } - $bIsDuplicate = true; - break; - } - } - if ($bIsDuplicate) - { - continue; - } - } - - } - else - { - if (!array_key_exists($oLink->GetKey(), $aExistingLinks)) - { - $oLink->DBClone(); - } - } - $oLink->DBWrite(); - } - foreach ($this->aRemoved as $iLinkId) - { - if (array_key_exists($iLinkId, $aExistingLinks)) - { - $oLink = $aExistingLinks[$iLinkId]; - if ($oAttDef->IsIndirect()) - { - $oLink->DBDelete(); - } - else - { - $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe); - if ($oExtKeyToRemote->IsNullAllowed()) - { - if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey()) - { - // Detach the link object from this - $oLink->Set($sExtKeyToMe, 0); - $oLink->DBUpdate(); - } - } - else - { - $oLink->DBDelete(); - } - } - } - } - // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored - foreach ($this->aModified as $iLinkId => $oLink) - { - if (array_key_exists($oLink->GetKey(), $aExistingLinks)) - { - $oLink->DBUpdate(); - } - else - { - $oLink->DBClone(); - } - } - - // End of the critical section - // - $oMtx->Unlock(); - } - - public function ToDBObjectSet($bShowObsolete = true) - { - $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); - $oLinkSearch = $this->GetFilter(); - if ($oAttDef->IsIndirect()) - { - $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); - $oLinkingAttDef = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToRemote); - $sTargetClass = $oLinkingAttDef->GetTargetClass(); - if (!$bShowObsolete && MetaModel::IsObsoletable($sTargetClass)) - { - $oNotObsolete = new BinaryExpression( - new FieldExpression('obsolescence_flag', $sTargetClass), - '=', - new ScalarExpression(0) - ); - $oNotObsoleteRemote = new DBObjectSearch($sTargetClass); - $oNotObsoleteRemote->AddConditionExpression($oNotObsolete); - $oLinkSearch->AddCondition_PointingTo($oNotObsoleteRemote, $sExtKeyToRemote); - } - } - $oLinkSet = new DBObjectSet($oLinkSearch); - $oLinkSet->SetShowObsoleteData($bShowObsolete); - if ($this->HasDelta()) - { - $oLinkSet->AddObjectArray($this->aAdded); - } - return $oLinkSet; - } + + +require_once('dbobjectiterator.php'); + + +/** + * The value for an attribute representing a set of links between the host object and "remote" objects + * + * @package iTopORM + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator +{ + protected $sHostClass; // subclass of DBObject + protected $sAttCode; // xxxxxx_list + protected $sClass; // class of the links + + /** + * @var DBObjectSet + */ + protected $oOriginalSet; + + /** + * @var DBObject[] array of iObjectId => DBObject + */ + protected $aOriginalObjects = null; + + /** + * @var bool + */ + protected $bHasDelta = false; + + /** + * Object from the original set, minus the removed objects + * @var DBObject[] array of iObjectId => DBObject + */ + protected $aPreserved = array(); + + /** + * @var DBObject[] New items + */ + protected $aAdded = array(); + + /** + * @var DBObject[] Modified items (could also be found in aPreserved) + */ + protected $aModified = array(); + + /** + * @var int[] Removed items + */ + protected $aRemoved = array(); + + /** + * @var int Position in the collection + */ + protected $iCursor = 0; + + /** + * __toString magical function overload. + */ + public function __toString() + { + return ''; + } + + /** + * ormLinkSet constructor. + * @param $sHostClass + * @param $sAttCode + * @param DBObjectSet|null $oOriginalSet + * @throws Exception + */ + public function __construct($sHostClass, $sAttCode, DBObjectSet $oOriginalSet = null) + { + $this->sHostClass = $sHostClass; + $this->sAttCode = $sAttCode; + $this->oOriginalSet = $oOriginalSet ? clone $oOriginalSet : null; + + $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sAttCode); + if (!$oAttDef instanceof AttributeLinkedSet) + { + throw new Exception("ormLinkSet: $sAttCode is not a link set"); + } + $this->sClass = $oAttDef->GetLinkedClass(); + if ($oOriginalSet && ($oOriginalSet->GetClass() != $this->sClass)) + { + throw new Exception("ormLinkSet: wrong class for the original set, found {$oOriginalSet->GetClass()} while expecting {$oAttDef->GetLinkedClass()}"); + } + } + + public function GetFilter() + { + return clone $this->oOriginalSet->GetFilter(); + } + + /** + * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB + * + * @param hash $aAttToLoad Format: alias => array of attribute_codes + * + * @return void + */ + public function OptimizeColumnLoad($aAttToLoad) + { + $this->oOriginalSet->OptimizeColumnLoad($aAttToLoad); + } + + /** + * @param DBObject $oLink + */ + public function AddItem(DBObject $oLink) + { + assert($oLink instanceof $this->sClass); + // No impact on the iteration algorithm + $iObjectId = $oLink->GetKey(); + $this->aAdded[$iObjectId] = $oLink; + $this->bHasDelta = true; + } + + /** + * @param DBObject $oObject + * @param string $sClassAlias + * @deprecated Since iTop 2.4, use ormLinkset->AddItem() instead. + */ + public function AddObject(DBObject $oObject, $sClassAlias = '') + { + $this->AddItem($oObject); + } + + /** + * @param $iObjectId + */ + public function RemoveItem($iObjectId) + { + if (array_key_exists($iObjectId, $this->aPreserved)) + { + unset($this->aPreserved[$iObjectId]); + $this->aRemoved[$iObjectId] = $iObjectId; + $this->bHasDelta = true; + } + else + { + if (array_key_exists($iObjectId, $this->aAdded)) + { + unset($this->aAdded[$iObjectId]); + } + } + } + + /** + * @param DBObject $oLink + */ + public function ModifyItem(DBObject $oLink) + { + assert($oLink instanceof $this->sClass); + + $iObjectId = $oLink->GetKey(); + if (array_key_exists($iObjectId, $this->aPreserved)) + { + unset($this->aPreserved[$iObjectId]); + $this->aModified[$iObjectId] = $oLink; + $this->bHasDelta = true; + } + } + + protected function LoadOriginalIds() + { + if ($this->aOriginalObjects === null) + { + if ($this->oOriginalSet) + { + $this->aOriginalObjects = $this->GetArrayOfIndex(); + $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified) + foreach ($this->aRemoved as $iObjectId) + { + if (array_key_exists($iObjectId, $this->aPreserved)) + { + unset($this->aPreserved[$iObjectId]); + } + } + foreach ($this->aModified as $iObjectId => $oLink) + { + if (array_key_exists($iObjectId, $this->aPreserved)) + { + unset($this->aPreserved[$iObjectId]); + } + } + } + else + { + + // Nothing to load + $this->aOriginalObjects = array(); + $this->aPreserved = array(); + } + } + } + + /** + * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. + * @return array + */ + protected function GetArrayOfIndex() + { + $aRet = array(); + $this->oOriginalSet->Rewind(); + $iRow = 0; + while ($oObject = $this->oOriginalSet->Fetch()) + { + $aRet[$oObject->GetKey()] = $iRow++; + } + return $aRet; + } + + /** + * @param bool $bWithId + * @return array + * @deprecated Since iTop 2.4, use foreach($this as $oItem){} instead + */ + public function ToArray($bWithId = true) + { + $aRet = array(); + foreach($this as $oItem) + { + if ($bWithId) + { + $aRet[$oItem->GetKey()] = $oItem; + } + else + { + $aRet[] = $oItem; + } + } + return $aRet; + } + + /** + * @param string $sAttCode + * @param bool $bWithId + * @return array + */ + public function GetColumnAsArray($sAttCode, $bWithId = true) + { + $aRet = array(); + foreach($this as $oItem) + { + if ($bWithId) + { + $aRet[$oItem->GetKey()] = $oItem->Get($sAttCode); + } + else + { + $aRet[] = $oItem->Get($sAttCode); + } + } + return $aRet; + } + + /** + * The class of the objects of the collection (at least a common ancestor) + * + * @return string + */ + public function GetClass() + { + return $this->sClass; + } + + /** + * The total number of objects in the collection + * + * @return int + */ + public function Count() + { + $this->LoadOriginalIds(); + $iRet = count($this->aPreserved) + count($this->aAdded) + count($this->aModified); + return $iRet; + } + + /** + * Position the cursor to the given 0-based position + * + * @param $iPosition + * @throws Exception + * @internal param int $iRow + */ + public function Seek($iPosition) + { + $this->LoadOriginalIds(); + + $iCount = $this->Count(); + if ($iPosition >= $iCount) + { + throw new Exception("Invalid position $iPosition: the link set is made of $iCount items."); + } + $this->rewind(); + for($iPos = 0 ; $iPos < $iPosition ; $iPos++) + { + $this->next(); + } + } + + /** + * Fetch the object at the current position in the collection and move the cursor to the next position. + * + * @return DBObject|null The fetched object or null when at the end + */ + public function Fetch() + { + $this->LoadOriginalIds(); + + $ret = $this->current(); + if ($ret === false) + { + $ret = null; + } + $this->next(); + return $ret; + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + $this->LoadOriginalIds(); + + $iPreservedCount = count($this->aPreserved); + if ($this->iCursor < $iPreservedCount) + { + $iRet = current($this->aPreserved); + $this->oOriginalSet->Seek($iRet); + $oRet = $this->oOriginalSet->Fetch(); + } + else + { + $iModifiedCount = count($this->aModified); + if($this->iCursor < $iPreservedCount + $iModifiedCount) + { + $oRet = current($this->aModified); + } + else + { + $oRet = current($this->aAdded); + } + } + return $oRet; + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + */ + public function next() + { + $this->LoadOriginalIds(); + + $iPreservedCount = count($this->aPreserved); + if ($this->iCursor < $iPreservedCount) + { + next($this->aPreserved); + } + else + { + $iModifiedCount = count($this->aModified); + if($this->iCursor < $iPreservedCount + $iModifiedCount) + { + next($this->aModified); + } + else + { + next($this->aAdded); + } + } + // Increment AFTER moving the internal cursors because when starting aModified / aAdded, we must leave it intact + $this->iCursor++; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + */ + public function key() + { + return $this->iCursor; + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + */ + public function valid() + { + $this->LoadOriginalIds(); + + $iCount = $this->Count(); + $bRet = ($this->iCursor < $iCount); + return $bRet; + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() + { + $this->LoadOriginalIds(); + + $this->iCursor = 0; + reset($this->aPreserved); + reset($this->aAdded); + reset($this->aModified); + } + + public function HasDelta() + { + return $this->bHasDelta; + } + + /** + * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this. + * @param ormLinkSet $oFellow + * @return bool|null + * @throws Exception + */ + public function Equals(ormLinkSet $oFellow) + { + $bRet = null; + if ($this === $oFellow) + { + $bRet = true; + } + else + { + if ( ($this->oOriginalSet !== $oFellow->oOriginalSet) + && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) + { + throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope'); + } + if ($this->HasDelta()) + { + throw new Exception('ormLinkSet::Equals assumes that left link set had no delta'); + } + $bRet = !$oFellow->HasDelta(); + } + return $bRet; + } + + public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow) + { + if ($oFellow === $this) + { + throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one'); + } + $bUpdateFromDelta = false; + if ($oFellow instanceof ormLinkSet) + { + if ( ($this->oOriginalSet === $oFellow->oOriginalSet) + || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) + { + $bUpdateFromDelta = true; + } + } + + if ($bUpdateFromDelta) + { + // Same original set -> simply update the delta + $this->iCursor = 0; + $this->aAdded = $oFellow->aAdded; + $this->aRemoved = $oFellow->aRemoved; + $this->aModified = $oFellow->aModified; + $this->aPreserved = $oFellow->aPreserved; + $this->bHasDelta = $oFellow->bHasDelta; + } + else + { + // For backward compatibility reasons, let's rebuild a delta... + + // Reset the delta + $this->iCursor = 0; + $this->aAdded = array(); + $this->aRemoved = array(); + $this->aModified = array(); + $this->aPreserved = ($this->aOriginalObjects === null) ? array() : $this->aOriginalObjects; + $this->bHasDelta = false; + + /** @var AttributeLinkedSet $oAttDef */ + $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); + $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + $sAdditionalKey = null; + if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) + { + $sAdditionalKey = $oAttDef->GetExtKeyToRemote(); + } + // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference) + $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey); + $aChanges = $oComparator->GetDifferences(); + foreach ($aChanges['added'] as $oLink) + { + $this->AddItem($oLink); + } + + foreach ($aChanges['modified'] as $oLink) + { + $this->ModifyItem($oLink); + } + + foreach ($aChanges['removed'] as $oLink) + { + $this->RemoveItem($oLink->GetKey()); + } + } + } + + /** + * Get the list of all modified (added, modified and removed) links + * + * @return array of link objects + * @throws \Exception + */ + public function ListModifiedLinks() + { + $aAdded = $this->aAdded; + $aModified = $this->aModified; + $aRemoved = array(); + if (count($this->aRemoved) > 0) + { + $oSearch = new DBObjectSearch($this->sClass); + $oSearch->AddCondition('id', $this->aRemoved, 'IN'); + $oSet = new DBObjectSet($oSearch); + $aRemoved = $oSet->ToArray(); + } + return array_merge($aAdded, $aModified, $aRemoved); + } + + /** + * @param DBObject $oHostObject + */ + public function DBWrite(DBObject $oHostObject) + { + /** @var AttributeLinkedSet $oAttDef */ + $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode); + $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); + $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a'; + + $aCheckLinks = array(); + $aCheckRemote = array(); + foreach ($this->aAdded as $oLink) + { + if ($oLink->IsNew()) + { + if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) + { + //todo: faire un test qui passe dans cette branche ! + $aCheckRemote[] = $oLink->Get($sExtKeyToRemote); + } + } + else + { + //todo: faire un test qui passe dans cette branche ! + $aCheckLinks[] = $oLink->GetKey(); + } + } + foreach ($this->aRemoved as $iLinkId) + { + $aCheckLinks[] = $iLinkId; + } + foreach ($this->aModified as $iLinkId => $oLink) + { + $aCheckLinks[] = $oLink->GetKey(); + } + + // Critical section : serialize any write access to these links + // + $oMtx = new iTopMutex('Write-'.$this->sClass); + $oMtx->Lock(); + + // Check for the existing links + // + /** @var DBObject[] $aExistingLinks */ + $aExistingLinks = array(); + /** @var Int[] $aExistingRemote */ + $aExistingRemote = array(); + if (count($aCheckLinks) > 0) + { + $oSearch = new DBObjectSearch($this->sClass); + $oSearch->AddCondition('id', $aCheckLinks, 'IN'); + $oSet = new DBObjectSet($oSearch); + $aExistingLinks = $oSet->ToArray(); + } + + // Check for the existing remote objects + // + if (count($aCheckRemote) > 0) + { + $oSearch = new DBObjectSearch($this->sClass); + $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '='); + $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN'); + $oSet = new DBObjectSet($oSearch); + $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote, true); + } + + // Write the links according to the existing links + // + foreach ($this->aAdded as $oLink) + { + // Make sure that the objects in the set point to "this" + $oLink->Set($sExtKeyToMe, $oHostObject->GetKey()); + + if ($oLink->IsNew()) + { + if (count($aCheckRemote) > 0) + { + $bIsDuplicate = false; + foreach($aExistingRemote as $sLinkKey => $sExtKey) + { + if ($sExtKey == $oLink->Get($sExtKeyToRemote)) + { + // Do not create a duplicate + // + In the case of a remove action followed by an add action + // of an existing link, + // the final state to consider is add action, + // so suppress the entry in the removed list. + if (array_key_exists($sLinkKey, $this->aRemoved)) + { + unset($this->aRemoved[$sLinkKey]); + } + $bIsDuplicate = true; + break; + } + } + if ($bIsDuplicate) + { + continue; + } + } + + } + else + { + if (!array_key_exists($oLink->GetKey(), $aExistingLinks)) + { + $oLink->DBClone(); + } + } + $oLink->DBWrite(); + } + foreach ($this->aRemoved as $iLinkId) + { + if (array_key_exists($iLinkId, $aExistingLinks)) + { + $oLink = $aExistingLinks[$iLinkId]; + if ($oAttDef->IsIndirect()) + { + $oLink->DBDelete(); + } + else + { + $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe); + if ($oExtKeyToRemote->IsNullAllowed()) + { + if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey()) + { + // Detach the link object from this + $oLink->Set($sExtKeyToMe, 0); + $oLink->DBUpdate(); + } + } + else + { + $oLink->DBDelete(); + } + } + } + } + // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored + foreach ($this->aModified as $iLinkId => $oLink) + { + if (array_key_exists($oLink->GetKey(), $aExistingLinks)) + { + $oLink->DBUpdate(); + } + else + { + $oLink->DBClone(); + } + } + + // End of the critical section + // + $oMtx->Unlock(); + } + + public function ToDBObjectSet($bShowObsolete = true) + { + $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); + $oLinkSearch = $this->GetFilter(); + if ($oAttDef->IsIndirect()) + { + $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); + $oLinkingAttDef = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToRemote); + $sTargetClass = $oLinkingAttDef->GetTargetClass(); + if (!$bShowObsolete && MetaModel::IsObsoletable($sTargetClass)) + { + $oNotObsolete = new BinaryExpression( + new FieldExpression('obsolescence_flag', $sTargetClass), + '=', + new ScalarExpression(0) + ); + $oNotObsoleteRemote = new DBObjectSearch($sTargetClass); + $oNotObsoleteRemote->AddConditionExpression($oNotObsolete); + $oLinkSearch->AddCondition_PointingTo($oNotObsoleteRemote, $sExtKeyToRemote); + } + } + $oLinkSet = new DBObjectSet($oLinkSearch); + $oLinkSet->SetShowObsoleteData($bShowObsolete); + if ($this->HasDelta()) + { + $oLinkSet->AddObjectArray($this->aAdded); + } + return $oLinkSet; + } } \ No newline at end of file diff --git a/core/ormpassword.class.inc.php b/core/ormpassword.class.inc.php index 25fcb63c3..4b008f992 100644 --- a/core/ormpassword.class.inc.php +++ b/core/ormpassword.class.inc.php @@ -1,129 +1,129 @@ - - - -require_once(APPROOT.'/core/simplecrypt.class.inc.php'); - -/** - * ormPassword - * encapsulate the behavior of a one way encrypted password stored hashed - * with a per password (as random as possible) salt, in order to prevent a "Rainbow table" hack. - * If a cryptographic random number generator is available (on Linux or Windows) - * it will be used for generating the salt. - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @package itopORM - */ - -class ormPassword -{ - protected $m_sHashed; - protected $m_sSalt; - - /** - * Constructor, initializes the password from the encrypted values - */ - public function __construct($sHash = '', $sSalt = '') - { - $this->m_sHashed = $sHash; - //only used for <= 2.5 hashed password - $this->m_sSalt = $sSalt; - } - - /** - * Encrypts the clear text password, with a unique salt - */ - public function SetPassword($sClearTextPassword) - { - $this->m_sHashed = password_hash($sClearTextPassword, PASSWORD_DEFAULT); - } - - /** - * Print the password: displays some stars - * @return string - */ - public function __toString() - { - return '*****'; // Password can not be read - } - - public function IsEmpty() - { - return ($this->m_hashed == null); - } - - public function GetHash() - { - return $this->m_sHashed; - } - - public function GetSalt() - { - return $this->m_sSalt; - } - - /** - * Displays the password: displays some stars - * @return string - */ - public function GetAsHTML() - { - return '*****'; // Password can not be read - } - - /** - * Check if the supplied clear text password matches the encrypted one - * @param string $sClearTextPassword - * @return boolean True if it matches, false otherwise - */ - public function CheckPassword($sClearTextPassword) - { - $bResult = false; - $aInfo = password_get_info($this->m_sHashed); - switch ($aInfo["algo"]) - { - case 0: - //unknown, assume it's a legacy password - $sHashedPwd = $this->ComputeHash($sClearTextPassword); - if ($this->m_sHashed == $sHashedPwd) - { - $bResult = true; - } - break; - default: - $bResult = password_verify($sClearTextPassword, $this->m_sHashed); - } - return $bResult; - } - - /** - * Computes the hashed version of a password using a unique salt - * for this password. A unique salt is generated if needed - * @return string - */ - protected function ComputeHash($sClearTextPwd) - { - if ($this->m_sSalt == null) - { - $this->m_sSalt = SimpleCrypt::GetNewSalt(); - } - return hash('sha256', $this->m_sSalt.$sClearTextPwd); - } -} + + + +require_once(APPROOT.'/core/simplecrypt.class.inc.php'); + +/** + * ormPassword + * encapsulate the behavior of a one way encrypted password stored hashed + * with a per password (as random as possible) salt, in order to prevent a "Rainbow table" hack. + * If a cryptographic random number generator is available (on Linux or Windows) + * it will be used for generating the salt. + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @package itopORM + */ + +class ormPassword +{ + protected $m_sHashed; + protected $m_sSalt; + + /** + * Constructor, initializes the password from the encrypted values + */ + public function __construct($sHash = '', $sSalt = '') + { + $this->m_sHashed = $sHash; + //only used for <= 2.5 hashed password + $this->m_sSalt = $sSalt; + } + + /** + * Encrypts the clear text password, with a unique salt + */ + public function SetPassword($sClearTextPassword) + { + $this->m_sHashed = password_hash($sClearTextPassword, PASSWORD_DEFAULT); + } + + /** + * Print the password: displays some stars + * @return string + */ + public function __toString() + { + return '*****'; // Password can not be read + } + + public function IsEmpty() + { + return ($this->m_hashed == null); + } + + public function GetHash() + { + return $this->m_sHashed; + } + + public function GetSalt() + { + return $this->m_sSalt; + } + + /** + * Displays the password: displays some stars + * @return string + */ + public function GetAsHTML() + { + return '*****'; // Password can not be read + } + + /** + * Check if the supplied clear text password matches the encrypted one + * @param string $sClearTextPassword + * @return boolean True if it matches, false otherwise + */ + public function CheckPassword($sClearTextPassword) + { + $bResult = false; + $aInfo = password_get_info($this->m_sHashed); + switch ($aInfo["algo"]) + { + case 0: + //unknown, assume it's a legacy password + $sHashedPwd = $this->ComputeHash($sClearTextPassword); + if ($this->m_sHashed == $sHashedPwd) + { + $bResult = true; + } + break; + default: + $bResult = password_verify($sClearTextPassword, $this->m_sHashed); + } + return $bResult; + } + + /** + * Computes the hashed version of a password using a unique salt + * for this password. A unique salt is generated if needed + * @return string + */ + protected function ComputeHash($sClearTextPwd) + { + if ($this->m_sSalt == null) + { + $this->m_sSalt = SimpleCrypt::GetNewSalt(); + } + return hash('sha256', $this->m_sSalt.$sClearTextPwd); + } +} ?> \ No newline at end of file diff --git a/core/ormstopwatch.class.inc.php b/core/ormstopwatch.class.inc.php index 956a2da63..751302e49 100644 --- a/core/ormstopwatch.class.inc.php +++ b/core/ormstopwatch.class.inc.php @@ -1,585 +1,585 @@ - - -require_once('backgroundprocess.inc.php'); - -/** - * ormStopWatch - * encapsulate the behavior of a stop watch that will be stored as an attribute of class AttributeStopWatch - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * ormStopWatch - * encapsulate the behavior of a stop watch that will be stored as an attribute of class AttributeStopWatch - * - * @package itopORM - */ -class ormStopWatch -{ - protected $iTimeSpent; // seconds - protected $iStarted; // unix time (seconds) - protected $iLastStart; // unix time (seconds) - protected $iStopped; // unix time (seconds) - protected $aThresholds; - - /** - * Constructor - */ - public function __construct($iTimeSpent = 0, $iStarted = null, $iLastStart = null, $iStopped = null) - { - $this->iTimeSpent = (int) $iTimeSpent; - $this->iStarted = $iStarted; - $this->iLastStart = $iLastStart; - $this->iStopped = $iStopped; - - $this->aThresholds = array(); - } - - /** - * Necessary for the triggers - */ - public function __toString() - { - return (string) $this->iTimeSpent; - } - - public function DefineThreshold($iPercent, $tDeadline = null, $bPassed = false, $bTriggered = false, $iOverrun = null, $aHighlightDef = null) - { - $this->aThresholds[$iPercent] = array( - 'deadline' => $tDeadline, // unix time (seconds) - 'triggered' => $bTriggered, - 'overrun' => $iOverrun, - 'highlight' => $aHighlightDef, // array('code' => string, 'persistent' => boolean) - ); - } - - public function MarkThresholdAsTriggered($iPercent) - { - $this->aThresholds[$iPercent]['triggered'] = true; - } - - public function GetTimeSpent() - { - return $this->iTimeSpent; - } - - /** - * Get the working elapsed time since the start of the stop watch - * even if it is currently running - * @param oAttDef AttributeDefinition Attribute hosting the stop watch - * @param oObject Hosting object (used for query parameters) - */ - public function GetElapsedTime($oAttDef, $oObject) - { - if (is_null($this->iLastStart)) - { - return $this->GetTimeSpent(); - } - else - { - $iElapsed = $this->ComputeDuration($oObject, $oAttDef, $this->iLastStart, time()); - return $this->iTimeSpent + $iElapsed; - } - } - - - public function GetStartDate() - { - return $this->iStarted; - } - - public function GetLastStartDate() - { - return $this->iLastStart; - } - - public function GetStopDate() - { - return $this->iStopped; - } - - public function GetThresholdDate($iPercent) - { - if (array_key_exists($iPercent, $this->aThresholds)) - { - return $this->aThresholds[$iPercent]['deadline']; - } - else - { - return null; - } - } - - public function GetOverrun($iPercent) - { - if (array_key_exists($iPercent, $this->aThresholds)) - { - return $this->aThresholds[$iPercent]['overrun']; - } - else - { - return null; - } - } - public function IsThresholdPassed($iPercent) - { - $bRet = false; - if (array_key_exists($iPercent, $this->aThresholds)) - { - $aThresholdData = $this->aThresholds[$iPercent]; - if (!is_null($aThresholdData['deadline']) && ($aThresholdData['deadline'] <= time())) - { - $bRet = true; - } - if (isset($aThresholdData['overrun']) && ($aThresholdData['overrun'] > 0)) - { - $bRet = true; - } - } - return $bRet; - } - public function IsThresholdTriggered($iPercent) - { - if (array_key_exists($iPercent, $this->aThresholds)) - { - return $this->aThresholds[$iPercent]['triggered']; - } - else - { - return false; - } - } - - public function GetHighlightCode() - { - $sCode = ''; - // Process the thresholds in ascending order - $aPercents = array(); - foreach($this->aThresholds as $iPercent => $aDefs) - { - $aPercents[] = $iPercent; - } - sort($aPercents, SORT_NUMERIC); - foreach($aPercents as $iPercent) - { - $aDefs = $this->aThresholds[$iPercent]; - if (array_key_exists('highlight', $aDefs) && is_array($aDefs['highlight']) && $this->IsThresholdPassed($iPercent)) - { - // If persistant or SW running... - if (($aDefs['highlight']['persistent'] == true) || (($aDefs['highlight']['persistent'] == false) && !is_null($this->iLastStart))) - { - $sCode = $aDefs['highlight']['code']; - } - } - } - return $sCode; - } - - public function GetAsHTML($oAttDef, $oHostObject = null) - { - $aProperties = array(); - - $aProperties['States'] = implode(', ', $oAttDef->GetStates()); - - if (is_null($this->iLastStart)) - { - if (is_null($this->iStarted)) - { - $aProperties['Elapsed'] = 'never started'; - } - else - { - $aProperties['Elapsed'] = $this->iTimeSpent.' s'; - } - } - else - { - $aProperties['Elapsed'] = 'running '; - } - - $aProperties['Started'] = $oAttDef->SecondsToDate($this->iStarted); - $aProperties['LastStart'] = $oAttDef->SecondsToDate($this->iLastStart); - $aProperties['Stopped'] = $oAttDef->SecondsToDate($this->iStopped); - - foreach ($this->aThresholds as $iPercent => $aThresholdData) - { - $sThresholdDesc = $oAttDef->SecondsToDate($aThresholdData['deadline']); - if ($aThresholdData['triggered']) - { - $sThresholdDesc .= " TRIGGERED"; - } - if ($aThresholdData['overrun']) - { - $sThresholdDesc .= " Overrun:".(int) $aThresholdData['overrun']." sec."; - } - $aProperties[$iPercent.'%'] = $sThresholdDesc; - } - $sRes = ""; - $sRes .= ""; - foreach ($aProperties as $sProperty => $sValue) - { - $sRes .= ""; - $sCell = str_replace("\n", "
    \n", $sValue); - $sRes .= ""; - $sRes .= ""; - } - $sRes .= ""; - $sRes .= "
    $sProperty$sCell
    "; - return $sRes; - } - - protected function ComputeGoal($oObject, $oAttDef) - { - $sMetricComputer = $oAttDef->Get('goal_computing'); - $oComputer = new $sMetricComputer(); - $aCallSpec = array($oComputer, 'ComputeMetric'); - if (!is_callable($aCallSpec)) - { - throw new CoreException("Unknown class/verb '$sMetricComputer/ComputeMetric'"); - } - $iRet = call_user_func($aCallSpec, $oObject); - return $iRet; - } - - protected function ComputeDeadline($oObject, $oAttDef, $iPercent, $iStartTime, $iDurationSec) - { - $sWorkingTimeComputer = $oAttDef->Get('working_time_computing'); - if ($sWorkingTimeComputer == '') - { - $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; - } - $aCallSpec = array($sWorkingTimeComputer, '__construct'); - if (!is_callable($aCallSpec)) - { - //throw new CoreException("Pas de constructeur pour $sWorkingTimeComputer!"); - } - $oComputer = new $sWorkingTimeComputer(); - $aCallSpec = array($oComputer, 'GetDeadline'); - if (!is_callable($aCallSpec)) - { - throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetDeadline'"); - } - // GetDeadline($oObject, $iDuration, DateTime $oStartDate) - $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 - $oDeadline = call_user_func($aCallSpec, $oObject, $iDurationSec, $oStartDate, $iPercent); - $iRet = $oDeadline->format('U'); - return $iRet; - } - - protected function ComputeDuration($oObject, $oAttDef, $iStartTime, $iEndTime) - { - $sWorkingTimeComputer = $oAttDef->Get('working_time_computing'); - if ($sWorkingTimeComputer == '') - { - $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; - } - $oComputer = new $sWorkingTimeComputer(); - $aCallSpec = array($oComputer, 'GetOpenDuration'); - if (!is_callable($aCallSpec)) - { - throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'"); - } - // GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate) - $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 - $oEndDate = new DateTime('@'.$iEndTime); - $iRet = call_user_func($aCallSpec, $oObject, $oStartDate, $oEndDate); - return $iRet; - } - - public function Reset($oObject, $oAttDef) - { - $this->iTimeSpent = 0; - $this->iStopped = null; - $this->iStarted = null; - - foreach ($this->aThresholds as $iPercent => &$aThresholdData) - { - $aThresholdData['triggered'] = false; - $aThresholdData['overrun'] = null; - } - - if (!is_null($this->iLastStart)) - { - // Currently running... starting again from now! - $this->iStarted = time(); - $this->iLastStart = time(); - $this->ComputeDeadlines($oObject, $oAttDef); - } - } - - /** - * Start or continue - * It is the responsibility of the caller to compute the deadlines - * (to avoid computing twice for the same result) - */ - public function Start($oObject, $oAttDef, $iNow = null) - { - if (!is_null($this->iLastStart)) - { - // Already started - return false; - } - - if (is_null($iNow)) - { - $iNow = time(); - } - - if (is_null($this->iStarted)) - { - $this->iStarted = $iNow; - } - $this->iLastStart = $iNow; - $this->iStopped = null; - - return true; - } - - /** - * Compute or recompute the goal and threshold deadlines - */ - public function ComputeDeadlines($oObject, $oAttDef) - { - if (is_null($this->iLastStart)) - { - // Currently stopped - do nothing - return false; - } - - $iDurationGoal = $this->ComputeGoal($oObject, $oAttDef); - $iComputationRefTime = time(); - foreach ($this->aThresholds as $iPercent => &$aThresholdData) - { - if (is_null($iDurationGoal)) - { - // No limit: leave null thresholds - $aThresholdData['deadline'] = null; - } - else - { - $iThresholdDuration = round($iPercent * $iDurationGoal / 100); - - if (class_exists('WorkingTimeRecorder')) - { - $sClass = get_class($oObject); - $sAttCode = $oAttDef->GetCode(); - WorkingTimeRecorder::Start($oObject, $iComputationRefTime, "ormStopWatch-Deadline-$iPercent-$sAttCode", 'Core:ExplainWTC:StopWatch-Deadline', array("Class:$sClass/Attribute:$sAttCode", $iPercent)); - } - $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $iPercent, $this->iLastStart, $iThresholdDuration - $this->iTimeSpent); - // OR $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $iPercent, $this->iStarted, $iThresholdDuration); - - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::End(); - } - } - if (is_null($aThresholdData['deadline']) || ($aThresholdData['deadline'] > time())) - { - // The threshold is in the future, reset - $aThresholdData['triggered'] = false; - $aThresholdData['overrun'] = null; - } - else - { - // The new threshold is in the past - // Note: the overrun can be wrong, but the correct algorithm to compute - // the overrun of a deadline in the past requires that the ormStopWatch keeps track of all its history!!! - } - } - - return true; - } - - /** - * Stop counting if not already done - */ - public function Stop($oObject, $oAttDef, $iNow = null) - { - if (is_null($this->iLastStart)) - { - // Already stopped - return false; - } - - if (is_null($iNow)) - { - $iNow = time(); - } - - if (class_exists('WorkingTimeRecorder')) - { - $sClass = get_class($oObject); - $sAttCode = $oAttDef->GetCode(); - WorkingTimeRecorder::Start($oObject, $iNow, "ormStopWatch-TimeSpent-$sAttCode", 'Core:ExplainWTC:StopWatch-TimeSpent', array("Class:$sClass/Attribute:$sAttCode"), true /*cumulative*/); - } - $iElapsed = $this->ComputeDuration($oObject, $oAttDef, $this->iLastStart, $iNow); - $this->iTimeSpent = $this->iTimeSpent + $iElapsed; - if (class_exists('WorkingTimeRecorder')) - { - WorkingTimeRecorder::End(); - } - - foreach ($this->aThresholds as $iPercent => &$aThresholdData) - { - if (!is_null($aThresholdData['deadline']) && ($iNow > $aThresholdData['deadline'])) - { - if ($aThresholdData['overrun'] > 0) - { - // Accumulate from last start - $aThresholdData['overrun'] += $iElapsed; - } - else - { - // First stop after the deadline has been passed - $iOverrun = $this->ComputeDuration($oObject, $oAttDef, $aThresholdData['deadline'], $iNow); - $aThresholdData['overrun'] = $iOverrun; - } - } - $aThresholdData['deadline'] = null; - } - - $this->iLastStart = null; - $this->iStopped = $iNow; - - return true; - } -} - -/** - * CheckStopWatchThresholds - * Implements the automatic actions - * - * @package itopORM - */ -class CheckStopWatchThresholds implements iBackgroundProcess -{ - public function GetPeriodicity() - { - return 10; // seconds - } - - public function Process($iTimeLimit) - { - $aList = array(); - foreach (MetaModel::GetClasses() as $sClass) - { - foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeStopWatch) - { - foreach ($oAttDef->ListThresholds() as $iThreshold => $aThresholdData) - { - $iPercent = $aThresholdData['percent']; // could be different than the index ! - - $sNow = date(AttributeDateTime::GetSQLFormat()); - $sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < '$sNow'"; - $oFilter = DBObjectSearch::FromOQL($sExpression); - $oSet = new DBObjectSet($oFilter); - $oSet->OptimizeColumnLoad(array($sAttCode)); - while ((time() < $iTimeLimit) && ($oObj = $oSet->Fetch())) - { - $sClass = get_class($oObj); - - $aList[] = $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold; - - // Execute planned actions - // - foreach ($aThresholdData['actions'] as $aActionData) - { - $sVerb = $aActionData['verb']; - $aParams = $aActionData['params']; - $aValues = array(); - foreach($aParams as $def) - { - if (is_string($def)) - { - // Old method (pre-2.1.0) non typed parameters - $aValues[] = $def; - } - else // if(is_array($def)) - { - $sParamType = array_key_exists('type', $def) ? $def['type'] : 'string'; - switch($sParamType) - { - case 'int': - $value = (int)$def['value']; - break; - - case 'float': - $value = (float)$def['value']; - break; - - case 'bool': - $value = (bool)$def['value']; - break; - - case 'reference': - $value = ${$def['value']}; - break; - - case 'string': - default: - $value = (string)$def['value']; - } - $aValues[] = $value; - } - } - $aCallSpec = array($oObj, $sVerb); - call_user_func_array($aCallSpec, $aValues); - } - - // Mark the threshold as "triggered" - // - $oSW = $oObj->Get($sAttCode); - $oSW->MarkThresholdAsTriggered($iThreshold); - $oObj->Set($sAttCode, $oSW); - - if($oObj->IsModified()) - { - CMDBObject::SetTrackInfo("Automatic - threshold triggered"); - - $oMyChange = CMDBObject::GetCurrentChange(); - $oObj->DBUpdateTracked($oMyChange, true /*skip security*/); - } - - // Activate any existing trigger - // - $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); - $oTriggerSet = new DBObjectSet( - DBObjectSearch::FromOQL("SELECT TriggerOnThresholdReached AS t WHERE t.target_class IN ('$sClassList') AND stop_watch_code=:stop_watch_code AND threshold_index = :threshold_index"), - array(), // order by - array('stop_watch_code' => $sAttCode, 'threshold_index' => $iThreshold) - ); - while ($oTrigger = $oTriggerSet->Fetch()) - { - $oTrigger->DoActivate($oObj->ToArgs('this')); - } - } - } - } - } - } - - $iProcessed = count($aList); - return "Triggered $iProcessed threshold(s):".implode(", ", $aList); - } -} + + +require_once('backgroundprocess.inc.php'); + +/** + * ormStopWatch + * encapsulate the behavior of a stop watch that will be stored as an attribute of class AttributeStopWatch + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * ormStopWatch + * encapsulate the behavior of a stop watch that will be stored as an attribute of class AttributeStopWatch + * + * @package itopORM + */ +class ormStopWatch +{ + protected $iTimeSpent; // seconds + protected $iStarted; // unix time (seconds) + protected $iLastStart; // unix time (seconds) + protected $iStopped; // unix time (seconds) + protected $aThresholds; + + /** + * Constructor + */ + public function __construct($iTimeSpent = 0, $iStarted = null, $iLastStart = null, $iStopped = null) + { + $this->iTimeSpent = (int) $iTimeSpent; + $this->iStarted = $iStarted; + $this->iLastStart = $iLastStart; + $this->iStopped = $iStopped; + + $this->aThresholds = array(); + } + + /** + * Necessary for the triggers + */ + public function __toString() + { + return (string) $this->iTimeSpent; + } + + public function DefineThreshold($iPercent, $tDeadline = null, $bPassed = false, $bTriggered = false, $iOverrun = null, $aHighlightDef = null) + { + $this->aThresholds[$iPercent] = array( + 'deadline' => $tDeadline, // unix time (seconds) + 'triggered' => $bTriggered, + 'overrun' => $iOverrun, + 'highlight' => $aHighlightDef, // array('code' => string, 'persistent' => boolean) + ); + } + + public function MarkThresholdAsTriggered($iPercent) + { + $this->aThresholds[$iPercent]['triggered'] = true; + } + + public function GetTimeSpent() + { + return $this->iTimeSpent; + } + + /** + * Get the working elapsed time since the start of the stop watch + * even if it is currently running + * @param oAttDef AttributeDefinition Attribute hosting the stop watch + * @param oObject Hosting object (used for query parameters) + */ + public function GetElapsedTime($oAttDef, $oObject) + { + if (is_null($this->iLastStart)) + { + return $this->GetTimeSpent(); + } + else + { + $iElapsed = $this->ComputeDuration($oObject, $oAttDef, $this->iLastStart, time()); + return $this->iTimeSpent + $iElapsed; + } + } + + + public function GetStartDate() + { + return $this->iStarted; + } + + public function GetLastStartDate() + { + return $this->iLastStart; + } + + public function GetStopDate() + { + return $this->iStopped; + } + + public function GetThresholdDate($iPercent) + { + if (array_key_exists($iPercent, $this->aThresholds)) + { + return $this->aThresholds[$iPercent]['deadline']; + } + else + { + return null; + } + } + + public function GetOverrun($iPercent) + { + if (array_key_exists($iPercent, $this->aThresholds)) + { + return $this->aThresholds[$iPercent]['overrun']; + } + else + { + return null; + } + } + public function IsThresholdPassed($iPercent) + { + $bRet = false; + if (array_key_exists($iPercent, $this->aThresholds)) + { + $aThresholdData = $this->aThresholds[$iPercent]; + if (!is_null($aThresholdData['deadline']) && ($aThresholdData['deadline'] <= time())) + { + $bRet = true; + } + if (isset($aThresholdData['overrun']) && ($aThresholdData['overrun'] > 0)) + { + $bRet = true; + } + } + return $bRet; + } + public function IsThresholdTriggered($iPercent) + { + if (array_key_exists($iPercent, $this->aThresholds)) + { + return $this->aThresholds[$iPercent]['triggered']; + } + else + { + return false; + } + } + + public function GetHighlightCode() + { + $sCode = ''; + // Process the thresholds in ascending order + $aPercents = array(); + foreach($this->aThresholds as $iPercent => $aDefs) + { + $aPercents[] = $iPercent; + } + sort($aPercents, SORT_NUMERIC); + foreach($aPercents as $iPercent) + { + $aDefs = $this->aThresholds[$iPercent]; + if (array_key_exists('highlight', $aDefs) && is_array($aDefs['highlight']) && $this->IsThresholdPassed($iPercent)) + { + // If persistant or SW running... + if (($aDefs['highlight']['persistent'] == true) || (($aDefs['highlight']['persistent'] == false) && !is_null($this->iLastStart))) + { + $sCode = $aDefs['highlight']['code']; + } + } + } + return $sCode; + } + + public function GetAsHTML($oAttDef, $oHostObject = null) + { + $aProperties = array(); + + $aProperties['States'] = implode(', ', $oAttDef->GetStates()); + + if (is_null($this->iLastStart)) + { + if (is_null($this->iStarted)) + { + $aProperties['Elapsed'] = 'never started'; + } + else + { + $aProperties['Elapsed'] = $this->iTimeSpent.' s'; + } + } + else + { + $aProperties['Elapsed'] = 'running '; + } + + $aProperties['Started'] = $oAttDef->SecondsToDate($this->iStarted); + $aProperties['LastStart'] = $oAttDef->SecondsToDate($this->iLastStart); + $aProperties['Stopped'] = $oAttDef->SecondsToDate($this->iStopped); + + foreach ($this->aThresholds as $iPercent => $aThresholdData) + { + $sThresholdDesc = $oAttDef->SecondsToDate($aThresholdData['deadline']); + if ($aThresholdData['triggered']) + { + $sThresholdDesc .= " TRIGGERED"; + } + if ($aThresholdData['overrun']) + { + $sThresholdDesc .= " Overrun:".(int) $aThresholdData['overrun']." sec."; + } + $aProperties[$iPercent.'%'] = $sThresholdDesc; + } + $sRes = ""; + $sRes .= ""; + foreach ($aProperties as $sProperty => $sValue) + { + $sRes .= ""; + $sCell = str_replace("\n", "
    \n", $sValue); + $sRes .= ""; + $sRes .= ""; + } + $sRes .= ""; + $sRes .= "
    $sProperty$sCell
    "; + return $sRes; + } + + protected function ComputeGoal($oObject, $oAttDef) + { + $sMetricComputer = $oAttDef->Get('goal_computing'); + $oComputer = new $sMetricComputer(); + $aCallSpec = array($oComputer, 'ComputeMetric'); + if (!is_callable($aCallSpec)) + { + throw new CoreException("Unknown class/verb '$sMetricComputer/ComputeMetric'"); + } + $iRet = call_user_func($aCallSpec, $oObject); + return $iRet; + } + + protected function ComputeDeadline($oObject, $oAttDef, $iPercent, $iStartTime, $iDurationSec) + { + $sWorkingTimeComputer = $oAttDef->Get('working_time_computing'); + if ($sWorkingTimeComputer == '') + { + $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; + } + $aCallSpec = array($sWorkingTimeComputer, '__construct'); + if (!is_callable($aCallSpec)) + { + //throw new CoreException("Pas de constructeur pour $sWorkingTimeComputer!"); + } + $oComputer = new $sWorkingTimeComputer(); + $aCallSpec = array($oComputer, 'GetDeadline'); + if (!is_callable($aCallSpec)) + { + throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetDeadline'"); + } + // GetDeadline($oObject, $iDuration, DateTime $oStartDate) + $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 + $oDeadline = call_user_func($aCallSpec, $oObject, $iDurationSec, $oStartDate, $iPercent); + $iRet = $oDeadline->format('U'); + return $iRet; + } + + protected function ComputeDuration($oObject, $oAttDef, $iStartTime, $iEndTime) + { + $sWorkingTimeComputer = $oAttDef->Get('working_time_computing'); + if ($sWorkingTimeComputer == '') + { + $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; + } + $oComputer = new $sWorkingTimeComputer(); + $aCallSpec = array($oComputer, 'GetOpenDuration'); + if (!is_callable($aCallSpec)) + { + throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'"); + } + // GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate) + $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 + $oEndDate = new DateTime('@'.$iEndTime); + $iRet = call_user_func($aCallSpec, $oObject, $oStartDate, $oEndDate); + return $iRet; + } + + public function Reset($oObject, $oAttDef) + { + $this->iTimeSpent = 0; + $this->iStopped = null; + $this->iStarted = null; + + foreach ($this->aThresholds as $iPercent => &$aThresholdData) + { + $aThresholdData['triggered'] = false; + $aThresholdData['overrun'] = null; + } + + if (!is_null($this->iLastStart)) + { + // Currently running... starting again from now! + $this->iStarted = time(); + $this->iLastStart = time(); + $this->ComputeDeadlines($oObject, $oAttDef); + } + } + + /** + * Start or continue + * It is the responsibility of the caller to compute the deadlines + * (to avoid computing twice for the same result) + */ + public function Start($oObject, $oAttDef, $iNow = null) + { + if (!is_null($this->iLastStart)) + { + // Already started + return false; + } + + if (is_null($iNow)) + { + $iNow = time(); + } + + if (is_null($this->iStarted)) + { + $this->iStarted = $iNow; + } + $this->iLastStart = $iNow; + $this->iStopped = null; + + return true; + } + + /** + * Compute or recompute the goal and threshold deadlines + */ + public function ComputeDeadlines($oObject, $oAttDef) + { + if (is_null($this->iLastStart)) + { + // Currently stopped - do nothing + return false; + } + + $iDurationGoal = $this->ComputeGoal($oObject, $oAttDef); + $iComputationRefTime = time(); + foreach ($this->aThresholds as $iPercent => &$aThresholdData) + { + if (is_null($iDurationGoal)) + { + // No limit: leave null thresholds + $aThresholdData['deadline'] = null; + } + else + { + $iThresholdDuration = round($iPercent * $iDurationGoal / 100); + + if (class_exists('WorkingTimeRecorder')) + { + $sClass = get_class($oObject); + $sAttCode = $oAttDef->GetCode(); + WorkingTimeRecorder::Start($oObject, $iComputationRefTime, "ormStopWatch-Deadline-$iPercent-$sAttCode", 'Core:ExplainWTC:StopWatch-Deadline', array("Class:$sClass/Attribute:$sAttCode", $iPercent)); + } + $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $iPercent, $this->iLastStart, $iThresholdDuration - $this->iTimeSpent); + // OR $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $iPercent, $this->iStarted, $iThresholdDuration); + + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::End(); + } + } + if (is_null($aThresholdData['deadline']) || ($aThresholdData['deadline'] > time())) + { + // The threshold is in the future, reset + $aThresholdData['triggered'] = false; + $aThresholdData['overrun'] = null; + } + else + { + // The new threshold is in the past + // Note: the overrun can be wrong, but the correct algorithm to compute + // the overrun of a deadline in the past requires that the ormStopWatch keeps track of all its history!!! + } + } + + return true; + } + + /** + * Stop counting if not already done + */ + public function Stop($oObject, $oAttDef, $iNow = null) + { + if (is_null($this->iLastStart)) + { + // Already stopped + return false; + } + + if (is_null($iNow)) + { + $iNow = time(); + } + + if (class_exists('WorkingTimeRecorder')) + { + $sClass = get_class($oObject); + $sAttCode = $oAttDef->GetCode(); + WorkingTimeRecorder::Start($oObject, $iNow, "ormStopWatch-TimeSpent-$sAttCode", 'Core:ExplainWTC:StopWatch-TimeSpent', array("Class:$sClass/Attribute:$sAttCode"), true /*cumulative*/); + } + $iElapsed = $this->ComputeDuration($oObject, $oAttDef, $this->iLastStart, $iNow); + $this->iTimeSpent = $this->iTimeSpent + $iElapsed; + if (class_exists('WorkingTimeRecorder')) + { + WorkingTimeRecorder::End(); + } + + foreach ($this->aThresholds as $iPercent => &$aThresholdData) + { + if (!is_null($aThresholdData['deadline']) && ($iNow > $aThresholdData['deadline'])) + { + if ($aThresholdData['overrun'] > 0) + { + // Accumulate from last start + $aThresholdData['overrun'] += $iElapsed; + } + else + { + // First stop after the deadline has been passed + $iOverrun = $this->ComputeDuration($oObject, $oAttDef, $aThresholdData['deadline'], $iNow); + $aThresholdData['overrun'] = $iOverrun; + } + } + $aThresholdData['deadline'] = null; + } + + $this->iLastStart = null; + $this->iStopped = $iNow; + + return true; + } +} + +/** + * CheckStopWatchThresholds + * Implements the automatic actions + * + * @package itopORM + */ +class CheckStopWatchThresholds implements iBackgroundProcess +{ + public function GetPeriodicity() + { + return 10; // seconds + } + + public function Process($iTimeLimit) + { + $aList = array(); + foreach (MetaModel::GetClasses() as $sClass) + { + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeStopWatch) + { + foreach ($oAttDef->ListThresholds() as $iThreshold => $aThresholdData) + { + $iPercent = $aThresholdData['percent']; // could be different than the index ! + + $sNow = date(AttributeDateTime::GetSQLFormat()); + $sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < '$sNow'"; + $oFilter = DBObjectSearch::FromOQL($sExpression); + $oSet = new DBObjectSet($oFilter); + $oSet->OptimizeColumnLoad(array($sAttCode)); + while ((time() < $iTimeLimit) && ($oObj = $oSet->Fetch())) + { + $sClass = get_class($oObj); + + $aList[] = $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold; + + // Execute planned actions + // + foreach ($aThresholdData['actions'] as $aActionData) + { + $sVerb = $aActionData['verb']; + $aParams = $aActionData['params']; + $aValues = array(); + foreach($aParams as $def) + { + if (is_string($def)) + { + // Old method (pre-2.1.0) non typed parameters + $aValues[] = $def; + } + else // if(is_array($def)) + { + $sParamType = array_key_exists('type', $def) ? $def['type'] : 'string'; + switch($sParamType) + { + case 'int': + $value = (int)$def['value']; + break; + + case 'float': + $value = (float)$def['value']; + break; + + case 'bool': + $value = (bool)$def['value']; + break; + + case 'reference': + $value = ${$def['value']}; + break; + + case 'string': + default: + $value = (string)$def['value']; + } + $aValues[] = $value; + } + } + $aCallSpec = array($oObj, $sVerb); + call_user_func_array($aCallSpec, $aValues); + } + + // Mark the threshold as "triggered" + // + $oSW = $oObj->Get($sAttCode); + $oSW->MarkThresholdAsTriggered($iThreshold); + $oObj->Set($sAttCode, $oSW); + + if($oObj->IsModified()) + { + CMDBObject::SetTrackInfo("Automatic - threshold triggered"); + + $oMyChange = CMDBObject::GetCurrentChange(); + $oObj->DBUpdateTracked($oMyChange, true /*skip security*/); + } + + // Activate any existing trigger + // + $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); + $oTriggerSet = new DBObjectSet( + DBObjectSearch::FromOQL("SELECT TriggerOnThresholdReached AS t WHERE t.target_class IN ('$sClassList') AND stop_watch_code=:stop_watch_code AND threshold_index = :threshold_index"), + array(), // order by + array('stop_watch_code' => $sAttCode, 'threshold_index' => $iThreshold) + ); + while ($oTrigger = $oTriggerSet->Fetch()) + { + $oTrigger->DoActivate($oObj->ToArgs('this')); + } + } + } + } + } + } + + $iProcessed = count($aList); + return "Triggered $iProcessed threshold(s):".implode(", ", $aList); + } +} diff --git a/core/querymodifier.class.inc.php b/core/querymodifier.class.inc.php index 3085d97ea..b68a70362 100644 --- a/core/querymodifier.class.inc.php +++ b/core/querymodifier.class.inc.php @@ -1,34 +1,34 @@ - - - -/** - * Interface iQueryModifier - * Defines the API to tweak queries (e.g. translate data on the fly) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -interface iQueryModifier -{ - public function __construct(); - - public function GetFieldExpression(QueryBuilderContext &$oBuild, $sClass, $sAttCode, $sColId, Expression $oFieldSQLExp, SQLQuery &$oSelect); -} -?> + + + +/** + * Interface iQueryModifier + * Defines the API to tweak queries (e.g. translate data on the fly) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +interface iQueryModifier +{ + public function __construct(); + + public function GetFieldExpression(QueryBuilderContext &$oBuild, $sClass, $sAttCode, $sColId, Expression $oFieldSQLExp, SQLQuery &$oSelect); +} +?> diff --git a/core/restservices.class.inc.php b/core/restservices.class.inc.php index 1d1f3dcd8..916dd5f72 100644 --- a/core/restservices.class.inc.php +++ b/core/restservices.class.inc.php @@ -1,777 +1,777 @@ - - -/** - * REST/json services - * - * Definition of common structures + the very minimum service provider (manage objects) - * - * @package REST Services - * @copyright Copyright (C) 2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @api - */ - -/** - * Element of the response formed by RestResultWithObjects - * - * @package REST Services - */ -class ObjectResult -{ - public $code; - public $message; - public $class; - public $key; - public $fields; - - /** - * Default constructor - */ - public function __construct($sClass = null, $iId = null) - { - $this->code = RestResult::OK; - $this->message = ''; - $this->class = $sClass; - $this->key = $iId; - $this->fields = array(); - } - - /** - * Helper to make an output value for a given attribute - * - * @param DBObject $oObject The object being reported - * @param string $sAttCode The attribute code (must be valid) - * @param boolean $bExtendedOutput Output all of the link set attributes ? - * @return string A scalar representation of the value - */ - protected function MakeResultValue(DBObject $oObject, $sAttCode, $bExtendedOutput = false) - { - if ($sAttCode == 'id') - { - $value = $oObject->GetKey(); - } - else - { - $sClass = get_class($oObject); - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef instanceof AttributeLinkedSet) - { - // Iterate on the set and build an array of array of attcode=>value - $oSet = $oObject->Get($sAttCode); - $value = array(); - while ($oLnk = $oSet->Fetch()) - { - $sLnkRefClass = $bExtendedOutput ? get_class($oLnk) : $oAttDef->GetLinkedClass(); - - $aLnkValues = array(); - foreach (MetaModel::ListAttributeDefs($sLnkRefClass) as $sLnkAttCode => $oLnkAttDef) - { - // Skip attributes pointing to the current object (redundant data) - if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) - { - continue; - } - // Skip any attribute of the link that points to the current object - $oLnkAttDef = MetaModel::GetAttributeDef($sLnkRefClass, $sLnkAttCode); - if (method_exists($oLnkAttDef, 'GetKeyAttCode')) - { - if ($oLnkAttDef->GetKeyAttCode() == $oAttDef->GetExtKeyToMe()) - { - continue; - } - } - - $aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode, $bExtendedOutput); - } - $value[] = $aLnkValues; - } - } - else - { - $value = $oAttDef->GetForJSON($oObject->Get($sAttCode)); - } - } - return $value; - } - - /** - * Report the value for the given object attribute - * - * @param DBObject $oObject The object being reported - * @param string $sAttCode The attribute code (must be valid) - * @param boolean $bExtendedOutput Output all of the link set attributes ? - * @return void - */ - public function AddField(DBObject $oObject, $sAttCode, $bExtendedOutput = false) - { - $this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput); - } -} - - - -/** - * REST response for services managing objects. Derive this structure to add information and/or constants - * - * @package Extensibility - * @package REST Services - * @api - */ -class RestResultWithObjects extends RestResult -{ - public $objects; - - /** - * Report the given object - * - * @param int An error code (RestResult::OK is no issue has been found) - * @param string $sMessage Description of the error if any, an empty string otherwise - * @param DBObject $oObject The object being reported - * @param array $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported. - * @param boolean $bExtendedOutput Output all of the link set attributes ? - * @return void - */ - public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false) - { - $sClass = get_class($oObject); - $oObjRes = new ObjectResult($sClass, $oObject->GetKey()); - $oObjRes->code = $iCode; - $oObjRes->message = $sMessage; - - $aFields = null; - if (!is_null($aFieldSpec)) - { - // Enum all classes in the hierarchy, starting with the current one - foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sRefClass) - { - if (array_key_exists($sRefClass, $aFieldSpec)) - { - $aFields = $aFieldSpec[$sRefClass]; - break; - } - } - } - if (is_null($aFields)) - { - // No fieldspec given, or not found... - $aFields = array('id', 'friendlyname'); - } - - foreach ($aFields as $sAttCode) - { - $oObjRes->AddField($oObject, $sAttCode, $bExtendedOutput); - } - - $sObjKey = get_class($oObject).'::'.$oObject->GetKey(); - $this->objects[$sObjKey] = $oObjRes; - } -} - -class RestResultWithRelations extends RestResultWithObjects -{ - public $relations; - - public function __construct() - { - parent::__construct(); - $this->relations = array(); - } - - public function AddRelation($sSrcKey, $sDestKey) - { - if (!array_key_exists($sSrcKey, $this->relations)) - { - $this->relations[$sSrcKey] = array(); - } - $this->relations[$sSrcKey][] = array('key' => $sDestKey); - } -} - -/** - * Deletion result codes for a target object (either deleted or updated) - * - * @package Extensibility - * @api - * @since 2.0.1 - */ -class RestDelete -{ - /** - * Result: Object deleted as per the initial request - */ - const OK = 0; - /** - * Result: general issue (user rights or ... ?) - */ - const ISSUE = 1; - /** - * Result: Must be deleted to preserve database integrity - */ - const AUTO_DELETE = 2; - /** - * Result: Must be deleted to preserve database integrity, but that is NOT possible - */ - const AUTO_DELETE_ISSUE = 3; - /** - * Result: Must be deleted to preserve database integrity, but this must be requested explicitely - */ - const REQUEST_EXPLICITELY = 4; - /** - * Result: Must be updated to preserve database integrity - */ - const AUTO_UPDATE = 5; - /** - * Result: Must be updated to preserve database integrity, but that is NOT possible - */ - const AUTO_UPDATE_ISSUE = 6; -} - -/** - * Implementation of core REST services (create/get/update... objects) - * - * @package Core - */ -class CoreServices implements iRestServiceProvider -{ - /** - * Enumerate services delivered by this class - * - * @param string $sVersion The version (e.g. 1.0) supported by the services - * @return array An array of hash 'verb' => verb, 'description' => description - */ - public function ListOperations($sVersion) - { - // 1.3 - iTop 2.2.0, Verb 'get_related': added the options 'redundancy' and 'direction' to take into account the redundancy in the impact analysis - // 1.2 - was documented in the wiki but never released ! Same as 1.3 - // 1.1 - In the reply, objects have a 'key' entry so that it is no more necessary to split class::key programmaticaly - // 1.0 - Initial implementation in iTop 2.0.1 - // - $aOps = array(); - if (in_array($sVersion, array('1.0', '1.1', '1.2', '1.3'))) - { - $aOps[] = array( - 'verb' => 'core/create', - 'description' => 'Create an object' - ); - $aOps[] = array( - 'verb' => 'core/update', - 'description' => 'Update an object' - ); - $aOps[] = array( - 'verb' => 'core/apply_stimulus', - 'description' => 'Apply a stimulus to change the state of an object' - ); - $aOps[] = array( - 'verb' => 'core/get', - 'description' => 'Search for objects' - ); - $aOps[] = array( - 'verb' => 'core/delete', - 'description' => 'Delete objects' - ); - $aOps[] = array( - 'verb' => 'core/get_related', - 'description' => 'Get related objects through the specified relation' - ); - $aOps[] = array( - 'verb' => 'core/check_credentials', - 'description' => 'Check user credentials' - ); - } - return $aOps; - } - - /** - * Enumerate services delivered by this class - * @param string $sVersion The version (e.g. 1.0) supported by the services - * @return RestResult The standardized result structure (at least a message) - * @throws Exception in case of internal failure. - */ - public function ExecOperation($sVersion, $sVerb, $aParams) - { - $oResult = new RestResultWithObjects(); - switch ($sVerb) - { - case 'core/create': - RestUtils::InitTrackingComment($aParams); - $sClass = RestUtils::GetClass($aParams, 'class'); - $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); - $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); - $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); - - if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for creating data of class $sClass"; - } - elseif (UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for massively creating data of class $sClass"; - } - else - { - $oObject = RestUtils::MakeObjectFromFields($sClass, $aFields); - $oObject->DBInsert(); - $oResult->AddObject(0, 'created', $oObject, $aShowFields, $bExtendedOutput); - } - break; - - case 'core/update': - RestUtils::InitTrackingComment($aParams); - $sClass = RestUtils::GetClass($aParams, 'class'); - $key = RestUtils::GetMandatoryParam($aParams, 'key'); - $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); - $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); - $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); - - // Note: the target class cannot be based on the result of FindObjectFromKey, because in case the user does not have read access, that function already fails with msg 'Nothing found' - $sTargetClass = RestUtils::GetObjectSetFromKey($sClass, $key)->GetFilter()->GetClass(); - if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for modifying data of class $sTargetClass"; - } - elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for massively modifying data of class $sTargetClass"; - } - else - { - $oObject = RestUtils::FindObjectFromKey($sClass, $key); - RestUtils::UpdateObjectFromFields($oObject, $aFields); - $oObject->DBUpdate(); - $oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput); - } - break; - - case 'core/apply_stimulus': - RestUtils::InitTrackingComment($aParams); - $sClass = RestUtils::GetClass($aParams, 'class'); - $key = RestUtils::GetMandatoryParam($aParams, 'key'); - $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); - $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); - $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); - $sStimulus = RestUtils::GetMandatoryParam($aParams, 'stimulus'); - - // Note: the target class cannot be based on the result of FindObjectFromKey, because in case the user does not have read access, that function already fails with msg 'Nothing found' - $sTargetClass = RestUtils::GetObjectSetFromKey($sClass, $key)->GetFilter()->GetClass(); - if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for modifying data of class $sTargetClass"; - } - elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_BULK_MODIFY) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for massively modifying data of class $sTargetClass"; - } - else - { - $oObject = RestUtils::FindObjectFromKey($sClass, $key); - RestUtils::UpdateObjectFromFields($oObject, $aFields); - - $aTransitions = $oObject->EnumTransitions(); - $aStimuli = MetaModel::EnumStimuli(get_class($oObject)); - if (!isset($aTransitions[$sStimulus])) - { - // Invalid stimulus - $oResult->code = RestResult::INTERNAL_ERROR; - $oResult->message = "Invalid stimulus: '$sStimulus' on the object ".$oObject->GetName()." in state '".$oObject->GetState()."'"; - } - else - { - $aTransition = $aTransitions[$sStimulus]; - $sTargetState = $aTransition['target_state']; - $aStates = MetaModel::EnumStates($sClass); - $aTargetStateDef = $aStates[$sTargetState]; - $aExpectedAttributes = $aTargetStateDef['attribute_list']; - - $aMissingMandatory = array(); - foreach($aExpectedAttributes as $sAttCode => $iExpectCode) - { - if ( ($iExpectCode & OPT_ATT_MANDATORY) && ($oObject->Get($sAttCode) == '')) - { - $aMissingMandatory[] = $sAttCode; - } - } - if (count($aMissingMandatory) == 0) - { - // If all the mandatory fields are already present, just apply the transition silently... - if ($oObject->ApplyStimulus($sStimulus)) - { - $oObject->DBUpdate(); - $oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput); - } - } - else - { - // Missing mandatory attributes for the transition - $oResult->code = RestResult::INTERNAL_ERROR; - $oResult->message = 'Missing mandatory attribute(s) for applying the stimulus: '.implode(', ', $aMissingMandatory).'.'; - } - } - } - break; - - case 'core/get': - $sClass = RestUtils::GetClass($aParams, 'class'); - $key = RestUtils::GetMandatoryParam($aParams, 'key'); - $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); - $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); - - $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); - $sTargetClass = $oObjectSet->GetFilter()->GetClass(); - - if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for reading data of class $sTargetClass"; - } - elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_BULK_READ) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for exporting data of class $sTargetClass"; - } - else - { - while ($oObject = $oObjectSet->Fetch()) - { - $oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput); - } - $oResult->message = "Found: ".$oObjectSet->Count(); - } - break; - - case 'core/delete': - $sClass = RestUtils::GetClass($aParams, 'class'); - $key = RestUtils::GetMandatoryParam($aParams, 'key'); - $bSimulate = RestUtils::GetOptionalParam($aParams, 'simulate', false); - - $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); - $sTargetClass = $oObjectSet->GetFilter()->GetClass(); - - if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_DELETE) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for deleting data of class $sTargetClass"; - } - elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_DELETE) != UR_ALLOWED_YES) - { - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for massively deleting data of class $sTargetClass"; - } - else - { - $aObjects = $oObjectSet->ToArray(); - $this->DeleteObjects($oResult, $aObjects, $bSimulate); - } - break; - - case 'core/get_related': - $oResult = new RestResultWithRelations(); - $sClass = RestUtils::GetClass($aParams, 'class'); - $key = RestUtils::GetMandatoryParam($aParams, 'key'); - $sRelation = RestUtils::GetMandatoryParam($aParams, 'relation'); - $iMaxRecursionDepth = RestUtils::GetOptionalParam($aParams, 'depth', 20 /* = MAX_RECURSION_DEPTH */); - $sDirection = RestUtils::GetOptionalParam($aParams, 'direction', null); - $bEnableRedundancy = RestUtils::GetOptionalParam($aParams, 'redundancy', false); - $bReverse = false; - - if (is_null($sDirection) && ($sRelation == 'depends on')) - { - // Legacy behavior, consider "depends on" as a forward relation - $sRelation = 'impacts'; - $sDirection = 'up'; - $bReverse = true; // emulate the legacy behavior by returning the edges - } - else if(is_null($sDirection)) - { - $sDirection = 'down'; - } - - $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); - if ($sDirection == 'down') - { - $oRelationGraph = $oObjectSet->GetRelatedObjectsDown($sRelation, $iMaxRecursionDepth, $bEnableRedundancy); - } - else if ($sDirection == 'up') - { - $oRelationGraph = $oObjectSet->GetRelatedObjectsUp($sRelation, $iMaxRecursionDepth, $bEnableRedundancy); - } - else - { - $oResult->code = RestResult::INTERNAL_ERROR; - $oResult->message = "Invalid value: '$sDirection' for the parameter 'direction'. Valid values are 'up' and 'down'"; - return $oResult; - - } - - if ($bEnableRedundancy) - { - // Remove the redundancy nodes from the output - $oIterator = new RelationTypeIterator($oRelationGraph, 'Node'); - foreach($oIterator as $oNode) - { - if ($oNode instanceof RelationRedundancyNode) - { - $oRelationGraph->FilterNode($oNode); - } - } - } - - $aIndexByClass = array(); - $oIterator = new RelationTypeIterator($oRelationGraph); - foreach($oIterator as $oElement) - { - if ($oElement instanceof RelationObjectNode) - { - $oObject = $oElement->GetProperty('object'); - if ($oObject) - { - if ($bEnableRedundancy) - { - // Add only the "reached" objects - if ($oElement->GetProperty('is_reached')) - { - $aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null; - $oResult->AddObject(0, '', $oObject); - } - } - else - { - $aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null; - $oResult->AddObject(0, '', $oObject); - } - } - } - else if ($oElement instanceof RelationEdge) - { - $oSrcObj = $oElement->GetSourceNode()->GetProperty('object'); - $oDestObj = $oElement->GetSinkNode()->GetProperty('object'); - $sSrcKey = get_class($oSrcObj).'::'.$oSrcObj->GetKey(); - $sDestKey = get_class($oDestObj).'::'.$oDestObj->GetKey(); - if ($bEnableRedundancy) - { - // Add only the edges where both source and destination are "reached" - if ($oElement->GetSourceNode()->GetProperty('is_reached') && $oElement->GetSinkNode()->GetProperty('is_reached')) - { - if ($bReverse) - { - $oResult->AddRelation($sDestKey, $sSrcKey); - } - else - { - $oResult->AddRelation($sSrcKey, $sDestKey); - } - } - } - else - { - if ($bReverse) - { - $oResult->AddRelation($sDestKey, $sSrcKey); - } - else - { - $oResult->AddRelation($sSrcKey, $sDestKey); - } - } - } - } - - if (count($aIndexByClass) > 0) - { - $aStats = array(); - $aUnauthorizedClasses = array(); - foreach ($aIndexByClass as $sClass => $aIds) - { - if (UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_READ) != UR_ALLOWED_YES) - { - $aUnauthorizedClasses[$sClass] = true; - } - $aStats[] = $sClass.'= '.count($aIds); - } - if (count($aUnauthorizedClasses) > 0) - { - $sClasses = implode(', ', array_keys($aUnauthorizedClasses)); - $oResult = new RestResult(); - $oResult->code = RestResult::UNAUTHORIZED; - $oResult->message = "The current user does not have enough permissions for exporting data of class(es): $sClasses"; - } - else - { - $oResult->message = "Scope: ".$oObjectSet->Count()."; Related objects: ".implode(', ', $aStats); - } - } - else - { - $oResult->message = "Nothing found"; - } - break; - - case 'core/check_credentials': - $oResult = new RestResult(); - $sUser = RestUtils::GetMandatoryParam($aParams, 'user'); - $sPassword = RestUtils::GetMandatoryParam($aParams, 'password'); - - if (UserRights::CheckCredentials($sUser, $sPassword) !== true) - { - $oResult->authorized = false; - } - else - { - $oResult->authorized = true; - } - break; - - default: - // unknown operation: handled at a higher level - } - return $oResult; - } - - /** - * Helper for object deletion - */ - public function DeleteObjects($oResult, $aObjects, $bSimulate) - { - $oDeletionPlan = new DeletionPlan(); - foreach($aObjects as $oObj) - { - if ($bSimulate) - { - $oObj->CheckToDelete($oDeletionPlan); - } - else - { - $oObj->DBDelete($oDeletionPlan); - } - } - - foreach ($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes) - { - foreach ($aDeletes as $iId => $aData) - { - $oToDelete = $aData['to_delete']; - $bAutoDel = (($aData['mode'] == DEL_SILENT) || ($aData['mode'] == DEL_AUTO)); - if (array_key_exists('issue', $aData)) - { - if ($bAutoDel) - { - if (isset($aData['requested_explicitely'])) // i.e. in the initial list of objects to delete - { - $iCode = RestDelete::ISSUE; - $sPlanned = 'Cannot be deleted: '.$aData['issue']; - } - else - { - $iCode = RestDelete::AUTO_DELETE_ISSUE; - $sPlanned = 'Should be deleted automatically... but: '.$aData['issue']; - } - } - else - { - $iCode = RestDelete::REQUEST_EXPLICITELY; - $sPlanned = 'Must be deleted explicitely... but: '.$aData['issue']; - } - } - else - { - if ($bAutoDel) - { - if (isset($aData['requested_explicitely'])) - { - $iCode = RestDelete::OK; - $sPlanned = ''; - } - else - { - $iCode = RestDelete::AUTO_DELETE; - $sPlanned = 'Deleted automatically'; - } - } - else - { - $iCode = RestDelete::REQUEST_EXPLICITELY; - $sPlanned = 'Must be deleted explicitely'; - } - } - $oResult->AddObject($iCode, $sPlanned, $oToDelete); - } - } - foreach ($oDeletionPlan->ListUpdates() as $sRemoteClass => $aToUpdate) - { - foreach ($aToUpdate as $iId => $aData) - { - $oToUpdate = $aData['to_reset']; - if (array_key_exists('issue', $aData)) - { - $iCode = RestDelete::AUTO_UPDATE_ISSUE; - $sPlanned = 'Should be updated automatically... but: '.$aData['issue']; - } - else - { - $iCode = RestDelete::AUTO_UPDATE; - $sPlanned = 'Reset external keys: '.$aData['attributes_list']; - } - $oResult->AddObject($iCode, $sPlanned, $oToUpdate); - } - } - - if ($oDeletionPlan->FoundStopper()) - { - if ($oDeletionPlan->FoundSecurityIssue()) - { - $iRes = RestResult::UNAUTHORIZED; - $sRes = 'Deletion not allowed on some objects'; - } - elseif ($oDeletionPlan->FoundManualOperation()) - { - $iRes = RestResult::UNSAFE; - $sRes = 'The deletion requires that other objects be deleted/updated, and those operations must be requested explicitely'; - } - else - { - $iRes = RestResult::INTERNAL_ERROR; - $sRes = 'Some issues have been encountered. See the list of planned changes for more information about the issue(s).'; - } - } - else - { - $iRes = RestResult::OK; - $sRes = 'Deleted: '.count($aObjects); - $iIndirect = $oDeletionPlan->GetTargetCount() - count($aObjects); - if ($iIndirect > 0) - { - $sRes .= ' plus (for DB integrity) '.$iIndirect; - } - } - $oResult->code = $iRes; - if ($bSimulate) - { - $oResult->message = 'SIMULATING: '.$sRes; - } - else - { - $oResult->message = $sRes; - } - } -} + + +/** + * REST/json services + * + * Definition of common structures + the very minimum service provider (manage objects) + * + * @package REST Services + * @copyright Copyright (C) 2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @api + */ + +/** + * Element of the response formed by RestResultWithObjects + * + * @package REST Services + */ +class ObjectResult +{ + public $code; + public $message; + public $class; + public $key; + public $fields; + + /** + * Default constructor + */ + public function __construct($sClass = null, $iId = null) + { + $this->code = RestResult::OK; + $this->message = ''; + $this->class = $sClass; + $this->key = $iId; + $this->fields = array(); + } + + /** + * Helper to make an output value for a given attribute + * + * @param DBObject $oObject The object being reported + * @param string $sAttCode The attribute code (must be valid) + * @param boolean $bExtendedOutput Output all of the link set attributes ? + * @return string A scalar representation of the value + */ + protected function MakeResultValue(DBObject $oObject, $sAttCode, $bExtendedOutput = false) + { + if ($sAttCode == 'id') + { + $value = $oObject->GetKey(); + } + else + { + $sClass = get_class($oObject); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef instanceof AttributeLinkedSet) + { + // Iterate on the set and build an array of array of attcode=>value + $oSet = $oObject->Get($sAttCode); + $value = array(); + while ($oLnk = $oSet->Fetch()) + { + $sLnkRefClass = $bExtendedOutput ? get_class($oLnk) : $oAttDef->GetLinkedClass(); + + $aLnkValues = array(); + foreach (MetaModel::ListAttributeDefs($sLnkRefClass) as $sLnkAttCode => $oLnkAttDef) + { + // Skip attributes pointing to the current object (redundant data) + if ($sLnkAttCode == $oAttDef->GetExtKeyToMe()) + { + continue; + } + // Skip any attribute of the link that points to the current object + $oLnkAttDef = MetaModel::GetAttributeDef($sLnkRefClass, $sLnkAttCode); + if (method_exists($oLnkAttDef, 'GetKeyAttCode')) + { + if ($oLnkAttDef->GetKeyAttCode() == $oAttDef->GetExtKeyToMe()) + { + continue; + } + } + + $aLnkValues[$sLnkAttCode] = $this->MakeResultValue($oLnk, $sLnkAttCode, $bExtendedOutput); + } + $value[] = $aLnkValues; + } + } + else + { + $value = $oAttDef->GetForJSON($oObject->Get($sAttCode)); + } + } + return $value; + } + + /** + * Report the value for the given object attribute + * + * @param DBObject $oObject The object being reported + * @param string $sAttCode The attribute code (must be valid) + * @param boolean $bExtendedOutput Output all of the link set attributes ? + * @return void + */ + public function AddField(DBObject $oObject, $sAttCode, $bExtendedOutput = false) + { + $this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput); + } +} + + + +/** + * REST response for services managing objects. Derive this structure to add information and/or constants + * + * @package Extensibility + * @package REST Services + * @api + */ +class RestResultWithObjects extends RestResult +{ + public $objects; + + /** + * Report the given object + * + * @param int An error code (RestResult::OK is no issue has been found) + * @param string $sMessage Description of the error if any, an empty string otherwise + * @param DBObject $oObject The object being reported + * @param array $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported. + * @param boolean $bExtendedOutput Output all of the link set attributes ? + * @return void + */ + public function AddObject($iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false) + { + $sClass = get_class($oObject); + $oObjRes = new ObjectResult($sClass, $oObject->GetKey()); + $oObjRes->code = $iCode; + $oObjRes->message = $sMessage; + + $aFields = null; + if (!is_null($aFieldSpec)) + { + // Enum all classes in the hierarchy, starting with the current one + foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sRefClass) + { + if (array_key_exists($sRefClass, $aFieldSpec)) + { + $aFields = $aFieldSpec[$sRefClass]; + break; + } + } + } + if (is_null($aFields)) + { + // No fieldspec given, or not found... + $aFields = array('id', 'friendlyname'); + } + + foreach ($aFields as $sAttCode) + { + $oObjRes->AddField($oObject, $sAttCode, $bExtendedOutput); + } + + $sObjKey = get_class($oObject).'::'.$oObject->GetKey(); + $this->objects[$sObjKey] = $oObjRes; + } +} + +class RestResultWithRelations extends RestResultWithObjects +{ + public $relations; + + public function __construct() + { + parent::__construct(); + $this->relations = array(); + } + + public function AddRelation($sSrcKey, $sDestKey) + { + if (!array_key_exists($sSrcKey, $this->relations)) + { + $this->relations[$sSrcKey] = array(); + } + $this->relations[$sSrcKey][] = array('key' => $sDestKey); + } +} + +/** + * Deletion result codes for a target object (either deleted or updated) + * + * @package Extensibility + * @api + * @since 2.0.1 + */ +class RestDelete +{ + /** + * Result: Object deleted as per the initial request + */ + const OK = 0; + /** + * Result: general issue (user rights or ... ?) + */ + const ISSUE = 1; + /** + * Result: Must be deleted to preserve database integrity + */ + const AUTO_DELETE = 2; + /** + * Result: Must be deleted to preserve database integrity, but that is NOT possible + */ + const AUTO_DELETE_ISSUE = 3; + /** + * Result: Must be deleted to preserve database integrity, but this must be requested explicitely + */ + const REQUEST_EXPLICITELY = 4; + /** + * Result: Must be updated to preserve database integrity + */ + const AUTO_UPDATE = 5; + /** + * Result: Must be updated to preserve database integrity, but that is NOT possible + */ + const AUTO_UPDATE_ISSUE = 6; +} + +/** + * Implementation of core REST services (create/get/update... objects) + * + * @package Core + */ +class CoreServices implements iRestServiceProvider +{ + /** + * Enumerate services delivered by this class + * + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return array An array of hash 'verb' => verb, 'description' => description + */ + public function ListOperations($sVersion) + { + // 1.3 - iTop 2.2.0, Verb 'get_related': added the options 'redundancy' and 'direction' to take into account the redundancy in the impact analysis + // 1.2 - was documented in the wiki but never released ! Same as 1.3 + // 1.1 - In the reply, objects have a 'key' entry so that it is no more necessary to split class::key programmaticaly + // 1.0 - Initial implementation in iTop 2.0.1 + // + $aOps = array(); + if (in_array($sVersion, array('1.0', '1.1', '1.2', '1.3'))) + { + $aOps[] = array( + 'verb' => 'core/create', + 'description' => 'Create an object' + ); + $aOps[] = array( + 'verb' => 'core/update', + 'description' => 'Update an object' + ); + $aOps[] = array( + 'verb' => 'core/apply_stimulus', + 'description' => 'Apply a stimulus to change the state of an object' + ); + $aOps[] = array( + 'verb' => 'core/get', + 'description' => 'Search for objects' + ); + $aOps[] = array( + 'verb' => 'core/delete', + 'description' => 'Delete objects' + ); + $aOps[] = array( + 'verb' => 'core/get_related', + 'description' => 'Get related objects through the specified relation' + ); + $aOps[] = array( + 'verb' => 'core/check_credentials', + 'description' => 'Check user credentials' + ); + } + return $aOps; + } + + /** + * Enumerate services delivered by this class + * @param string $sVersion The version (e.g. 1.0) supported by the services + * @return RestResult The standardized result structure (at least a message) + * @throws Exception in case of internal failure. + */ + public function ExecOperation($sVersion, $sVerb, $aParams) + { + $oResult = new RestResultWithObjects(); + switch ($sVerb) + { + case 'core/create': + RestUtils::InitTrackingComment($aParams); + $sClass = RestUtils::GetClass($aParams, 'class'); + $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); + + if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for creating data of class $sClass"; + } + elseif (UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for massively creating data of class $sClass"; + } + else + { + $oObject = RestUtils::MakeObjectFromFields($sClass, $aFields); + $oObject->DBInsert(); + $oResult->AddObject(0, 'created', $oObject, $aShowFields, $bExtendedOutput); + } + break; + + case 'core/update': + RestUtils::InitTrackingComment($aParams); + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); + + // Note: the target class cannot be based on the result of FindObjectFromKey, because in case the user does not have read access, that function already fails with msg 'Nothing found' + $sTargetClass = RestUtils::GetObjectSetFromKey($sClass, $key)->GetFilter()->GetClass(); + if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for modifying data of class $sTargetClass"; + } + elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for massively modifying data of class $sTargetClass"; + } + else + { + $oObject = RestUtils::FindObjectFromKey($sClass, $key); + RestUtils::UpdateObjectFromFields($oObject, $aFields); + $oObject->DBUpdate(); + $oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput); + } + break; + + case 'core/apply_stimulus': + RestUtils::InitTrackingComment($aParams); + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $aFields = RestUtils::GetMandatoryParam($aParams, 'fields'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); + $sStimulus = RestUtils::GetMandatoryParam($aParams, 'stimulus'); + + // Note: the target class cannot be based on the result of FindObjectFromKey, because in case the user does not have read access, that function already fails with msg 'Nothing found' + $sTargetClass = RestUtils::GetObjectSetFromKey($sClass, $key)->GetFilter()->GetClass(); + if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_MODIFY) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for modifying data of class $sTargetClass"; + } + elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_BULK_MODIFY) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for massively modifying data of class $sTargetClass"; + } + else + { + $oObject = RestUtils::FindObjectFromKey($sClass, $key); + RestUtils::UpdateObjectFromFields($oObject, $aFields); + + $aTransitions = $oObject->EnumTransitions(); + $aStimuli = MetaModel::EnumStimuli(get_class($oObject)); + if (!isset($aTransitions[$sStimulus])) + { + // Invalid stimulus + $oResult->code = RestResult::INTERNAL_ERROR; + $oResult->message = "Invalid stimulus: '$sStimulus' on the object ".$oObject->GetName()." in state '".$oObject->GetState()."'"; + } + else + { + $aTransition = $aTransitions[$sStimulus]; + $sTargetState = $aTransition['target_state']; + $aStates = MetaModel::EnumStates($sClass); + $aTargetStateDef = $aStates[$sTargetState]; + $aExpectedAttributes = $aTargetStateDef['attribute_list']; + + $aMissingMandatory = array(); + foreach($aExpectedAttributes as $sAttCode => $iExpectCode) + { + if ( ($iExpectCode & OPT_ATT_MANDATORY) && ($oObject->Get($sAttCode) == '')) + { + $aMissingMandatory[] = $sAttCode; + } + } + if (count($aMissingMandatory) == 0) + { + // If all the mandatory fields are already present, just apply the transition silently... + if ($oObject->ApplyStimulus($sStimulus)) + { + $oObject->DBUpdate(); + $oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput); + } + } + else + { + // Missing mandatory attributes for the transition + $oResult->code = RestResult::INTERNAL_ERROR; + $oResult->message = 'Missing mandatory attribute(s) for applying the stimulus: '.implode(', ', $aMissingMandatory).'.'; + } + } + } + break; + + case 'core/get': + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields'); + $bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+'); + + $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); + $sTargetClass = $oObjectSet->GetFilter()->GetClass(); + + if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for reading data of class $sTargetClass"; + } + elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_BULK_READ) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for exporting data of class $sTargetClass"; + } + else + { + while ($oObject = $oObjectSet->Fetch()) + { + $oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput); + } + $oResult->message = "Found: ".$oObjectSet->Count(); + } + break; + + case 'core/delete': + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $bSimulate = RestUtils::GetOptionalParam($aParams, 'simulate', false); + + $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); + $sTargetClass = $oObjectSet->GetFilter()->GetClass(); + + if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_DELETE) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for deleting data of class $sTargetClass"; + } + elseif (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_DELETE) != UR_ALLOWED_YES) + { + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for massively deleting data of class $sTargetClass"; + } + else + { + $aObjects = $oObjectSet->ToArray(); + $this->DeleteObjects($oResult, $aObjects, $bSimulate); + } + break; + + case 'core/get_related': + $oResult = new RestResultWithRelations(); + $sClass = RestUtils::GetClass($aParams, 'class'); + $key = RestUtils::GetMandatoryParam($aParams, 'key'); + $sRelation = RestUtils::GetMandatoryParam($aParams, 'relation'); + $iMaxRecursionDepth = RestUtils::GetOptionalParam($aParams, 'depth', 20 /* = MAX_RECURSION_DEPTH */); + $sDirection = RestUtils::GetOptionalParam($aParams, 'direction', null); + $bEnableRedundancy = RestUtils::GetOptionalParam($aParams, 'redundancy', false); + $bReverse = false; + + if (is_null($sDirection) && ($sRelation == 'depends on')) + { + // Legacy behavior, consider "depends on" as a forward relation + $sRelation = 'impacts'; + $sDirection = 'up'; + $bReverse = true; // emulate the legacy behavior by returning the edges + } + else if(is_null($sDirection)) + { + $sDirection = 'down'; + } + + $oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key); + if ($sDirection == 'down') + { + $oRelationGraph = $oObjectSet->GetRelatedObjectsDown($sRelation, $iMaxRecursionDepth, $bEnableRedundancy); + } + else if ($sDirection == 'up') + { + $oRelationGraph = $oObjectSet->GetRelatedObjectsUp($sRelation, $iMaxRecursionDepth, $bEnableRedundancy); + } + else + { + $oResult->code = RestResult::INTERNAL_ERROR; + $oResult->message = "Invalid value: '$sDirection' for the parameter 'direction'. Valid values are 'up' and 'down'"; + return $oResult; + + } + + if ($bEnableRedundancy) + { + // Remove the redundancy nodes from the output + $oIterator = new RelationTypeIterator($oRelationGraph, 'Node'); + foreach($oIterator as $oNode) + { + if ($oNode instanceof RelationRedundancyNode) + { + $oRelationGraph->FilterNode($oNode); + } + } + } + + $aIndexByClass = array(); + $oIterator = new RelationTypeIterator($oRelationGraph); + foreach($oIterator as $oElement) + { + if ($oElement instanceof RelationObjectNode) + { + $oObject = $oElement->GetProperty('object'); + if ($oObject) + { + if ($bEnableRedundancy) + { + // Add only the "reached" objects + if ($oElement->GetProperty('is_reached')) + { + $aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null; + $oResult->AddObject(0, '', $oObject); + } + } + else + { + $aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null; + $oResult->AddObject(0, '', $oObject); + } + } + } + else if ($oElement instanceof RelationEdge) + { + $oSrcObj = $oElement->GetSourceNode()->GetProperty('object'); + $oDestObj = $oElement->GetSinkNode()->GetProperty('object'); + $sSrcKey = get_class($oSrcObj).'::'.$oSrcObj->GetKey(); + $sDestKey = get_class($oDestObj).'::'.$oDestObj->GetKey(); + if ($bEnableRedundancy) + { + // Add only the edges where both source and destination are "reached" + if ($oElement->GetSourceNode()->GetProperty('is_reached') && $oElement->GetSinkNode()->GetProperty('is_reached')) + { + if ($bReverse) + { + $oResult->AddRelation($sDestKey, $sSrcKey); + } + else + { + $oResult->AddRelation($sSrcKey, $sDestKey); + } + } + } + else + { + if ($bReverse) + { + $oResult->AddRelation($sDestKey, $sSrcKey); + } + else + { + $oResult->AddRelation($sSrcKey, $sDestKey); + } + } + } + } + + if (count($aIndexByClass) > 0) + { + $aStats = array(); + $aUnauthorizedClasses = array(); + foreach ($aIndexByClass as $sClass => $aIds) + { + if (UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_READ) != UR_ALLOWED_YES) + { + $aUnauthorizedClasses[$sClass] = true; + } + $aStats[] = $sClass.'= '.count($aIds); + } + if (count($aUnauthorizedClasses) > 0) + { + $sClasses = implode(', ', array_keys($aUnauthorizedClasses)); + $oResult = new RestResult(); + $oResult->code = RestResult::UNAUTHORIZED; + $oResult->message = "The current user does not have enough permissions for exporting data of class(es): $sClasses"; + } + else + { + $oResult->message = "Scope: ".$oObjectSet->Count()."; Related objects: ".implode(', ', $aStats); + } + } + else + { + $oResult->message = "Nothing found"; + } + break; + + case 'core/check_credentials': + $oResult = new RestResult(); + $sUser = RestUtils::GetMandatoryParam($aParams, 'user'); + $sPassword = RestUtils::GetMandatoryParam($aParams, 'password'); + + if (UserRights::CheckCredentials($sUser, $sPassword) !== true) + { + $oResult->authorized = false; + } + else + { + $oResult->authorized = true; + } + break; + + default: + // unknown operation: handled at a higher level + } + return $oResult; + } + + /** + * Helper for object deletion + */ + public function DeleteObjects($oResult, $aObjects, $bSimulate) + { + $oDeletionPlan = new DeletionPlan(); + foreach($aObjects as $oObj) + { + if ($bSimulate) + { + $oObj->CheckToDelete($oDeletionPlan); + } + else + { + $oObj->DBDelete($oDeletionPlan); + } + } + + foreach ($oDeletionPlan->ListDeletes() as $sTargetClass => $aDeletes) + { + foreach ($aDeletes as $iId => $aData) + { + $oToDelete = $aData['to_delete']; + $bAutoDel = (($aData['mode'] == DEL_SILENT) || ($aData['mode'] == DEL_AUTO)); + if (array_key_exists('issue', $aData)) + { + if ($bAutoDel) + { + if (isset($aData['requested_explicitely'])) // i.e. in the initial list of objects to delete + { + $iCode = RestDelete::ISSUE; + $sPlanned = 'Cannot be deleted: '.$aData['issue']; + } + else + { + $iCode = RestDelete::AUTO_DELETE_ISSUE; + $sPlanned = 'Should be deleted automatically... but: '.$aData['issue']; + } + } + else + { + $iCode = RestDelete::REQUEST_EXPLICITELY; + $sPlanned = 'Must be deleted explicitely... but: '.$aData['issue']; + } + } + else + { + if ($bAutoDel) + { + if (isset($aData['requested_explicitely'])) + { + $iCode = RestDelete::OK; + $sPlanned = ''; + } + else + { + $iCode = RestDelete::AUTO_DELETE; + $sPlanned = 'Deleted automatically'; + } + } + else + { + $iCode = RestDelete::REQUEST_EXPLICITELY; + $sPlanned = 'Must be deleted explicitely'; + } + } + $oResult->AddObject($iCode, $sPlanned, $oToDelete); + } + } + foreach ($oDeletionPlan->ListUpdates() as $sRemoteClass => $aToUpdate) + { + foreach ($aToUpdate as $iId => $aData) + { + $oToUpdate = $aData['to_reset']; + if (array_key_exists('issue', $aData)) + { + $iCode = RestDelete::AUTO_UPDATE_ISSUE; + $sPlanned = 'Should be updated automatically... but: '.$aData['issue']; + } + else + { + $iCode = RestDelete::AUTO_UPDATE; + $sPlanned = 'Reset external keys: '.$aData['attributes_list']; + } + $oResult->AddObject($iCode, $sPlanned, $oToUpdate); + } + } + + if ($oDeletionPlan->FoundStopper()) + { + if ($oDeletionPlan->FoundSecurityIssue()) + { + $iRes = RestResult::UNAUTHORIZED; + $sRes = 'Deletion not allowed on some objects'; + } + elseif ($oDeletionPlan->FoundManualOperation()) + { + $iRes = RestResult::UNSAFE; + $sRes = 'The deletion requires that other objects be deleted/updated, and those operations must be requested explicitely'; + } + else + { + $iRes = RestResult::INTERNAL_ERROR; + $sRes = 'Some issues have been encountered. See the list of planned changes for more information about the issue(s).'; + } + } + else + { + $iRes = RestResult::OK; + $sRes = 'Deleted: '.count($aObjects); + $iIndirect = $oDeletionPlan->GetTargetCount() - count($aObjects); + if ($iIndirect > 0) + { + $sRes .= ' plus (for DB integrity) '.$iIndirect; + } + } + $oResult->code = $iRes; + if ($bSimulate) + { + $oResult->message = 'SIMULATING: '.$sRes; + } + else + { + $oResult->message = $sRes; + } + } +} diff --git a/core/sqlobjectquery.class.inc.php b/core/sqlobjectquery.class.inc.php index 18eafec1a..2bdd85e2e 100644 --- a/core/sqlobjectquery.class.inc.php +++ b/core/sqlobjectquery.class.inc.php @@ -1,677 +1,677 @@ - - - -/** - * SQLObjectQuery - * build a mySQL compatible SQL query - * - * @copyright Copyright (C) 2015-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * SQLObjectQuery - * build a mySQL compatible SQL query - * - * @package iTopORM - */ - - -class SQLObjectQuery extends SQLQuery -{ - public $m_aContextData = null; - public $m_iOriginalTableCount = 0; - private $m_sTable = ''; - private $m_sTableAlias = ''; - private $m_aFields = array(); - private $m_aGroupBy = array(); - private $m_oConditionExpr = null; - private $m_bToDelete = true; // The current table must be listed for deletion ? - private $m_aValues = array(); // Values to set in case of an update query - private $m_oSelectedIdField = null; - private $m_aJoinSelects = array(); - protected $m_bBeautifulQuery = false; - - // Data set by PrepareRendering() - private $__aFrom; - private $__aFields; - private $__aGroupBy; - private $__aDelTables; - private $__aSetValues; - private $__aSelectedIdFields; - - - public function __construct($sTable, $sTableAlias, $aFields, $bToDelete = true, $aValues = array(), $oSelectedIdField = null) - { - parent::__construct(); - - // This check is not needed but for developping purposes - //if (!CMDBSource::IsTable($sTable)) - //{ - // throw new CoreException("Unknown table '$sTable'"); - //} - - // $aFields must be an array of "alias"=>"expr" - // $oConditionExpr must be a condition tree - // $aValues is an array of "alias"=>value - - $this->m_sTable = $sTable; - $this->m_sTableAlias = $sTableAlias; - $this->m_aFields = $aFields; - $this->m_aGroupBy = null; - $this->m_oConditionExpr = null; - $this->m_bToDelete = $bToDelete; - $this->m_aValues = $aValues; - $this->m_oSelectedIdField = $oSelectedIdField; - } - - public function GetTableAlias() - { - return $this->m_sTableAlias; - } - - public function DisplayHtml() - { - if (count($this->m_aFields) == 0) $sFields = ""; - else - { - $aFieldDesc = array(); - foreach ($this->m_aFields as $sAlias => $oExpression) - { - $aFieldDesc[] = $oExpression->RenderExpression(false)." as $sAlias"; - } - $sFields = " => ".implode(', ', $aFieldDesc); - } - echo "$this->m_sTable$sFields
    \n"; - // #@# todo - display html of an expression tree - //$this->m_oConditionExpr->DisplayHtml() - if (count($this->m_aJoinSelects) > 0) - { - echo "Joined to...
    \n"; - echo "
      \n"; - foreach ($this->m_aJoinSelects as $aJoinInfo) - { - $sJoinType = $aJoinInfo["jointype"]; - $oSQLQuery = $aJoinInfo["select"]; - if (isset($aJoinInfo["on_expression"])) - { - $sOnCondition = $aJoinInfo["on_expression"]->RenderExpression(false); - - echo "
    • Join '$sJoinType', ON ($sOnCondition)".$oSQLQuery->DisplayHtml()."
    • \n"; - } - else - { - $sLeftField = $aJoinInfo["leftfield"]; - $sRightField = $aJoinInfo["rightfield"]; - $sRightTableAlias = $aJoinInfo["righttablealias"]; - - echo "
    • Join '$sJoinType', $sLeftField, $sRightTableAlias.$sRightField".$oSQLQuery->DisplayHtml()."
    • \n"; - } - } - echo "
    "; - } - $this->PrepareRendering(); - echo "From ...
    \n"; - echo "
    \n";
    -		print_r($this->__aFrom);
    -		echo "
    "; - } - - public function SetSelect($aExpressions) - { - $this->m_aFields = $aExpressions; - } - - public function SortSelectedFields() - { - ksort($this->m_aFields); - } - - public function AddSelect($sAlias, $oExpression) - { - $this->m_aFields[$sAlias] = $oExpression; - } - - public function SetGroupBy($aExpressions) - { - $this->m_aGroupBy = $aExpressions; - } - - public function SetCondition($oConditionExpr) - { - $this->m_oConditionExpr = $oConditionExpr; - } - - public function AddCondition($oConditionExpr) - { - if (is_null($this->m_oConditionExpr)) - { - $this->m_oConditionExpr = $oConditionExpr; - } - else - { - $this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oConditionExpr); - } - } - - private function AddJoin($sJoinType, $oSQLQuery, $sLeftField, $sRightField, $sRightTableAlias = '') - { - assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__)); - // No need to check this here but for development purposes - //if (!CMDBSource::IsField($this->m_sTable, $sLeftField)) - //{ - // throw new CoreException("Unknown field '$sLeftField' in table '".$this->m_sTable); - //} - - if (empty($sRightTableAlias)) - { - $sRightTableAlias = $oSQLQuery->m_sTableAlias; - } -// #@# Could not be verified here because the namespace is unknown - do we need to check it there? -// -// if (!CMDBSource::IsField($sRightTable, $sRightField)) -// { -// throw new CoreException("Unknown field '$sRightField' in table '".$sRightTable."'"); -// } - $this->m_aJoinSelects[] = array( - "jointype" => $sJoinType, - "select" => $oSQLQuery, - "leftfield" => $sLeftField, - "rightfield" => $sRightField, - "righttablealias" => $sRightTableAlias - ); - } - public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '') - { - $this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRightTable); - } - public function AddInnerJoinTree($oSQLQuery, $sLeftFieldLeft, $sLeftFieldRight, $sRightFieldLeft, $sRightFieldRight, $sRightTableAlias = '', $iOperatorCode = TREE_OPERATOR_BELOW, $bInvertOnClause = false) - { - assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__)); - if (empty($sRightTableAlias)) - { - $sRightTableAlias = $oSQLQuery->m_sTableAlias; - } - $this->m_aJoinSelects[] = array( - "jointype" => 'inner_tree', - "select" => $oSQLQuery, - "leftfield" => $sLeftFieldLeft, - "rightfield" => $sLeftFieldRight, - "rightfield_left" => $sRightFieldLeft, - "rightfield_right" => $sRightFieldRight, - "righttablealias" => $sRightTableAlias, - "tree_operator" => $iOperatorCode, - 'invert_on_clause' => $bInvertOnClause - ); - } - public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField) - { - return $this->AddJoin("left", $oSQLQuery, $sLeftField, $sRightField); - } - - public function AddInnerJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression) - { - $this->m_aJoinSelects[] = array( - "jointype" => 'inner', - "select" => $oSQLQuery, - "on_expression" => $oOnExpression - ); - } - - public function AddLeftJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression) - { - $this->m_aJoinSelects[] = array( - "jointype" => 'left', - "select" => $oSQLQuery, - "on_expression" => $oOnExpression - ); - } - - // Interface, build the SQL query - - /** - * @param array $aArgs - * @return string - * @throws CoreException - */ - public function RenderDelete($aArgs = array()) - { - $this->PrepareRendering(); - - // Target: DELETE myAlias1, myAlias2 FROM t1 as myAlias1, t2 as myAlias2, t3 as topreserve WHERE ... - - $sDelete = self::ClauseDelete($this->__aDelTables); - $sFrom = self::ClauseFrom($this->__aFrom); - // #@# safety net to redo ? - /* - if ($this->m_oConditionExpr->IsAny()) - -- if (count($aConditions) == 0) -- - { - throw new CoreException("Building a request wich will delete every object of a given table -looks suspicious- please use truncate instead..."); - } - */ - if (is_null($this->m_oConditionExpr)) - { - // Delete all !!! - } - else - { - $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); - return "DELETE $sDelete FROM $sFrom WHERE $sWhere"; - } - return ''; - } - - /** - * Needed for the unions - */ - public function RenderSelectClause() - { - $this->PrepareRendering(); - $sSelect = self::ClauseSelect($this->__aFields); - return $sSelect; - } - - /** - * Needed for the unions - * @param $aOrderBy - * @return string - * @throws CoreException - */ - public function RenderOrderByClause($aOrderBy) - { - $this->PrepareRendering(); - $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); - return $sOrderBy; - } - - // Interface, build the SQL query - - /** - * @param array $aArgs - * @return string - * @throws CoreException - */ - public function RenderUpdate($aArgs = array()) - { - $this->PrepareRendering(); - $sFrom = self::ClauseFrom($this->__aFrom); - $sValues = self::ClauseValues($this->__aSetValues); - $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); - return "UPDATE $sFrom SET $sValues WHERE $sWhere"; - } - - // Interface, build the SQL query - - /** - * @param array $aOrderBy - * @param array $aArgs - * @param int $iLimitCount - * @param int $iLimitStart - * @param bool $bGetCount - * @param bool $bBeautifulQuery - * @return string - * @throws CoreException - */ - public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false) - { - $this->m_bBeautifulQuery = $bBeautifulQuery; - $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; - $sIndent = $this->m_bBeautifulQuery ? " " : null; - - $this->PrepareRendering(); - $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); - $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; - } - else - { - $sLimit = ''; - } - if ($bGetCount) - { - if (count($this->__aSelectedIdFields) > 0) - { - $aCountFields = array(); - foreach ($this->__aSelectedIdFields as $sFieldExpr) - { - $aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count - } - $sCountFields = implode(', ', $aCountFields); - // Count can be limited for performance reason, in this case the total amount is not important, - // we only need to know if the number of entries is greater than a certain amount. - $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep DISTINCT $sCountFields $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; - } - else - { - $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; - } - } - else - { - $sSelect = self::ClauseSelect($this->__aFields, $sLineSep); - $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); - if (!empty($sOrderBy)) - { - $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; - } - - $sSQL = "SELECT$sLineSep DISTINCT $sSelect$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sOrderBy $sLimit"; - } - return $sSQL; - } - - // Interface, build the SQL query - - /** - * @param array $aArgs - * @param bool $bBeautifulQuery - * @param array $aOrderBy - * @param int $iLimitCount - * @param int $iLimitStart - * @return string - * @throws CoreException - */ - public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) - { - $this->m_bBeautifulQuery = $bBeautifulQuery; - $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; - $sIndent = $this->m_bBeautifulQuery ? " " : null; - - $this->PrepareRendering(); - - $sSelect = self::ClauseSelect($this->__aFields); - $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); - $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); - $sGroupBy = self::ClauseGroupBy($this->__aGroupBy); - $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); - if (!empty($sGroupBy)) - { - $sGroupBy = "GROUP BY $sGroupBy$sLineSep"; - } - if (!empty($sOrderBy)) - { - $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; - } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; - } - else - { - $sLimit = ''; - } - $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sGroupBy $sOrderBy$sLineSep $sLimit"; - return $sSQL; - } - - // Purpose: prepare the query data, once for all - private function PrepareRendering() - { - if (is_null($this->__aFrom)) - { - $this->__aFrom = array(); - $this->__aFields = array(); - $this->__aGroupBy = array(); - $this->__aDelTables = array(); - $this->__aSetValues = array(); - $this->__aSelectedIdFields = array(); - - $this->PrepareSingleTable($this, $this->__aFrom, '', array('jointype' => 'first')); - } - } - - private function PrepareSingleTable(SQLObjectQuery $oRootQuery, &$aFrom, $sCallerAlias = '', $aJoinData) - { - $aTranslationTable[$this->m_sTable]['*'] = $this->m_sTableAlias; - $sJoinCond = ''; - - // Handle the various kinds of join (or first table in the list) - // - if (empty($aJoinData['righttablealias'])) - { - $sRightTableAlias = $this->m_sTableAlias; - } - else - { - $sRightTableAlias = $aJoinData['righttablealias']; - } - switch ($aJoinData['jointype']) - { - case "first": - $aFrom[$this->m_sTableAlias] = array("jointype"=>"first", "tablename"=>$this->m_sTable, "joincondition"=>""); - break; - case "inner": - case "left": - if (isset($aJoinData["on_expression"])) - { - $sJoinCond = $aJoinData["on_expression"]->RenderExpression(true); - } - else - { - $sJoinCond = "`$sCallerAlias`.`{$aJoinData['leftfield']}` = `$sRightTableAlias`.`{$aJoinData['rightfield']}`"; - } - $aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond"); - break; - case "inner_tree": - if ($aJoinData['invert_on_clause']) - { - $sRootLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`"; - $sRootRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`"; - $sNodeLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`"; - $sNodeRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`"; - } - else - { - $sNodeLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`"; - $sNodeRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`"; - $sRootLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`"; - $sRootRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`"; - } - switch($aJoinData['tree_operator']) - { - case TREE_OPERATOR_BELOW: - $sJoinCond = "$sNodeLeft >= $sRootLeft AND $sNodeLeft <= $sRootRight"; - break; - - case TREE_OPERATOR_BELOW_STRICT: - $sJoinCond = "$sNodeLeft > $sRootLeft AND $sNodeLeft < $sRootRight"; - break; - - case TREE_OPERATOR_NOT_BELOW: // Complementary of 'BELOW' - $sJoinCond = "$sNodeLeft < $sRootLeft OR $sNodeLeft > $sRootRight"; - break; - - case TREE_OPERATOR_NOT_BELOW_STRICT: // Complementary of BELOW_STRICT - $sJoinCond = "$sNodeLeft <= $sRootLeft OR $sNodeLeft >= $sRootRight"; - break; - - case TREE_OPERATOR_ABOVE: - $sJoinCond = "$sNodeLeft <= $sRootLeft AND $sNodeRight >= $sRootRight"; - break; - - case TREE_OPERATOR_ABOVE_STRICT: - $sJoinCond = "$sNodeLeft < $sRootLeft AND $sNodeRight > $sRootRight"; - break; - - case TREE_OPERATOR_NOT_ABOVE: // Complementary of 'ABOVE' - $sJoinCond = "$sNodeLeft > $sRootLeft OR $sNodeRight < $sRootRight"; - break; - - case TREE_OPERATOR_NOT_ABOVE_STRICT: // Complementary of ABOVE_STRICT - $sJoinCond = "$sNodeLeft >= $sRootLeft OR $sNodeRight <= $sRootRight"; - break; - - } - $aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond"); - break; - } - - // Given the alias, modify the fields and conditions - // before adding them into the current lists - // - foreach($this->m_aFields as $sAlias => $oExpression) - { - $oRootQuery->__aFields["`$sAlias`"] = $oExpression->RenderExpression(true); - } - if ($this->m_aGroupBy) - { - foreach($this->m_aGroupBy as $sAlias => $oExpression) - { - $oRootQuery->__aGroupBy["`$sAlias`"] = $oExpression->RenderExpression(true); - } - } - if ($this->m_bToDelete) - { - $oRootQuery->__aDelTables[] = "`{$this->m_sTableAlias}`"; - } - foreach($this->m_aValues as $sFieldName=>$value) - { - $oRootQuery->__aSetValues["`{$this->m_sTableAlias}`.`$sFieldName`"] = $value; // quoted further! - } - - if (!is_null($this->m_oSelectedIdField)) - { - $oRootQuery->__aSelectedIdFields[] = $this->m_oSelectedIdField->RenderExpression(true); - } - - // loop on joins, to complete the list of tables/fields/conditions - // - $aTempFrom = array(); // temporary subset of 'from' specs, to be grouped in the final query - foreach ($this->m_aJoinSelects as $aJoinData) - { - $oRightSelect = $aJoinData["select"]; - - $oRightSelect->PrepareSingleTable($oRootQuery, $aTempFrom, $this->m_sTableAlias, $aJoinData); - } - $aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom; - - return $this->m_sTableAlias; - } - - public function OptimizeJoins($aUsedTables, $bTopCall = true) - { - $this->m_iOriginalTableCount = $this->CountTables(); - if ($bTopCall) - { - // Top call: complete the list of tables absolutely required to perform the right query - $this->CollectUsedTables($aUsedTables); - } - - $aToDiscard = array(); - foreach ($this->m_aJoinSelects as $i => $aJoinInfo) - { - $oSQLQuery = $aJoinInfo["select"]; - $sTableAlias = $oSQLQuery->GetTableAlias(); - if ($oSQLQuery->OptimizeJoins($aUsedTables, false) && !array_key_exists($sTableAlias, $aUsedTables)) - { - $aToDiscard[] = $i; - } - } - foreach ($aToDiscard as $i) - { - unset($this->m_aJoinSelects[$i]); - } - - return (count($this->m_aJoinSelects) == 0); - } - - public function CountTables() - { - $iRet = 1; - foreach ($this->m_aJoinSelects as $i => $aJoinInfo) - { - $oSQLQuery = $aJoinInfo["select"]; - $iRet += $oSQLQuery->CountTables(); - } - return $iRet; - } - - public function CollectUsedTables(&$aTables) - { - $this->m_oConditionExpr->CollectUsedParents($aTables); - foreach($this->m_aFields as $sFieldAlias => $oField) - { - $oField->CollectUsedParents($aTables); - } - if ($this->m_aGroupBy) - { - foreach($this->m_aGroupBy as $sAlias => $oExpression) - { - $oExpression->CollectUsedParents($aTables); - } - } - if (!is_null($this->m_oSelectedIdField)) - { - $this->m_oSelectedIdField->CollectUsedParents($aTables); - } - - foreach ($this->m_aJoinSelects as $i => $aJoinInfo) - { - $oSQLQuery = $aJoinInfo["select"]; - if ($oSQLQuery->HasRequiredTables($aTables)) - { - // There is something required in the branch, then this node is a MUST - if (isset($aJoinInfo['righttablealias'])) - { - $aTables[$aJoinInfo['righttablealias']] = true; - } - if (isset($aJoinInfo["on_expression"])) - { - $aJoinInfo["on_expression"]->CollectUsedParents($aTables); - } - } - } - - return $aTables; - } - - // Is required in the JOIN, and therefore we must ensure that the join expression will be valid - protected function HasRequiredTables(&$aTables) - { - $bResult = false; - if (array_key_exists($this->m_sTableAlias, $aTables)) - { - $bResult = true; - } - foreach ($this->m_aJoinSelects as $i => $aJoinInfo) - { - $oSQLQuery = $aJoinInfo["select"]; - if ($oSQLQuery->HasRequiredTables($aTables)) - { - // There is something required in the branch, then this node is a MUST - if (isset($aJoinInfo['righttablealias'])) - { - $aTables[$aJoinInfo['righttablealias']] = true; - } - if (isset($aJoinInfo["on_expression"])) - { - $aJoinInfo["on_expression"]->CollectUsedParents($aTables); - } - $bResult = true; - } - } - // None of the tables is in the list of required tables - return $bResult; - } - -} + + + +/** + * SQLObjectQuery + * build a mySQL compatible SQL query + * + * @copyright Copyright (C) 2015-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * SQLObjectQuery + * build a mySQL compatible SQL query + * + * @package iTopORM + */ + + +class SQLObjectQuery extends SQLQuery +{ + public $m_aContextData = null; + public $m_iOriginalTableCount = 0; + private $m_sTable = ''; + private $m_sTableAlias = ''; + private $m_aFields = array(); + private $m_aGroupBy = array(); + private $m_oConditionExpr = null; + private $m_bToDelete = true; // The current table must be listed for deletion ? + private $m_aValues = array(); // Values to set in case of an update query + private $m_oSelectedIdField = null; + private $m_aJoinSelects = array(); + protected $m_bBeautifulQuery = false; + + // Data set by PrepareRendering() + private $__aFrom; + private $__aFields; + private $__aGroupBy; + private $__aDelTables; + private $__aSetValues; + private $__aSelectedIdFields; + + + public function __construct($sTable, $sTableAlias, $aFields, $bToDelete = true, $aValues = array(), $oSelectedIdField = null) + { + parent::__construct(); + + // This check is not needed but for developping purposes + //if (!CMDBSource::IsTable($sTable)) + //{ + // throw new CoreException("Unknown table '$sTable'"); + //} + + // $aFields must be an array of "alias"=>"expr" + // $oConditionExpr must be a condition tree + // $aValues is an array of "alias"=>value + + $this->m_sTable = $sTable; + $this->m_sTableAlias = $sTableAlias; + $this->m_aFields = $aFields; + $this->m_aGroupBy = null; + $this->m_oConditionExpr = null; + $this->m_bToDelete = $bToDelete; + $this->m_aValues = $aValues; + $this->m_oSelectedIdField = $oSelectedIdField; + } + + public function GetTableAlias() + { + return $this->m_sTableAlias; + } + + public function DisplayHtml() + { + if (count($this->m_aFields) == 0) $sFields = ""; + else + { + $aFieldDesc = array(); + foreach ($this->m_aFields as $sAlias => $oExpression) + { + $aFieldDesc[] = $oExpression->RenderExpression(false)." as $sAlias"; + } + $sFields = " => ".implode(', ', $aFieldDesc); + } + echo "$this->m_sTable$sFields
    \n"; + // #@# todo - display html of an expression tree + //$this->m_oConditionExpr->DisplayHtml() + if (count($this->m_aJoinSelects) > 0) + { + echo "Joined to...
    \n"; + echo "
      \n"; + foreach ($this->m_aJoinSelects as $aJoinInfo) + { + $sJoinType = $aJoinInfo["jointype"]; + $oSQLQuery = $aJoinInfo["select"]; + if (isset($aJoinInfo["on_expression"])) + { + $sOnCondition = $aJoinInfo["on_expression"]->RenderExpression(false); + + echo "
    • Join '$sJoinType', ON ($sOnCondition)".$oSQLQuery->DisplayHtml()."
    • \n"; + } + else + { + $sLeftField = $aJoinInfo["leftfield"]; + $sRightField = $aJoinInfo["rightfield"]; + $sRightTableAlias = $aJoinInfo["righttablealias"]; + + echo "
    • Join '$sJoinType', $sLeftField, $sRightTableAlias.$sRightField".$oSQLQuery->DisplayHtml()."
    • \n"; + } + } + echo "
    "; + } + $this->PrepareRendering(); + echo "From ...
    \n"; + echo "
    \n";
    +		print_r($this->__aFrom);
    +		echo "
    "; + } + + public function SetSelect($aExpressions) + { + $this->m_aFields = $aExpressions; + } + + public function SortSelectedFields() + { + ksort($this->m_aFields); + } + + public function AddSelect($sAlias, $oExpression) + { + $this->m_aFields[$sAlias] = $oExpression; + } + + public function SetGroupBy($aExpressions) + { + $this->m_aGroupBy = $aExpressions; + } + + public function SetCondition($oConditionExpr) + { + $this->m_oConditionExpr = $oConditionExpr; + } + + public function AddCondition($oConditionExpr) + { + if (is_null($this->m_oConditionExpr)) + { + $this->m_oConditionExpr = $oConditionExpr; + } + else + { + $this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oConditionExpr); + } + } + + private function AddJoin($sJoinType, $oSQLQuery, $sLeftField, $sRightField, $sRightTableAlias = '') + { + assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__)); + // No need to check this here but for development purposes + //if (!CMDBSource::IsField($this->m_sTable, $sLeftField)) + //{ + // throw new CoreException("Unknown field '$sLeftField' in table '".$this->m_sTable); + //} + + if (empty($sRightTableAlias)) + { + $sRightTableAlias = $oSQLQuery->m_sTableAlias; + } +// #@# Could not be verified here because the namespace is unknown - do we need to check it there? +// +// if (!CMDBSource::IsField($sRightTable, $sRightField)) +// { +// throw new CoreException("Unknown field '$sRightField' in table '".$sRightTable."'"); +// } + $this->m_aJoinSelects[] = array( + "jointype" => $sJoinType, + "select" => $oSQLQuery, + "leftfield" => $sLeftField, + "rightfield" => $sRightField, + "righttablealias" => $sRightTableAlias + ); + } + public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '') + { + $this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRightTable); + } + public function AddInnerJoinTree($oSQLQuery, $sLeftFieldLeft, $sLeftFieldRight, $sRightFieldLeft, $sRightFieldRight, $sRightTableAlias = '', $iOperatorCode = TREE_OPERATOR_BELOW, $bInvertOnClause = false) + { + assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__)); + if (empty($sRightTableAlias)) + { + $sRightTableAlias = $oSQLQuery->m_sTableAlias; + } + $this->m_aJoinSelects[] = array( + "jointype" => 'inner_tree', + "select" => $oSQLQuery, + "leftfield" => $sLeftFieldLeft, + "rightfield" => $sLeftFieldRight, + "rightfield_left" => $sRightFieldLeft, + "rightfield_right" => $sRightFieldRight, + "righttablealias" => $sRightTableAlias, + "tree_operator" => $iOperatorCode, + 'invert_on_clause' => $bInvertOnClause + ); + } + public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField) + { + return $this->AddJoin("left", $oSQLQuery, $sLeftField, $sRightField); + } + + public function AddInnerJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression) + { + $this->m_aJoinSelects[] = array( + "jointype" => 'inner', + "select" => $oSQLQuery, + "on_expression" => $oOnExpression + ); + } + + public function AddLeftJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression) + { + $this->m_aJoinSelects[] = array( + "jointype" => 'left', + "select" => $oSQLQuery, + "on_expression" => $oOnExpression + ); + } + + // Interface, build the SQL query + + /** + * @param array $aArgs + * @return string + * @throws CoreException + */ + public function RenderDelete($aArgs = array()) + { + $this->PrepareRendering(); + + // Target: DELETE myAlias1, myAlias2 FROM t1 as myAlias1, t2 as myAlias2, t3 as topreserve WHERE ... + + $sDelete = self::ClauseDelete($this->__aDelTables); + $sFrom = self::ClauseFrom($this->__aFrom); + // #@# safety net to redo ? + /* + if ($this->m_oConditionExpr->IsAny()) + -- if (count($aConditions) == 0) -- + { + throw new CoreException("Building a request wich will delete every object of a given table -looks suspicious- please use truncate instead..."); + } + */ + if (is_null($this->m_oConditionExpr)) + { + // Delete all !!! + } + else + { + $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); + return "DELETE $sDelete FROM $sFrom WHERE $sWhere"; + } + return ''; + } + + /** + * Needed for the unions + */ + public function RenderSelectClause() + { + $this->PrepareRendering(); + $sSelect = self::ClauseSelect($this->__aFields); + return $sSelect; + } + + /** + * Needed for the unions + * @param $aOrderBy + * @return string + * @throws CoreException + */ + public function RenderOrderByClause($aOrderBy) + { + $this->PrepareRendering(); + $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); + return $sOrderBy; + } + + // Interface, build the SQL query + + /** + * @param array $aArgs + * @return string + * @throws CoreException + */ + public function RenderUpdate($aArgs = array()) + { + $this->PrepareRendering(); + $sFrom = self::ClauseFrom($this->__aFrom); + $sValues = self::ClauseValues($this->__aSetValues); + $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); + return "UPDATE $sFrom SET $sValues WHERE $sWhere"; + } + + // Interface, build the SQL query + + /** + * @param array $aOrderBy + * @param array $aArgs + * @param int $iLimitCount + * @param int $iLimitStart + * @param bool $bGetCount + * @param bool $bBeautifulQuery + * @return string + * @throws CoreException + */ + public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false) + { + $this->m_bBeautifulQuery = $bBeautifulQuery; + $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; + $sIndent = $this->m_bBeautifulQuery ? " " : null; + + $this->PrepareRendering(); + $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); + $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + if ($bGetCount) + { + if (count($this->__aSelectedIdFields) > 0) + { + $aCountFields = array(); + foreach ($this->__aSelectedIdFields as $sFieldExpr) + { + $aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count + } + $sCountFields = implode(', ', $aCountFields); + // Count can be limited for performance reason, in this case the total amount is not important, + // we only need to know if the number of entries is greater than a certain amount. + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep DISTINCT $sCountFields $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; + } + else + { + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; + } + } + else + { + $sSelect = self::ClauseSelect($this->__aFields, $sLineSep); + $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; + } + + $sSQL = "SELECT$sLineSep DISTINCT $sSelect$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sOrderBy $sLimit"; + } + return $sSQL; + } + + // Interface, build the SQL query + + /** + * @param array $aArgs + * @param bool $bBeautifulQuery + * @param array $aOrderBy + * @param int $iLimitCount + * @param int $iLimitStart + * @return string + * @throws CoreException + */ + public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) + { + $this->m_bBeautifulQuery = $bBeautifulQuery; + $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; + $sIndent = $this->m_bBeautifulQuery ? " " : null; + + $this->PrepareRendering(); + + $sSelect = self::ClauseSelect($this->__aFields); + $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); + $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); + $sGroupBy = self::ClauseGroupBy($this->__aGroupBy); + $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); + if (!empty($sGroupBy)) + { + $sGroupBy = "GROUP BY $sGroupBy$sLineSep"; + } + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; + } + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sGroupBy $sOrderBy$sLineSep $sLimit"; + return $sSQL; + } + + // Purpose: prepare the query data, once for all + private function PrepareRendering() + { + if (is_null($this->__aFrom)) + { + $this->__aFrom = array(); + $this->__aFields = array(); + $this->__aGroupBy = array(); + $this->__aDelTables = array(); + $this->__aSetValues = array(); + $this->__aSelectedIdFields = array(); + + $this->PrepareSingleTable($this, $this->__aFrom, '', array('jointype' => 'first')); + } + } + + private function PrepareSingleTable(SQLObjectQuery $oRootQuery, &$aFrom, $sCallerAlias = '', $aJoinData) + { + $aTranslationTable[$this->m_sTable]['*'] = $this->m_sTableAlias; + $sJoinCond = ''; + + // Handle the various kinds of join (or first table in the list) + // + if (empty($aJoinData['righttablealias'])) + { + $sRightTableAlias = $this->m_sTableAlias; + } + else + { + $sRightTableAlias = $aJoinData['righttablealias']; + } + switch ($aJoinData['jointype']) + { + case "first": + $aFrom[$this->m_sTableAlias] = array("jointype"=>"first", "tablename"=>$this->m_sTable, "joincondition"=>""); + break; + case "inner": + case "left": + if (isset($aJoinData["on_expression"])) + { + $sJoinCond = $aJoinData["on_expression"]->RenderExpression(true); + } + else + { + $sJoinCond = "`$sCallerAlias`.`{$aJoinData['leftfield']}` = `$sRightTableAlias`.`{$aJoinData['rightfield']}`"; + } + $aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond"); + break; + case "inner_tree": + if ($aJoinData['invert_on_clause']) + { + $sRootLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`"; + $sRootRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`"; + $sNodeLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`"; + $sNodeRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`"; + } + else + { + $sNodeLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`"; + $sNodeRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`"; + $sRootLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`"; + $sRootRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`"; + } + switch($aJoinData['tree_operator']) + { + case TREE_OPERATOR_BELOW: + $sJoinCond = "$sNodeLeft >= $sRootLeft AND $sNodeLeft <= $sRootRight"; + break; + + case TREE_OPERATOR_BELOW_STRICT: + $sJoinCond = "$sNodeLeft > $sRootLeft AND $sNodeLeft < $sRootRight"; + break; + + case TREE_OPERATOR_NOT_BELOW: // Complementary of 'BELOW' + $sJoinCond = "$sNodeLeft < $sRootLeft OR $sNodeLeft > $sRootRight"; + break; + + case TREE_OPERATOR_NOT_BELOW_STRICT: // Complementary of BELOW_STRICT + $sJoinCond = "$sNodeLeft <= $sRootLeft OR $sNodeLeft >= $sRootRight"; + break; + + case TREE_OPERATOR_ABOVE: + $sJoinCond = "$sNodeLeft <= $sRootLeft AND $sNodeRight >= $sRootRight"; + break; + + case TREE_OPERATOR_ABOVE_STRICT: + $sJoinCond = "$sNodeLeft < $sRootLeft AND $sNodeRight > $sRootRight"; + break; + + case TREE_OPERATOR_NOT_ABOVE: // Complementary of 'ABOVE' + $sJoinCond = "$sNodeLeft > $sRootLeft OR $sNodeRight < $sRootRight"; + break; + + case TREE_OPERATOR_NOT_ABOVE_STRICT: // Complementary of ABOVE_STRICT + $sJoinCond = "$sNodeLeft >= $sRootLeft OR $sNodeRight <= $sRootRight"; + break; + + } + $aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond"); + break; + } + + // Given the alias, modify the fields and conditions + // before adding them into the current lists + // + foreach($this->m_aFields as $sAlias => $oExpression) + { + $oRootQuery->__aFields["`$sAlias`"] = $oExpression->RenderExpression(true); + } + if ($this->m_aGroupBy) + { + foreach($this->m_aGroupBy as $sAlias => $oExpression) + { + $oRootQuery->__aGroupBy["`$sAlias`"] = $oExpression->RenderExpression(true); + } + } + if ($this->m_bToDelete) + { + $oRootQuery->__aDelTables[] = "`{$this->m_sTableAlias}`"; + } + foreach($this->m_aValues as $sFieldName=>$value) + { + $oRootQuery->__aSetValues["`{$this->m_sTableAlias}`.`$sFieldName`"] = $value; // quoted further! + } + + if (!is_null($this->m_oSelectedIdField)) + { + $oRootQuery->__aSelectedIdFields[] = $this->m_oSelectedIdField->RenderExpression(true); + } + + // loop on joins, to complete the list of tables/fields/conditions + // + $aTempFrom = array(); // temporary subset of 'from' specs, to be grouped in the final query + foreach ($this->m_aJoinSelects as $aJoinData) + { + $oRightSelect = $aJoinData["select"]; + + $oRightSelect->PrepareSingleTable($oRootQuery, $aTempFrom, $this->m_sTableAlias, $aJoinData); + } + $aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom; + + return $this->m_sTableAlias; + } + + public function OptimizeJoins($aUsedTables, $bTopCall = true) + { + $this->m_iOriginalTableCount = $this->CountTables(); + if ($bTopCall) + { + // Top call: complete the list of tables absolutely required to perform the right query + $this->CollectUsedTables($aUsedTables); + } + + $aToDiscard = array(); + foreach ($this->m_aJoinSelects as $i => $aJoinInfo) + { + $oSQLQuery = $aJoinInfo["select"]; + $sTableAlias = $oSQLQuery->GetTableAlias(); + if ($oSQLQuery->OptimizeJoins($aUsedTables, false) && !array_key_exists($sTableAlias, $aUsedTables)) + { + $aToDiscard[] = $i; + } + } + foreach ($aToDiscard as $i) + { + unset($this->m_aJoinSelects[$i]); + } + + return (count($this->m_aJoinSelects) == 0); + } + + public function CountTables() + { + $iRet = 1; + foreach ($this->m_aJoinSelects as $i => $aJoinInfo) + { + $oSQLQuery = $aJoinInfo["select"]; + $iRet += $oSQLQuery->CountTables(); + } + return $iRet; + } + + public function CollectUsedTables(&$aTables) + { + $this->m_oConditionExpr->CollectUsedParents($aTables); + foreach($this->m_aFields as $sFieldAlias => $oField) + { + $oField->CollectUsedParents($aTables); + } + if ($this->m_aGroupBy) + { + foreach($this->m_aGroupBy as $sAlias => $oExpression) + { + $oExpression->CollectUsedParents($aTables); + } + } + if (!is_null($this->m_oSelectedIdField)) + { + $this->m_oSelectedIdField->CollectUsedParents($aTables); + } + + foreach ($this->m_aJoinSelects as $i => $aJoinInfo) + { + $oSQLQuery = $aJoinInfo["select"]; + if ($oSQLQuery->HasRequiredTables($aTables)) + { + // There is something required in the branch, then this node is a MUST + if (isset($aJoinInfo['righttablealias'])) + { + $aTables[$aJoinInfo['righttablealias']] = true; + } + if (isset($aJoinInfo["on_expression"])) + { + $aJoinInfo["on_expression"]->CollectUsedParents($aTables); + } + } + } + + return $aTables; + } + + // Is required in the JOIN, and therefore we must ensure that the join expression will be valid + protected function HasRequiredTables(&$aTables) + { + $bResult = false; + if (array_key_exists($this->m_sTableAlias, $aTables)) + { + $bResult = true; + } + foreach ($this->m_aJoinSelects as $i => $aJoinInfo) + { + $oSQLQuery = $aJoinInfo["select"]; + if ($oSQLQuery->HasRequiredTables($aTables)) + { + // There is something required in the branch, then this node is a MUST + if (isset($aJoinInfo['righttablealias'])) + { + $aTables[$aJoinInfo['righttablealias']] = true; + } + if (isset($aJoinInfo["on_expression"])) + { + $aJoinInfo["on_expression"]->CollectUsedParents($aTables); + } + $bResult = true; + } + } + // None of the tables is in the list of required tables + return $bResult; + } + +} diff --git a/core/sqlquery.class.inc.php b/core/sqlquery.class.inc.php index cbb0ec066..c7bd75589 100644 --- a/core/sqlquery.class.inc.php +++ b/core/sqlquery.class.inc.php @@ -1,201 +1,201 @@ - - - -/** - * SQLQuery - * build an mySQL compatible SQL query - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * SQLQuery - * build an mySQL compatible SQL query - * - * @package iTopORM - */ - -require_once('cmdbsource.class.inc.php'); - - -abstract class SQLQuery -{ - private $m_SourceOQL = ''; - protected $m_bBeautifulQuery = false; - - public function __construct() - { - } - - /** - * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects - **/ - public function DeepClone() - { - return unserialize(serialize($this)); - } - - public function SetSourceOQL($sOQL) - { - $this->m_SourceOQL = $sOQL; - } - - public function GetSourceOQL() - { - return $this->m_SourceOQL; - } - - abstract public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = ''); - - abstract public function DisplayHtml(); - abstract public function RenderDelete($aArgs = array()); - abstract public function RenderUpdate($aArgs = array()); - abstract public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false); - abstract public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0); - - abstract public function OptimizeJoins($aUsedTables, $bTopCall = true); - - protected static function ClauseSelect($aFields, $sLineSep = '') - { - $aSelect = array(); - foreach ($aFields as $sFieldAlias => $sSQLExpr) - { - $aSelect[] = "$sSQLExpr AS $sFieldAlias"; - } - $sSelect = implode(",$sLineSep ", $aSelect); - return $sSelect; - } - - protected static function ClauseGroupBy($aGroupBy) - { - $sRes = implode(', ', $aGroupBy); - return $sRes; - } - - protected static function ClauseDelete($aDelTableAliases) - { - $aDelTables = array(); - foreach ($aDelTableAliases as $sTableAlias) - { - $aDelTables[] = "$sTableAlias"; - } - $sDelTables = implode(', ', $aDelTables); - return $sDelTables; - } - - /** - * @param $aFrom - * @param null $sIndent - * @param int $iIndentLevel - * @return string - * @throws CoreException - */ - protected static function ClauseFrom($aFrom, $sIndent = null, $iIndentLevel = 0) - { - $sLineBreakLong = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel + 1) : ''; - $sLineBreak = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel) : ''; - - $sFrom = ""; - foreach ($aFrom as $sTableAlias => $aJoinInfo) - { - switch ($aJoinInfo["jointype"]) - { - case "first": - $sFrom .= $sLineBreakLong."`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; - $sFrom .= self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1); - break; - case "inner": - case "inner_tree": - if (count($aJoinInfo["subfrom"]) > 0) - { - $sFrom .= $sLineBreak."INNER JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; - $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1); - $sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"]; - } - else - { - // Unions do not suffer parenthesis around the "table AS alias" - $sFrom .= $sLineBreak."INNER JOIN $sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; - $sFrom .= $sLineBreak." ON ".$aJoinInfo["joincondition"]; - } - break; - case "left": - if (count($aJoinInfo["subfrom"]) > 0) - { - $sFrom .= $sLineBreak."LEFT JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; - $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1); - $sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"]; - } - else - { - // Unions do not suffer parenthesis around the "table AS alias" - $sFrom .= $sLineBreak."LEFT JOIN $sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; - $sFrom .= $sLineBreak." ON ".$aJoinInfo["joincondition"]; - } - break; - default: - throw new CoreException("Unknown jointype: '".$aJoinInfo["jointype"]."'"); - } - } - return $sFrom; - } - - protected static function ClauseValues($aValues) - { - $aSetValues = array(); - foreach ($aValues as $sFieldSpec => $value) - { - $aSetValues[] = "$sFieldSpec = ".CMDBSource::Quote($value); - } - $sSetValues = implode(', ', $aSetValues); - return $sSetValues; - } - - protected static function ClauseWhere($oConditionExpr, $aArgs = array()) - { - if (is_null($oConditionExpr)) - { - return '1'; - } - else - { - return $oConditionExpr->RenderExpression(true, $aArgs); - } - } - - /** - * @param array $aOrderBy - * @param array $aExistingFields - * @return string - * @throws CoreException - */ - protected static function ClauseOrderBy($aOrderBy, $aExistingFields) - { - $aOrderBySpec = array(); - foreach($aOrderBy as $sFieldAlias => $bAscending) - { - // Note: sFieldAlias must have backticks around column aliases - $aOrderBySpec[] = $sFieldAlias.($bAscending ? " ASC" : " DESC"); - } - $sOrderBy = implode(", ", $aOrderBySpec); - return $sOrderBy; - } -} + + + +/** + * SQLQuery + * build an mySQL compatible SQL query + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * SQLQuery + * build an mySQL compatible SQL query + * + * @package iTopORM + */ + +require_once('cmdbsource.class.inc.php'); + + +abstract class SQLQuery +{ + private $m_SourceOQL = ''; + protected $m_bBeautifulQuery = false; + + public function __construct() + { + } + + /** + * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects + **/ + public function DeepClone() + { + return unserialize(serialize($this)); + } + + public function SetSourceOQL($sOQL) + { + $this->m_SourceOQL = $sOQL; + } + + public function GetSourceOQL() + { + return $this->m_SourceOQL; + } + + abstract public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = ''); + + abstract public function DisplayHtml(); + abstract public function RenderDelete($aArgs = array()); + abstract public function RenderUpdate($aArgs = array()); + abstract public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false); + abstract public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0); + + abstract public function OptimizeJoins($aUsedTables, $bTopCall = true); + + protected static function ClauseSelect($aFields, $sLineSep = '') + { + $aSelect = array(); + foreach ($aFields as $sFieldAlias => $sSQLExpr) + { + $aSelect[] = "$sSQLExpr AS $sFieldAlias"; + } + $sSelect = implode(",$sLineSep ", $aSelect); + return $sSelect; + } + + protected static function ClauseGroupBy($aGroupBy) + { + $sRes = implode(', ', $aGroupBy); + return $sRes; + } + + protected static function ClauseDelete($aDelTableAliases) + { + $aDelTables = array(); + foreach ($aDelTableAliases as $sTableAlias) + { + $aDelTables[] = "$sTableAlias"; + } + $sDelTables = implode(', ', $aDelTables); + return $sDelTables; + } + + /** + * @param $aFrom + * @param null $sIndent + * @param int $iIndentLevel + * @return string + * @throws CoreException + */ + protected static function ClauseFrom($aFrom, $sIndent = null, $iIndentLevel = 0) + { + $sLineBreakLong = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel + 1) : ''; + $sLineBreak = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel) : ''; + + $sFrom = ""; + foreach ($aFrom as $sTableAlias => $aJoinInfo) + { + switch ($aJoinInfo["jointype"]) + { + case "first": + $sFrom .= $sLineBreakLong."`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1); + break; + case "inner": + case "inner_tree": + if (count($aJoinInfo["subfrom"]) > 0) + { + $sFrom .= $sLineBreak."INNER JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1); + $sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"]; + } + else + { + // Unions do not suffer parenthesis around the "table AS alias" + $sFrom .= $sLineBreak."INNER JOIN $sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= $sLineBreak." ON ".$aJoinInfo["joincondition"]; + } + break; + case "left": + if (count($aJoinInfo["subfrom"]) > 0) + { + $sFrom .= $sLineBreak."LEFT JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1); + $sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"]; + } + else + { + // Unions do not suffer parenthesis around the "table AS alias" + $sFrom .= $sLineBreak."LEFT JOIN $sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`"; + $sFrom .= $sLineBreak." ON ".$aJoinInfo["joincondition"]; + } + break; + default: + throw new CoreException("Unknown jointype: '".$aJoinInfo["jointype"]."'"); + } + } + return $sFrom; + } + + protected static function ClauseValues($aValues) + { + $aSetValues = array(); + foreach ($aValues as $sFieldSpec => $value) + { + $aSetValues[] = "$sFieldSpec = ".CMDBSource::Quote($value); + } + $sSetValues = implode(', ', $aSetValues); + return $sSetValues; + } + + protected static function ClauseWhere($oConditionExpr, $aArgs = array()) + { + if (is_null($oConditionExpr)) + { + return '1'; + } + else + { + return $oConditionExpr->RenderExpression(true, $aArgs); + } + } + + /** + * @param array $aOrderBy + * @param array $aExistingFields + * @return string + * @throws CoreException + */ + protected static function ClauseOrderBy($aOrderBy, $aExistingFields) + { + $aOrderBySpec = array(); + foreach($aOrderBy as $sFieldAlias => $bAscending) + { + // Note: sFieldAlias must have backticks around column aliases + $aOrderBySpec[] = $sFieldAlias.($bAscending ? " ASC" : " DESC"); + } + $sOrderBy = implode(", ", $aOrderBySpec); + return $sOrderBy; + } +} diff --git a/core/sqlunionquery.class.inc.php b/core/sqlunionquery.class.inc.php index 90972ae98..cfc115966 100644 --- a/core/sqlunionquery.class.inc.php +++ b/core/sqlunionquery.class.inc.php @@ -1,208 +1,208 @@ - - - -/** - * SQLUnionQuery - * build a mySQL compatible SQL query - * - * @copyright Copyright (C) 2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * SQLUnionQuery - * build a mySQL compatible SQL query - * - * @package iTopORM - */ - - -class SQLUnionQuery extends SQLQuery -{ - protected $aQueries; - protected $aGroupBy; - protected $aSelectExpr; - - public function __construct($aQueries, $aGroupBy, $aSelectExpr = array()) - { - parent::__construct(); - - $this->aQueries = array(); - foreach ($aQueries as $oSQLQuery) - { - $this->aQueries[] = $oSQLQuery->DeepClone(); - } - $this->aGroupBy = $aGroupBy; - $this->aSelectExpr = $aSelectExpr; - } - - public function DisplayHtml() - { - $aQueriesHtml = array(); - foreach ($this->aQueries as $oSQLQuery) - { - $aQueriesHtml[] = '

    '.$oSQLQuery->DisplayHtml().'

    '; - } - echo implode('UNION', $aQueriesHtml); - } - - public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '') - { - foreach ($this->aQueries as $oSubSQLQuery) - { - $oSubSQLQuery->AddInnerJoin($oSQLQuery->DeepClone(), $sLeftField, $sRightField, $sRightTable = ''); - } - } - - /** - * @param array $aArgs - * @throws Exception - */ - public function RenderDelete($aArgs = array()) - { - throw new Exception(__class__.'::'.__function__.'Not implemented !'); - } - - // Interface, build the SQL query - - /** - * @param array $aArgs - * @throws Exception - */ - public function RenderUpdate($aArgs = array()) - { - throw new Exception(__class__.'::'.__function__.'Not implemented !'); - } - - // Interface, build the SQL query - public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false) - { - $this->m_bBeautifulQuery = $bBeautifulQuery; - $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; - - $aSelects = array(); - foreach ($this->aQueries as $oSQLQuery) - { - // Render SELECTS without orderby/limit/count - $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); - } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; - } - else - { - $sLimit = ''; - } - - if ($bGetCount) - { - $sSelects = '('.implode(" $sLimit)$sLineSep UNION$sLineSep(", $aSelects)." $sLimit)"; - $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; - $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep) AS _union_tatooine_"; - } - else - { - $sOrderBy = $this->aQueries[0]->RenderOrderByClause($aOrderBy); - if (!empty($sOrderBy)) - { - $sOrderBy = "ORDER BY $sOrderBy$sLineSep $sLimit"; - $sSQL = '('.implode(")$sLineSep UNION$sLineSep (", $aSelects).')'.$sLineSep.$sOrderBy; - } - else - { - $sSQL = '('.implode(" $sLimit)$sLineSep UNION$sLineSep (", $aSelects)." $sLimit)"; - } - } - return $sSQL; - } - - // Interface, build the SQL query - - /** - * @param array $aArgs - * @param bool $bBeautifulQuery - * @param array $aOrderBy - * @param int $iLimitCount - * @param int $iLimitStart - * @return string - * @throws CoreException - */ - public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) - { - $this->m_bBeautifulQuery = $bBeautifulQuery; - $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; - - $aSelects = array(); - foreach ($this->aQueries as $oSQLQuery) - { - // Render SELECTS without orderby/limit/count - $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); - } - $sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')'; - $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; - - $aSelectAliases = array(); - $aGroupAliases = array(); - foreach ($this->aGroupBy as $sGroupAlias => $trash) - { - $aSelectAliases[$sGroupAlias] = "`$sGroupAlias`"; - $aGroupAliases[] = "`$sGroupAlias`"; - } - foreach($this->aSelectExpr as $sSelectAlias => $oExpr) - { - $aSelectAliases[$sSelectAlias] = $oExpr->RenderExpression(true)." AS `$sSelectAlias`"; - } - - $sSelect = implode(",$sLineSep ", $aSelectAliases); - $sGroupBy = implode(', ', $aGroupAliases); - - $sOrderBy = self::ClauseOrderBy($aOrderBy, $aSelectAliases); - if (!empty($sGroupBy)) - { - $sGroupBy = "GROUP BY $sGroupBy$sLineSep"; - } - if (!empty($sOrderBy)) - { - $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; - } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; - } - else - { - $sLimit = ''; - } - - - $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep $sGroupBy $sOrderBy$sLineSep $sLimit"; - return $sSQL; - } - - - public function OptimizeJoins($aUsedTables, $bTopCall = true) - { - foreach ($this->aQueries as $oSQLQuery) - { - $oSQLQuery->OptimizeJoins($aUsedTables); - } - } -} + + + +/** + * SQLUnionQuery + * build a mySQL compatible SQL query + * + * @copyright Copyright (C) 2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * SQLUnionQuery + * build a mySQL compatible SQL query + * + * @package iTopORM + */ + + +class SQLUnionQuery extends SQLQuery +{ + protected $aQueries; + protected $aGroupBy; + protected $aSelectExpr; + + public function __construct($aQueries, $aGroupBy, $aSelectExpr = array()) + { + parent::__construct(); + + $this->aQueries = array(); + foreach ($aQueries as $oSQLQuery) + { + $this->aQueries[] = $oSQLQuery->DeepClone(); + } + $this->aGroupBy = $aGroupBy; + $this->aSelectExpr = $aSelectExpr; + } + + public function DisplayHtml() + { + $aQueriesHtml = array(); + foreach ($this->aQueries as $oSQLQuery) + { + $aQueriesHtml[] = '

    '.$oSQLQuery->DisplayHtml().'

    '; + } + echo implode('UNION', $aQueriesHtml); + } + + public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '') + { + foreach ($this->aQueries as $oSubSQLQuery) + { + $oSubSQLQuery->AddInnerJoin($oSQLQuery->DeepClone(), $sLeftField, $sRightField, $sRightTable = ''); + } + } + + /** + * @param array $aArgs + * @throws Exception + */ + public function RenderDelete($aArgs = array()) + { + throw new Exception(__class__.'::'.__function__.'Not implemented !'); + } + + // Interface, build the SQL query + + /** + * @param array $aArgs + * @throws Exception + */ + public function RenderUpdate($aArgs = array()) + { + throw new Exception(__class__.'::'.__function__.'Not implemented !'); + } + + // Interface, build the SQL query + public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false) + { + $this->m_bBeautifulQuery = $bBeautifulQuery; + $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; + + $aSelects = array(); + foreach ($this->aQueries as $oSQLQuery) + { + // Render SELECTS without orderby/limit/count + $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); + } + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + + if ($bGetCount) + { + $sSelects = '('.implode(" $sLimit)$sLineSep UNION$sLineSep(", $aSelects)." $sLimit)"; + $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep) AS _union_tatooine_"; + } + else + { + $sOrderBy = $this->aQueries[0]->RenderOrderByClause($aOrderBy); + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy$sLineSep $sLimit"; + $sSQL = '('.implode(")$sLineSep UNION$sLineSep (", $aSelects).')'.$sLineSep.$sOrderBy; + } + else + { + $sSQL = '('.implode(" $sLimit)$sLineSep UNION$sLineSep (", $aSelects)." $sLimit)"; + } + } + return $sSQL; + } + + // Interface, build the SQL query + + /** + * @param array $aArgs + * @param bool $bBeautifulQuery + * @param array $aOrderBy + * @param int $iLimitCount + * @param int $iLimitStart + * @return string + * @throws CoreException + */ + public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) + { + $this->m_bBeautifulQuery = $bBeautifulQuery; + $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; + + $aSelects = array(); + foreach ($this->aQueries as $oSQLQuery) + { + // Render SELECTS without orderby/limit/count + $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); + } + $sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')'; + $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; + + $aSelectAliases = array(); + $aGroupAliases = array(); + foreach ($this->aGroupBy as $sGroupAlias => $trash) + { + $aSelectAliases[$sGroupAlias] = "`$sGroupAlias`"; + $aGroupAliases[] = "`$sGroupAlias`"; + } + foreach($this->aSelectExpr as $sSelectAlias => $oExpr) + { + $aSelectAliases[$sSelectAlias] = $oExpr->RenderExpression(true)." AS `$sSelectAlias`"; + } + + $sSelect = implode(",$sLineSep ", $aSelectAliases); + $sGroupBy = implode(', ', $aGroupAliases); + + $sOrderBy = self::ClauseOrderBy($aOrderBy, $aSelectAliases); + if (!empty($sGroupBy)) + { + $sGroupBy = "GROUP BY $sGroupBy$sLineSep"; + } + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; + } + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + + + $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep $sGroupBy $sOrderBy$sLineSep $sLimit"; + return $sSQL; + } + + + public function OptimizeJoins($aUsedTables, $bTopCall = true) + { + foreach ($this->aQueries as $oSQLQuery) + { + $oSQLQuery->OptimizeJoins($aUsedTables); + } + } +} diff --git a/core/stimulus.class.inc.php b/core/stimulus.class.inc.php index 4b263b498..88cd7764b 100644 --- a/core/stimulus.class.inc.php +++ b/core/stimulus.class.inc.php @@ -1,138 +1,138 @@ - - - -/** - * Object lifecycle management: stimulus - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * A stimulus is the trigger that makes the lifecycle go ahead (state machine) - * - * @package iTopORM - */ - -// #@# Really dirty !!! -// #@# TO BE CLEANED -> ALIGN WITH OTHER METAMODEL DECLARATIONS - -class ObjectStimulus -{ - private $m_aParams = array(); - private $m_sHostClass = null; - private $m_sCode = null; - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $this->m_aParams = $aParams; - $this->ConsistencyCheck(); - } - - public function SetHostClass($sHostClass) - { - $this->m_sHostClass = $sHostClass; - } - public function GetHostClass() - { - return $this->m_sHostClass; - } - public function GetCode() - { - return $this->m_sCode; - } - - public function GetLabel() - { - return Dict::S('Class:'.$this->m_sHostClass.'/Stimulus:'.$this->m_sCode, $this->m_sCode); - } - public function GetDescription() - { - return Dict::S('Class:'.$this->m_sHostClass.'/Stimulus:'.$this->m_sCode.'+', ''); - } - - public function GetLabel_Obsolete() - { - // Written for compatibility with a data model written prior to version 0.9.1 - if (array_key_exists('label', $this->m_aParams)) - { - return $this->m_aParams['label']; - } - else - { - return $this->GetLabel(); - } - } - - public function GetDescription_Obsolete() - { - // Written for compatibility with a data model written prior to version 0.9.1 - if (array_key_exists('description', $this->m_aParams)) - { - return $this->m_aParams['description']; - } - else - { - return $this->GetDescription(); - } - } - -// obsolete- public function Get($sParamName) {return $this->m_aParams[$sParamName];} - - // Note: I could factorize this code with the parameter management made for the AttributeDef class - // to be overloaded - static protected function ListExpectedParams() - { - return array(); - } - - private function ConsistencyCheck() - { - - // Check that any mandatory param has been specified - // - $aExpectedParams = $this->ListExpectedParams(); - foreach($aExpectedParams as $sParamName) - { - if (!array_key_exists($sParamName, $this->m_aParams)) - { - $aBacktrace = debug_backtrace(); - $sTargetClass = $aBacktrace[2]["class"]; - $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; - throw new CoreException("missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); - } - } - } -} - - - -class StimulusUserAction extends ObjectStimulus -{ - // Entry in the menus -} - -class StimulusInternal extends ObjectStimulus -{ - // Applied from page xxxx -} - -?> + + + +/** + * Object lifecycle management: stimulus + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * A stimulus is the trigger that makes the lifecycle go ahead (state machine) + * + * @package iTopORM + */ + +// #@# Really dirty !!! +// #@# TO BE CLEANED -> ALIGN WITH OTHER METAMODEL DECLARATIONS + +class ObjectStimulus +{ + private $m_aParams = array(); + private $m_sHostClass = null; + private $m_sCode = null; + + public function __construct($sCode, $aParams) + { + $this->m_sCode = $sCode; + $this->m_aParams = $aParams; + $this->ConsistencyCheck(); + } + + public function SetHostClass($sHostClass) + { + $this->m_sHostClass = $sHostClass; + } + public function GetHostClass() + { + return $this->m_sHostClass; + } + public function GetCode() + { + return $this->m_sCode; + } + + public function GetLabel() + { + return Dict::S('Class:'.$this->m_sHostClass.'/Stimulus:'.$this->m_sCode, $this->m_sCode); + } + public function GetDescription() + { + return Dict::S('Class:'.$this->m_sHostClass.'/Stimulus:'.$this->m_sCode.'+', ''); + } + + public function GetLabel_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('label', $this->m_aParams)) + { + return $this->m_aParams['label']; + } + else + { + return $this->GetLabel(); + } + } + + public function GetDescription_Obsolete() + { + // Written for compatibility with a data model written prior to version 0.9.1 + if (array_key_exists('description', $this->m_aParams)) + { + return $this->m_aParams['description']; + } + else + { + return $this->GetDescription(); + } + } + +// obsolete- public function Get($sParamName) {return $this->m_aParams[$sParamName];} + + // Note: I could factorize this code with the parameter management made for the AttributeDef class + // to be overloaded + static protected function ListExpectedParams() + { + return array(); + } + + private function ConsistencyCheck() + { + + // Check that any mandatory param has been specified + // + $aExpectedParams = $this->ListExpectedParams(); + foreach($aExpectedParams as $sParamName) + { + if (!array_key_exists($sParamName, $this->m_aParams)) + { + $aBacktrace = debug_backtrace(); + $sTargetClass = $aBacktrace[2]["class"]; + $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; + throw new CoreException("missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); + } + } + } +} + + + +class StimulusUserAction extends ObjectStimulus +{ + // Entry in the menus +} + +class StimulusInternal extends ObjectStimulus +{ + // Applied from page xxxx +} + +?> diff --git a/core/templatestring.class.inc.php b/core/templatestring.class.inc.php index c8d9a390e..59df9efdf 100644 --- a/core/templatestring.class.inc.php +++ b/core/templatestring.class.inc.php @@ -1,178 +1,178 @@ - - - -/** - * Simple helper class to interpret and transform a template string - * - * Usage: - * $oString = new TemplateString("Blah $this->friendlyname$ is in location $this->location_id->name$ ('$this->location_id->org_id->name$)"); - * echo $oString->Render(array('this' => $oContact)); - - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * Helper class - */ -class TemplateStringPlaceholder -{ - public $sToken; - public $sAttCode; - public $sFunction; - public $sParamName; - public $bIsValid; - - public function __construct($sToken) - { - $this->sToken = $sToken; - $this->sAttcode = ''; - $this->sFunction = ''; - $this->sParamName = ''; - $this->bIsValid = false; // Validity may be false in general, but it can work anyway (thanks to specialization) when rendering - } -} - -/** - * Class TemplateString - */ -class TemplateString -{ - protected $m_sRaw; - protected $m_aPlaceholders; - - public function __construct($sRaw) - { - $this->m_sRaw = $sRaw; - $this->m_aPlaceholders = null; - } - - /** - * Split the string into placholders - * @param Hash $aParamTypes Class of the expected parameters: hash array of '' => '' - * @return void - */ - protected function Analyze($aParamTypes = array()) - { - if (!is_null($this->m_aPlaceholders)) return; - - $this->m_aPlaceholders = array(); - if (preg_match_all('/\\$([a-z0-9_]+(->[a-z0-9_]+)*)\\$/', $this->m_sRaw, $aMatches)) - { - foreach($aMatches[1] as $sPlaceholder) - { - $oPlaceholder = new TemplateStringPlaceholder($sPlaceholder); - $oPlaceholder->bIsValid = false; - foreach ($aParamTypes as $sParamName => $sClass) - { - $sParamPrefix = $sParamName.'->'; - if (substr($sPlaceholder, 0, strlen($sParamPrefix)) == $sParamPrefix) - { - // Todo - detect functions (label...) - $oPlaceholder->sFunction = ''; - - $oPlaceholder->sParamName = $sParamName; - $sAttcode = substr($sPlaceholder, strlen($sParamPrefix)); - $oPlaceholder->sAttcode = $sAttcode; - $oPlaceholder->bIsValid = MetaModel::IsValidAttCode($sClass, $sAttcode, true /* extended */); - } - } - - $this->m_aPlaceholders[] = $oPlaceholder; - } - } - } - - /** - * Return the placeholders (for reporting purposes) - * @return void - */ - public function GetPlaceholders() - { - return $this->m_aPlaceholders; - } - - /** - * Check the format when possible - * @param Hash $aParamTypes Class of the expected parameters: hash array of '' => '' - * @return void - */ - public function IsValid($aParamTypes = array()) - { - $this->Analyze($aParamTypes); - - foreach($this->m_aPlaceholders as $oPlaceholder) - { - if (!$oPlaceholder->bIsValid) - { - if (count($aParamTypes) == 0) - { - return false; - } - if (array_key_exists($oPlaceholder->sParamName, $aParamTypes)) - { - return false; - } - } - } - return true; - } - - /** - * Apply the given parameters to replace the placeholders - * @param Hash $aParamValues Value of the expected parameters: hash array of '' => '' - * @return void - */ - public function Render($aParamValues = array()) - { - $aParamTypes = array(); - foreach($aParamValues as $sParamName => $value) - { - $aParamTypes[$sParamName] = get_class($value); - } - $this->Analyze($aParamTypes); - - $aSearch = array(); - $aReplace = array(); - foreach($this->m_aPlaceholders as $oPlaceholder) - { - if (array_key_exists($oPlaceholder->sParamName, $aParamValues)) - { - $oRef = $aParamValues[$oPlaceholder->sParamName]; - try - { - $value = $oRef->Get($oPlaceholder->sAttcode); - $aSearch[] = '$'.$oPlaceholder->sToken.'$'; - $aReplace[] = $value; - $oPlaceholder->bIsValid = true; - } - catch(Exception $e) - { - $oPlaceholder->bIsValid = false; - } - } - else - { - $oPlaceholder->bIsValid = false; - } - } - return str_replace($aSearch, $aReplace, $this->m_sRaw); - } -} + + + +/** + * Simple helper class to interpret and transform a template string + * + * Usage: + * $oString = new TemplateString("Blah $this->friendlyname$ is in location $this->location_id->name$ ('$this->location_id->org_id->name$)"); + * echo $oString->Render(array('this' => $oContact)); + + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * Helper class + */ +class TemplateStringPlaceholder +{ + public $sToken; + public $sAttCode; + public $sFunction; + public $sParamName; + public $bIsValid; + + public function __construct($sToken) + { + $this->sToken = $sToken; + $this->sAttcode = ''; + $this->sFunction = ''; + $this->sParamName = ''; + $this->bIsValid = false; // Validity may be false in general, but it can work anyway (thanks to specialization) when rendering + } +} + +/** + * Class TemplateString + */ +class TemplateString +{ + protected $m_sRaw; + protected $m_aPlaceholders; + + public function __construct($sRaw) + { + $this->m_sRaw = $sRaw; + $this->m_aPlaceholders = null; + } + + /** + * Split the string into placholders + * @param Hash $aParamTypes Class of the expected parameters: hash array of '' => '' + * @return void + */ + protected function Analyze($aParamTypes = array()) + { + if (!is_null($this->m_aPlaceholders)) return; + + $this->m_aPlaceholders = array(); + if (preg_match_all('/\\$([a-z0-9_]+(->[a-z0-9_]+)*)\\$/', $this->m_sRaw, $aMatches)) + { + foreach($aMatches[1] as $sPlaceholder) + { + $oPlaceholder = new TemplateStringPlaceholder($sPlaceholder); + $oPlaceholder->bIsValid = false; + foreach ($aParamTypes as $sParamName => $sClass) + { + $sParamPrefix = $sParamName.'->'; + if (substr($sPlaceholder, 0, strlen($sParamPrefix)) == $sParamPrefix) + { + // Todo - detect functions (label...) + $oPlaceholder->sFunction = ''; + + $oPlaceholder->sParamName = $sParamName; + $sAttcode = substr($sPlaceholder, strlen($sParamPrefix)); + $oPlaceholder->sAttcode = $sAttcode; + $oPlaceholder->bIsValid = MetaModel::IsValidAttCode($sClass, $sAttcode, true /* extended */); + } + } + + $this->m_aPlaceholders[] = $oPlaceholder; + } + } + } + + /** + * Return the placeholders (for reporting purposes) + * @return void + */ + public function GetPlaceholders() + { + return $this->m_aPlaceholders; + } + + /** + * Check the format when possible + * @param Hash $aParamTypes Class of the expected parameters: hash array of '' => '' + * @return void + */ + public function IsValid($aParamTypes = array()) + { + $this->Analyze($aParamTypes); + + foreach($this->m_aPlaceholders as $oPlaceholder) + { + if (!$oPlaceholder->bIsValid) + { + if (count($aParamTypes) == 0) + { + return false; + } + if (array_key_exists($oPlaceholder->sParamName, $aParamTypes)) + { + return false; + } + } + } + return true; + } + + /** + * Apply the given parameters to replace the placeholders + * @param Hash $aParamValues Value of the expected parameters: hash array of '' => '' + * @return void + */ + public function Render($aParamValues = array()) + { + $aParamTypes = array(); + foreach($aParamValues as $sParamName => $value) + { + $aParamTypes[$sParamName] = get_class($value); + } + $this->Analyze($aParamTypes); + + $aSearch = array(); + $aReplace = array(); + foreach($this->m_aPlaceholders as $oPlaceholder) + { + if (array_key_exists($oPlaceholder->sParamName, $aParamValues)) + { + $oRef = $aParamValues[$oPlaceholder->sParamName]; + try + { + $value = $oRef->Get($oPlaceholder->sAttcode); + $aSearch[] = '$'.$oPlaceholder->sToken.'$'; + $aReplace[] = $value; + $oPlaceholder->bIsValid = true; + } + catch(Exception $e) + { + $oPlaceholder->bIsValid = false; + } + } + else + { + $oPlaceholder->bIsValid = false; + } + } + return str_replace($aSearch, $aReplace, $this->m_sRaw); + } +} ?> \ No newline at end of file diff --git a/core/trigger.class.inc.php b/core/trigger.class.inc.php index b8e7693ec..971c3f8f7 100644 --- a/core/trigger.class.inc.php +++ b/core/trigger.class.inc.php @@ -1,393 +1,393 @@ - - - -/** - * Persistent class Trigger and derived - * User defined triggers, that may be used in conjunction with user defined actions - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * A user defined trigger, to customize the application - * A trigger will activate an action - * - * @package iTopORM - */ -abstract class Trigger extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("action_list", array("linked_class"=>"lnkTriggerAction", "ext_key_to_me"=>"trigger_id", "ext_key_to_remote"=>"action_id", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('finalclass', 'description', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - public function DoActivate($aContextArgs) - { - // Find the related actions - $oLinkedActions = $this->Get('action_list'); - while ($oLink = $oLinkedActions->Fetch()) - { - $iActionId = $oLink->Get('action_id'); - $oAction = MetaModel::GetObject('Action', $iActionId); - if ($oAction->IsActive()) - { - $oAction->DoExecute($this, $aContextArgs); - } - } - } - - /** - * Check whether the given object is in the scope of this trigger - * and can potentially be the subject of notifications - * @param DBObject $oObject The object to check - * @return bool - */ - public function IsInScope(DBObject $oObject) - { - // By default the answer is no - // Overload this function in your own derived class for a different behavior - return false; - } -} - -abstract class TriggerOnObject extends Trigger -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_onobject", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("class_category"=>"bizmodel", "more_values"=>"User,UserExternal,UserInternal,UserLDAP,UserLocal", "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("filter", array("allowed_values"=>null, "sql"=>"filter", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('default_search', array('description', 'target_class')); // Default criteria of the search banner -// MetaModel::Init_SetZListItems('standard_search', array('name', 'target_class', 'description')); // Criteria of the search form - } - - public function DoCheckToWrite() - { - parent::DoCheckToWrite(); - - $sFilter = trim($this->Get('filter')); - if (strlen($sFilter) > 0) - { - try - { - $oSearch = DBObjectSearch::FromOQL($sFilter); - - if (!MetaModel::IsParentClass($this->Get('target_class'), $oSearch->GetClass())) - { - $this->m_aCheckIssues[] = Dict::Format('TriggerOnObject:WrongFilterClass', $this->Get('target_class')); - } - } - catch(OqlException $e) - { - $this->m_aCheckIssues[] = Dict::Format('TriggerOnObject:WrongFilterQuery', $e->getMessage()); - } - } - } - - /** - * Check whether the given object is in the scope of this trigger - * and can potentially be the subject of notifications - * @param DBObject $oObject The object to check - * @return bool - */ - public function IsInScope(DBObject $oObject) - { - $sRootClass = $this->Get('target_class'); - return ($oObject instanceof $sRootClass); - } - - public function DoActivate($aContextArgs) - { - $bGo = true; - if (isset($aContextArgs['this->object()'])) - { - $bGo = $this->IsTargetObject($aContextArgs['this->object()']->GetKey()); - } - if ($bGo) - { - parent::DoActivate($aContextArgs); - } - } - - public function IsTargetObject($iObjectId) - { - $sFilter = trim($this->Get('filter')); - if (strlen($sFilter) > 0) - { - $oSearch = DBObjectSearch::FromOQL($sFilter); - $oSearch->AddCondition('id', $iObjectId, '='); - $oSet = new DBObjectSet($oSearch); - $bRet = ($oSet->Count() > 0); - } - else - { - $bRet = true; - } - return $bRet; - } -} -/** - * To trigger notifications when a ticket is updated from the portal - */ -class TriggerOnPortalUpdate extends TriggerOnObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_onportalupdate", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // Attributes to be displayed for a list - // Search criteria - } -} - -abstract class TriggerOnStateChange extends TriggerOnObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_onstatechange", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("state", array("allowed_values"=>null, "sql"=>"state", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'state')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class TriggerOnStateEnter extends TriggerOnStateChange -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_onstateenter", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class TriggerOnStateLeave extends TriggerOnStateChange -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_onstateleave", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('')); // Criteria of the advanced search form - } -} - -class TriggerOnObjectCreate extends TriggerOnObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_onobjcreate", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} - -class lnkTriggerAction extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "", - "state_attcode" => "", - "reconc_keys" => array('action_id', 'trigger_id'), - "db_table" => "priv_link_action_trigger", - "db_key_field" => "link_id", - "db_finalclass_field" => "", - "display_template" => "", - "is_link" => true, - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> '', "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("action_name", array("allowed_values"=>null, "extkey_attcode"=> 'action_id', "target_attcode"=>"name"))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> '', "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("trigger_name", array("allowed_values"=>null, "extkey_attcode"=> 'trigger_id', "target_attcode"=>"description"))); - MetaModel::Init_AddAttribute(new AttributeInteger("order", array("allowed_values"=>null, "sql"=>"order", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('action_id', 'trigger_id', 'order')); // Attributes to be displayed for a list - MetaModel::Init_SetZListItems('list', array('action_id', 'trigger_id', 'order')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('action_id', 'trigger_id', 'order')); // Criteria of the std search form - MetaModel::Init_SetZListItems('advanced_search', array('action_id', 'trigger_id', 'order')); // Criteria of the advanced search form - } -} - -class TriggerOnThresholdReached extends TriggerOnObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "description", - "state_attcode" => "", - "reconc_keys" => array('description'), - "db_table" => "priv_trigger_threshold", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeString("stop_watch_code", array("allowed_values"=>null, "sql"=>"stop_watch_code", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("threshold_index", array("allowed_values"=>null, "sql"=>"threshold_index", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'stop_watch_code', 'threshold_index', 'filter', 'action_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('target_class', 'threshold_index', 'threshold_index')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } -} -?> + + + +/** + * Persistent class Trigger and derived + * User defined triggers, that may be used in conjunction with user defined actions + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * A user defined trigger, to customize the application + * A trigger will activate an action + * + * @package iTopORM + */ +abstract class Trigger extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger", + "db_key_field" => "id", + "db_finalclass_field" => "realclass", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("action_list", array("linked_class"=>"lnkTriggerAction", "ext_key_to_me"=>"trigger_id", "ext_key_to_remote"=>"action_id", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('finalclass', 'description', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass')); // Attributes to be displayed for a list + // Search criteria +// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } + + public function DoActivate($aContextArgs) + { + // Find the related actions + $oLinkedActions = $this->Get('action_list'); + while ($oLink = $oLinkedActions->Fetch()) + { + $iActionId = $oLink->Get('action_id'); + $oAction = MetaModel::GetObject('Action', $iActionId); + if ($oAction->IsActive()) + { + $oAction->DoExecute($this, $aContextArgs); + } + } + } + + /** + * Check whether the given object is in the scope of this trigger + * and can potentially be the subject of notifications + * @param DBObject $oObject The object to check + * @return bool + */ + public function IsInScope(DBObject $oObject) + { + // By default the answer is no + // Overload this function in your own derived class for a different behavior + return false; + } +} + +abstract class TriggerOnObject extends Trigger +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_onobject", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeClass("target_class", array("class_category"=>"bizmodel", "more_values"=>"User,UserExternal,UserInternal,UserLDAP,UserLocal", "sql"=>"target_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeOQL("filter", array("allowed_values"=>null, "sql"=>"filter", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('default_search', array('description', 'target_class')); // Default criteria of the search banner +// MetaModel::Init_SetZListItems('standard_search', array('name', 'target_class', 'description')); // Criteria of the search form + } + + public function DoCheckToWrite() + { + parent::DoCheckToWrite(); + + $sFilter = trim($this->Get('filter')); + if (strlen($sFilter) > 0) + { + try + { + $oSearch = DBObjectSearch::FromOQL($sFilter); + + if (!MetaModel::IsParentClass($this->Get('target_class'), $oSearch->GetClass())) + { + $this->m_aCheckIssues[] = Dict::Format('TriggerOnObject:WrongFilterClass', $this->Get('target_class')); + } + } + catch(OqlException $e) + { + $this->m_aCheckIssues[] = Dict::Format('TriggerOnObject:WrongFilterQuery', $e->getMessage()); + } + } + } + + /** + * Check whether the given object is in the scope of this trigger + * and can potentially be the subject of notifications + * @param DBObject $oObject The object to check + * @return bool + */ + public function IsInScope(DBObject $oObject) + { + $sRootClass = $this->Get('target_class'); + return ($oObject instanceof $sRootClass); + } + + public function DoActivate($aContextArgs) + { + $bGo = true; + if (isset($aContextArgs['this->object()'])) + { + $bGo = $this->IsTargetObject($aContextArgs['this->object()']->GetKey()); + } + if ($bGo) + { + parent::DoActivate($aContextArgs); + } + } + + public function IsTargetObject($iObjectId) + { + $sFilter = trim($this->Get('filter')); + if (strlen($sFilter) > 0) + { + $oSearch = DBObjectSearch::FromOQL($sFilter); + $oSearch->AddCondition('id', $iObjectId, '='); + $oSet = new DBObjectSet($oSearch); + $bRet = ($oSet->Count() > 0); + } + else + { + $bRet = true; + } + return $bRet; + } +} +/** + * To trigger notifications when a ticket is updated from the portal + */ +class TriggerOnPortalUpdate extends TriggerOnObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_onportalupdate", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'description')); // Attributes to be displayed for a list + // Search criteria + } +} + +abstract class TriggerOnStateChange extends TriggerOnObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_onstatechange", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("state", array("allowed_values"=>null, "sql"=>"state", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class', 'state')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class TriggerOnStateEnter extends TriggerOnStateChange +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_onstateenter", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class TriggerOnStateLeave extends TriggerOnStateChange +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_onstateleave", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('')); // Criteria of the advanced search form + } +} + +class TriggerOnObjectCreate extends TriggerOnObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_onobjcreate", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} + +class lnkTriggerAction extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array('action_id', 'trigger_id'), + "db_table" => "priv_link_action_trigger", + "db_key_field" => "link_id", + "db_finalclass_field" => "", + "display_template" => "", + "is_link" => true, + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> '', "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("action_name", array("allowed_values"=>null, "extkey_attcode"=> 'action_id', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> '', "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("trigger_name", array("allowed_values"=>null, "extkey_attcode"=> 'trigger_id', "target_attcode"=>"description"))); + MetaModel::Init_AddAttribute(new AttributeInteger("order", array("allowed_values"=>null, "sql"=>"order", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('action_id', 'trigger_id', 'order')); // Attributes to be displayed for a list + MetaModel::Init_SetZListItems('list', array('action_id', 'trigger_id', 'order')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('action_id', 'trigger_id', 'order')); // Criteria of the std search form + MetaModel::Init_SetZListItems('advanced_search', array('action_id', 'trigger_id', 'order')); // Criteria of the advanced search form + } +} + +class TriggerOnThresholdReached extends TriggerOnObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "grant_by_profile,core/cmdb,application", + "key_type" => "autoincrement", + "name_attcode" => "description", + "state_attcode" => "", + "reconc_keys" => array('description'), + "db_table" => "priv_trigger_threshold", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeString("stop_watch_code", array("allowed_values"=>null, "sql"=>"stop_watch_code", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("threshold_index", array("allowed_values"=>null, "sql"=>"threshold_index", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('description', 'target_class', 'stop_watch_code', 'threshold_index', 'filter', 'action_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('target_class', 'threshold_index', 'threshold_index')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form +// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form + } +} +?> diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index ad8ea5ea9..1dbb563e8 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -1,1844 +1,1844 @@ - - - -/** - * User rights management API - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class UserRightException extends CoreException -{ -} - - -define('UR_ALLOWED_NO', 0); -define('UR_ALLOWED_YES', 1); -define('UR_ALLOWED_DEPENDS', 2); - -define('UR_ACTION_READ', 1); // View an object -define('UR_ACTION_MODIFY', 2); // Create/modify an object/attribute -define('UR_ACTION_DELETE', 3); // Delete an object - -define('UR_ACTION_BULK_READ', 4); // Export multiple objects -define('UR_ACTION_BULK_MODIFY', 5); // Create/modify multiple objects -define('UR_ACTION_BULK_DELETE', 6); // Delete multiple objects - -define('UR_ACTION_CREATE', 7); // Instantiate an object - -define('UR_ACTION_APPLICATION_DEFINED', 10000); // Application specific actions (CSV import, View schema...) - -/** - * User management module API - * - * @package iTopORM - */ -abstract class UserRightsAddOnAPI -{ - abstract public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US'); // could be used during initial installation - - abstract public function Init(); // loads data (possible optimizations) - - // Used to build select queries showing only objects visible for the given user - abstract public function GetSelectFilter($sLogin, $sClass, $aSettings = array()); // returns a filter object - - abstract public function IsActionAllowed($oUser, $sClass, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null); - abstract public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null); - abstract public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null); - abstract public function IsAdministrator($oUser); - abstract public function IsPortalUser($oUser); - abstract public function FlushPrivileges(); - - - /** - * Default behavior for addons that do not support profiles - * - * @param $oUser User - * @return array - */ - public function ListProfiles($oUser) - { - return array(); - } - - /** - * ... - */ - public function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null) - { - if ($sAttCode == null) - { - $sAttCode = $this->GetOwnerOrganizationAttCode($sClass); - } - if (empty($sAttCode)) - { - return $oFilter = new DBObjectSearch($sClass); - } - - $oExpression = new FieldExpression($sAttCode, $sClass); - $oFilter = new DBObjectSearch($sClass); - $oListExpr = ListExpression::FromScalars($aAllowedOrgs); - - $oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr); - $oFilter->AddConditionExpression($oCondition); - - if ($this->HasSharing()) - { - if (($sAttCode == 'id') && isset($aSettings['bSearchMode']) && $aSettings['bSearchMode']) - { - // Querying organizations (or derived) - // and the expected list of organizations will be used as a search criteria - // Therefore the query can also return organization having objects shared with the allowed organizations - // - // 1) build the list of organizations sharing something with the allowed organizations - // Organization <== sharing_org_id == SharedObject having org_id IN {user orgs} - $oShareSearch = new DBObjectSearch('SharedObject'); - $oOrgField = new FieldExpression('org_id', 'SharedObject'); - $oShareSearch->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr)); - - $oSearchSharers = new DBObjectSearch('Organization'); - $oSearchSharers->AllowAllData(); - $oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id'); - $aSharers = array(); - foreach($oSearchSharers->ToDataArray(array('id')) as $aRow) - { - $aSharers[] = $aRow['id']; - } - // 2) Enlarge the overall results: ... OR id IN(id1, id2, id3) - if (count($aSharers) > 0) - { - $oSharersList = ListExpression::FromScalars($aSharers); - $oFilter->MergeConditionExpression(new BinaryExpression($oExpression, 'IN', $oSharersList)); - } - } - - $aShareProperties = SharedObject::GetSharedClassProperties($sClass); - if ($aShareProperties) - { - $sShareClass = $aShareProperties['share_class']; - $sShareAttCode = $aShareProperties['attcode']; - - $oSearchShares = new DBObjectSearch($sShareClass); - $oSearchShares->AllowAllData(); - - $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization'); - $oOrgField = new FieldExpression('org_id', $sShareClass); - $oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr)); - $aShared = array(); - foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow) - { - $aShared[] = $aRow[$sShareAttCode]; - } - if (count($aShared) > 0) - { - $oObjId = new FieldExpression('id', $sClass); - $oSharedIdList = ListExpression::FromScalars($aShared); - $oFilter->MergeConditionExpression(new BinaryExpression($oObjId, 'IN', $oSharedIdList)); - } - } - } // if HasSharing - - return $oFilter; - } -} - - -require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); -abstract class User extends cmdbAbstractObject -{ - public static function Init() - { - $aParams = array - ( - "category" => "core,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "login", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_user", - "db_key_field" => "id", - "db_finalclass_field" => "", - "display_template" => "", - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeExternalKey("contactid", array("targetclass"=>"Person", "allowed_values"=>null, "sql"=>"contactid", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalField("last_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"name"))); - MetaModel::Init_AddAttribute(new AttributeExternalField("first_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"first_name"))); - MetaModel::Init_AddAttribute(new AttributeExternalField("email", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"email"))); - MetaModel::Init_AddAttribute(new AttributeExternalField("org_id", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"org_id"))); - - MetaModel::Init_AddAttribute(new AttributeString("login", array("allowed_values"=>null, "sql"=>"login", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>"EN US", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values" => new ValueSetEnum('enabled,disabled'), "sql"=>"status", "default_value"=>"enabled", "is_null_allowed"=>false, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("profile_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"userid", "ext_key_to_remote"=>"profileid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("allowed_org_list", array("linked_class"=>"URP_UserOrg", "ext_key_to_me"=>"userid", "ext_key_to_remote"=>"allowed_org_id", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'language', 'status', 'profile_list', 'allowed_org_list')); // Unused as it's an abstract class ! - MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'status', 'org_id')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid', 'email', 'language', 'status', 'org_id')); // Criteria of the std search form - MetaModel::Init_SetZListItems('default_search', array('login', 'contactid', 'org_id')); // Default criteria of the search banner - } - - abstract public function CheckCredentials($sPassword); - abstract public function TrustWebServerContext(); - abstract public function CanChangePassword(); - abstract public function ChangePassword($sOldPassword, $sNewPassword); - - /* - * Compute a name in best effort mode - */ - public function GetFriendlyName() - { - if (!MetaModel::IsValidAttCode(get_class($this), 'contactid')) - { - return $this->Get('login'); - } - if ($this->Get('contactid') != 0) - { - $sFirstName = $this->Get('first_name'); - $sLastName = $this->Get('last_name'); - $sEmail = $this->Get('email'); - if (strlen($sFirstName) > 0) - { - return "$sFirstName $sLastName"; - } - elseif (strlen($sEmail) > 0) - { - return "$sLastName <$sEmail>"; - } - else - { - return $sLastName; - } - } - return $this->Get('login'); - } - - protected $oContactObject; - - /** - * Fetch and memorize the associated contact (if any) - */ - public function GetContactObject() - { - if (is_null($this->oContactObject)) - { - if (MetaModel::IsValidAttCode(get_class($this), 'contactid') && ($this->Get('contactid') != 0)) - { - $this->oContactObject = MetaModel::GetObject('Contact', $this->Get('contactid')); - } - } - return $this->oContactObject; - } - - /** - * Overload the standard behavior. - * - * @throws \CoreException - */ - public function DoCheckToWrite() - { - parent::DoCheckToWrite(); - - // Note: This MUST be factorized later: declare unique keys (set of columns) in the data model - $aChanges = $this->ListChanges(); - if (array_key_exists('login', $aChanges)) - { - if (strcasecmp($this->Get('login'), $this->GetOriginal('login')) !== 0) - { - $sNewLogin = $aChanges['login']; - $oSearch = DBObjectSearch::FromOQL_AllData("SELECT User WHERE login = :newlogin"); - if (!$this->IsNew()) - { - $oSearch->AddCondition('id', $this->GetKey(), '!='); - } - $oSet = new DBObjectSet($oSearch, array(), array('newlogin' => $sNewLogin)); - if ($oSet->Count() > 0) - { - $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:LoginMustBeUnique', $sNewLogin); - } - } - } - // Check that this user has at least one profile assigned when profiles have changed - if (array_key_exists('profile_list', $aChanges)) - { - $oSet = $this->Get('profile_list'); - if ($oSet->Count() == 0) - { - $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:AtLeastOneProfileIsNeeded'); - } - } - // Only administrators can manage administrators - if (UserRights::IsAdministrator($this) && !UserRights::IsAdministrator()) - { - $this->m_aCheckIssues[] = Dict::Format('UI:Login:Error:AccessRestricted'); - } - - if (!UserRights::IsAdministrator()) - { - $oUser = UserRights::GetUserObject(); - $oAddon = UserRights::GetModuleInstance(); - if (!is_null($oUser) && method_exists($oAddon, 'GetUserOrgs')) - { - $aOrgs = $oAddon->GetUserOrgs($oUser, ''); - if (count($aOrgs) > 0) - { - // Check that the modified User belongs to one of our organization - if (!in_array($this->GetOriginal('org_id'), $aOrgs) || !in_array($this->Get('org_id'), $aOrgs)) - { - $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:UserOrganizationNotAllowed'); - } - // Check users with restricted organizations when allowed organizations have changed - if ($this->IsNew() || array_key_exists('allowed_org_list', $aChanges)) - { - $oSet = $this->get('allowed_org_list'); - if ($oSet->Count() == 0) - { - $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:AtLeastOneOrganizationIsNeeded'); - } - else - { - $aModifiedLinks = $oSet->ListModifiedLinks(); - foreach($aModifiedLinks as $oLink) - { - if (!in_array($oLink->Get('allowed_org_id'), $aOrgs)) - { - $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:OrganizationNotAllowed'); - } - } - } - } - } - } - } - } - - function GetGrantAsHtml($sClass, $iAction) - { - if (UserRights::IsActionAllowed($sClass, $iAction, null, $this)) - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; - } - else - { - return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; - } - } - - function DoShowGrantSumary($oPage, $sClassCategory) - { - if (UserRights::IsAdministrator($this)) - { - // Looks dirty, but ok that's THE ONE - $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); - return; - } - - $oKPI = new ExecutionKPI(); - - $aDisplayData = array(); - foreach (MetaModel::GetClasses($sClassCategory) as $sClass) - { - $aClassStimuli = MetaModel::EnumStimuli($sClass); - if (count($aClassStimuli) > 0) - { - $aStimuli = array(); - foreach ($aClassStimuli as $sStimulusCode => $oStimulus) - { - if (UserRights::IsStimulusAllowed($sClass, $sStimulusCode, null, $this)) - { - $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; - } - } - $sStimuli = implode(', ', $aStimuli); - } - else - { - $sStimuli = ''.Dict::S('UI:UserManagement:NoLifeCycleApplicable').''; - } - - $aDisplayData[] = array( - 'class' => MetaModel::GetName($sClass), - 'read' => $this->GetGrantAsHtml($sClass, UR_ACTION_READ), - 'bulkread' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_READ), - 'write' => $this->GetGrantAsHtml($sClass, UR_ACTION_MODIFY), - 'bulkwrite' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_MODIFY), - 'delete' => $this->GetGrantAsHtml($sClass, UR_ACTION_DELETE), - 'bulkdelete' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_DELETE), - 'stimuli' => $sStimuli, - ); - } - - $oKPI->ComputeAndReport('Computation of user rights'); - - $aDisplayConfig = array(); - $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); - $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); - $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); - $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); - $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); - $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); - $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); - $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); - $oPage->table($aDisplayConfig, $aDisplayData); - } - - function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, $bEditMode); - if (!$bEditMode) - { - $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); - $this->DoShowGrantSumary($oPage, 'bizmodel,grant_by_profile'); - - // debug - if (false) - { - $oPage->SetCurrentTab('More on user rigths (dev only)'); - $oPage->add("

    User rights

    \n"); - $this->DoShowGrantSumary($oPage, 'addon/userrights'); - $oPage->add("

    Change log

    \n"); - $this->DoShowGrantSumary($oPage, 'core/cmdb'); - $oPage->add("

    Application

    \n"); - $this->DoShowGrantSumary($oPage, 'application'); - $oPage->add("

    GUI

    \n"); - $this->DoShowGrantSumary($oPage, 'gui'); - - } - } - } - - public function CheckToDelete(&$oDeletionPlan) - { - if (MetaModel::GetConfig()->Get('demo_mode')) - { - // Users deletion is NOT allowed in demo mode - $oDeletionPlan->AddToDelete($this, null); - $oDeletionPlan->SetDeletionIssues($this, array('deletion not allowed in demo mode.'), true); - $oDeletionPlan->ComputeResults(); - return false; - } - return parent::CheckToDelete($oDeletionPlan); - } - - protected function DBDeleteSingleObject() - { - if (MetaModel::GetConfig()->Get('demo_mode')) - { - // Users deletion is NOT allowed in demo mode - return; - } - parent::DBDeleteSingleObject(); - } -} - -/** - * Abstract class for all types of "internal" authentication i.e. users - * for which the application is supplied a login and a password opposed - * to "external" users for whom the authentication is performed outside - * of the application (by the web server for example). - * Note that "internal" users do not necessary correspond to a local authentication - * they may be authenticated by a remote system, like in authent-ldap. - */ -abstract class UserInternal extends User -{ - // Nothing special, just a base class to categorize this type of authenticated users - public static function Init() - { - $aParams = array - ( - "category" => "core,grant_by_profile", - "key_type" => "autoincrement", - "name_attcode" => "login", - "state_attcode" => "", - "reconc_keys" => array('login'), - "db_table" => "priv_internaluser", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // When set, this token allows for password reset - MetaModel::Init_AddAttribute(new AttributeOneWayPassword("reset_pwd_token", array("allowed_values"=>null, "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Display lists - MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'status', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'status', 'org_id')); // Attributes to be displayed for a list - // Search criteria - MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid', 'status', 'org_id')); // Criteria of the std search form - } - - /** - * Use with care! - */ - public function SetPassword($sNewPassword) - { - } - - /** - * The email recipient is the person who is allowed to regain control when the password gets lost - * Throws an exception if the feature cannot be available - */ - public function GetResetPasswordEmail() - { - if (!MetaModel::IsValidAttCode(get_class($this), 'contactid')) - { - throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact')); - } - $iContactId = $this->Get('contactid'); - if ($iContactId == 0) - { - throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact')); - } - $oContact = MetaModel::GetObject('Contact', $iContactId); - // Determine the email attribute (the first one will be our choice) - foreach (MetaModel::ListAttributeDefs(get_class($oContact)) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeEmailAddress) - { - $sEmailAttCode = $sAttCode; - // we've got one, exit the loop - break; - } - } - if (!isset($sEmailAttCode)) - { - throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmailAtt')); - } - $sRes = trim($oContact->Get($sEmailAttCode)); - return $sRes; - } -} - -/** - * Self register extension - * - * @package iTopORM - */ -interface iSelfRegister -{ - /** - * Called when no user is found in iTop for the corresponding 'name'. This method - * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly - * @param string $sName The typed-in user name - * @param string $sPassword The typed-in password - * @param string $sLoginMode The login method used (cas|form|basic|url) - * @param string $sAuthentication The authentication method used (any|internal|external) - * @return bool true if the user is a valid one, false otherwise - */ - public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication); - - /** - * Called after the user has been authenticated and found in iTop. This method can - * Update the user's definition on the fly (profiles...) to keep it in sync with an external source - * @param User $oUser The user to update/synchronize - * @param string $sLoginMode The login mode used (cas|form|basic|url) - * @param string $sAuthentication The authentication method used - * @return void - */ - public static function UpdateUser(User $oUser, $sLoginMode, $sAuthentication); -} - -/** - * User management core API - * - * @package iTopORM - */ -class UserRights -{ - /** @var UserRightsAddOnAPI $m_oAddOn */ - protected static $m_oAddOn; - protected static $m_oUser; - protected static $m_oRealUser; - protected static $m_sSelfRegisterAddOn = null; - - public static function SelectModule($sModuleName) - { - if (!class_exists($sModuleName)) - { - throw new CoreException("Could not select this module, '$sModuleName' in not a valid class name"); - return; - } - if (!is_subclass_of($sModuleName, 'UserRightsAddOnAPI')) - { - throw new CoreException("Could not select this module, the class '$sModuleName' is not derived from UserRightsAddOnAPI"); - return; - } - self::$m_oAddOn = new $sModuleName; - self::$m_oAddOn->Init(); - self::$m_oUser = null; - self::$m_oRealUser = null; - } - - public static function SelectSelfRegister($sModuleName) - { - if (!class_exists($sModuleName)) - { - throw new CoreException("Could not select the class, '$sModuleName' for self register, is not a valid class name"); - } - self::$m_sSelfRegisterAddOn = $sModuleName; - } - - public static function GetModuleInstance() - { - return self::$m_oAddOn; - } - - // Installation: create the very first user - public static function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') - { - $bRes = self::$m_oAddOn->CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage); - self::FlushPrivileges(true /* reset admin cache */); - return $bRes; - } - - public static function IsLoggedIn() - { - if (self::$m_oUser == null) - { - return false; - } - else - { - return true; - } - } - - public static function Login($sName, $sAuthentication = 'any') - { - $oUser = self::FindUser($sName, $sAuthentication); - if (is_null($oUser)) - { - return false; - } - self::$m_oUser = $oUser; - - if (isset($_SESSION['impersonate_user'])) - { - self::$m_oRealUser = self::$m_oUser; - self::$m_oUser = self::FindUser($_SESSION['impersonate_user']); - } - - Dict::SetUserLanguage(self::GetUserLanguage()); - return true; - } - - public static function CheckCredentials($sName, $sPassword, $sLoginMode = 'form', $sAuthentication = 'any') - { - $oUser = self::FindUser($sName, $sAuthentication); - if (is_null($oUser)) - { - // Check if the user does not exist at all or if it is just disabled - if (self::FindUser($sName, $sAuthentication, true) == null) - { - // User does not exist at all - return self::CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication); - } - else - { - // User is actually disabled - return false; - } - } - - if (!$oUser->CheckCredentials($sPassword)) - { - return false; - } - self::UpdateUser($oUser, $sLoginMode, $sAuthentication); - return true; - } - - public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication) - { - if (self::$m_sSelfRegisterAddOn != null) - { - return call_user_func(array(self::$m_sSelfRegisterAddOn, 'CheckCredentialsAndCreateUser'), $sName, $sPassword, $sLoginMode, $sAuthentication); - } - } - - public static function UpdateUser($oUser, $sLoginMode, $sAuthentication) - { - if (self::$m_sSelfRegisterAddOn != null) - { - call_user_func(array(self::$m_sSelfRegisterAddOn, 'UpdateUser'), $oUser, $sLoginMode, $sAuthentication); - } - } - - public static function TrustWebServerContext() - { - if (!is_null(self::$m_oUser)) - { - return self::$m_oUser->TrustWebServerContext(); - } - else - { - return false; - } - } - - /** - * Tells whether or not the archive mode is allowed to the current user - * @return boolean - */ - static function CanBrowseArchive() - { - if (is_null(self::$m_oUser)) - { - $bRet = false; - } - elseif (isset($_SESSION['archive_allowed'])) - { - $bRet = $_SESSION['archive_allowed']; - } - else - { - // As of now, anybody can switch to the archive mode as soon as there is an archivable class - $bRet = (count(MetaModel::EnumArchivableClasses()) > 0); - $_SESSION['archive_allowed'] = $bRet; - } - return $bRet; - } - - public static function CanChangePassword() - { - if (MetaModel::DBIsReadOnly()) - { - return false; - } - - if (!is_null(self::$m_oUser)) - { - return self::$m_oUser->CanChangePassword(); - } - else - { - return false; - } - } - - public static function ChangePassword($sOldPassword, $sNewPassword, $sName = '') - { - if (empty($sName)) - { - $oUser = self::$m_oUser; - } - else - { - // find the id out of the login string - $oUser = self::FindUser($sName); - } - if (is_null($oUser)) - { - return false; - } - else - { - $oUser->AllowWrite(true); - return $oUser->ChangePassword($sOldPassword, $sNewPassword); - } - } - - /** - * @param string $sName Login identifier of the user to impersonate - * @return bool True if an impersonation occurred - */ - public static function Impersonate($sName) - { - if (!self::CheckLogin()) return false; - - $bRet = false; - $oUser = self::FindUser($sName); - if ($oUser) - { - $bRet = true; - if (is_null(self::$m_oRealUser)) - { - // First impersonation - self::$m_oRealUser = self::$m_oUser; - } - if (self::$m_oRealUser && (self::$m_oRealUser->GetKey() == $oUser->GetKey())) - { - // Equivalent to "Deimpersonate" - self::Deimpersonate(); - } - else - { - // Do impersonate! - self::$m_oUser = $oUser; - Dict::SetUserLanguage(self::GetUserLanguage()); - $_SESSION['impersonate_user'] = $sName; - self::_ResetSessionCache(); - } - } - return $bRet; - } - - public static function Deimpersonate() - { - if (!is_null(self::$m_oRealUser)) - { - self::$m_oUser = self::$m_oRealUser; - Dict::SetUserLanguage(self::GetUserLanguage()); - unset($_SESSION['impersonate_user']); - self::_ResetSessionCache(); - } - } - - public static function GetUser() - { - if (is_null(self::$m_oUser)) - { - return ''; - } - else - { - return self::$m_oUser->Get('login'); - } - } - - public static function GetUserObject() - { - if (is_null(self::$m_oUser)) - { - return null; - } - else - { - return self::$m_oUser; - } - } - - public static function GetUserLanguage() - { - if (is_null(self::$m_oUser)) - { - return 'EN US'; - - } - else - { - return self::$m_oUser->Get('language'); - } - } - - public static function GetUserId($sName = '') - { - if (empty($sName)) - { - // return current user id - if (is_null(self::$m_oUser)) - { - return null; - } - return self::$m_oUser->GetKey(); - } - else - { - // find the id out of the login string - $oUser = self::$m_oAddOn->FindUser($sName); - if (is_null($oUser)) - { - return null; - } - return $oUser->GetKey(); - } - } - - public static function GetContactId($sName = '') - { - if (empty($sName)) - { - $oUser = self::$m_oUser; - } - else - { - $oUser = FindUser($sName); - } - if (is_null($oUser)) - { - return ''; - } - if (!MetaModel::IsValidAttCode(get_class($oUser), 'contactid')) - { - return ''; - } - return $oUser->Get('contactid'); - } - - public static function GetContactObject() - { - if (is_null(self::$m_oUser)) - { - return null; - } - else - { - return self::$m_oUser->GetContactObject(); - } - } - - // Render the user name in best effort mode - public static function GetUserFriendlyName($sName = '') - { - if (empty($sName)) - { - $oUser = self::$m_oUser; - } - else - { - $oUser = FindUser($sName); - } - if (is_null($oUser)) - { - return ''; - } - return $oUser->GetFriendlyName(); - } - - public static function IsImpersonated() - { - if (is_null(self::$m_oRealUser)) - { - return false; - } - return true; - } - - public static function GetRealUser() - { - if (is_null(self::$m_oRealUser)) - { - return ''; - } - return self::$m_oRealUser->Get('login'); - } - - public static function GetRealUserObject() - { - return self::$m_oRealUser; - } - - public static function GetRealUserId() - { - if (is_null(self::$m_oRealUser)) - { - return ''; - } - return self::$m_oRealUser->GetKey(); - } - - public static function GetRealUserFriendlyName() - { - if (is_null(self::$m_oRealUser)) - { - return ''; - } - return self::$m_oRealUser->GetFriendlyName(); - } - - protected static function CheckLogin() - { - if (!self::IsLoggedIn()) - { - //throw new UserRightException('No user logged in', array()); - return false; - } - return true; - } - - /** - * Add additional filter for organization silos to all the requests. - * - * @param $sClass - * @param array $aSettings - * - * @return bool|\Expression - */ - public static function GetSelectFilter($sClass, $aSettings = array()) - { - // When initializing, we need to let everything pass trough - if (!self::CheckLogin()) {return true;} - - if (self::IsAdministrator()) {return true;} - - try - { - // Check Bug 1436 for details - if (MetaModel::HasCategory($sClass, 'bizmodel')) - { - return self::$m_oAddOn->GetSelectFilter(self::$m_oUser, $sClass, $aSettings); - } - else - { - return true; - } - } catch (Exception $e) - { - return false; - } - } - - /** - * @param string $sClass - * @param int $iActionCode - * @param DBObjectSet $oInstanceSet - * @param User $oUser - * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS) - */ - public static function IsActionAllowed($sClass, $iActionCode, /*dbObjectSet*/$oInstanceSet = null, $oUser = null) - { - // When initializing, we need to let everything pass trough - if (!self::CheckLogin()) return UR_ALLOWED_YES; - - if (MetaModel::DBIsReadOnly()) - { - if ($iActionCode == UR_ACTION_CREATE) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_MODIFY) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_BULK_MODIFY) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO; - } - - $aPredefinedObjects = call_user_func(array($sClass, 'GetPredefinedObjects')); - if ($aPredefinedObjects != null) - { - // As opposed to the read-only DB, modifying an object is allowed - // (the constant columns will be marked as read-only) - // - if ($iActionCode == UR_ACTION_CREATE) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO; - } - - if (self::IsAdministrator($oUser)) return UR_ALLOWED_YES; - - if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile')) - { - if (is_null($oUser)) - { - $oUser = self::$m_oUser; - } - if ($iActionCode == UR_ACTION_CREATE) - { - // The addons currently DO NOT handle the case "CREATE" - // Therefore it is considered to be equivalent to "MODIFY" - $iActionCode = UR_ACTION_MODIFY; - } - return self::$m_oAddOn->IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet); - } - elseif(($iActionCode == UR_ACTION_READ) && MetaModel::HasCategory($sClass, 'view_in_gui')) - { - return UR_ALLOWED_YES; - } - else - { - // Other classes could be edited/listed by the administrators - return UR_ALLOWED_NO; - } - } - - public static function IsStimulusAllowed($sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null) - { - // When initializing, we need to let everything pass trough - if (!self::CheckLogin()) return true; - - if (MetaModel::DBIsReadOnly()) - { - return false; - } - - if (self::IsAdministrator($oUser)) return true; - - if (MetaModel::HasCategory($sClass, 'bizmodel')) - { - if (is_null($oUser)) - { - $oUser = self::$m_oUser; - } - return self::$m_oAddOn->IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet); - } - else - { - // Other classes could be edited/listed by the administrators - return false; - } - } - - /** - * @param string $sClass - * @param string $sAttCode - * @param int $iActionCode - * @param DBObjectSet $oInstanceSet - * @param User $oUser - * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO) - */ - public static function IsActionAllowedOnAttribute($sClass, $sAttCode, $iActionCode, /*dbObjectSet*/$oInstanceSet = null, $oUser = null) - { - // When initializing, we need to let everything pass trough - if (!self::CheckLogin()) return UR_ALLOWED_YES; - - if (MetaModel::DBIsReadOnly()) - { - if ($iActionCode == UR_ACTION_MODIFY) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO; - if ($iActionCode == UR_ACTION_BULK_MODIFY) return falUR_ALLOWED_NOse; - if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO; - } - - if (self::IsAdministrator($oUser)) return UR_ALLOWED_YES; - - if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile')) - { - if (is_null($oUser)) - { - $oUser = self::$m_oUser; - } - return self::$m_oAddOn->IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet); - } - - // this module is forbidden for non admins - if (MetaModel::HasCategory($sClass, 'addon/userrights')) return UR_ALLOWED_NO; - - // the rest is allowed - return UR_ALLOWED_YES; - - - } - - protected static $m_aAdmins = array(); - public static function IsAdministrator($oUser = null) - { - if (!self::CheckLogin()) return false; - - if (is_null($oUser)) - { - $oUser = self::$m_oUser; - } - $iUser = $oUser->GetKey(); - if (!isset(self::$m_aAdmins[$iUser])) - { - self::$m_aAdmins[$iUser] = self::$m_oAddOn->IsAdministrator($oUser); - } - return self::$m_aAdmins[$iUser]; - } - - protected static $m_aPortalUsers = array(); - public static function IsPortalUser($oUser = null) - { - if (!self::CheckLogin()) return false; - - if (is_null($oUser)) - { - $oUser = self::$m_oUser; - } - $iUser = $oUser->GetKey(); - if (!isset(self::$m_aPortalUsers[$iUser])) - { - self::$m_aPortalUsers[$iUser] = self::$m_oAddOn->IsPortalUser($oUser); - } - return self::$m_aPortalUsers[$iUser]; - } - - public static function GetAllowedPortals() - { - $aAllowedPortals = array(); - $aPortalsConf = PortalDispatcherData::GetData(); - $aDispatchers = array(); - foreach ($aPortalsConf as $sPortalId => $aConf) - { - $sHandlerClass = $aConf['handler']; - $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId); - } - - foreach ($aDispatchers as $sPortalId => $oDispatcher) - { - if ($oDispatcher->IsUserAllowed()) - { - $aAllowedPortals[] = array( - 'id' => $sPortalId, - 'label' => $oDispatcher->GetLabel(), - 'url' => $oDispatcher->GetUrl(), - ); - } - } - return $aAllowedPortals; - } - - public static function ListProfiles($oUser = null) - { - if (is_null($oUser)) - { - $oUser = self::$m_oUser; - } - if ($oUser === null) - { - // Not logged in: no profile at all - $aProfiles = array(); - } - elseif ((self::$m_oUser !== null) && ($oUser->GetKey() == self::$m_oUser->GetKey())) - { - // Data about the current user can be found into the session data - if (array_key_exists('profile_list', $_SESSION)) - { - $aProfiles = $_SESSION['profile_list']; - } - } - - if (!isset($aProfiles)) - { - $aProfiles = self::$m_oAddOn->ListProfiles($oUser); - } - return $aProfiles; - } - - /** - * @param string $sProfileName Profile name to search for - * @param User|null $oUser - * - * @return bool - */ - public static function HasProfile($sProfileName, $oUser = null) - { - $bRet = in_array($sProfileName, self::ListProfiles($oUser)); - return $bRet; - } - - /** - * Reset cached data - * @param Bool Reset admin cache as well - * @return void - */ - public static function FlushPrivileges($bResetAdminCache = false) - { - if ($bResetAdminCache) - { - self::$m_aAdmins = array(); - self::$m_aPortalUsers = array(); - } - if (!isset($_SESSION) && !utils::IsModeCLI()) - { - session_name('itop-'.md5(APPROOT)); - session_start(); - } - self::_ResetSessionCache(); - if (self::$m_oAddOn) - { - self::$m_oAddOn->FlushPrivileges(); - } - } - - static $m_aCacheUsers; - - /** - * Find a user based on its login and its type of authentication - * - * @param string $sLogin Login/identifier of the user - * @param string $sAuthentication Type of authentication used: internal|external|any - * @param bool $bAllowDisabledUsers Whether or not to retrieve disabled users (status != enabled) - * - * @return User The found user or null - * @throws \OQLException - */ - protected static function FindUser($sLogin, $sAuthentication = 'any', $bAllowDisabledUsers = false) - { - if ($sAuthentication == 'any') - { - $oUser = self::FindUser($sLogin, 'internal'); - if ($oUser == null) - { - $oUser = self::FindUser($sLogin, 'external'); - } - } - else - { - if (!isset(self::$m_aCacheUsers)) - { - self::$m_aCacheUsers = array('internal' => array(), 'external' => array()); - } - - if (!isset(self::$m_aCacheUsers[$sAuthentication][$sLogin])) - { - switch($sAuthentication) - { - case 'external': - $sBaseClass = 'UserExternal'; - break; - - case 'internal': - $sBaseClass = 'UserInternal'; - break; - - default: - echo "

    sAuthentication = $sAuthentication

    \n"; - assert(false); // should never happen - } - $oSearch = DBObjectSearch::FromOQL("SELECT $sBaseClass WHERE login = :login"); - if (!$bAllowDisabledUsers) - { - $oSearch->AddCondition('status', 'enabled'); - } - $oSet = new DBObjectSet($oSearch, array(), array('login' => $sLogin)); - $oUser = $oSet->fetch(); - self::$m_aCacheUsers[$sAuthentication][$sLogin] = $oUser; - } - $oUser = self::$m_aCacheUsers[$sAuthentication][$sLogin]; - } - return $oUser; - } - - public static function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null) - { - return self::$m_oAddOn->MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings, $sAttCode); - } - - public static function _InitSessionCache() - { - // Cache data about the current user into the session - if (isset($_SESSION)) - { - $_SESSION['profile_list'] = self::ListProfiles(); - } - } - - public static function _ResetSessionCache() - { - if (isset($_SESSION['profile_list'])) - { - unset($_SESSION['profile_list']); - } - if (isset($_SESSION['archive_allowed'])) - { - unset($_SESSION['archive_allowed']); - } - } -} - -/** - * Helper class to get the number/list of items for which a given action is allowed/possible - */ -class ActionChecker -{ - var $oFilter; - var $iActionCode; - var $iAllowedCount = null; - var $aAllowedIDs = null; - - public function __construct(DBSearch $oFilter, $iActionCode) - { - $this->oFilter = $oFilter; - $this->iActionCode = $iActionCode; - $this->iAllowedCount = null; - $this->aAllowedIDs = null; - } - - /** - * returns the number of objects for which the action is allowed - * @return integer The number of "allowed" objects 0..N - */ - public function GetAllowedCount() - { - if ($this->iAllowedCount == null) $this->CheckObjects(); - return $this->iAllowedCount; - } - - /** - * If IsAllowed returned UR_ALLOWED_DEPENDS, this methods returns - * an array of ObjKey => Status (true|false) - * @return array - */ - public function GetAllowedIDs() - { - if ($this->aAllowedIDs == null) $this->IsAllowed(); - return $this->aAllowedIDs; - } - - /** - * Check if the speficied stimulus is allowed for the set of objects - * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS - */ - public function IsAllowed() - { - $sClass = $this->oFilter->GetClass(); - $oSet = new DBObjectSet($this->oFilter); - $iActionAllowed = UserRights::IsActionAllowed($sClass, $this->iActionCode, $oSet); - if ($iActionAllowed == UR_ALLOWED_DEPENDS) - { - // Check for each object if the action is allowed or not - $this->aAllowedIDs = array(); - $oSet->Rewind(); - $this->iAllowedCount = 0; - while($oObj = $oSet->Fetch()) - { - $oObjSet = DBObjectSet::FromArray($sClass, array($oObj)); - if (UserRights::IsActionAllowed($sClass, $this->iActionCode, $oObjSet) == UR_ALLOWED_NO) - { - $this->aAllowedIDs[$oObj->GetKey()] = false; - } - else - { - // Assume UR_ALLOWED_YES, since there is just one object ! - $this->aAllowedIDs[$oObj->GetKey()] = true; - $this->iAllowedCount++; - } - } - } - else if ($iActionAllowed == UR_ALLOWED_YES) - { - $this->iAllowedCount = $oSet->Count(); - $this->aAllowedIDs = array(); // Optimization: not filled when Ok for all objects - } - else // UR_ALLOWED_NO - { - $this->iAllowedCount = 0; - $this->aAllowedIDs = array(); - } - return $iActionAllowed; - } -} - -/** - * Helper class to get the number/list of items for which a given stimulus can be applied (allowed & possible) - */ -class StimulusChecker extends ActionChecker -{ - var $sState = null; - - public function __construct(DBSearch $oFilter, $sState, $iStimulusCode) - { - parent::__construct($oFilter, $iStimulusCode); - $this->sState = $sState; - } - - /** - * Check if the speficied stimulus is allowed for the set of objects - * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS - */ - public function IsAllowed() - { - $sClass = $this->oFilter->GetClass(); - if (MetaModel::IsAbstract($sClass)) return UR_ALLOWED_NO; // Safeguard, not implemented if the base class of the set is abstract ! - - $oSet = new DBObjectSet($this->oFilter); - $iActionAllowed = UserRights::IsStimulusAllowed($sClass, $this->iActionCode, $oSet); - if ($iActionAllowed == UR_ALLOWED_NO) - { - $this->iAllowedCount = 0; - $this->aAllowedIDs = array(); - } - else // Even if UR_ALLOWED_YES, we need to check if each object is in the appropriate state - { - // Hmmm, may not be needed right now because we limit the "multiple" action to object in - // the same state... may be useful later on if we want to extend this behavior... - - // Check for each object if the action is allowed or not - $this->aAllowedIDs = array(); - $oSet->Rewind(); - $iAllowedCount = 0; - $iActionAllowed = UR_ALLOWED_DEPENDS; - while($oObj = $oSet->Fetch()) - { - $aTransitions = $oObj->EnumTransitions(); - if (array_key_exists($this->iActionCode, $aTransitions)) - { - // Temporary optimization possible: since the current implementation - // of IsActionAllowed does not perform a 'per instance' check, we could - // skip this second validation phase and assume it would return UR_ALLOWED_YES - $oObjSet = DBObjectSet::FromArray($sClass, array($oObj)); - if (!UserRights::IsStimulusAllowed($sClass, $this->iActionCode, $oObjSet)) - { - $this->aAllowedIDs[$oObj->GetKey()] = false; - } - else - { - // Assume UR_ALLOWED_YES, since there is just one object ! - $this->aAllowedIDs[$oObj->GetKey()] = true; - $this->iState = $oObj->GetState(); - $this->iAllowedCount++; - } - } - else - { - $this->aAllowedIDs[$oObj->GetKey()] = false; - } - } - } - - if ($this->iAllowedCount == $oSet->Count()) - { - $iActionAllowed = UR_ALLOWED_YES; - } - if ($this->iAllowedCount == 0) - { - $iActionAllowed = UR_ALLOWED_NO; - } - - return $iActionAllowed; - } - - public function GetState() - { - return $this->iState; - } -} - -/** - * Self-register extension to allow the automatic creation & update of CAS users - * - * @package iTopORM - * - */ -class CAS_SelfRegister implements iSelfRegister -{ - /** - * Called when no user is found in iTop for the corresponding 'name'. This method - * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly - * @param string $sName The CAS authenticated user name - * @param string $sPassword Ignored - * @param string $sLoginMode The login mode used (cas|form|basic|url) - * @param string $sAuthentication The authentication method used - * @return bool true if the user is a valid one, false otherwise - */ - public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication) - { - $bOk = true; - if ($sLoginMode != 'cas') return false; // Must be authenticated via CAS - - $sCASMemberships = MetaModel::GetConfig()->Get('cas_memberof'); - $bFound = false; - if (!empty($sCASMemberships)) - { - if (phpCAS::hasAttribute('memberOf')) - { - // A list of groups is specified, the user must a be member of (at least) one of them to pass - $aCASMemberships = array(); - $aTmp = explode(';', $sCASMemberships); - setlocale(LC_ALL, "en_US.utf8"); // !!! WARNING: this is needed to have the iconv //TRANSLIT working fine below !!! - foreach($aTmp as $sGroupName) - { - $aCASMemberships[] = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Just in case remove accents and spaces... - } - - $aMemberOf = phpCAS::getAttribute('memberOf'); - if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array - $aFilteredGroupNames = array(); - foreach($aMemberOf as $sGroupName) - { - phpCAS::log("Info: user if a member of the group: ".$sGroupName); - $sGroupName = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Remove accents and spaces as well - $aFilteredGroupNames[] = $sGroupName; - $bIsMember = false; - foreach($aCASMemberships as $sCASPattern) - { - if (self::IsPattern($sCASPattern)) - { - if (preg_match($sCASPattern, $sGroupName)) - { - $bIsMember = true; - break; - } - } - else if ($sCASPattern == $sGroupName) - { - $bIsMember = true; - break; - } - } - if ($bIsMember) - { - $bCASUserSynchro = MetaModel::GetConfig()->Get('cas_user_synchro'); - if ($bCASUserSynchro) - { - // If needed create a new user for this email/profile - phpCAS::log('Info: cas_user_synchro is ON'); - $bOk = self::CreateCASUser(phpCAS::getUser(), $aMemberOf); - if($bOk) - { - $bFound = true; - } - else - { - phpCAS::log("User ".phpCAS::getUser()." cannot be created in iTop. Logging off..."); - } - } - else - { - phpCAS::log('Info: cas_user_synchro is OFF'); - $bFound = true; - } - break; - } - } - if($bOk && !$bFound) - { - phpCAS::log("User ".phpCAS::getUser().", none of his/her groups (".implode('; ', $aFilteredGroupNames).") match any of the required groups: ".implode('; ', $aCASMemberships)); - } - } - else - { - // Too bad, the user is not part of any of the group => not allowed - phpCAS::log("No 'memberOf' attribute found for user ".phpCAS::getUser().". Are you using the SAML protocol (S1) ?"); - } - } - else - { - // No membership: no way to create the user that should exist prior to authentication - phpCAS::log("User ".phpCAS::getUser().": missing user account in iTop (or iTop badly configured, Cf setting cas_memberof)"); - $bFound = false; - } - - if (!$bFound) - { - // The user is not part of the allowed groups, => log out - $sUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; - $sCASLogoutUrl = MetaModel::GetConfig()->Get('cas_logout_redirect_service'); - if (empty($sCASLogoutUrl)) - { - $sCASLogoutUrl = $sUrl; - } - phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page - // Will never return ! - } - return $bFound; - } - - /** - * Called after the user has been authenticated and found in iTop. This method can - * Update the user's definition (profiles...) on the fly to keep it in sync with an external source - * @param User $oUser The user to update/synchronize - * @param string $sLoginMode The login mode used (cas|form|basic|url) - * @param string $sAuthentication The authentication method used - * @return void - */ - public static function UpdateUser(User $oUser, $sLoginMode, $sAuthentication) - { - $bCASUpdateProfiles = MetaModel::GetConfig()->Get('cas_update_profiles'); - if (($sLoginMode == 'cas') && $bCASUpdateProfiles && (phpCAS::hasAttribute('memberOf'))) - { - $aMemberOf = phpCAS::getAttribute('memberOf'); - if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array - - return self::SetProfilesFromCAS($oUser, $aMemberOf); - } - // No groups defined in CAS or not CAS at all: do nothing... - return true; - } - - /** - * Helper method to create a CAS based user - * @param string $sEmail - * @param array $aGroups - * @return bool true on success, false otherwise - */ - protected static function CreateCASUser($sEmail, $aGroups) - { - if (!MetaModel::IsValidClass('URP_Profiles')) - { - phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry."); - return false; - } - - $oUser = MetaModel::GetObjectByName('UserExternal', $sEmail, false); - if ($oUser == null) - { - // Create the user, link it to a contact - phpCAS::log("Info: the user '$sEmail' does not exist. A new UserExternal will be created."); - $oSearch = new DBObjectSearch('Person'); - $oSearch->AddCondition('email', $sEmail); - $oSet = new DBObjectSet($oSearch); - $iContactId = 0; - switch($oSet->Count()) - { - case 0: - phpCAS::log("Error: found no contact with the email: '$sEmail'. Cannot create the user in iTop."); - return false; - - case 1: - $oContact = $oSet->Fetch(); - $iContactId = $oContact->GetKey(); - phpCAS::log("Info: Found 1 contact '".$oContact->GetName()."' (id=$iContactId) corresponding to the email '$sEmail'."); - break; - - default: - phpCAS::log("Error: ".$oSet->Count()." contacts have the same email: '$sEmail'. Cannot create a user for this email."); - return false; - } - - $oUser = new UserExternal(); - $oUser->Set('login', $sEmail); - $oUser->Set('contactid', $iContactId); - $oUser->Set('language', MetaModel::GetConfig()->GetDefaultLanguage()); - } - else - { - phpCAS::log("Info: the user '$sEmail' already exists (id=".$oUser->GetKey().")."); - } - - // Now synchronize the profiles - if (!self::SetProfilesFromCAS($oUser, $aGroups)) - { - return false; - } - else - { - if ($oUser->IsNew() || $oUser->IsModified()) - { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $oMyChange->Set("userinfo", 'CAS/LDAP Synchro'); - $oMyChange->DBInsert(); - if ($oUser->IsNew()) - { - $oUser->DBInsertTracked($oMyChange); - } - else - { - $oUser->DBUpdateTracked($oMyChange); - } - } - - return true; - } - } - - protected static function SetProfilesFromCAS($oUser, $aGroups) - { - if (!MetaModel::IsValidClass('URP_Profiles')) - { - phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry."); - return false; - } - - // read all the existing profiles - $oProfilesSearch = new DBObjectSearch('URP_Profiles'); - $oProfilesSet = new DBObjectSet($oProfilesSearch); - $aAllProfiles = array(); - while($oProfile = $oProfilesSet->Fetch()) - { - $aAllProfiles[strtolower($oProfile->GetName())] = $oProfile->GetKey(); - } - - // Translate the CAS/LDAP group names into iTop profile names - $aProfiles = array(); - $sPattern = MetaModel::GetConfig()->Get('cas_profile_pattern'); - foreach($aGroups as $sGroupName) - { - if (preg_match($sPattern, $sGroupName, $aMatches)) - { - if (array_key_exists(strtolower($aMatches[1]), $aAllProfiles)) - { - $aProfiles[] = $aAllProfiles[strtolower($aMatches[1])]; - phpCAS::log("Info: Adding the profile '{$aMatches[1]}' from CAS."); - } - else - { - phpCAS::log("Warning: {$aMatches[1]} is not a valid iTop profile (extracted from group name: '$sGroupName'). Ignored."); - } - } - else - { - phpCAS::log("Info: The CAS group '$sGroupName' does not seem to match an iTop pattern. Ignored."); - } - } - if (count($aProfiles) == 0) - { - phpCAS::log("Info: The user '".$oUser->GetName()."' has no profiles retrieved from CAS. Default profile(s) will be used."); - - // Second attempt: check if there is/are valid default profile(s) - $sCASDefaultProfiles = MetaModel::GetConfig()->Get('cas_default_profiles'); - $aCASDefaultProfiles = explode(';', $sCASDefaultProfiles); - foreach($aCASDefaultProfiles as $sDefaultProfileName) - { - if (array_key_exists(strtolower($sDefaultProfileName), $aAllProfiles)) - { - $aProfiles[] = $aAllProfiles[strtolower($sDefaultProfileName)]; - phpCAS::log("Info: Adding the default profile '".$aAllProfiles[strtolower($sDefaultProfileName)]."' from CAS."); - } - else - { - phpCAS::log("Warning: the default profile {$sDefaultProfileName} is not a valid iTop profile. Ignored."); - } - } - - if (count($aProfiles) == 0) - { - phpCAS::log("Error: The user '".$oUser->GetName()."' has no profiles in iTop, and therefore cannot be created."); - return false; - } - } - - // Now synchronize the profiles - $oProfilesSet = DBObjectSet::FromScratch('URP_UserProfile'); - foreach($aProfiles as $iProfileId) - { - $oLink = new URP_UserProfile(); - $oLink->Set('profileid', $iProfileId); - $oLink->Set('reason', 'CAS/LDAP Synchro'); - $oProfilesSet->AddObject($oLink); - } - $oUser->Set('profile_list', $oProfilesSet); - phpCAS::log("Info: the user '".$oUser->GetName()."' (id=".$oUser->GetKey().") now has the following profiles: '".implode("', '", $aProfiles)."'."); - if ($oUser->IsModified()) - { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $oMyChange->Set("userinfo", 'CAS/LDAP Synchro'); - $oMyChange->DBInsert(); - if ($oUser->IsNew()) - { - $oUser->DBInsertTracked($oMyChange); - } - else - { - $oUser->DBUpdateTracked($oMyChange); - } - } - - return true; - } - /** - * Helper function to check if the supplied string is a litteral string or a regular expression pattern - * @param string $sCASPattern - * @return bool True if it's a regular expression pattern, false otherwise - */ - protected static function IsPattern($sCASPattern) - { - if ((substr($sCASPattern, 0, 1) == '/') && (substr($sCASPattern, -1) == '/')) - { - // the string is enclosed by slashes, let's assume it's a pattern - return true; - } - else - { - return false; - } - } -} - -// By default enable the 'CAS_SelfRegister' defined above + + + +/** + * User rights management API + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +class UserRightException extends CoreException +{ +} + + +define('UR_ALLOWED_NO', 0); +define('UR_ALLOWED_YES', 1); +define('UR_ALLOWED_DEPENDS', 2); + +define('UR_ACTION_READ', 1); // View an object +define('UR_ACTION_MODIFY', 2); // Create/modify an object/attribute +define('UR_ACTION_DELETE', 3); // Delete an object + +define('UR_ACTION_BULK_READ', 4); // Export multiple objects +define('UR_ACTION_BULK_MODIFY', 5); // Create/modify multiple objects +define('UR_ACTION_BULK_DELETE', 6); // Delete multiple objects + +define('UR_ACTION_CREATE', 7); // Instantiate an object + +define('UR_ACTION_APPLICATION_DEFINED', 10000); // Application specific actions (CSV import, View schema...) + +/** + * User management module API + * + * @package iTopORM + */ +abstract class UserRightsAddOnAPI +{ + abstract public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US'); // could be used during initial installation + + abstract public function Init(); // loads data (possible optimizations) + + // Used to build select queries showing only objects visible for the given user + abstract public function GetSelectFilter($sLogin, $sClass, $aSettings = array()); // returns a filter object + + abstract public function IsActionAllowed($oUser, $sClass, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null); + abstract public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null); + abstract public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null); + abstract public function IsAdministrator($oUser); + abstract public function IsPortalUser($oUser); + abstract public function FlushPrivileges(); + + + /** + * Default behavior for addons that do not support profiles + * + * @param $oUser User + * @return array + */ + public function ListProfiles($oUser) + { + return array(); + } + + /** + * ... + */ + public function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null) + { + if ($sAttCode == null) + { + $sAttCode = $this->GetOwnerOrganizationAttCode($sClass); + } + if (empty($sAttCode)) + { + return $oFilter = new DBObjectSearch($sClass); + } + + $oExpression = new FieldExpression($sAttCode, $sClass); + $oFilter = new DBObjectSearch($sClass); + $oListExpr = ListExpression::FromScalars($aAllowedOrgs); + + $oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr); + $oFilter->AddConditionExpression($oCondition); + + if ($this->HasSharing()) + { + if (($sAttCode == 'id') && isset($aSettings['bSearchMode']) && $aSettings['bSearchMode']) + { + // Querying organizations (or derived) + // and the expected list of organizations will be used as a search criteria + // Therefore the query can also return organization having objects shared with the allowed organizations + // + // 1) build the list of organizations sharing something with the allowed organizations + // Organization <== sharing_org_id == SharedObject having org_id IN {user orgs} + $oShareSearch = new DBObjectSearch('SharedObject'); + $oOrgField = new FieldExpression('org_id', 'SharedObject'); + $oShareSearch->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr)); + + $oSearchSharers = new DBObjectSearch('Organization'); + $oSearchSharers->AllowAllData(); + $oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id'); + $aSharers = array(); + foreach($oSearchSharers->ToDataArray(array('id')) as $aRow) + { + $aSharers[] = $aRow['id']; + } + // 2) Enlarge the overall results: ... OR id IN(id1, id2, id3) + if (count($aSharers) > 0) + { + $oSharersList = ListExpression::FromScalars($aSharers); + $oFilter->MergeConditionExpression(new BinaryExpression($oExpression, 'IN', $oSharersList)); + } + } + + $aShareProperties = SharedObject::GetSharedClassProperties($sClass); + if ($aShareProperties) + { + $sShareClass = $aShareProperties['share_class']; + $sShareAttCode = $aShareProperties['attcode']; + + $oSearchShares = new DBObjectSearch($sShareClass); + $oSearchShares->AllowAllData(); + + $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization'); + $oOrgField = new FieldExpression('org_id', $sShareClass); + $oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr)); + $aShared = array(); + foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow) + { + $aShared[] = $aRow[$sShareAttCode]; + } + if (count($aShared) > 0) + { + $oObjId = new FieldExpression('id', $sClass); + $oSharedIdList = ListExpression::FromScalars($aShared); + $oFilter->MergeConditionExpression(new BinaryExpression($oObjId, 'IN', $oSharedIdList)); + } + } + } // if HasSharing + + return $oFilter; + } +} + + +require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); +abstract class User extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "login", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_user", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + + MetaModel::Init_AddAttribute(new AttributeExternalKey("contactid", array("targetclass"=>"Person", "allowed_values"=>null, "sql"=>"contactid", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalField("last_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"name"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("first_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"first_name"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("email", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"email"))); + MetaModel::Init_AddAttribute(new AttributeExternalField("org_id", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"org_id"))); + + MetaModel::Init_AddAttribute(new AttributeString("login", array("allowed_values"=>null, "sql"=>"login", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>"EN US", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values" => new ValueSetEnum('enabled,disabled'), "sql"=>"status", "default_value"=>"enabled", "is_null_allowed"=>false, "depends_on"=>array()))); + + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("profile_list", array("linked_class"=>"URP_UserProfile", "ext_key_to_me"=>"userid", "ext_key_to_remote"=>"profileid", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("allowed_org_list", array("linked_class"=>"URP_UserOrg", "ext_key_to_me"=>"userid", "ext_key_to_remote"=>"allowed_org_id", "allowed_values"=>null, "count_min"=>1, "count_max"=>0, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'language', 'status', 'profile_list', 'allowed_org_list')); // Unused as it's an abstract class ! + MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'status', 'org_id')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid', 'email', 'language', 'status', 'org_id')); // Criteria of the std search form + MetaModel::Init_SetZListItems('default_search', array('login', 'contactid', 'org_id')); // Default criteria of the search banner + } + + abstract public function CheckCredentials($sPassword); + abstract public function TrustWebServerContext(); + abstract public function CanChangePassword(); + abstract public function ChangePassword($sOldPassword, $sNewPassword); + + /* + * Compute a name in best effort mode + */ + public function GetFriendlyName() + { + if (!MetaModel::IsValidAttCode(get_class($this), 'contactid')) + { + return $this->Get('login'); + } + if ($this->Get('contactid') != 0) + { + $sFirstName = $this->Get('first_name'); + $sLastName = $this->Get('last_name'); + $sEmail = $this->Get('email'); + if (strlen($sFirstName) > 0) + { + return "$sFirstName $sLastName"; + } + elseif (strlen($sEmail) > 0) + { + return "$sLastName <$sEmail>"; + } + else + { + return $sLastName; + } + } + return $this->Get('login'); + } + + protected $oContactObject; + + /** + * Fetch and memorize the associated contact (if any) + */ + public function GetContactObject() + { + if (is_null($this->oContactObject)) + { + if (MetaModel::IsValidAttCode(get_class($this), 'contactid') && ($this->Get('contactid') != 0)) + { + $this->oContactObject = MetaModel::GetObject('Contact', $this->Get('contactid')); + } + } + return $this->oContactObject; + } + + /** + * Overload the standard behavior. + * + * @throws \CoreException + */ + public function DoCheckToWrite() + { + parent::DoCheckToWrite(); + + // Note: This MUST be factorized later: declare unique keys (set of columns) in the data model + $aChanges = $this->ListChanges(); + if (array_key_exists('login', $aChanges)) + { + if (strcasecmp($this->Get('login'), $this->GetOriginal('login')) !== 0) + { + $sNewLogin = $aChanges['login']; + $oSearch = DBObjectSearch::FromOQL_AllData("SELECT User WHERE login = :newlogin"); + if (!$this->IsNew()) + { + $oSearch->AddCondition('id', $this->GetKey(), '!='); + } + $oSet = new DBObjectSet($oSearch, array(), array('newlogin' => $sNewLogin)); + if ($oSet->Count() > 0) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:LoginMustBeUnique', $sNewLogin); + } + } + } + // Check that this user has at least one profile assigned when profiles have changed + if (array_key_exists('profile_list', $aChanges)) + { + $oSet = $this->Get('profile_list'); + if ($oSet->Count() == 0) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:AtLeastOneProfileIsNeeded'); + } + } + // Only administrators can manage administrators + if (UserRights::IsAdministrator($this) && !UserRights::IsAdministrator()) + { + $this->m_aCheckIssues[] = Dict::Format('UI:Login:Error:AccessRestricted'); + } + + if (!UserRights::IsAdministrator()) + { + $oUser = UserRights::GetUserObject(); + $oAddon = UserRights::GetModuleInstance(); + if (!is_null($oUser) && method_exists($oAddon, 'GetUserOrgs')) + { + $aOrgs = $oAddon->GetUserOrgs($oUser, ''); + if (count($aOrgs) > 0) + { + // Check that the modified User belongs to one of our organization + if (!in_array($this->GetOriginal('org_id'), $aOrgs) || !in_array($this->Get('org_id'), $aOrgs)) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:UserOrganizationNotAllowed'); + } + // Check users with restricted organizations when allowed organizations have changed + if ($this->IsNew() || array_key_exists('allowed_org_list', $aChanges)) + { + $oSet = $this->get('allowed_org_list'); + if ($oSet->Count() == 0) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:AtLeastOneOrganizationIsNeeded'); + } + else + { + $aModifiedLinks = $oSet->ListModifiedLinks(); + foreach($aModifiedLinks as $oLink) + { + if (!in_array($oLink->Get('allowed_org_id'), $aOrgs)) + { + $this->m_aCheckIssues[] = Dict::Format('Class:User/Error:OrganizationNotAllowed'); + } + } + } + } + } + } + } + } + + function GetGrantAsHtml($sClass, $iAction) + { + if (UserRights::IsActionAllowed($sClass, $iAction, null, $this)) + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:Yes').''; + } + else + { + return ''.Dict::S('UI:UserManagement:ActionAllowed:No').''; + } + } + + function DoShowGrantSumary($oPage, $sClassCategory) + { + if (UserRights::IsAdministrator($this)) + { + // Looks dirty, but ok that's THE ONE + $oPage->p(Dict::S('UI:UserManagement:AdminProfile+')); + return; + } + + $oKPI = new ExecutionKPI(); + + $aDisplayData = array(); + foreach (MetaModel::GetClasses($sClassCategory) as $sClass) + { + $aClassStimuli = MetaModel::EnumStimuli($sClass); + if (count($aClassStimuli) > 0) + { + $aStimuli = array(); + foreach ($aClassStimuli as $sStimulusCode => $oStimulus) + { + if (UserRights::IsStimulusAllowed($sClass, $sStimulusCode, null, $this)) + { + $aStimuli[] = ''.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').''; + } + } + $sStimuli = implode(', ', $aStimuli); + } + else + { + $sStimuli = ''.Dict::S('UI:UserManagement:NoLifeCycleApplicable').''; + } + + $aDisplayData[] = array( + 'class' => MetaModel::GetName($sClass), + 'read' => $this->GetGrantAsHtml($sClass, UR_ACTION_READ), + 'bulkread' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_READ), + 'write' => $this->GetGrantAsHtml($sClass, UR_ACTION_MODIFY), + 'bulkwrite' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_MODIFY), + 'delete' => $this->GetGrantAsHtml($sClass, UR_ACTION_DELETE), + 'bulkdelete' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_DELETE), + 'stimuli' => $sStimuli, + ); + } + + $oKPI->ComputeAndReport('Computation of user rights'); + + $aDisplayConfig = array(); + $aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+')); + $aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+')); + $aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+')); + $aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+')); + $aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+')); + $aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+')); + $aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+')); + $aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+')); + $oPage->table($aDisplayConfig, $aDisplayData); + } + + function DisplayBareRelations(WebPage $oPage, $bEditMode = false) + { + parent::DisplayBareRelations($oPage, $bEditMode); + if (!$bEditMode) + { + $oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix')); + $this->DoShowGrantSumary($oPage, 'bizmodel,grant_by_profile'); + + // debug + if (false) + { + $oPage->SetCurrentTab('More on user rigths (dev only)'); + $oPage->add("

    User rights

    \n"); + $this->DoShowGrantSumary($oPage, 'addon/userrights'); + $oPage->add("

    Change log

    \n"); + $this->DoShowGrantSumary($oPage, 'core/cmdb'); + $oPage->add("

    Application

    \n"); + $this->DoShowGrantSumary($oPage, 'application'); + $oPage->add("

    GUI

    \n"); + $this->DoShowGrantSumary($oPage, 'gui'); + + } + } + } + + public function CheckToDelete(&$oDeletionPlan) + { + if (MetaModel::GetConfig()->Get('demo_mode')) + { + // Users deletion is NOT allowed in demo mode + $oDeletionPlan->AddToDelete($this, null); + $oDeletionPlan->SetDeletionIssues($this, array('deletion not allowed in demo mode.'), true); + $oDeletionPlan->ComputeResults(); + return false; + } + return parent::CheckToDelete($oDeletionPlan); + } + + protected function DBDeleteSingleObject() + { + if (MetaModel::GetConfig()->Get('demo_mode')) + { + // Users deletion is NOT allowed in demo mode + return; + } + parent::DBDeleteSingleObject(); + } +} + +/** + * Abstract class for all types of "internal" authentication i.e. users + * for which the application is supplied a login and a password opposed + * to "external" users for whom the authentication is performed outside + * of the application (by the web server for example). + * Note that "internal" users do not necessary correspond to a local authentication + * they may be authenticated by a remote system, like in authent-ldap. + */ +abstract class UserInternal extends User +{ + // Nothing special, just a base class to categorize this type of authenticated users + public static function Init() + { + $aParams = array + ( + "category" => "core,grant_by_profile", + "key_type" => "autoincrement", + "name_attcode" => "login", + "state_attcode" => "", + "reconc_keys" => array('login'), + "db_table" => "priv_internaluser", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // When set, this token allows for password reset + MetaModel::Init_AddAttribute(new AttributeOneWayPassword("reset_pwd_token", array("allowed_values"=>null, "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); + + // Display lists + MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'status', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'status', 'org_id')); // Attributes to be displayed for a list + // Search criteria + MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid', 'status', 'org_id')); // Criteria of the std search form + } + + /** + * Use with care! + */ + public function SetPassword($sNewPassword) + { + } + + /** + * The email recipient is the person who is allowed to regain control when the password gets lost + * Throws an exception if the feature cannot be available + */ + public function GetResetPasswordEmail() + { + if (!MetaModel::IsValidAttCode(get_class($this), 'contactid')) + { + throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact')); + } + $iContactId = $this->Get('contactid'); + if ($iContactId == 0) + { + throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact')); + } + $oContact = MetaModel::GetObject('Contact', $iContactId); + // Determine the email attribute (the first one will be our choice) + foreach (MetaModel::ListAttributeDefs(get_class($oContact)) as $sAttCode => $oAttDef) + { + if ($oAttDef instanceof AttributeEmailAddress) + { + $sEmailAttCode = $sAttCode; + // we've got one, exit the loop + break; + } + } + if (!isset($sEmailAttCode)) + { + throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmailAtt')); + } + $sRes = trim($oContact->Get($sEmailAttCode)); + return $sRes; + } +} + +/** + * Self register extension + * + * @package iTopORM + */ +interface iSelfRegister +{ + /** + * Called when no user is found in iTop for the corresponding 'name'. This method + * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly + * @param string $sName The typed-in user name + * @param string $sPassword The typed-in password + * @param string $sLoginMode The login method used (cas|form|basic|url) + * @param string $sAuthentication The authentication method used (any|internal|external) + * @return bool true if the user is a valid one, false otherwise + */ + public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication); + + /** + * Called after the user has been authenticated and found in iTop. This method can + * Update the user's definition on the fly (profiles...) to keep it in sync with an external source + * @param User $oUser The user to update/synchronize + * @param string $sLoginMode The login mode used (cas|form|basic|url) + * @param string $sAuthentication The authentication method used + * @return void + */ + public static function UpdateUser(User $oUser, $sLoginMode, $sAuthentication); +} + +/** + * User management core API + * + * @package iTopORM + */ +class UserRights +{ + /** @var UserRightsAddOnAPI $m_oAddOn */ + protected static $m_oAddOn; + protected static $m_oUser; + protected static $m_oRealUser; + protected static $m_sSelfRegisterAddOn = null; + + public static function SelectModule($sModuleName) + { + if (!class_exists($sModuleName)) + { + throw new CoreException("Could not select this module, '$sModuleName' in not a valid class name"); + return; + } + if (!is_subclass_of($sModuleName, 'UserRightsAddOnAPI')) + { + throw new CoreException("Could not select this module, the class '$sModuleName' is not derived from UserRightsAddOnAPI"); + return; + } + self::$m_oAddOn = new $sModuleName; + self::$m_oAddOn->Init(); + self::$m_oUser = null; + self::$m_oRealUser = null; + } + + public static function SelectSelfRegister($sModuleName) + { + if (!class_exists($sModuleName)) + { + throw new CoreException("Could not select the class, '$sModuleName' for self register, is not a valid class name"); + } + self::$m_sSelfRegisterAddOn = $sModuleName; + } + + public static function GetModuleInstance() + { + return self::$m_oAddOn; + } + + // Installation: create the very first user + public static function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US') + { + $bRes = self::$m_oAddOn->CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage); + self::FlushPrivileges(true /* reset admin cache */); + return $bRes; + } + + public static function IsLoggedIn() + { + if (self::$m_oUser == null) + { + return false; + } + else + { + return true; + } + } + + public static function Login($sName, $sAuthentication = 'any') + { + $oUser = self::FindUser($sName, $sAuthentication); + if (is_null($oUser)) + { + return false; + } + self::$m_oUser = $oUser; + + if (isset($_SESSION['impersonate_user'])) + { + self::$m_oRealUser = self::$m_oUser; + self::$m_oUser = self::FindUser($_SESSION['impersonate_user']); + } + + Dict::SetUserLanguage(self::GetUserLanguage()); + return true; + } + + public static function CheckCredentials($sName, $sPassword, $sLoginMode = 'form', $sAuthentication = 'any') + { + $oUser = self::FindUser($sName, $sAuthentication); + if (is_null($oUser)) + { + // Check if the user does not exist at all or if it is just disabled + if (self::FindUser($sName, $sAuthentication, true) == null) + { + // User does not exist at all + return self::CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication); + } + else + { + // User is actually disabled + return false; + } + } + + if (!$oUser->CheckCredentials($sPassword)) + { + return false; + } + self::UpdateUser($oUser, $sLoginMode, $sAuthentication); + return true; + } + + public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication) + { + if (self::$m_sSelfRegisterAddOn != null) + { + return call_user_func(array(self::$m_sSelfRegisterAddOn, 'CheckCredentialsAndCreateUser'), $sName, $sPassword, $sLoginMode, $sAuthentication); + } + } + + public static function UpdateUser($oUser, $sLoginMode, $sAuthentication) + { + if (self::$m_sSelfRegisterAddOn != null) + { + call_user_func(array(self::$m_sSelfRegisterAddOn, 'UpdateUser'), $oUser, $sLoginMode, $sAuthentication); + } + } + + public static function TrustWebServerContext() + { + if (!is_null(self::$m_oUser)) + { + return self::$m_oUser->TrustWebServerContext(); + } + else + { + return false; + } + } + + /** + * Tells whether or not the archive mode is allowed to the current user + * @return boolean + */ + static function CanBrowseArchive() + { + if (is_null(self::$m_oUser)) + { + $bRet = false; + } + elseif (isset($_SESSION['archive_allowed'])) + { + $bRet = $_SESSION['archive_allowed']; + } + else + { + // As of now, anybody can switch to the archive mode as soon as there is an archivable class + $bRet = (count(MetaModel::EnumArchivableClasses()) > 0); + $_SESSION['archive_allowed'] = $bRet; + } + return $bRet; + } + + public static function CanChangePassword() + { + if (MetaModel::DBIsReadOnly()) + { + return false; + } + + if (!is_null(self::$m_oUser)) + { + return self::$m_oUser->CanChangePassword(); + } + else + { + return false; + } + } + + public static function ChangePassword($sOldPassword, $sNewPassword, $sName = '') + { + if (empty($sName)) + { + $oUser = self::$m_oUser; + } + else + { + // find the id out of the login string + $oUser = self::FindUser($sName); + } + if (is_null($oUser)) + { + return false; + } + else + { + $oUser->AllowWrite(true); + return $oUser->ChangePassword($sOldPassword, $sNewPassword); + } + } + + /** + * @param string $sName Login identifier of the user to impersonate + * @return bool True if an impersonation occurred + */ + public static function Impersonate($sName) + { + if (!self::CheckLogin()) return false; + + $bRet = false; + $oUser = self::FindUser($sName); + if ($oUser) + { + $bRet = true; + if (is_null(self::$m_oRealUser)) + { + // First impersonation + self::$m_oRealUser = self::$m_oUser; + } + if (self::$m_oRealUser && (self::$m_oRealUser->GetKey() == $oUser->GetKey())) + { + // Equivalent to "Deimpersonate" + self::Deimpersonate(); + } + else + { + // Do impersonate! + self::$m_oUser = $oUser; + Dict::SetUserLanguage(self::GetUserLanguage()); + $_SESSION['impersonate_user'] = $sName; + self::_ResetSessionCache(); + } + } + return $bRet; + } + + public static function Deimpersonate() + { + if (!is_null(self::$m_oRealUser)) + { + self::$m_oUser = self::$m_oRealUser; + Dict::SetUserLanguage(self::GetUserLanguage()); + unset($_SESSION['impersonate_user']); + self::_ResetSessionCache(); + } + } + + public static function GetUser() + { + if (is_null(self::$m_oUser)) + { + return ''; + } + else + { + return self::$m_oUser->Get('login'); + } + } + + public static function GetUserObject() + { + if (is_null(self::$m_oUser)) + { + return null; + } + else + { + return self::$m_oUser; + } + } + + public static function GetUserLanguage() + { + if (is_null(self::$m_oUser)) + { + return 'EN US'; + + } + else + { + return self::$m_oUser->Get('language'); + } + } + + public static function GetUserId($sName = '') + { + if (empty($sName)) + { + // return current user id + if (is_null(self::$m_oUser)) + { + return null; + } + return self::$m_oUser->GetKey(); + } + else + { + // find the id out of the login string + $oUser = self::$m_oAddOn->FindUser($sName); + if (is_null($oUser)) + { + return null; + } + return $oUser->GetKey(); + } + } + + public static function GetContactId($sName = '') + { + if (empty($sName)) + { + $oUser = self::$m_oUser; + } + else + { + $oUser = FindUser($sName); + } + if (is_null($oUser)) + { + return ''; + } + if (!MetaModel::IsValidAttCode(get_class($oUser), 'contactid')) + { + return ''; + } + return $oUser->Get('contactid'); + } + + public static function GetContactObject() + { + if (is_null(self::$m_oUser)) + { + return null; + } + else + { + return self::$m_oUser->GetContactObject(); + } + } + + // Render the user name in best effort mode + public static function GetUserFriendlyName($sName = '') + { + if (empty($sName)) + { + $oUser = self::$m_oUser; + } + else + { + $oUser = FindUser($sName); + } + if (is_null($oUser)) + { + return ''; + } + return $oUser->GetFriendlyName(); + } + + public static function IsImpersonated() + { + if (is_null(self::$m_oRealUser)) + { + return false; + } + return true; + } + + public static function GetRealUser() + { + if (is_null(self::$m_oRealUser)) + { + return ''; + } + return self::$m_oRealUser->Get('login'); + } + + public static function GetRealUserObject() + { + return self::$m_oRealUser; + } + + public static function GetRealUserId() + { + if (is_null(self::$m_oRealUser)) + { + return ''; + } + return self::$m_oRealUser->GetKey(); + } + + public static function GetRealUserFriendlyName() + { + if (is_null(self::$m_oRealUser)) + { + return ''; + } + return self::$m_oRealUser->GetFriendlyName(); + } + + protected static function CheckLogin() + { + if (!self::IsLoggedIn()) + { + //throw new UserRightException('No user logged in', array()); + return false; + } + return true; + } + + /** + * Add additional filter for organization silos to all the requests. + * + * @param $sClass + * @param array $aSettings + * + * @return bool|\Expression + */ + public static function GetSelectFilter($sClass, $aSettings = array()) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) {return true;} + + if (self::IsAdministrator()) {return true;} + + try + { + // Check Bug 1436 for details + if (MetaModel::HasCategory($sClass, 'bizmodel')) + { + return self::$m_oAddOn->GetSelectFilter(self::$m_oUser, $sClass, $aSettings); + } + else + { + return true; + } + } catch (Exception $e) + { + return false; + } + } + + /** + * @param string $sClass + * @param int $iActionCode + * @param DBObjectSet $oInstanceSet + * @param User $oUser + * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS) + */ + public static function IsActionAllowed($sClass, $iActionCode, /*dbObjectSet*/$oInstanceSet = null, $oUser = null) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return UR_ALLOWED_YES; + + if (MetaModel::DBIsReadOnly()) + { + if ($iActionCode == UR_ACTION_CREATE) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_MODIFY) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_BULK_MODIFY) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO; + } + + $aPredefinedObjects = call_user_func(array($sClass, 'GetPredefinedObjects')); + if ($aPredefinedObjects != null) + { + // As opposed to the read-only DB, modifying an object is allowed + // (the constant columns will be marked as read-only) + // + if ($iActionCode == UR_ACTION_CREATE) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO; + } + + if (self::IsAdministrator($oUser)) return UR_ALLOWED_YES; + + if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile')) + { + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + if ($iActionCode == UR_ACTION_CREATE) + { + // The addons currently DO NOT handle the case "CREATE" + // Therefore it is considered to be equivalent to "MODIFY" + $iActionCode = UR_ACTION_MODIFY; + } + return self::$m_oAddOn->IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet); + } + elseif(($iActionCode == UR_ACTION_READ) && MetaModel::HasCategory($sClass, 'view_in_gui')) + { + return UR_ALLOWED_YES; + } + else + { + // Other classes could be edited/listed by the administrators + return UR_ALLOWED_NO; + } + } + + public static function IsStimulusAllowed($sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return true; + + if (MetaModel::DBIsReadOnly()) + { + return false; + } + + if (self::IsAdministrator($oUser)) return true; + + if (MetaModel::HasCategory($sClass, 'bizmodel')) + { + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + return self::$m_oAddOn->IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet); + } + else + { + // Other classes could be edited/listed by the administrators + return false; + } + } + + /** + * @param string $sClass + * @param string $sAttCode + * @param int $iActionCode + * @param DBObjectSet $oInstanceSet + * @param User $oUser + * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO) + */ + public static function IsActionAllowedOnAttribute($sClass, $sAttCode, $iActionCode, /*dbObjectSet*/$oInstanceSet = null, $oUser = null) + { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) return UR_ALLOWED_YES; + + if (MetaModel::DBIsReadOnly()) + { + if ($iActionCode == UR_ACTION_MODIFY) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO; + if ($iActionCode == UR_ACTION_BULK_MODIFY) return falUR_ALLOWED_NOse; + if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO; + } + + if (self::IsAdministrator($oUser)) return UR_ALLOWED_YES; + + if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile')) + { + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + return self::$m_oAddOn->IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet); + } + + // this module is forbidden for non admins + if (MetaModel::HasCategory($sClass, 'addon/userrights')) return UR_ALLOWED_NO; + + // the rest is allowed + return UR_ALLOWED_YES; + + + } + + protected static $m_aAdmins = array(); + public static function IsAdministrator($oUser = null) + { + if (!self::CheckLogin()) return false; + + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + $iUser = $oUser->GetKey(); + if (!isset(self::$m_aAdmins[$iUser])) + { + self::$m_aAdmins[$iUser] = self::$m_oAddOn->IsAdministrator($oUser); + } + return self::$m_aAdmins[$iUser]; + } + + protected static $m_aPortalUsers = array(); + public static function IsPortalUser($oUser = null) + { + if (!self::CheckLogin()) return false; + + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + $iUser = $oUser->GetKey(); + if (!isset(self::$m_aPortalUsers[$iUser])) + { + self::$m_aPortalUsers[$iUser] = self::$m_oAddOn->IsPortalUser($oUser); + } + return self::$m_aPortalUsers[$iUser]; + } + + public static function GetAllowedPortals() + { + $aAllowedPortals = array(); + $aPortalsConf = PortalDispatcherData::GetData(); + $aDispatchers = array(); + foreach ($aPortalsConf as $sPortalId => $aConf) + { + $sHandlerClass = $aConf['handler']; + $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId); + } + + foreach ($aDispatchers as $sPortalId => $oDispatcher) + { + if ($oDispatcher->IsUserAllowed()) + { + $aAllowedPortals[] = array( + 'id' => $sPortalId, + 'label' => $oDispatcher->GetLabel(), + 'url' => $oDispatcher->GetUrl(), + ); + } + } + return $aAllowedPortals; + } + + public static function ListProfiles($oUser = null) + { + if (is_null($oUser)) + { + $oUser = self::$m_oUser; + } + if ($oUser === null) + { + // Not logged in: no profile at all + $aProfiles = array(); + } + elseif ((self::$m_oUser !== null) && ($oUser->GetKey() == self::$m_oUser->GetKey())) + { + // Data about the current user can be found into the session data + if (array_key_exists('profile_list', $_SESSION)) + { + $aProfiles = $_SESSION['profile_list']; + } + } + + if (!isset($aProfiles)) + { + $aProfiles = self::$m_oAddOn->ListProfiles($oUser); + } + return $aProfiles; + } + + /** + * @param string $sProfileName Profile name to search for + * @param User|null $oUser + * + * @return bool + */ + public static function HasProfile($sProfileName, $oUser = null) + { + $bRet = in_array($sProfileName, self::ListProfiles($oUser)); + return $bRet; + } + + /** + * Reset cached data + * @param Bool Reset admin cache as well + * @return void + */ + public static function FlushPrivileges($bResetAdminCache = false) + { + if ($bResetAdminCache) + { + self::$m_aAdmins = array(); + self::$m_aPortalUsers = array(); + } + if (!isset($_SESSION) && !utils::IsModeCLI()) + { + session_name('itop-'.md5(APPROOT)); + session_start(); + } + self::_ResetSessionCache(); + if (self::$m_oAddOn) + { + self::$m_oAddOn->FlushPrivileges(); + } + } + + static $m_aCacheUsers; + + /** + * Find a user based on its login and its type of authentication + * + * @param string $sLogin Login/identifier of the user + * @param string $sAuthentication Type of authentication used: internal|external|any + * @param bool $bAllowDisabledUsers Whether or not to retrieve disabled users (status != enabled) + * + * @return User The found user or null + * @throws \OQLException + */ + protected static function FindUser($sLogin, $sAuthentication = 'any', $bAllowDisabledUsers = false) + { + if ($sAuthentication == 'any') + { + $oUser = self::FindUser($sLogin, 'internal'); + if ($oUser == null) + { + $oUser = self::FindUser($sLogin, 'external'); + } + } + else + { + if (!isset(self::$m_aCacheUsers)) + { + self::$m_aCacheUsers = array('internal' => array(), 'external' => array()); + } + + if (!isset(self::$m_aCacheUsers[$sAuthentication][$sLogin])) + { + switch($sAuthentication) + { + case 'external': + $sBaseClass = 'UserExternal'; + break; + + case 'internal': + $sBaseClass = 'UserInternal'; + break; + + default: + echo "

    sAuthentication = $sAuthentication

    \n"; + assert(false); // should never happen + } + $oSearch = DBObjectSearch::FromOQL("SELECT $sBaseClass WHERE login = :login"); + if (!$bAllowDisabledUsers) + { + $oSearch->AddCondition('status', 'enabled'); + } + $oSet = new DBObjectSet($oSearch, array(), array('login' => $sLogin)); + $oUser = $oSet->fetch(); + self::$m_aCacheUsers[$sAuthentication][$sLogin] = $oUser; + } + $oUser = self::$m_aCacheUsers[$sAuthentication][$sLogin]; + } + return $oUser; + } + + public static function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null) + { + return self::$m_oAddOn->MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings, $sAttCode); + } + + public static function _InitSessionCache() + { + // Cache data about the current user into the session + if (isset($_SESSION)) + { + $_SESSION['profile_list'] = self::ListProfiles(); + } + } + + public static function _ResetSessionCache() + { + if (isset($_SESSION['profile_list'])) + { + unset($_SESSION['profile_list']); + } + if (isset($_SESSION['archive_allowed'])) + { + unset($_SESSION['archive_allowed']); + } + } +} + +/** + * Helper class to get the number/list of items for which a given action is allowed/possible + */ +class ActionChecker +{ + var $oFilter; + var $iActionCode; + var $iAllowedCount = null; + var $aAllowedIDs = null; + + public function __construct(DBSearch $oFilter, $iActionCode) + { + $this->oFilter = $oFilter; + $this->iActionCode = $iActionCode; + $this->iAllowedCount = null; + $this->aAllowedIDs = null; + } + + /** + * returns the number of objects for which the action is allowed + * @return integer The number of "allowed" objects 0..N + */ + public function GetAllowedCount() + { + if ($this->iAllowedCount == null) $this->CheckObjects(); + return $this->iAllowedCount; + } + + /** + * If IsAllowed returned UR_ALLOWED_DEPENDS, this methods returns + * an array of ObjKey => Status (true|false) + * @return array + */ + public function GetAllowedIDs() + { + if ($this->aAllowedIDs == null) $this->IsAllowed(); + return $this->aAllowedIDs; + } + + /** + * Check if the speficied stimulus is allowed for the set of objects + * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS + */ + public function IsAllowed() + { + $sClass = $this->oFilter->GetClass(); + $oSet = new DBObjectSet($this->oFilter); + $iActionAllowed = UserRights::IsActionAllowed($sClass, $this->iActionCode, $oSet); + if ($iActionAllowed == UR_ALLOWED_DEPENDS) + { + // Check for each object if the action is allowed or not + $this->aAllowedIDs = array(); + $oSet->Rewind(); + $this->iAllowedCount = 0; + while($oObj = $oSet->Fetch()) + { + $oObjSet = DBObjectSet::FromArray($sClass, array($oObj)); + if (UserRights::IsActionAllowed($sClass, $this->iActionCode, $oObjSet) == UR_ALLOWED_NO) + { + $this->aAllowedIDs[$oObj->GetKey()] = false; + } + else + { + // Assume UR_ALLOWED_YES, since there is just one object ! + $this->aAllowedIDs[$oObj->GetKey()] = true; + $this->iAllowedCount++; + } + } + } + else if ($iActionAllowed == UR_ALLOWED_YES) + { + $this->iAllowedCount = $oSet->Count(); + $this->aAllowedIDs = array(); // Optimization: not filled when Ok for all objects + } + else // UR_ALLOWED_NO + { + $this->iAllowedCount = 0; + $this->aAllowedIDs = array(); + } + return $iActionAllowed; + } +} + +/** + * Helper class to get the number/list of items for which a given stimulus can be applied (allowed & possible) + */ +class StimulusChecker extends ActionChecker +{ + var $sState = null; + + public function __construct(DBSearch $oFilter, $sState, $iStimulusCode) + { + parent::__construct($oFilter, $iStimulusCode); + $this->sState = $sState; + } + + /** + * Check if the speficied stimulus is allowed for the set of objects + * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS + */ + public function IsAllowed() + { + $sClass = $this->oFilter->GetClass(); + if (MetaModel::IsAbstract($sClass)) return UR_ALLOWED_NO; // Safeguard, not implemented if the base class of the set is abstract ! + + $oSet = new DBObjectSet($this->oFilter); + $iActionAllowed = UserRights::IsStimulusAllowed($sClass, $this->iActionCode, $oSet); + if ($iActionAllowed == UR_ALLOWED_NO) + { + $this->iAllowedCount = 0; + $this->aAllowedIDs = array(); + } + else // Even if UR_ALLOWED_YES, we need to check if each object is in the appropriate state + { + // Hmmm, may not be needed right now because we limit the "multiple" action to object in + // the same state... may be useful later on if we want to extend this behavior... + + // Check for each object if the action is allowed or not + $this->aAllowedIDs = array(); + $oSet->Rewind(); + $iAllowedCount = 0; + $iActionAllowed = UR_ALLOWED_DEPENDS; + while($oObj = $oSet->Fetch()) + { + $aTransitions = $oObj->EnumTransitions(); + if (array_key_exists($this->iActionCode, $aTransitions)) + { + // Temporary optimization possible: since the current implementation + // of IsActionAllowed does not perform a 'per instance' check, we could + // skip this second validation phase and assume it would return UR_ALLOWED_YES + $oObjSet = DBObjectSet::FromArray($sClass, array($oObj)); + if (!UserRights::IsStimulusAllowed($sClass, $this->iActionCode, $oObjSet)) + { + $this->aAllowedIDs[$oObj->GetKey()] = false; + } + else + { + // Assume UR_ALLOWED_YES, since there is just one object ! + $this->aAllowedIDs[$oObj->GetKey()] = true; + $this->iState = $oObj->GetState(); + $this->iAllowedCount++; + } + } + else + { + $this->aAllowedIDs[$oObj->GetKey()] = false; + } + } + } + + if ($this->iAllowedCount == $oSet->Count()) + { + $iActionAllowed = UR_ALLOWED_YES; + } + if ($this->iAllowedCount == 0) + { + $iActionAllowed = UR_ALLOWED_NO; + } + + return $iActionAllowed; + } + + public function GetState() + { + return $this->iState; + } +} + +/** + * Self-register extension to allow the automatic creation & update of CAS users + * + * @package iTopORM + * + */ +class CAS_SelfRegister implements iSelfRegister +{ + /** + * Called when no user is found in iTop for the corresponding 'name'. This method + * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly + * @param string $sName The CAS authenticated user name + * @param string $sPassword Ignored + * @param string $sLoginMode The login mode used (cas|form|basic|url) + * @param string $sAuthentication The authentication method used + * @return bool true if the user is a valid one, false otherwise + */ + public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication) + { + $bOk = true; + if ($sLoginMode != 'cas') return false; // Must be authenticated via CAS + + $sCASMemberships = MetaModel::GetConfig()->Get('cas_memberof'); + $bFound = false; + if (!empty($sCASMemberships)) + { + if (phpCAS::hasAttribute('memberOf')) + { + // A list of groups is specified, the user must a be member of (at least) one of them to pass + $aCASMemberships = array(); + $aTmp = explode(';', $sCASMemberships); + setlocale(LC_ALL, "en_US.utf8"); // !!! WARNING: this is needed to have the iconv //TRANSLIT working fine below !!! + foreach($aTmp as $sGroupName) + { + $aCASMemberships[] = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Just in case remove accents and spaces... + } + + $aMemberOf = phpCAS::getAttribute('memberOf'); + if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array + $aFilteredGroupNames = array(); + foreach($aMemberOf as $sGroupName) + { + phpCAS::log("Info: user if a member of the group: ".$sGroupName); + $sGroupName = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Remove accents and spaces as well + $aFilteredGroupNames[] = $sGroupName; + $bIsMember = false; + foreach($aCASMemberships as $sCASPattern) + { + if (self::IsPattern($sCASPattern)) + { + if (preg_match($sCASPattern, $sGroupName)) + { + $bIsMember = true; + break; + } + } + else if ($sCASPattern == $sGroupName) + { + $bIsMember = true; + break; + } + } + if ($bIsMember) + { + $bCASUserSynchro = MetaModel::GetConfig()->Get('cas_user_synchro'); + if ($bCASUserSynchro) + { + // If needed create a new user for this email/profile + phpCAS::log('Info: cas_user_synchro is ON'); + $bOk = self::CreateCASUser(phpCAS::getUser(), $aMemberOf); + if($bOk) + { + $bFound = true; + } + else + { + phpCAS::log("User ".phpCAS::getUser()." cannot be created in iTop. Logging off..."); + } + } + else + { + phpCAS::log('Info: cas_user_synchro is OFF'); + $bFound = true; + } + break; + } + } + if($bOk && !$bFound) + { + phpCAS::log("User ".phpCAS::getUser().", none of his/her groups (".implode('; ', $aFilteredGroupNames).") match any of the required groups: ".implode('; ', $aCASMemberships)); + } + } + else + { + // Too bad, the user is not part of any of the group => not allowed + phpCAS::log("No 'memberOf' attribute found for user ".phpCAS::getUser().". Are you using the SAML protocol (S1) ?"); + } + } + else + { + // No membership: no way to create the user that should exist prior to authentication + phpCAS::log("User ".phpCAS::getUser().": missing user account in iTop (or iTop badly configured, Cf setting cas_memberof)"); + $bFound = false; + } + + if (!$bFound) + { + // The user is not part of the allowed groups, => log out + $sUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; + $sCASLogoutUrl = MetaModel::GetConfig()->Get('cas_logout_redirect_service'); + if (empty($sCASLogoutUrl)) + { + $sCASLogoutUrl = $sUrl; + } + phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page + // Will never return ! + } + return $bFound; + } + + /** + * Called after the user has been authenticated and found in iTop. This method can + * Update the user's definition (profiles...) on the fly to keep it in sync with an external source + * @param User $oUser The user to update/synchronize + * @param string $sLoginMode The login mode used (cas|form|basic|url) + * @param string $sAuthentication The authentication method used + * @return void + */ + public static function UpdateUser(User $oUser, $sLoginMode, $sAuthentication) + { + $bCASUpdateProfiles = MetaModel::GetConfig()->Get('cas_update_profiles'); + if (($sLoginMode == 'cas') && $bCASUpdateProfiles && (phpCAS::hasAttribute('memberOf'))) + { + $aMemberOf = phpCAS::getAttribute('memberOf'); + if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array + + return self::SetProfilesFromCAS($oUser, $aMemberOf); + } + // No groups defined in CAS or not CAS at all: do nothing... + return true; + } + + /** + * Helper method to create a CAS based user + * @param string $sEmail + * @param array $aGroups + * @return bool true on success, false otherwise + */ + protected static function CreateCASUser($sEmail, $aGroups) + { + if (!MetaModel::IsValidClass('URP_Profiles')) + { + phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry."); + return false; + } + + $oUser = MetaModel::GetObjectByName('UserExternal', $sEmail, false); + if ($oUser == null) + { + // Create the user, link it to a contact + phpCAS::log("Info: the user '$sEmail' does not exist. A new UserExternal will be created."); + $oSearch = new DBObjectSearch('Person'); + $oSearch->AddCondition('email', $sEmail); + $oSet = new DBObjectSet($oSearch); + $iContactId = 0; + switch($oSet->Count()) + { + case 0: + phpCAS::log("Error: found no contact with the email: '$sEmail'. Cannot create the user in iTop."); + return false; + + case 1: + $oContact = $oSet->Fetch(); + $iContactId = $oContact->GetKey(); + phpCAS::log("Info: Found 1 contact '".$oContact->GetName()."' (id=$iContactId) corresponding to the email '$sEmail'."); + break; + + default: + phpCAS::log("Error: ".$oSet->Count()." contacts have the same email: '$sEmail'. Cannot create a user for this email."); + return false; + } + + $oUser = new UserExternal(); + $oUser->Set('login', $sEmail); + $oUser->Set('contactid', $iContactId); + $oUser->Set('language', MetaModel::GetConfig()->GetDefaultLanguage()); + } + else + { + phpCAS::log("Info: the user '$sEmail' already exists (id=".$oUser->GetKey().")."); + } + + // Now synchronize the profiles + if (!self::SetProfilesFromCAS($oUser, $aGroups)) + { + return false; + } + else + { + if ($oUser->IsNew() || $oUser->IsModified()) + { + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $oMyChange->Set("userinfo", 'CAS/LDAP Synchro'); + $oMyChange->DBInsert(); + if ($oUser->IsNew()) + { + $oUser->DBInsertTracked($oMyChange); + } + else + { + $oUser->DBUpdateTracked($oMyChange); + } + } + + return true; + } + } + + protected static function SetProfilesFromCAS($oUser, $aGroups) + { + if (!MetaModel::IsValidClass('URP_Profiles')) + { + phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry."); + return false; + } + + // read all the existing profiles + $oProfilesSearch = new DBObjectSearch('URP_Profiles'); + $oProfilesSet = new DBObjectSet($oProfilesSearch); + $aAllProfiles = array(); + while($oProfile = $oProfilesSet->Fetch()) + { + $aAllProfiles[strtolower($oProfile->GetName())] = $oProfile->GetKey(); + } + + // Translate the CAS/LDAP group names into iTop profile names + $aProfiles = array(); + $sPattern = MetaModel::GetConfig()->Get('cas_profile_pattern'); + foreach($aGroups as $sGroupName) + { + if (preg_match($sPattern, $sGroupName, $aMatches)) + { + if (array_key_exists(strtolower($aMatches[1]), $aAllProfiles)) + { + $aProfiles[] = $aAllProfiles[strtolower($aMatches[1])]; + phpCAS::log("Info: Adding the profile '{$aMatches[1]}' from CAS."); + } + else + { + phpCAS::log("Warning: {$aMatches[1]} is not a valid iTop profile (extracted from group name: '$sGroupName'). Ignored."); + } + } + else + { + phpCAS::log("Info: The CAS group '$sGroupName' does not seem to match an iTop pattern. Ignored."); + } + } + if (count($aProfiles) == 0) + { + phpCAS::log("Info: The user '".$oUser->GetName()."' has no profiles retrieved from CAS. Default profile(s) will be used."); + + // Second attempt: check if there is/are valid default profile(s) + $sCASDefaultProfiles = MetaModel::GetConfig()->Get('cas_default_profiles'); + $aCASDefaultProfiles = explode(';', $sCASDefaultProfiles); + foreach($aCASDefaultProfiles as $sDefaultProfileName) + { + if (array_key_exists(strtolower($sDefaultProfileName), $aAllProfiles)) + { + $aProfiles[] = $aAllProfiles[strtolower($sDefaultProfileName)]; + phpCAS::log("Info: Adding the default profile '".$aAllProfiles[strtolower($sDefaultProfileName)]."' from CAS."); + } + else + { + phpCAS::log("Warning: the default profile {$sDefaultProfileName} is not a valid iTop profile. Ignored."); + } + } + + if (count($aProfiles) == 0) + { + phpCAS::log("Error: The user '".$oUser->GetName()."' has no profiles in iTop, and therefore cannot be created."); + return false; + } + } + + // Now synchronize the profiles + $oProfilesSet = DBObjectSet::FromScratch('URP_UserProfile'); + foreach($aProfiles as $iProfileId) + { + $oLink = new URP_UserProfile(); + $oLink->Set('profileid', $iProfileId); + $oLink->Set('reason', 'CAS/LDAP Synchro'); + $oProfilesSet->AddObject($oLink); + } + $oUser->Set('profile_list', $oProfilesSet); + phpCAS::log("Info: the user '".$oUser->GetName()."' (id=".$oUser->GetKey().") now has the following profiles: '".implode("', '", $aProfiles)."'."); + if ($oUser->IsModified()) + { + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $oMyChange->Set("userinfo", 'CAS/LDAP Synchro'); + $oMyChange->DBInsert(); + if ($oUser->IsNew()) + { + $oUser->DBInsertTracked($oMyChange); + } + else + { + $oUser->DBUpdateTracked($oMyChange); + } + } + + return true; + } + /** + * Helper function to check if the supplied string is a litteral string or a regular expression pattern + * @param string $sCASPattern + * @return bool True if it's a regular expression pattern, false otherwise + */ + protected static function IsPattern($sCASPattern) + { + if ((substr($sCASPattern, 0, 1) == '/') && (substr($sCASPattern, -1) == '/')) + { + // the string is enclosed by slashes, let's assume it's a pattern + return true; + } + else + { + return false; + } + } +} + +// By default enable the 'CAS_SelfRegister' defined above UserRights::SelectSelfRegister('CAS_SelfRegister'); \ No newline at end of file diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index 5ac87bfaa..85dc509a7 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -1,462 +1,462 @@ - - - -/** - * Value set definitions (from a fixed list or from a query, etc.) - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once('MyHelpers.class.inc.php'); - -/** - * ValueSetDefinition - * value sets API and implementations - * - * @package iTopORM - */ -abstract class ValueSetDefinition -{ - protected $m_bIsLoaded = false; - protected $m_aValues = array(); - - - // Displayable description that could be computed out of the std usage context - public function GetValuesDescription() - { - $aValues = $this->GetValues(array(), ''); - $aDisplayedValues = array(); - foreach($aValues as $key => $value) - { - $aDisplayedValues[] = "$key => $value"; - } - $sAllowedValues = implode(', ', $aDisplayedValues); - return $sAllowedValues; - } - - - public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') - { - if (!$this->m_bIsLoaded) - { - $this->LoadValues($aArgs); - $this->m_bIsLoaded = true; - } - if (strlen($sContains) == 0) - { - // No filtering - $aRet = $this->m_aValues; - } - else - { - // Filter on results containing the needle - $aRet = array(); - foreach ($this->m_aValues as $sKey=>$sValue) - { - if (stripos($sValue, $sContains) !== false) - { - $aRet[$sKey] = $sValue; - } - } - } - // Sort on the display value - asort($aRet); - return $aRet; - } - - abstract protected function LoadValues($aArgs); -} - - -/** - * Set of existing values for an attribute, given a search filter - * - * @package iTopORM - */ -class ValueSetObjects extends ValueSetDefinition -{ - protected $m_sContains; - protected $m_sOperation; - protected $m_sFilterExpr; // in OQL - protected $m_sValueAttCode; - protected $m_aOrderBy; - protected $m_aExtraConditions; - private $m_bAllowAllData; - private $m_aModifierProperties; - private $m_bSort; - private $m_iLimit; - - - /** - * @param hash $aOrderBy Array of '[.]attcode' => bAscending - */ - public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false, $aModifierProperties = array()) - { - $this->m_sContains = ''; - $this->m_sOperation = ''; - $this->m_sFilterExpr = $sFilterExp; - $this->m_sValueAttCode = $sValueAttCode; - $this->m_aOrderBy = $aOrderBy; - $this->m_bAllowAllData = $bAllowAllData; - $this->m_aModifierProperties = $aModifierProperties; - $this->m_aExtraConditions = array(); - $this->m_bSort = true; - $this->m_iLimit = 0; - } - - public function SetModifierProperty($sPluginClass, $sProperty, $value) - { - $this->m_aModifierProperties[$sPluginClass][$sProperty] = $value; - } - - public function AddCondition(DBSearch $oFilter) - { - $this->m_aExtraConditions[] = $oFilter; - } - - public function ToObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) - { - if ($this->m_bAllowAllData) - { - $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); - } - else - { - $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr); - } - foreach($this->m_aExtraConditions as $oExtraFilter) - { - $oFilter = $oFilter->Intersect($oExtraFilter); - } - foreach($this->m_aModifierProperties as $sPluginClass => $aProperties) - { - foreach ($aProperties as $sProperty => $value) - { - $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value); - } - } - if ($iAdditionalValue > 0) - { - $oSearchAdditionalValue = new DBObjectSearch($oFilter->GetClass()); - $oSearchAdditionalValue->AddConditionExpression( new BinaryExpression( - new FieldExpression('id', $oSearchAdditionalValue->GetClassAlias()), - '=', - new VariableExpression('current_extkey_id')) - ); - $oSearchAdditionalValue->AllowAllData(); - $oSearchAdditionalValue->SetArchiveMode(true); - $oSearchAdditionalValue->SetInternalParams( array('current_extkey_id' => $iAdditionalValue) ); - - $oFilter = new DBUnionSearch(array($oFilter, $oSearchAdditionalValue)); - } - - return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); - } - - /** - * @param $aArgs - * @param string $sContains - * @param string $sOperation for the values @see self::LoadValues() - * - * @return array - * @throws CoreException - * @throws OQLException - */ - public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') - { - if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) - { - $this->LoadValues($aArgs, $sContains, $sOperation); - $this->m_bIsLoaded = true; - } - // The results are already filtered and sorted (on friendly name) - $aRet = $this->m_aValues; - return $aRet; - } - - /** - * @param $aArgs - * @param string $sContains - * @param string $sOperation 'contains' or 'equals_start_with' - * - * @return bool - * @throws \CoreException - * @throws \OQLException - */ - protected function LoadValues($aArgs, $sContains = '', $sOperation = 'contains') - { - $this->m_sContains = $sContains; - $this->m_sOperation = $sOperation; - - $this->m_aValues = array(); - - if ($this->m_bAllowAllData) - { - $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); - } - else - { - $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr); - } - if (!$oFilter) return false; - foreach($this->m_aExtraConditions as $oExtraFilter) - { - $oFilter = $oFilter->Intersect($oExtraFilter); - } - foreach($this->m_aModifierProperties as $sPluginClass => $aProperties) - { - foreach ($aProperties as $sProperty => $value) - { - $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value); - } - } - - $oExpression = DBObjectSearch::GetPolymorphicExpression($oFilter->GetClass(), 'friendlyname'); - $aFields = $oExpression->ListRequiredFields(); - $sClass = $oFilter->GetClass(); - foreach($aFields as $sField) - { - $aFieldItems = explode('.', $sField); - if ($aFieldItems[0] != $sClass) - { - $sOperation = 'contains'; - break; - } - } - - switch ($sOperation) - { - case 'equals': - $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); - $sClassAlias = $oFilter->GetClassAlias(); - $aFilters = array(); - $oValueExpr = new ScalarExpression($sContains); - foreach($aAttributes as $sAttribute) - { - $oNewFilter = $oFilter->DeepClone(); - $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); - $oCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr); - $oNewFilter->AddConditionExpression($oCondition); - $aFilters[] = $oNewFilter; - } - // Unions are much faster than OR conditions - $oFilter = new DBUnionSearch($aFilters); - break; - case 'start_with': - $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); - $sClassAlias = $oFilter->GetClassAlias(); - $aFilters = array(); - $oValueExpr = new ScalarExpression($sContains.'%'); - foreach($aAttributes as $sAttribute) - { - $oNewFilter = $oFilter->DeepClone(); - $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); - $oCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); - $oNewFilter->AddConditionExpression($oCondition); - $aFilters[] = $oNewFilter; - } - // Unions are much faster than OR conditions - $oFilter = new DBUnionSearch($aFilters); - break; - - default: - $oValueExpr = new ScalarExpression('%'.$sContains.'%'); - $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias()); - $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); - $oFilter->AddConditionExpression($oNewCondition); - break; - } - - $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs, null, $this->m_iLimit, 0, $this->m_bSort); - if (empty($this->m_sValueAttCode)) - { - $aAttToLoad = array($oFilter->GetClassAlias() => array('friendlyname')); - } - else - { - $aAttToLoad = array($oFilter->GetClassAlias() => array($this->m_sValueAttCode)); - } - $oObjects->OptimizeColumnLoad($aAttToLoad); - while ($oObject = $oObjects->Fetch()) - { - if (empty($this->m_sValueAttCode)) - { - $this->m_aValues[$oObject->GetKey()] = $oObject->GetName(); - } - else - { - $this->m_aValues[$oObject->GetKey()] = $oObject->Get($this->m_sValueAttCode); - } - } - return true; - } - - public function GetValuesDescription() - { - return 'Filter: '.$this->m_sFilterExpr; - } - - public function GetFilterExpression() - { - return $this->m_sFilterExpr; - } - - /** - * @param $iLimit - */ - public function SetLimit($iLimit) - { - $this->m_iLimit = $iLimit; - } - - /** - * @param $bSort - */ - public function SetSort($bSort) - { - $this->m_bSort = $bSort; - } -} - - -/** - * Fixed set values (could be hardcoded in the business model) - * - * @package iTopORM - */ -class ValueSetEnum extends ValueSetDefinition -{ - protected $m_values; - - public function __construct($Values) - { - $this->m_values = $Values; - } - - // Helper to export the datat model - public function GetValueList() - { - $this->LoadValues($aArgs = array()); - return $this->m_aValues; - } - - protected function LoadValues($aArgs) - { - if (is_array($this->m_values)) - { - $aValues = $this->m_values; - } - elseif (is_string($this->m_values) && strlen($this->m_values) > 0) - { - $aValues = array(); - foreach (explode(",", $this->m_values) as $sVal) - { - $sVal = trim($sVal); - $sKey = $sVal; - $aValues[$sKey] = $sVal; - } - } - else - { - $aValues = array(); - } - $this->m_aValues = $aValues; - return true; - } -} - -/** - * Fixed set values, defined as a range: 0..59 (with an optional increment) - * - * @package iTopORM - */ -class ValueSetRange extends ValueSetDefinition -{ - protected $m_iStart; - protected $m_iEnd; - - public function __construct($iStart, $iEnd, $iStep = 1) - { - $this->m_iStart = $iStart; - $this->m_iEnd = $iEnd; - $this->m_iStep = $iStep; - } - - protected function LoadValues($aArgs) - { - $iValue = $this->m_iStart; - for($iValue = $this->m_iStart; $iValue <= $this->m_iEnd; $iValue += $this->m_iStep) - { - $this->m_aValues[$iValue] = $iValue; - } - return true; - } -} - - -/** - * Data model classes - * - * @package iTopORM - */ -class ValueSetEnumClasses extends ValueSetEnum -{ - protected $m_sCategories; - - public function __construct($sCategories = '', $sAdditionalValues = '') - { - $this->m_sCategories = $sCategories; - parent::__construct($sAdditionalValues); - } - - protected function LoadValues($aArgs) - { - // Call the parent to parse the additional values... - parent::LoadValues($aArgs); - - // Translate the labels of the additional values - foreach($this->m_aValues as $sClass => $void) - { - if (MetaModel::IsValidClass($sClass)) - { - $this->m_aValues[$sClass] = MetaModel::GetName($sClass); - } - else - { - unset($this->m_aValues[$sClass]); - } - } - - // Then, add the classes from the category definition - foreach (MetaModel::GetClasses($this->m_sCategories) as $sClass) - { - if (MetaModel::IsValidClass($sClass)) - { - $this->m_aValues[$sClass] = MetaModel::GetName($sClass); - } - else - { - unset($this->m_aValues[$sClass]); - } - } - - return true; - } -} + + + +/** + * Value set definitions (from a fixed list or from a query, etc.) + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('MyHelpers.class.inc.php'); + +/** + * ValueSetDefinition + * value sets API and implementations + * + * @package iTopORM + */ +abstract class ValueSetDefinition +{ + protected $m_bIsLoaded = false; + protected $m_aValues = array(); + + + // Displayable description that could be computed out of the std usage context + public function GetValuesDescription() + { + $aValues = $this->GetValues(array(), ''); + $aDisplayedValues = array(); + foreach($aValues as $key => $value) + { + $aDisplayedValues[] = "$key => $value"; + } + $sAllowedValues = implode(', ', $aDisplayedValues); + return $sAllowedValues; + } + + + public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') + { + if (!$this->m_bIsLoaded) + { + $this->LoadValues($aArgs); + $this->m_bIsLoaded = true; + } + if (strlen($sContains) == 0) + { + // No filtering + $aRet = $this->m_aValues; + } + else + { + // Filter on results containing the needle + $aRet = array(); + foreach ($this->m_aValues as $sKey=>$sValue) + { + if (stripos($sValue, $sContains) !== false) + { + $aRet[$sKey] = $sValue; + } + } + } + // Sort on the display value + asort($aRet); + return $aRet; + } + + abstract protected function LoadValues($aArgs); +} + + +/** + * Set of existing values for an attribute, given a search filter + * + * @package iTopORM + */ +class ValueSetObjects extends ValueSetDefinition +{ + protected $m_sContains; + protected $m_sOperation; + protected $m_sFilterExpr; // in OQL + protected $m_sValueAttCode; + protected $m_aOrderBy; + protected $m_aExtraConditions; + private $m_bAllowAllData; + private $m_aModifierProperties; + private $m_bSort; + private $m_iLimit; + + + /** + * @param hash $aOrderBy Array of '[.]attcode' => bAscending + */ + public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false, $aModifierProperties = array()) + { + $this->m_sContains = ''; + $this->m_sOperation = ''; + $this->m_sFilterExpr = $sFilterExp; + $this->m_sValueAttCode = $sValueAttCode; + $this->m_aOrderBy = $aOrderBy; + $this->m_bAllowAllData = $bAllowAllData; + $this->m_aModifierProperties = $aModifierProperties; + $this->m_aExtraConditions = array(); + $this->m_bSort = true; + $this->m_iLimit = 0; + } + + public function SetModifierProperty($sPluginClass, $sProperty, $value) + { + $this->m_aModifierProperties[$sPluginClass][$sProperty] = $value; + } + + public function AddCondition(DBSearch $oFilter) + { + $this->m_aExtraConditions[] = $oFilter; + } + + public function ToObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) + { + if ($this->m_bAllowAllData) + { + $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); + } + else + { + $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr); + } + foreach($this->m_aExtraConditions as $oExtraFilter) + { + $oFilter = $oFilter->Intersect($oExtraFilter); + } + foreach($this->m_aModifierProperties as $sPluginClass => $aProperties) + { + foreach ($aProperties as $sProperty => $value) + { + $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value); + } + } + if ($iAdditionalValue > 0) + { + $oSearchAdditionalValue = new DBObjectSearch($oFilter->GetClass()); + $oSearchAdditionalValue->AddConditionExpression( new BinaryExpression( + new FieldExpression('id', $oSearchAdditionalValue->GetClassAlias()), + '=', + new VariableExpression('current_extkey_id')) + ); + $oSearchAdditionalValue->AllowAllData(); + $oSearchAdditionalValue->SetArchiveMode(true); + $oSearchAdditionalValue->SetInternalParams( array('current_extkey_id' => $iAdditionalValue) ); + + $oFilter = new DBUnionSearch(array($oFilter, $oSearchAdditionalValue)); + } + + return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); + } + + /** + * @param $aArgs + * @param string $sContains + * @param string $sOperation for the values @see self::LoadValues() + * + * @return array + * @throws CoreException + * @throws OQLException + */ + public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') + { + if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) + { + $this->LoadValues($aArgs, $sContains, $sOperation); + $this->m_bIsLoaded = true; + } + // The results are already filtered and sorted (on friendly name) + $aRet = $this->m_aValues; + return $aRet; + } + + /** + * @param $aArgs + * @param string $sContains + * @param string $sOperation 'contains' or 'equals_start_with' + * + * @return bool + * @throws \CoreException + * @throws \OQLException + */ + protected function LoadValues($aArgs, $sContains = '', $sOperation = 'contains') + { + $this->m_sContains = $sContains; + $this->m_sOperation = $sOperation; + + $this->m_aValues = array(); + + if ($this->m_bAllowAllData) + { + $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); + } + else + { + $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr); + } + if (!$oFilter) return false; + foreach($this->m_aExtraConditions as $oExtraFilter) + { + $oFilter = $oFilter->Intersect($oExtraFilter); + } + foreach($this->m_aModifierProperties as $sPluginClass => $aProperties) + { + foreach ($aProperties as $sProperty => $value) + { + $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value); + } + } + + $oExpression = DBObjectSearch::GetPolymorphicExpression($oFilter->GetClass(), 'friendlyname'); + $aFields = $oExpression->ListRequiredFields(); + $sClass = $oFilter->GetClass(); + foreach($aFields as $sField) + { + $aFieldItems = explode('.', $sField); + if ($aFieldItems[0] != $sClass) + { + $sOperation = 'contains'; + break; + } + } + + switch ($sOperation) + { + case 'equals': + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); + $sClassAlias = $oFilter->GetClassAlias(); + $aFilters = array(); + $oValueExpr = new ScalarExpression($sContains); + foreach($aAttributes as $sAttribute) + { + $oNewFilter = $oFilter->DeepClone(); + $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); + $oCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr); + $oNewFilter->AddConditionExpression($oCondition); + $aFilters[] = $oNewFilter; + } + // Unions are much faster than OR conditions + $oFilter = new DBUnionSearch($aFilters); + break; + case 'start_with': + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); + $sClassAlias = $oFilter->GetClassAlias(); + $aFilters = array(); + $oValueExpr = new ScalarExpression($sContains.'%'); + foreach($aAttributes as $sAttribute) + { + $oNewFilter = $oFilter->DeepClone(); + $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); + $oCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oNewFilter->AddConditionExpression($oCondition); + $aFilters[] = $oNewFilter; + } + // Unions are much faster than OR conditions + $oFilter = new DBUnionSearch($aFilters); + break; + + default: + $oValueExpr = new ScalarExpression('%'.$sContains.'%'); + $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias()); + $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oFilter->AddConditionExpression($oNewCondition); + break; + } + + $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs, null, $this->m_iLimit, 0, $this->m_bSort); + if (empty($this->m_sValueAttCode)) + { + $aAttToLoad = array($oFilter->GetClassAlias() => array('friendlyname')); + } + else + { + $aAttToLoad = array($oFilter->GetClassAlias() => array($this->m_sValueAttCode)); + } + $oObjects->OptimizeColumnLoad($aAttToLoad); + while ($oObject = $oObjects->Fetch()) + { + if (empty($this->m_sValueAttCode)) + { + $this->m_aValues[$oObject->GetKey()] = $oObject->GetName(); + } + else + { + $this->m_aValues[$oObject->GetKey()] = $oObject->Get($this->m_sValueAttCode); + } + } + return true; + } + + public function GetValuesDescription() + { + return 'Filter: '.$this->m_sFilterExpr; + } + + public function GetFilterExpression() + { + return $this->m_sFilterExpr; + } + + /** + * @param $iLimit + */ + public function SetLimit($iLimit) + { + $this->m_iLimit = $iLimit; + } + + /** + * @param $bSort + */ + public function SetSort($bSort) + { + $this->m_bSort = $bSort; + } +} + + +/** + * Fixed set values (could be hardcoded in the business model) + * + * @package iTopORM + */ +class ValueSetEnum extends ValueSetDefinition +{ + protected $m_values; + + public function __construct($Values) + { + $this->m_values = $Values; + } + + // Helper to export the datat model + public function GetValueList() + { + $this->LoadValues($aArgs = array()); + return $this->m_aValues; + } + + protected function LoadValues($aArgs) + { + if (is_array($this->m_values)) + { + $aValues = $this->m_values; + } + elseif (is_string($this->m_values) && strlen($this->m_values) > 0) + { + $aValues = array(); + foreach (explode(",", $this->m_values) as $sVal) + { + $sVal = trim($sVal); + $sKey = $sVal; + $aValues[$sKey] = $sVal; + } + } + else + { + $aValues = array(); + } + $this->m_aValues = $aValues; + return true; + } +} + +/** + * Fixed set values, defined as a range: 0..59 (with an optional increment) + * + * @package iTopORM + */ +class ValueSetRange extends ValueSetDefinition +{ + protected $m_iStart; + protected $m_iEnd; + + public function __construct($iStart, $iEnd, $iStep = 1) + { + $this->m_iStart = $iStart; + $this->m_iEnd = $iEnd; + $this->m_iStep = $iStep; + } + + protected function LoadValues($aArgs) + { + $iValue = $this->m_iStart; + for($iValue = $this->m_iStart; $iValue <= $this->m_iEnd; $iValue += $this->m_iStep) + { + $this->m_aValues[$iValue] = $iValue; + } + return true; + } +} + + +/** + * Data model classes + * + * @package iTopORM + */ +class ValueSetEnumClasses extends ValueSetEnum +{ + protected $m_sCategories; + + public function __construct($sCategories = '', $sAdditionalValues = '') + { + $this->m_sCategories = $sCategories; + parent::__construct($sAdditionalValues); + } + + protected function LoadValues($aArgs) + { + // Call the parent to parse the additional values... + parent::LoadValues($aArgs); + + // Translate the labels of the additional values + foreach($this->m_aValues as $sClass => $void) + { + if (MetaModel::IsValidClass($sClass)) + { + $this->m_aValues[$sClass] = MetaModel::GetName($sClass); + } + else + { + unset($this->m_aValues[$sClass]); + } + } + + // Then, add the classes from the category definition + foreach (MetaModel::GetClasses($this->m_sCategories) as $sClass) + { + if (MetaModel::IsValidClass($sClass)) + { + $this->m_aValues[$sClass] = MetaModel::GetName($sClass); + } + else + { + unset($this->m_aValues[$sClass]); + } + } + + return true; + } +} diff --git a/css/blue_green.css b/css/blue_green.css index 18585c3cd..75837a430 100644 --- a/css/blue_green.css +++ b/css/blue_green.css @@ -1,222 +1,222 @@ -/* CSS Document */ -body { - font-family: Verdana, Arial, Helevtica; - font-size: smaller; - background-color: #68a; - color:#000000; - margin: 0; /* Remove body margin/padding */ - padding: 0; - overflow: hidden; /* Remove scroll bars on browser window */ -} - -table { - border: 1px solid #000000; -} - -.raw_output { - font-family: Courier-New, Courier, Arial, Helevtica; - font-size: smaller; - background-color: #eeeeee; - color: #000000; - border: 1px dashed #000000; - padding: 0.25em; - margin-top: 1em; -} - -th { - font-family: Verdana, Arial, Helvetica; - font-size: smaller; - color: #000000; - background-color:#ace27d; -} - -td { - font-family: Verdana, Arial, Helvetica; - font-size: smaller; - background-color: #b7cfe8; -} - -tr.clicked td { - font-family: Verdana, Arial, Helvetica; - font-size: smaller; - background-color: #ffcfe8; -} - -td.label { - font-family: Verdana, Arial, Helvetica; - font-size: smaller; - color: #000000; - background-color:#ace27d; - padding: 0.2em; -} - -td a, td a:visited { - text-decoration:none; - color:#000000; -} -td a:hover { - text-decoration:underline; - color:#FFFFFF; -} - -a.small_action { - font-family: Verdana, Arial, Helvetica; - font-size: smaller; - color: #000000; - text-decoration:none; -} - -.display_block { - noborder: 1px dashed #CCC; - background: #79b; - padding:0.25em; -} -div#TopPane .display_block { - background: #f0eee0; - padding:0.25em; - text-align:center; -} -div#TopPane label { - color:#000; - background: #f0eee0; -} - -div#TopPane td { - color:#000; - background: #f0eee0; -} - -.loading { - noborder: 1px dashed #CCC; - background: #b9c1c8; - padding:0.25em; -} - -label { - font-family:Georgia, "Times New Roman", Times, serif; - color:#FFFFFF; - text-align:right; -} - -input.textSearch { - border:1px solid #333; - noheight:1.2em; - font-size:0.8em; - font-family:Verdana, Arial, Helvetica, sans-serif; - color:#000000; -} - -/* By Rom */ -.csvimport_createobj { - color: #AA0000; - background-color:#EEEEEE; -} -.csvimport_error { - font-weight: bold; - color: #FF0000; - background-color:#EEEEEE; -} -.csvimport_warning { - color: #CC8888; - background-color:#EEEEEE; -} -.csvimport_ok { - color: #00000; - background-color:#BBFFBB; -} -.csvimport_reconkey { - font-style: italic; - color: #888888; - background-color:#FFFFF; -} -.csvimport_extreconkey { - color: #888888; - background-color:#FFFFFF; -} - -.treeview, .treeview ul { - padding: 0; - margin: 0; - list-style: none; -} - -.treeview li { - margin: 0; - padding: 3px 0pt 3px 16px; - font-size:0.9em; -} - -ul.dir li { - padding: 2px 0 0 16px; -} - -.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } -.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } -.treeview .expandable { background-image: url(../images/tv-expandable.gif); } -.treeview .last { background-image: url(../images/tv-item-last.gif); } -.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } -.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } - -#Header { padding: 0; background:#ccc url(../images/bandeau2.gif) repeat-x center;} -div.iTopLogo { - background:url(../images/iTop.gif) no-repeat center; - width:100px; - height:56px; -} -div.iTopLogo span { - display:none; -} - -#MySplitter { - /* Height is set to match window size in $().ready() below */ - border:0px; - margin:4px; - padding:0px; - min-width: 100px; /* Splitter can't be too thin ... */ - min-height: 100px; /* ... or too flat */ -} -#LeftPane { - background: #f0eee0; - padding: 4px; - overflow: auto; /* Scroll bars appear as needed */ - color:#666; -} -#TopPane { /* Top nested in right pane */ - background: #f0eee0; - padding: 4px; - height: 150px; /* Initial height */ - min-height: 75px; /* Minimum height */ - overflow: auto; - color:#666; -} -#RightPane { /* Bottom nested in right pane */ - background: #79b; - height:150px; /* Initial height */ - min-height:130px; - no.padding:15px; - no.margin:10px; - overflow:auto; - color:#fff; -} - -#BottomPane { /* Bottom nested in right pane */ - background: #79b; - padding: 4px; - overflow: auto; - color:#fff; -} -#MySplitter .vsplitbar { - width: 7px; - height: 50px; - background: #68a url(../images/vgrabber2.gif) no-repeat center; -} -#MySplitter .vsplitbar.active, #MySplitter .vsplitbar:hover { - background: #68a url(../images/vgrabber2_active.gif) no-repeat center; -} -#MySplitter .hsplitbar { - height: 8px; - background: #68a url(../images/hgrabber2.gif) no-repeat center; -} -#MySplitter .hsplitbar.active, #MySplitter .hsplitbar:hover { - background: #68a url(../images/hgrabber2_active.gif) no-repeat center; -} +/* CSS Document */ +body { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + background-color: #68a; + color:#000000; + margin: 0; /* Remove body margin/padding */ + padding: 0; + overflow: hidden; /* Remove scroll bars on browser window */ +} + +table { + border: 1px solid #000000; +} + +.raw_output { + font-family: Courier-New, Courier, Arial, Helevtica; + font-size: smaller; + background-color: #eeeeee; + color: #000000; + border: 1px dashed #000000; + padding: 0.25em; + margin-top: 1em; +} + +th { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + background-color:#ace27d; +} + +td { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + background-color: #b7cfe8; +} + +tr.clicked td { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + background-color: #ffcfe8; +} + +td.label { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + background-color:#ace27d; + padding: 0.2em; +} + +td a, td a:visited { + text-decoration:none; + color:#000000; +} +td a:hover { + text-decoration:underline; + color:#FFFFFF; +} + +a.small_action { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + text-decoration:none; +} + +.display_block { + noborder: 1px dashed #CCC; + background: #79b; + padding:0.25em; +} +div#TopPane .display_block { + background: #f0eee0; + padding:0.25em; + text-align:center; +} +div#TopPane label { + color:#000; + background: #f0eee0; +} + +div#TopPane td { + color:#000; + background: #f0eee0; +} + +.loading { + noborder: 1px dashed #CCC; + background: #b9c1c8; + padding:0.25em; +} + +label { + font-family:Georgia, "Times New Roman", Times, serif; + color:#FFFFFF; + text-align:right; +} + +input.textSearch { + border:1px solid #333; + noheight:1.2em; + font-size:0.8em; + font-family:Verdana, Arial, Helvetica, sans-serif; + color:#000000; +} + +/* By Rom */ +.csvimport_createobj { + color: #AA0000; + background-color:#EEEEEE; +} +.csvimport_error { + font-weight: bold; + color: #FF0000; + background-color:#EEEEEE; +} +.csvimport_warning { + color: #CC8888; + background-color:#EEEEEE; +} +.csvimport_ok { + color: #00000; + background-color:#BBFFBB; +} +.csvimport_reconkey { + font-style: italic; + color: #888888; + background-color:#FFFFF; +} +.csvimport_extreconkey { + color: #888888; + background-color:#FFFFFF; +} + +.treeview, .treeview ul { + padding: 0; + margin: 0; + list-style: none; +} + +.treeview li { + margin: 0; + padding: 3px 0pt 3px 16px; + font-size:0.9em; +} + +ul.dir li { + padding: 2px 0 0 16px; +} + +.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } +.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } +.treeview .expandable { background-image: url(../images/tv-expandable.gif); } +.treeview .last { background-image: url(../images/tv-item-last.gif); } +.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } +.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } + +#Header { padding: 0; background:#ccc url(../images/bandeau2.gif) repeat-x center;} +div.iTopLogo { + background:url(../images/iTop.gif) no-repeat center; + width:100px; + height:56px; +} +div.iTopLogo span { + display:none; +} + +#MySplitter { + /* Height is set to match window size in $().ready() below */ + border:0px; + margin:4px; + padding:0px; + min-width: 100px; /* Splitter can't be too thin ... */ + min-height: 100px; /* ... or too flat */ +} +#LeftPane { + background: #f0eee0; + padding: 4px; + overflow: auto; /* Scroll bars appear as needed */ + color:#666; +} +#TopPane { /* Top nested in right pane */ + background: #f0eee0; + padding: 4px; + height: 150px; /* Initial height */ + min-height: 75px; /* Minimum height */ + overflow: auto; + color:#666; +} +#RightPane { /* Bottom nested in right pane */ + background: #79b; + height:150px; /* Initial height */ + min-height:130px; + no.padding:15px; + no.margin:10px; + overflow:auto; + color:#fff; +} + +#BottomPane { /* Bottom nested in right pane */ + background: #79b; + padding: 4px; + overflow: auto; + color:#fff; +} +#MySplitter .vsplitbar { + width: 7px; + height: 50px; + background: #68a url(../images/vgrabber2.gif) no-repeat center; +} +#MySplitter .vsplitbar.active, #MySplitter .vsplitbar:hover { + background: #68a url(../images/vgrabber2_active.gif) no-repeat center; +} +#MySplitter .hsplitbar { + height: 8px; + background: #68a url(../images/hgrabber2.gif) no-repeat center; +} +#MySplitter .hsplitbar.active, #MySplitter .hsplitbar:hover { + background: #68a url(../images/hgrabber2_active.gif) no-repeat center; +} diff --git a/css/date.picker.css b/css/date.picker.css index a394482a0..b537f5039 100644 --- a/css/date.picker.css +++ b/css/date.picker.css @@ -1,117 +1,117 @@ - - -table.jCalendar { - border: 1px solid #000; - background: #aaa; - border-collapse: separate; - border-spacing: 2px; -} -table.jCalendar th { - background: #333; - color: #fff; - font-weight: bold; - padding: 3px 5px; -} -table.jCalendar td { - background: #ccc; - color: #000; - padding: 3px 5px; - text-align: center; -} -table.jCalendar td.other-month { - background: #ddd; - color: #aaa; -} -table.jCalendar td.today { - background: #666; - color: #fff; -} -table.jCalendar td.selected { - background: #f66; - color: #fff; -} -table.jCalendar td.selected:hover { - background: #f33; - color: #fff; -} -table.jCalendar td:hover, table.jCalendar td.dp-hover { - background: #fff; - color: #000; -} -table.jCalendar td.disabled, table.jCalendar td.disabled:hover { - background: #bbb; - color: #888; -} - -/* For the popup */ - -/* NOTE - you will probably want to style a.dp-choose-date - see how I did it in demo.css */ - -div.dp-popup { - position: relative; - background: #ccc; - font-size: 10px; - font-family: arial, sans-serif; - padding: 2px; - width: 171px; - line-height: 1.2em; -} -div#dp-popup { - position: absolute; - z-index: 199; -} -div.dp-popup h2 { - font-size: 12px; - text-align: center; - margin: 2px 0; - padding: 0; -} -a#dp-close { - font-size: 11px; - padding: 4px 0; - text-align: center; - display: block; -} -a#dp-close:hover { - text-decoration: underline; -} -div.dp-popup a { - color: #000; - text-decoration: none; - padding: 3px 2px 0; -} -div.dp-popup div.dp-nav-prev { - position: absolute; - top: 2px; - left: 4px; - width: 100px; -} -div.dp-popup div.dp-nav-prev a { - float: left; -} -/* Opera needs the rules to be this specific otherwise it doesn't change the cursor back to pointer after you have disabled and re-enabled a link */ -div.dp-popup div.dp-nav-prev a, div.dp-popup div.dp-nav-next a { - cursor: pointer; -} -div.dp-popup div.dp-nav-prev a.disabled, div.dp-popup div.dp-nav-next a.disabled { - cursor: default; -} -div.dp-popup div.dp-nav-next { - position: absolute; - top: 2px; - right: 4px; - width: 100px; -} -div.dp-popup div.dp-nav-next a { - float: right; -} -div.dp-popup a.disabled { - cursor: default; - color: #aaa; -} -div.dp-popup td { - cursor: pointer; -} -div.dp-popup td.disabled { - cursor: default; + + +table.jCalendar { + border: 1px solid #000; + background: #aaa; + border-collapse: separate; + border-spacing: 2px; +} +table.jCalendar th { + background: #333; + color: #fff; + font-weight: bold; + padding: 3px 5px; +} +table.jCalendar td { + background: #ccc; + color: #000; + padding: 3px 5px; + text-align: center; +} +table.jCalendar td.other-month { + background: #ddd; + color: #aaa; +} +table.jCalendar td.today { + background: #666; + color: #fff; +} +table.jCalendar td.selected { + background: #f66; + color: #fff; +} +table.jCalendar td.selected:hover { + background: #f33; + color: #fff; +} +table.jCalendar td:hover, table.jCalendar td.dp-hover { + background: #fff; + color: #000; +} +table.jCalendar td.disabled, table.jCalendar td.disabled:hover { + background: #bbb; + color: #888; +} + +/* For the popup */ + +/* NOTE - you will probably want to style a.dp-choose-date - see how I did it in demo.css */ + +div.dp-popup { + position: relative; + background: #ccc; + font-size: 10px; + font-family: arial, sans-serif; + padding: 2px; + width: 171px; + line-height: 1.2em; +} +div#dp-popup { + position: absolute; + z-index: 199; +} +div.dp-popup h2 { + font-size: 12px; + text-align: center; + margin: 2px 0; + padding: 0; +} +a#dp-close { + font-size: 11px; + padding: 4px 0; + text-align: center; + display: block; +} +a#dp-close:hover { + text-decoration: underline; +} +div.dp-popup a { + color: #000; + text-decoration: none; + padding: 3px 2px 0; +} +div.dp-popup div.dp-nav-prev { + position: absolute; + top: 2px; + left: 4px; + width: 100px; +} +div.dp-popup div.dp-nav-prev a { + float: left; +} +/* Opera needs the rules to be this specific otherwise it doesn't change the cursor back to pointer after you have disabled and re-enabled a link */ +div.dp-popup div.dp-nav-prev a, div.dp-popup div.dp-nav-next a { + cursor: pointer; +} +div.dp-popup div.dp-nav-prev a.disabled, div.dp-popup div.dp-nav-next a.disabled { + cursor: default; +} +div.dp-popup div.dp-nav-next { + position: absolute; + top: 2px; + right: 4px; + width: 100px; +} +div.dp-popup div.dp-nav-next a { + float: right; +} +div.dp-popup a.disabled { + cursor: default; + color: #aaa; +} +div.dp-popup td { + cursor: pointer; +} +div.dp-popup td.disabled { + cursor: default; } \ No newline at end of file diff --git a/css/default.css b/css/default.css index eaabed838..66afc959c 100644 --- a/css/default.css +++ b/css/default.css @@ -1,165 +1,165 @@ -/* CSS Document */ -body { - font-family: Verdana, Arial, Helevtica; - font-size: smaller; - background-color: #ffffff; - color:#000000; - margin: 0; /* Remove body margin/padding */ - padding: 0; - overflow: hidden; /* Remove scroll bars on browser window */ -} - -table { - border: 1px solid #000000; -} - -.raw_output { - font-family: Courier-New, Courier, Arial, Helevtica; - font-size: smaller; - background-color: #eeeeee; - color: #000000; - border: 1px dashed #000000; - padding: 0.25em; - margin-top: 1em; -} - -th { - font-family: Verdana, Arial, Helevtica; - font-size: smaller; - color: #000000; - background-color:#E1DEB5; -} - -td { - font-family: Verdana, Arial, Helevtica; - font-size: smaller; -} - -td.label { - font-family: Verdana, Arial, Helevtica; - font-size: smaller; - color: #000000; - background-color:#E1DEB5; - padding: 0.2em; -} - -a.small_action { - font-family: Verdana, Arial, Helvetica; - font-size: smaller; - color: #000000; - text-decoration:none; -} - -.display_block { - border: 1px dashed #CCC; - background: #CFC; - padding:0.25em; -} - -.loading { - border: 1px dashed #CCC; - background: #FCC; - padding:0.25em; -} - -/* By Rom */ -.csvimport_createobj { - color: #AA0000; - background-color:#EEEEEE; -} -.csvimport_error { - font-weight: bold; - color: #FF0000; - background-color:#EEEEEE; -} -.csvimport_warning { - color: #CC8888; - background-color:#EEEEEE; -} -.csvimport_ok { - color: #00000; - background-color:#BBFFBB; -} -.csvimport_reconkey { - font-style: italic; - color: #888888; - background-color:#FFFFF; -} -.csvimport_extreconkey { - color: #888888; - background-color:#FFFFFF; -} - -.treeview, .treeview ul { - padding: 0; - margin: 0; - list-style: none; -} - -.treeview li { - margin: 0; - padding: 3px 0pt 3px 16px; -} - -ul.dir li { padding: 2px 0 0 16px; } - -.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } -.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } -.treeview .expandable { background-image: url(../images/tv-expandable.gif); } -.treeview .last { background-image: url(../images/tv-item-last.gif); } -.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } -.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } - -#MySplitter { - /* Height is set to match window size in $().ready() below */ - border:0px; - margin:4px; - padding:0px; - min-width: 100px; /* Splitter can't be too thin ... */ - min-height: 100px; /* ... or too flat */ -} -#LeftPane { - background: #f0eee0; - padding: 4px; - overflow: auto; /* Scroll bars appear as needed */ - color:#666; -} -#TopPane { /* Top nested in right pane */ - background: #f0eee0; - padding: 4px; - height: 150px; /* Initial height */ - min-height: 75px; /* Minimum height */ - overflow: auto; - color:#666; -} -#RightPane { /* Bottom nested in right pane */ - background: #79b; - height:150px; /* Initial height */ - min-height:130px; - no.padding:15px; - no.margin:10px; - overflow:auto; - color:#fff; -} - -#BottomPane { /* Bottom nested in right pane */ - background: #79b; - padding: 4px; - overflow: auto; - color:#fff; -} -#MySplitter .vsplitbar { - width: 7px; - height: 50px; - background: #68a url(../images/vgrabber2.gif) no-repeat center; -} -#MySplitter .vsplitbar.active, #MySplitter .vsplitbar:hover { - background: #68a url(../images/vgrabber2_active.gif) no-repeat center; -} -#MySplitter .hsplitbar { - height: 8px; - background: #68a url(../images/hgrabber2.gif) no-repeat center; -} -#MySplitter .hsplitbar.active, #MySplitter .hsplitbar:hover { - background: #68a url(../images/hgrabber2_active.gif) no-repeat center; -} +/* CSS Document */ +body { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + background-color: #ffffff; + color:#000000; + margin: 0; /* Remove body margin/padding */ + padding: 0; + overflow: hidden; /* Remove scroll bars on browser window */ +} + +table { + border: 1px solid #000000; +} + +.raw_output { + font-family: Courier-New, Courier, Arial, Helevtica; + font-size: smaller; + background-color: #eeeeee; + color: #000000; + border: 1px dashed #000000; + padding: 0.25em; + margin-top: 1em; +} + +th { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + color: #000000; + background-color:#E1DEB5; +} + +td { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; +} + +td.label { + font-family: Verdana, Arial, Helevtica; + font-size: smaller; + color: #000000; + background-color:#E1DEB5; + padding: 0.2em; +} + +a.small_action { + font-family: Verdana, Arial, Helvetica; + font-size: smaller; + color: #000000; + text-decoration:none; +} + +.display_block { + border: 1px dashed #CCC; + background: #CFC; + padding:0.25em; +} + +.loading { + border: 1px dashed #CCC; + background: #FCC; + padding:0.25em; +} + +/* By Rom */ +.csvimport_createobj { + color: #AA0000; + background-color:#EEEEEE; +} +.csvimport_error { + font-weight: bold; + color: #FF0000; + background-color:#EEEEEE; +} +.csvimport_warning { + color: #CC8888; + background-color:#EEEEEE; +} +.csvimport_ok { + color: #00000; + background-color:#BBFFBB; +} +.csvimport_reconkey { + font-style: italic; + color: #888888; + background-color:#FFFFF; +} +.csvimport_extreconkey { + color: #888888; + background-color:#FFFFFF; +} + +.treeview, .treeview ul { + padding: 0; + margin: 0; + list-style: none; +} + +.treeview li { + margin: 0; + padding: 3px 0pt 3px 16px; +} + +ul.dir li { padding: 2px 0 0 16px; } + +.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } +.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } +.treeview .expandable { background-image: url(../images/tv-expandable.gif); } +.treeview .last { background-image: url(../images/tv-item-last.gif); } +.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } +.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } + +#MySplitter { + /* Height is set to match window size in $().ready() below */ + border:0px; + margin:4px; + padding:0px; + min-width: 100px; /* Splitter can't be too thin ... */ + min-height: 100px; /* ... or too flat */ +} +#LeftPane { + background: #f0eee0; + padding: 4px; + overflow: auto; /* Scroll bars appear as needed */ + color:#666; +} +#TopPane { /* Top nested in right pane */ + background: #f0eee0; + padding: 4px; + height: 150px; /* Initial height */ + min-height: 75px; /* Minimum height */ + overflow: auto; + color:#666; +} +#RightPane { /* Bottom nested in right pane */ + background: #79b; + height:150px; /* Initial height */ + min-height:130px; + no.padding:15px; + no.margin:10px; + overflow:auto; + color:#fff; +} + +#BottomPane { /* Bottom nested in right pane */ + background: #79b; + padding: 4px; + overflow: auto; + color:#fff; +} +#MySplitter .vsplitbar { + width: 7px; + height: 50px; + background: #68a url(../images/vgrabber2.gif) no-repeat center; +} +#MySplitter .vsplitbar.active, #MySplitter .vsplitbar:hover { + background: #68a url(../images/vgrabber2_active.gif) no-repeat center; +} +#MySplitter .hsplitbar { + height: 8px; + background: #68a url(../images/hgrabber2.gif) no-repeat center; +} +#MySplitter .hsplitbar.active, #MySplitter .hsplitbar:hover { + background: #68a url(../images/hgrabber2_active.gif) no-repeat center; +} diff --git a/css/font-combodo/glyphs/0.svg b/css/font-combodo/glyphs/0.svg index ad232eb48..022d8f225 100644 --- a/css/font-combodo/glyphs/0.svg +++ b/css/font-combodo/glyphs/0.svg @@ -1,26 +1,26 @@ - - - - - - - - + + + + + + + + diff --git a/css/font-combodo/glyphs/1.svg b/css/font-combodo/glyphs/1.svg index 2750be5d4..d78c67055 100644 --- a/css/font-combodo/glyphs/1.svg +++ b/css/font-combodo/glyphs/1.svg @@ -1,13 +1,13 @@ - - - - - - + + + + + + diff --git a/css/font-combodo/glyphs/2.svg b/css/font-combodo/glyphs/2.svg index 7709ca27e..23589d7b7 100644 --- a/css/font-combodo/glyphs/2.svg +++ b/css/font-combodo/glyphs/2.svg @@ -1,18 +1,18 @@ - - - - - - - + + + + + + + diff --git a/css/font-combodo/glyphs/3.svg b/css/font-combodo/glyphs/3.svg index ffe595e0c..4be5fc9b2 100644 --- a/css/font-combodo/glyphs/3.svg +++ b/css/font-combodo/glyphs/3.svg @@ -1,17 +1,17 @@ - - - - - - - + + + + + + + diff --git a/css/font-combodo/glyphs/4.svg b/css/font-combodo/glyphs/4.svg index 9e807f579..bc525b3b1 100644 --- a/css/font-combodo/glyphs/4.svg +++ b/css/font-combodo/glyphs/4.svg @@ -1,20 +1,20 @@ - - - - - - - + + + + + + + diff --git a/css/font-combodo/glyphs/C.svg b/css/font-combodo/glyphs/C.svg index f1a82079b..65d2c1c1e 100644 --- a/css/font-combodo/glyphs/C.svg +++ b/css/font-combodo/glyphs/C.svg @@ -1,65 +1,65 @@ - - - - - - + + + + + + diff --git a/css/font-combodo/glyphs/D.svg b/css/font-combodo/glyphs/D.svg index 78fba5683..a16025f3e 100644 --- a/css/font-combodo/glyphs/D.svg +++ b/css/font-combodo/glyphs/D.svg @@ -1,23 +1,23 @@ - - - - - - + + + + + + diff --git a/css/font-combodo/glyphs/I.svg b/css/font-combodo/glyphs/I.svg index 2e6eec3e4..3b4b7c50b 100644 --- a/css/font-combodo/glyphs/I.svg +++ b/css/font-combodo/glyphs/I.svg @@ -1,36 +1,36 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/css/font-combodo/test.html b/css/font-combodo/test.html index 1ff460b0d..434ac047b 100644 --- a/css/font-combodo/test.html +++ b/css/font-combodo/test.html @@ -1,84 +1,84 @@ - - - -Combodo Font test page - - - - - -

    Combodo Font test page

    -

    Parameters

    -
    -

    Text color:

    -

    Icon size:

    -

    Rotation:

    -

    Flip: (NB: flip and rotation cannot be combined)

    -

    Animation:

    - -
    -

    Icons

    -
    - - - - + + + +Combodo Font test page + + + + + +

    Combodo Font test page

    +

    Parameters

    +
    +

    Text color:

    +

    Icon size:

    +

    Rotation:

    +

    Flip: (NB: flip and rotation cannot be combined)

    +

    Animation:

    + +
    +

    Icons

    +
    + + + + \ No newline at end of file diff --git a/css/jqModal.css b/css/jqModal.css index 41e059343..1da9fdd03 100644 --- a/css/jqModal.css +++ b/css/jqModal.css @@ -1,39 +1,39 @@ -/* jqModal base Styling courtesy of; - Brice Burgess */ - -/* The Window's CSS z-index value is respected (takes priority). If none is supplied, - the Window's z-index value will be set to 3000 by default (via jqModal.js). */ - -.jqmWindow { - display: none; - - position: fixed; - no.top: 17%; - no.left: 50%; - - no.margin-left: -300px; - no.width: 700px; - - background-color: #EEE; - color: #333; - border: 1px solid black; - padding: 12px; - - z-index:9999; -} - -.jqmOverlay { background-color: #000; } - -/* Background iframe styling for IE6. Prevents ActiveX bleed-through ( form elements, etc.) */ +* iframe.jqm {position:absolute;top:0;left:0;z-index:-1; + width: expression(this.parentNode.offsetWidth+'px'); + height: expression(this.parentNode.offsetHeight+'px'); +} + +/* Fixed posistioning emulation for IE6 + Star selector used to hide definition from browsers other than IE6 + For valid CSS, use a conditional include instead */ +* html .jqmWindow { + position: absolute; + top: expression((document.documentElement.scrollTop || document.body.scrollTop) + Math.round(17 * (document.documentElement.offsetHeight || document.body.clientHeight) / 100) + 'px'); +} diff --git a/css/jquery-ui-timepicker-addon.css b/css/jquery-ui-timepicker-addon.css index 95a22420c..586a7f04d 100644 --- a/css/jquery-ui-timepicker-addon.css +++ b/css/jquery-ui-timepicker-addon.css @@ -1,30 +1,30 @@ -.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } -.ui-timepicker-div dl { text-align: left; } -.ui-timepicker-div dl dt { float: left; clear:left; padding: 0 0 0 5px; } -.ui-timepicker-div dl dd { margin: 0 10px 10px 40%; } -.ui-timepicker-div td { font-size: 90%; } -.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } -.ui-timepicker-div .ui_tpicker_unit_hide{ display: none; } - -.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input { background: none; color: inherit; border: none; outline: none; border-bottom: solid 1px #555; width: 95%; } -.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input:focus { border-bottom-color: #aaa; } - -.ui-timepicker-rtl{ direction: rtl; } -.ui-timepicker-rtl dl { text-align: right; padding: 0 5px 0 0; } -.ui-timepicker-rtl dl dt{ float: right; clear: right; } -.ui-timepicker-rtl dl dd { margin: 0 40% 10px 10px; } - -/* Shortened version style */ -.ui-timepicker-div.ui-timepicker-oneLine { padding-right: 2px; } -.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time, -.ui-timepicker-div.ui-timepicker-oneLine dt { display: none; } -.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label { display: block; padding-top: 2px; } -.ui-timepicker-div.ui-timepicker-oneLine dl { text-align: right; } -.ui-timepicker-div.ui-timepicker-oneLine dl dd, -.ui-timepicker-div.ui-timepicker-oneLine dl dd > div { display:inline-block; margin:0; } -.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before, -.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before { content:':'; display:inline-block; } -.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before, -.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before { content:'.'; display:inline-block; } -.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide, +.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } +.ui-timepicker-div dl { text-align: left; } +.ui-timepicker-div dl dt { float: left; clear:left; padding: 0 0 0 5px; } +.ui-timepicker-div dl dd { margin: 0 10px 10px 40%; } +.ui-timepicker-div td { font-size: 90%; } +.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } +.ui-timepicker-div .ui_tpicker_unit_hide{ display: none; } + +.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input { background: none; color: inherit; border: none; outline: none; border-bottom: solid 1px #555; width: 95%; } +.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input:focus { border-bottom-color: #aaa; } + +.ui-timepicker-rtl{ direction: rtl; } +.ui-timepicker-rtl dl { text-align: right; padding: 0 5px 0 0; } +.ui-timepicker-rtl dl dt{ float: right; clear: right; } +.ui-timepicker-rtl dl dd { margin: 0 40% 10px 10px; } + +/* Shortened version style */ +.ui-timepicker-div.ui-timepicker-oneLine { padding-right: 2px; } +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time, +.ui-timepicker-div.ui-timepicker-oneLine dt { display: none; } +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label { display: block; padding-top: 2px; } +.ui-timepicker-div.ui-timepicker-oneLine dl { text-align: right; } +.ui-timepicker-div.ui-timepicker-oneLine dl dd, +.ui-timepicker-div.ui-timepicker-oneLine dl dd > div { display:inline-block; margin:0; } +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before, +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before { content:':'; display:inline-block; } +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before, +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before { content:'.'; display:inline-block; } +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide, .ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{ display: none; } \ No newline at end of file diff --git a/css/jquery.tabs.css b/css/jquery.tabs.css index ff9bf60b4..ae64133f5 100644 --- a/css/jquery.tabs.css +++ b/css/jquery.tabs.css @@ -1,76 +1,76 @@ -/* Caution! Ensure accessibility in print and other media types... */ -@media projection, screen { /* Use class for showing/hiding tab content, so that visibility can be better controlled in different media types... */ - .tabs-hide { - display: none; - } -} - -/* Hide useless elements in print layouts... */ -@media print { - .tabs-nav { - display: none; - } -} - -/* Skin */ -.tabs-nav { - list-style: none; - margin: 0; - padding: 0 0 0 4px; -} -.tabs-nav:after { /* clearing without presentational markup, IE gets extra treatment */ - display: block; - clear: both; - content: " "; -} -.tabs-nav li { - float: left; - margin: 0 0 0 1px; -} -.tabs-nav a { - display: block; - position: relative; - top: 1px; - z-index: 2; - padding: 6px 10px 0; - width: 64px; - height: 18px; - color: #27537a; - font-size: 12px; - font-weight: bold; - line-height: 1.2; - text-align: center; - text-decoration: none; - background: url(../images/tab.png) no-repeat; -} -.tabs-nav .tabs-selected a { - padding-top: 7px; - color: #000; -} -.tabs-nav .tabs-selected a, .tabs-nav a:hover, .tabs-nav a:focus, .tabs-nav a:active { - background-position: 0 -50px; - outline: 0; /* @ Firefox, switch off dotted border */ -} -.tabs-nav .tabs-disabled a:hover, .tabs-nav .tabs-disabled a:focus, .tabs-nav .tabs-disabled a:active { - background-position: 0 0; -} -.tabs-nav .tabs-selected a:link, .tabs-nav .tabs-selected a:visited, -.tabs-nav .tabs-disabled a:link, .tabs-nav .tabs-disabled a:visited { /* @ Opera, use pseudo classes otherwise it confuses cursor... */ - cursor: text; -} -.tabs-nav a:hover, .tabs-nav a:focus, .tabs-nav a:active { /* @ Opera, we need to be explicit again here now... */ - cursor: pointer; -} -.tabs-nav .tabs-disabled { - opacity: .4; -} -.tabs-container { - border-top: 1px solid #97a5b0; - padding: 1em 8px; - background: #fff; /* declare background color for container to avoid distorted fonts in IE while fading */ -} -/* Uncomment this if you want a little spinner to be shown next to the tab title while an Ajax tab gets loaded -.tabs-loading span { - padding: 0 0 0 20px; - background: url(loading.gif) no-repeat 0 50%; +/* Caution! Ensure accessibility in print and other media types... */ +@media projection, screen { /* Use class for showing/hiding tab content, so that visibility can be better controlled in different media types... */ + .tabs-hide { + display: none; + } +} + +/* Hide useless elements in print layouts... */ +@media print { + .tabs-nav { + display: none; + } +} + +/* Skin */ +.tabs-nav { + list-style: none; + margin: 0; + padding: 0 0 0 4px; +} +.tabs-nav:after { /* clearing without presentational markup, IE gets extra treatment */ + display: block; + clear: both; + content: " "; +} +.tabs-nav li { + float: left; + margin: 0 0 0 1px; +} +.tabs-nav a { + display: block; + position: relative; + top: 1px; + z-index: 2; + padding: 6px 10px 0; + width: 64px; + height: 18px; + color: #27537a; + font-size: 12px; + font-weight: bold; + line-height: 1.2; + text-align: center; + text-decoration: none; + background: url(../images/tab.png) no-repeat; +} +.tabs-nav .tabs-selected a { + padding-top: 7px; + color: #000; +} +.tabs-nav .tabs-selected a, .tabs-nav a:hover, .tabs-nav a:focus, .tabs-nav a:active { + background-position: 0 -50px; + outline: 0; /* @ Firefox, switch off dotted border */ +} +.tabs-nav .tabs-disabled a:hover, .tabs-nav .tabs-disabled a:focus, .tabs-nav .tabs-disabled a:active { + background-position: 0 0; +} +.tabs-nav .tabs-selected a:link, .tabs-nav .tabs-selected a:visited, +.tabs-nav .tabs-disabled a:link, .tabs-nav .tabs-disabled a:visited { /* @ Opera, use pseudo classes otherwise it confuses cursor... */ + cursor: text; +} +.tabs-nav a:hover, .tabs-nav a:focus, .tabs-nav a:active { /* @ Opera, we need to be explicit again here now... */ + cursor: pointer; +} +.tabs-nav .tabs-disabled { + opacity: .4; +} +.tabs-container { + border-top: 1px solid #97a5b0; + padding: 1em 8px; + background: #fff; /* declare background color for container to avoid distorted fonts in IE while fading */ +} +/* Uncomment this if you want a little spinner to be shown next to the tab title while an Ajax tab gets loaded +.tabs-loading span { + padding: 0 0 0 20px; + background: url(loading.gif) no-repeat 0 50%; }*/ \ No newline at end of file diff --git a/css/jquery.treeview.css b/css/jquery.treeview.css index b5f038600..7ffac8ecf 100644 --- a/css/jquery.treeview.css +++ b/css/jquery.treeview.css @@ -1,45 +1,45 @@ -.treeview, .treeview ul { - padding: 0; - margin: 0; - list-style: none; -} - -.treeview div.hitarea { - height: 15px; - width: 15px; - margin-left: -15px; - float: left; - cursor: pointer; -} -/* fix for IE6 */ -* html div.hitarea { - background: #fff; - filter: alpha(opacity=0); - display: inline; - float:none; -} - -.treeview li { - margin: 0; - padding: 3px 0pt 3px 16px; -} - -.treeview a.selected { - background-color: #eee; -} - -#treecontrol { margin: 1em 0; } - -.treeview .hover { color: red; cursor: pointer; } - -.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } -.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } -.treeview .expandable { background-image: url(../images/tv-expandable.gif); } -.treeview .last { background-image: url(../images/tv-item-last.gif); } -.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } -.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } - -.filetree li { padding: 3px 0 1px 16px; } -.filetree span.folder, .filetree span.file { padding-left: 16px; display: block; height: 15px; } -.filetree span.folder { background: url(../images/folder.gif) 0 0 no-repeat; } -.filetree span.file { background: url(../images/file.gif) 0 0 no-repeat; } +.treeview, .treeview ul { + padding: 0; + margin: 0; + list-style: none; +} + +.treeview div.hitarea { + height: 15px; + width: 15px; + margin-left: -15px; + float: left; + cursor: pointer; +} +/* fix for IE6 */ +* html div.hitarea { + background: #fff; + filter: alpha(opacity=0); + display: inline; + float:none; +} + +.treeview li { + margin: 0; + padding: 3px 0pt 3px 16px; +} + +.treeview a.selected { + background-color: #eee; +} + +#treecontrol { margin: 1em 0; } + +.treeview .hover { color: red; cursor: pointer; } + +.treeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } +.treeview .collapsable { background-image: url(../images/tv-collapsable.gif); } +.treeview .expandable { background-image: url(../images/tv-expandable.gif); } +.treeview .last { background-image: url(../images/tv-item-last.gif); } +.treeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } +.treeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } + +.filetree li { padding: 3px 0 1px 16px; } +.filetree span.folder, .filetree span.file { padding-left: 16px; display: block; height: 15px; } +.filetree span.folder { background: url(../images/folder.gif) 0 0 no-repeat; } +.filetree span.file { background: url(../images/file.gif) 0 0 no-repeat; } diff --git a/css/light-grey.scss b/css/light-grey.scss index 00fb6f8da..117557ec7 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -1,3262 +1,3262 @@ -@import 'css-variables.scss'; - -$hilight-color: $highlight-color; -$summary-details-background: $grey-color; -$main-header-background: $frame-background-color; -$table-even-background: $frame-background-color; -$popup-menu-highlight-color: $highlight-color; -$popup-menu-text-color: #000; -$popup-menu-background-color: #fff; -$popup-menu-text-higlight-color: #fff; -$breadcrumb-color: #555; -$breadcrumb-text-color: #fff; -$breadcrumb-highlight-color: $highlight-color; -$breadcrumb-text-highlight-color: #fff; - -/* CSS Document */ -body { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 10pt; - color: $text-color; - margin: 0; /* Remove body margin/padding */ - padding: 0; - overflow: hidden; /* Remove scroll bars on browser window */ -} - -body.printable-version { - margin:1.5em; - overflow:auto; -} - -/* to prevent flicker, hide the pane's content until it's ready */ -.ui-layout-center, .ui-layout-north, .ui-layout-south { - display: none; -} -.ui-layout-content { - padding-left: 10px; -} -.ui-layout-content .ui-tabs-nav li { - /* Overriding jQuery UI theme to see active tab better */ - margin-bottom: 2px; - - &.ui-tabs-active{ - padding-bottom: 3px; - } -} - -.raw_output { - font-family: Courier-New, Courier, Arial, Helvetica; - font-size: 8pt; - background-color: #eeeeee; - color: $text-color; - border: 1px dashed $text-color; - padding: 0.25em; - margin-top: 1em; -} - -h1 { - font-family: Tahoma, Verdana, Arial, Helvetica; - color: $text-color; - font-weight: bold; - font-size: 12pt; -} -h2 { - font-family: Tahoma, Verdana, Arial, Helvetica; - color: $text-color; - font-weight: normal; - font-size: 12pt; -} -h3 { - font-family: Tahoma, Verdana, Arial, Helvetica; - color: $text-color; - font-weight: normal; - font-size: 10pt; -} - -label { - cursor: pointer; -} - -.hilite, .hilite a, .hilite a:visited { - color: $hilight-color; - text-decoration: none; -} -table.datatable { - width: 100%; - border: 0; - padding: 0; -} -td.menucontainer { - text-align: right; -} -table.listResults { - padding: 0px; - border-top: 3px solid $frame-background-color; - border-left: 3px solid $frame-background-color; - border-bottom: 3px solid #e6e6e1; - border-right: 3px solid #e6e6e1; - width: 100%; - background-color: #fff; -} - -table.listResults td { - padding: 2px; -} - -table.listResults td .view-image { - // Counteract the forced dimensions (usefull for displaying in the details view) - width: inherit !important; - height: inherit !important; - img { - max-width: 48px !important; - max-height: 48px !important; - display: block; - margin-left: auto; - margin-right: auto; - } -} - -table.listResults > tbody > tr.selected > * { - background-color: $combodo-orange; -} - -table.listResults > tbody > tr > * { - transition: background-color 400ms linear; -} - -table.listResults > tbody > tr:hover > * { - cursor: pointer; -} - -table.listResults > tbody > tr.selected:hover > * { - /* hover on lines is currently done toggling td.hover, and having a rule for links */ - background-color: lighten($combodo-orange, 20%); -} - -.edit-image { - .view-image { - display: inline-block; - - img[src=""], - img[src="null"] { - // Hiding "broken" image when src is not set - visibility: hidden; - } - - &.dirty { - // The image will be modified when saving the changes - - &.compat { - // Browser not supporting FileReader - background-image: url("ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png"); - img { - opacity: 0.3; - } - } - } - } - - .edit-buttons { - display: inline-block; - vertical-align: top; - margin-top: 4px; - margin-left: 3px; - - .button { - cursor: pointer; - margin-bottom: 3px; - padding: 2px; - background-color: $highlight-color; - - &.disabled { - cursor: default; - background-color: $grey-color; - opacity: 0.3; - } - .ui-icon { - background-image: url("ui-lightness/images/ui-icons_ffffff_256x240.png"); - } - } - } - - .file-input { - display: block; - } -} - -/* Center the image both horizontally and vertically, withing a box which size is fixed (depends on the attribute definition) */ -.details .view-image { - text-align: center; - padding: 2px; - border: 2px solid #DDDDDD; - border-radius: 6px; - - img { - display: inline-block; - vertical-align: middle; - max-width: 90% !important; - max-height: 90% !important; - } - .helper-middle { - // Helper to center the image (requires a span dedicated to this) - display: inline-block; - height: 100%; - vertical-align: middle; - } -} - -table.listContainer { - border: 0; - padding: 0; - margin: 0; - width: 100%; - clear: both; -} - -tr.containerHeader, tr.containerHeader td { - background: transparent; -} - -tr.even td, .wizContainer tr.even td { - background-color: $table-even-background; -} -tr.red_even td, .wizContainer tr.red_even td { - background-color: #f97e75; - color: #fff; -} -tr.red td, .wizContainer tr.red td { - background-color: #f9a397; - color: #fff; -} -tr.orange_even td, .wizContainer tr.orange_even td { - background-color: #f4d07a; -} -tr.orange td, .wizContainer tr.orange td { - background-color: #f4e96c; -} -tr.green_even td, .wizContainer tr.green_even td { - background-color: #bee5a3; -} -tr.green td, .wizContainer tr.green td { - background-color: #b3e5b4; -} - -tr td.hover, tr.even td.hover, .hover a, .hover a:visited, .hover a:hover, .wizContainer tr.even td.hover, .wizContainer tr td.hover { - background-color: #fdf5d0; - color: $text-color; -} - -th { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 8pt; - color: $complement-color; - height:20px; - background: $frame-background-color bottom repeat-x; -} - -th.header { - cursor: pointer; - background-repeat: no-repeat; - background-position: center right; - background-repeat: no-repeat; - padding-right: 16px; // some space for the asc/desc icons -} - -th.headerSortUp { - background-image: url(../images/asc.gif); - text-decoration: underline; - cursor: pointer; -} - -th.headerSortDown { - background-image: url(../images/desc.gif); - text-decoration: underline; - cursor: pointer; -} - -td { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 12px; - color:#696969; - nobackground-color: #ffffff; - padding: 0px; -} - -tr.clicked td { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: smaller; - background-color: #ffcfe8; -} - -td.label { - vertical-align: top; -} -td.label span { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 12px; - color: #000000; - padding: 5px; - padding-right: 10px; - font-weight:bold; - vertical-align: top; - text-align: right; - display: block; -} -fieldset td.label span { - padding: 3px; - padding-right: 10px; -} -fieldset { - margin-top: 3px; - -moz-border-radius: 6px; - -webkit-border-radius: 6px; - border-radius: 6px; - border-style: solid; - border-color: #ddd; -} - -legend { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 12px; - padding:8px; - color: #fff; - background-color: $complement-color; - font-weight: bold; - -moz-border-radius: 6px; - -webkit-border-radius: 6px; - border-radius: 6px; -} -legend.transparent { - background: transparent; - color: #333333; - font-size: 1em; - font-weight: normal; - padding: 0; -} -.ui-widget-content td legend a, .ui-widget-content td legend a:hover, .ui-widget-content td legend a:visited { - color: #fff; -} - -.ui-widget-content td a, p a, p a:visited, td a, td a:visited { - text-decoration:none; - color: $complement-color; -} -.ui-widget-content td a.cke_button, .ui-widget-content td a.cke_toolbox_collapser, .ui-widget-content td a.cke_combo_button, cke_dialog a { - padding-left: 0; - background-image: none; -} - -.ui-widget-content td a:hover, p a:hover, td a:hover { - text-decoration:underline; - color:$highlight-color; -} -.cke_reset_all *:hover { - text-decoration: none; - color: #000; -} -table.cke_dialog_contents a.cke_dialog_ui_button_ok { - color: #000; - border-color: $highlight-color; - background: $highlight-color; -} -.cke_notifications_area { - display: none; -} -td a.no-arrow, td a.no-arrow:visited, .SearchDrawer a.no-arrow, .SearchDrawer a.no-arrow:visited { - text-decoration:none; - color:#000000; - padding-left:0px; - background: inherit; -} -td a.no-arrow:hover { - text-decoration:underline; - color:#d81515; - padding-left:0px; - background: inherit; -} -td a, -td a:visited{ - &:hover{ - .text_decoration{ - color: darken($highlight-color, 6%); - } - } - - .text_decoration{ - vertical-align: baseline; - text-decoration: none; - color: $highlight-color; - margin-right: 8px; - transition: color 0.2s ease-in-out; - } -} - -a.small_action { - font-family: Tahoma, Verdana, Arial, Helvetica; - font-size: 8pt; - color: #000000; - text-decoration:none; -} -.display_block { - padding:0.25em; -} -.actions_details { - float:right; - margin-top:10px; - margin-right:10px; - padding-left: 5px; - padding-top: 2px; - padding-bottom: 2px; - background: $highlight-color url(../images/actions_left.png?v=#{$version}) no-repeat left; -} -.actions_details span{ - background: url(../images/actions_right.png?v=#{$version}) no-repeat right; - color: #fff; - font-weight: bold; - padding-top: 2px; - padding-bottom: 2px; - padding-right: 12px; -} -.actions_details a { - text-decoration:none; -} -.loading { - noborder: 1px dashed #CCC; - background: #b9c1c8; - padding:0.25em; -} - -input.textSearch { - border:1px solid #000; - font-family:Tahoma,Verdana, Arial, Helvetica, sans-serif; - font-size: 12px; - color:#000000; -} - -.ac_input { - border: 1px solid #7f9db9; - background: #fff url(../images/ac-background.gif) no-repeat right; -} -/* By Rom */ -.csvimport_createobj { - color: #AA0000; - background-color:#EEEEEE; -} -.csvimport_error { - font-weight: bold; - color: #FF0000; - background-color:#EEEEEE; -} -.csvimport_warning { - color: #CC8888; - background-color:#EEEEEE; -} -.csvimport_ok { - color: #00000; - background-color:#BBFFBB; -} -.csvimport_reconkey { - font-style: italic; - color: #888888; - background-color:#FFFFF; -} -.csvimport_extreconkey { - color: #888888; - background-color:#FFFFFF; -} -#accordion { - display:none; -} - -#accordion h3 { - padding: 10px; -} - -.ui-accordion-content ul { - list-style:none; - list-style-image: url(data:0); - padding-left:16px; - margin-top: 8px; -} - -.ui-accordion-content li.submenu { - margin-top: 8px; -} - -.ui-accordion-content ul ul { - padding: 8px 0px 8px 8px; - margin:0; - list-style:none; - list-style-image: url(data:0); - border: 0; -} - -.nothing { - noborder-top: 1px solid #8b8b8b; - padding: 4px 0px 0px 16px; - font-size:8pt; - background: url(../images/green-square.gif) no-repeat bottom left; - color:#83b217; - font-weight:bold; - text-decoration:none; -} -div.ui-accordion-content { - padding-top: 10px; - padding-left: 10px; -} -.ui-accordion-content a, ui-accordion-content a:visited { - color: $complement-color; - text-decoration:none; -} - -.ui-accordion-content a:hover { - color: $highlight-color; - text-decoration: none; -} - -.ui-accordion-content ul { - padding-left: 0; - margin-top: 0; -} - -.ui-accordion-content li { - color:$grey-color; - text-decoration:none; - margin: 0; - padding: 0px 0pt 0px 16px; - font-size: 9pt; - background: url(../images/mini-arrow-orange.gif) no-repeat top left; - font-weight:normal; - border: 0; -} - -a.CollapsibleLabel, td a.CollapsibleLabel { - margin: 0; - padding: 0px 0pt 0px 16px; - font-size:8pt; - text-decoration:none; - color:$grey-color; - background: url(../images/mini-arrow-orange.gif) no-repeat left; -} - -/* Beware: IE6 does not support multiple selector with multiple classes, only the last class is used */ -a.CollapsibleLabel.open, td a.CollapsibleLabel.open { - margin: 0; - padding: 0px 0pt 0px 16px; - font-size:8pt; - text-decoration:none; - color: $highlight-color; - background: url(../images/mini-arrow-orange-open.gif) no-repeat left; -} - -.page_header { - background-color:$frame-background-color; - padding:5px; -} -/* move up a header immediately following a display block (i.e. "actions" menu) */ -.display_block + .page_header { - margin-top: -8px; -} - -.notreeview li { background: url(../images/tv-item.gif) 0 0 no-repeat; } -.notreeview .collapsable { background-image: url(../images/tv-collapsable.gif); } -.notreeview .expandable { background-image: url(../images/tv-expandable.gif); } -.notreeview .last { background-image: url(../images/tv-item-last.gif); } -.notreeview .lastCollapsable { background-image: url(../images/tv-collapsable-last.gif); } -.notreeview .lastExpandable { background-image: url(../images/tv-expandable-last.gif); } - -#OrganizationSelection { - padding:5px 0px 16px 20px; -} - -/* popup menus */ -div.itop_popup { - margin: 0; - padding: 0; - float:right; -} -div.itop_popup > ul > li { - list-style: none; - cursor: pointer; - position: relative; -} - -div.actions_menu > ul { - height:19px; - line-height: 17px; - vertical-align: middle; - display:block; - nowidth:70px; /* Nasty work-around for IE... en attendant mieux */ - padding-left: 5px; - background: $highlight-color url(../images/actions_left.png?v=#{$version}) no-repeat top left; - cursor: pointer; - margin: 0; -} - -div.actions_menu > ul > li { - float: left; - list-style: none; - font-size: 11px; - font-family: Tahoma,sans-serif; - height: 17px; - padding-right: 16px; - padding-left: 4px; - background: url(../images/actions_right.png?v=#{$version}) no-repeat top right transparent; - font-weight: bold; - color: $popup-menu-text-higlight-color; - vertical-align: middle; - margin: 0; -} -#logOffBtn > ul > li { - list-style: none; - vertical-align: middle; - margin: 0; - padding-left: 10px; - padding-right: 10px; - cursor: pointer; -} -#logOffBtn > ul { - list-style: none; - vertical-align: middle; - margin: 0; - padding: 0; - height: 25px; -} - -.itop_popup li a, #logOffBtn li a { - display: block; - padding: 5px 12px; - text-decoration: none; - nowidth: 70px; - color: $popup-menu-text-color; - font-weight: bold; - white-space: nowrap; - background: $popup-menu-background-color; -} - -#logOffBtn li span { - display: block; - padding: 5px 12px; - text-decoration: none; - nowidth: 70px; - color: $popup-menu-text-color; - white-space: nowrap; - background: $popup-menu-background-color; -} -.itop_popup ul { - padding-left: 0; -} - -.menucontainer div.toolkit_menu { - margin-left: 10px; -} - -.itop_popup li a:hover, #logOffBtn li a:hover { - background: #1A4473; -} - -.itop_popup ul > li > ul, #logOffBtn ul > li > ul -{ - border: 1px solid black; - background: #fff; -} - -.itop_popup li > ul, #logOffBtn li > ul -{ margin: 0; - padding: 0; - position: absolute; - display: none; - border-top: 1px solid white; - z-index: 1500; -} - -.itop_popup li ul li, #logOffBtn li ul li { - float: none; - display: inline; -} - -.itop_popup li ul li a, #logOffBtn li ul li a { - width: auto; - text-align: left; -} - -.itop_popup li ul li a:hover, #logOffBtn li ul li a:hover{ - background: $popup-menu-highlight-color; - color: $popup-menu-text-higlight-color; - font-weight: bold; -} -.itop_popup > ul { - margin: 0; -} -hr.menu-separator { - border: none 0; - border-top: 1px solid #ccc; - color: #ccc; - background-color: transparent; - height: 1px; - margin: 3px; - cursor: default; -} -/************************************/ -.wizHeader { - background: $complement-color; - padding: 15px; -} -.wizContainer { - border: 5px solid $complement-color; - background: $complement-light; - padding: 5px; -} - -.wizContainer table tr td { - background: transparent; -} -.alignRight { - text-align: right; - padding: 3px; -} - -.alignLeft { - text-align: left; - padding: 3px; -} - -.red { - background-color: #ff6000; - color: #000; -} - -th.red { - background: url(../images/red-header.gif) bottom left repeat-x; - color: #000; -} - -.green { - background-color: #00cc00; - color: #000; -} -th.green { - background: url(../images/green-header.gif) bottom left repeat-x; - color: #000; -} - -.orange { - background-color: #ffde00; - color: #000; -} - -th.orange { - background: url(../images/orange-header.gif) bottom left repeat-x; - color: #000; -} - -/* For Date Picker: Creates a little calendar icon - * instead of a text link for "Choose date" - */ -td a.dp-choose-date, a.dp-choose-date, td a.dp-choose-date:hover, a.dp-choose-date:hover, td a.dp-choose-date:visited, a.dp-choose-date:visited { - float: left; - width: 16px; - height: 16px; - padding: 0; - margin: 5px 3px 0; - display: block; - text-indent: -2000px; - overflow: hidden; - background: url(../images/calendar.png?v=#{$version}) no-repeat; -} -td a.dp-choose-date.dp-disabled, a.dp-choose-date.dp-disabled { - background-position: 0 -20px; - cursor: default; -} -/* For Date Picker: makes the input field shorter once the date picker code - * has run (to allow space for the calendar icon) - */ -input.dp-applied { - width: 140px; - float: left; -} - -/* For search forms */ -.mini_tabs a { - text-decoration: none; - font-weight:bold; - color: #ccc; - background-color:#333; - padding-left: 1em; - padding-right: 1em; - padding-bottom: 0.25em; -} -.mini_tabs a.selected { - color: #fff; - background-color: $complement-color; - padding-top: 0.25em; -} -.mini_tabs ul { - margin: -10px; -} -.mini_tabs ul li { - float: right; - list-style: none; - nopadding-left: 1em; - nopadding-right: 1em; - margin-top: 0; -} -/* Search forms v2 */ -.search_box{ - box-sizing: border-box; - position: relative; - z-index: 1100; /* To be over the table block/unblock UI. Not very sure about this. */ - text-align: center; /* Used when form is closed */ - - /* Sizing reset */ - *{ - box-sizing: border-box; - } -} -.search_form_handler{ - position: relative; - z-index: 10; - width: 100%; - margin-left: auto; - margin-right: auto; - font-size: 12px; - text-align: left; /* To compensate .search_box:text-align */ - border: 1px solid $complement-color; - //transition: width 0.3s ease-in-out; - - /* Sizing reset */ - *{ - box-sizing: border-box; - } - /* Hyperlink reset */ - a{ - color: inherit; - text-decoration: none; - } - /* Input reset */ - input[type="text"], - select{ - padding: 1px 2px; - } - - &:not(.closed){ - .sf_title{ - .sft_short{ - display: none; - } - - .sft_hint, - .sft_toggler{ - margin-top: 4px; - } - .sft_toggler{ - transform: rotateX(180deg); - transition: transform 0.5s linear; - } - } - } - &.closed{ - margin-bottom: 0.5em; - width: 150px; - overflow: hidden; - border-radius: 4px; - background-color: $complement-color; - - .sf_criterion_area{ - height: 0; - opacity: 0; - padding: 0; - } - .sf_title { - padding: 6px 8px; - text-align: center; - font-size: 12px; - - .sft_long{ - display: none; - } - .sft_hint{ - display: none; - } - } - } - - &:not(.no_auto_submit){ - .sft_hint{ - display: none; - } - .sfc_fg_apply{ - display: none; - } - } - &.no_auto_submit{ - .sfc_fg_search{ - display: none; - } - } - - .sf_title{ - transition: opacity 0.3s, background-color 0.3s, color 0.3s linear; - padding: 8px 10px; - margin: 0; - color: #ffffff; - background-color: $complement-color; - cursor: pointer; - .sft_hint{ - font-size: 8pt; - font-style: italic; - } - .sft_toggler{ - margin-left: 0.7em; - transition: color 0.2s ease-in-out, transform 0.4s ease-in-out; - - &:hover{ - color: $gray-extra-light; - } - } - } - .sf_message{ - display: none; - margin: 8px 8px 0px 8px; - border-radius: 0px; - } - .sf_criterion_area{ - /*display: none;*/ - padding: 8px 8px 3px 8px; /* padding-bottom must equals to padding-top - .search_form_criteria:margin-bottom */ - background-color: $white; - - .sf_criterion_row{ - position: relative;; - - &:not(:first-child){ - margin-top: 20px; - - &::before{ - content: ""; - position: absolute; - top: -12px; - left: 0px; - width: 100%; - border-top: 1px solid $search-criteria-box-border-color; - } - &::after{ - content: "or"; /* TODO: Make this into a dict entry */ - position: absolute; - top: -20px; - left: 8px; - padding-left: 5px; - padding-right: 5px; - color: $gray-light; - background-color: $white; /* Must match .sf_criterion_area:background-color */ - } - } - - .sf_criterion_group{ - display: inline; - } - } - - /* Common style between criterion and more criterion */ - .search_form_criteria, - .sf_more_criterion, - .sf_button{ - position: relative; - display: inline-block; - margin-right: 10px; - margin-bottom: 5px; - vertical-align: top; - - &.opened{ - margin-bottom: 0px; /* To compensate the .sfc/.sfm_header:padding-bottom: 13px */ - - .sfc_header, - .sfm_header{ - border-bottom: none !important; - box-shadow: none !important; - padding-bottom: 13px; /* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */ - } - } - - > *{ - padding: 7px 8px; - vertical-align: top; - border: $search-criteria-box-border; - border-radius: $search-criteria-box-radius; - box-shadow: $box-shadow-regular; - } - .sfc_form_group, - .sfm_content{ - position: absolute; - z-index: -1; - min-width: 100%; - left: 0px; - margin-top: -1px; - } - } - - /* Criteria tags */ - .search_form_criteria{ - /* Non editable criteria */ - &.locked{ - background-color: $gray-extra-light; - - .sfc_title{ - user-select: none; - cursor: initial; - } - } - /* Draft criteria (modifications not applied) */ - &.draft{ - .sfc_header, - .sfc_form_group{ - border-style: dashed; - } - - .sfc_title{ - font-style: italic; - } - } - /* Opened criteria (form group displayed) */ - &.opened{ - z-index: 1; /* To be over other criterion */ - - .sfc_toggle{ - transform: rotateX(-180deg); - } - .sfc_form_group{ - display: block; - } - } - &.opened_left{ - .sfc_form_group{ - left: auto; - right: 0px; - } - } - - /* Add "and" on criterion but the one and submit button */ - &:not(:last-of-type){ - margin-right: 30px; - - &::after{ - /* TODO: Find an elegant way to do this, without hardcoding the content (could be a -Missing mandatory argument $sName

    "; - exit; - } - return $value; -} - -function IsAValidTestClass($sClassName) -{ - // Must be a child of TestHandler - // - if (!is_subclass_of($sClassName, 'TestHandler')) return false; - - // Must not be abstract - // - $oReflectionClass = new ReflectionClass($sClassName); - if (!$oReflectionClass->isInstantiable()) return false; - - return true; -} - -function GetTestClassLine($sClassName) -{ - $oReflectionClass = new ReflectionClass($sClassName); - return $oReflectionClass->getStartLine(); -} - -function DisplayEvents($aEvents, $sTitle) -{ - echo "

    $sTitle

    \n"; - if (count($aEvents) > 0) - { - echo "
      \n"; - foreach ($aEvents as $sEvent) - { - echo "
    • $sEvent
    • \n"; - } - echo "
    \n"; - } - else - { - echo "

    none

    \n"; - } -} - -/////////////////////////////////////////////////////////////////////////////// -// Main -/////////////////////////////////////////////////////////////////////////////// - -date_default_timezone_set('Europe/Paris'); - -require_once('../approot.inc.php'); -require_once(APPROOT.'/application/utils.inc.php'); -require_once('./test.class.inc.php'); -require_once('./testlist.inc.php'); - -require_once(APPROOT.'/core/cmdbobject.class.inc.php'); - - -$sTodo = utils::ReadParam("todo", ""); -if ($sTodo == '') -{ - // Show the list of tests - // - echo "

    Existing tests

    \n"; - echo "
      \n"; - foreach (get_declared_classes() as $sClassName) - { - if (!IsAValidTestClass($sClassName)) continue; - - $sName = call_user_func(array($sClassName, 'GetName')); - $sDescription = call_user_func(array($sClassName, 'GetDescription')); - echo "
    • $sName ($sDescription)
    • \n"; - } - echo "
    \n"; -} -else if ($sTodo == 'exec') -{ - // Execute a test - // - $sTestClass = ReadMandatoryParam("testid"); - - if (!IsAValidTestClass($sTestClass)) - { - echo "

    Wrong value for testid, expecting a valid class name

    \n"; - } - else - { - $oTest = new $sTestClass(); - $iStartLine = GetTestClassLine($sTestClass); - echo "

    Testing: ".$oTest->GetName()."

    \n"; - echo "
    testlist.inc.php: $iStartLine
    \n"; - $bRes = $oTest->Execute(); - } - -/* -MyHelpers::var_dump_html($oTest->GetResults()); -MyHelpers::var_dump_html($oTest->GetWarnings()); -MyHelpers::var_dump_html($oTest->GetErrors()); -*/ - - if ($bRes) - { - echo "

    Success :-)

    \n"; - DisplayEvents($oTest->GetResults(), 'Results'); - } - else - { - echo "

    Failure :-(

    \n"; - } - DisplayEvents($oTest->GetErrors(), 'Errors'); - DisplayEvents($oTest->GetWarnings(), 'Warnings'); - - // Render the output - // - echo "

    Actual output

    \n"; - echo "
    \n"; - echo $oTest->GetOutput(); - echo "
    \n"; -} -else -{ -} - - -?> + + + +/** + * Core test page + * + * @copyright Copyright (C) 2010-2014 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +?> + +Missing mandatory argument $sName

    "; + exit; + } + return $value; +} + +function IsAValidTestClass($sClassName) +{ + // Must be a child of TestHandler + // + if (!is_subclass_of($sClassName, 'TestHandler')) return false; + + // Must not be abstract + // + $oReflectionClass = new ReflectionClass($sClassName); + if (!$oReflectionClass->isInstantiable()) return false; + + return true; +} + +function GetTestClassLine($sClassName) +{ + $oReflectionClass = new ReflectionClass($sClassName); + return $oReflectionClass->getStartLine(); +} + +function DisplayEvents($aEvents, $sTitle) +{ + echo "

    $sTitle

    \n"; + if (count($aEvents) > 0) + { + echo "
      \n"; + foreach ($aEvents as $sEvent) + { + echo "
    • $sEvent
    • \n"; + } + echo "
    \n"; + } + else + { + echo "

    none

    \n"; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Main +/////////////////////////////////////////////////////////////////////////////// + +date_default_timezone_set('Europe/Paris'); + +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/utils.inc.php'); +require_once('./test.class.inc.php'); +require_once('./testlist.inc.php'); + +require_once(APPROOT.'/core/cmdbobject.class.inc.php'); + + +$sTodo = utils::ReadParam("todo", ""); +if ($sTodo == '') +{ + // Show the list of tests + // + echo "

    Existing tests

    \n"; + echo "
      \n"; + foreach (get_declared_classes() as $sClassName) + { + if (!IsAValidTestClass($sClassName)) continue; + + $sName = call_user_func(array($sClassName, 'GetName')); + $sDescription = call_user_func(array($sClassName, 'GetDescription')); + echo "
    • $sName ($sDescription)
    • \n"; + } + echo "
    \n"; +} +else if ($sTodo == 'exec') +{ + // Execute a test + // + $sTestClass = ReadMandatoryParam("testid"); + + if (!IsAValidTestClass($sTestClass)) + { + echo "

    Wrong value for testid, expecting a valid class name

    \n"; + } + else + { + $oTest = new $sTestClass(); + $iStartLine = GetTestClassLine($sTestClass); + echo "

    Testing: ".$oTest->GetName()."

    \n"; + echo "
    testlist.inc.php: $iStartLine
    \n"; + $bRes = $oTest->Execute(); + } + +/* +MyHelpers::var_dump_html($oTest->GetResults()); +MyHelpers::var_dump_html($oTest->GetWarnings()); +MyHelpers::var_dump_html($oTest->GetErrors()); +*/ + + if ($bRes) + { + echo "

    Success :-)

    \n"; + DisplayEvents($oTest->GetResults(), 'Results'); + } + else + { + echo "

    Failure :-(

    \n"; + } + DisplayEvents($oTest->GetErrors(), 'Errors'); + DisplayEvents($oTest->GetWarnings(), 'Warnings'); + + // Render the output + // + echo "

    Actual output

    \n"; + echo "
    \n"; + echo $oTest->GetOutput(); + echo "
    \n"; +} +else +{ +} + + +?> diff --git a/test/unittestautoload.php b/test/unittestautoload.php index 1dbfed3b2..4c523c2e8 100644 --- a/test/unittestautoload.php +++ b/test/unittestautoload.php @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/webservices/backoffice.dataloader.php b/webservices/backoffice.dataloader.php index ece7b67f6..601b90b95 100644 --- a/webservices/backoffice.dataloader.php +++ b/webservices/backoffice.dataloader.php @@ -1,170 +1,170 @@ - - - -/** - * Does load data from XML files (currently used in the setup and the backoffice data loader utility) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * This page is called to load an XML file into the database - * parameters - * 'file' string Name of the file to load - */ -define('SAFE_MINIMUM_MEMORY', 256*1024*1024); - -require_once('../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); - -require_once(APPROOT.'/application/startup.inc.php'); - -require_once(APPROOT.'/application/loginwebpage.class.inc.php'); -LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin) - -// required because the class xmldataloader is reporting errors in the setup.log file -require_once(APPROOT.'/setup/setuppage.class.inc.php'); -require_once(APPROOT.'/setup/xmldataloader.class.inc.php'); - - -function SetMemoryLimit($oP) -{ - $sMemoryLimit = trim(ini_get('memory_limit')); - if (empty($sMemoryLimit)) - { - // On some PHP installations, memory_limit does not exist as a PHP setting! - // (encountered on a 5.2.0 under Windows) - // In that case, ini_set will not work, let's keep track of this and proceed with the data load - $oP->p("No memory limit has been defined in this instance of PHP"); - } - else - { - // Check that the limit will allow us to load the data - // - $iMemoryLimit = utils::ConvertToBytes($sMemoryLimit); - if ($iMemoryLimit < SAFE_MINIMUM_MEMORY) - { - if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE) - { - $oP->p("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself."); - } - else - { - $oP->p("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY."."); - } - } - } -} - - -//////////////////////////////////////////////////////////////////////////////// -// -// Main -// -//////////////////////////////////////////////////////////////////////////////// - -// Never cache this page -header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 -header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past - -/** - * Main program - */ -$sFileName = Utils::ReadParam('file', '', false, 'raw_data'); - -$oP = new WebPage("iTop - Backoffice data loader"); - - -try -{ - // Note: the data model must be loaded first - $oDataLoader = new XMLDataLoader(); - - if (empty($sFileName)) - { - throw(new Exception("Missing argument 'file'")); - } - if (!file_exists($sFileName)) - { - throw(new Exception("File $sFileName does not exist")); - } - - SetMemoryLimit($oP); - - - // The XMLDataLoader constructor has initialized the DB, let's start a transaction - CMDBSource::Query('START TRANSACTION'); - - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); - $oP->p("Starting data load."); - $oDataLoader->StartSession($oChange); - - $oDataLoader->LoadFile($sFileName); - - $oP->p("Ending data load session"); - if ($oDataLoader->EndSession(true /* strict */)) - { - $iCountCreated = $oDataLoader->GetCountCreated(); - CMDBSource::Query('COMMIT'); - - $oP->p("Data successfully written into the DB: $iCountCreated objects created"); - } - else - { - CMDBSource::Query('ROLLBACK'); - $oP->p("Some issues have been encountered, changes will not be recorded, please review the source data"); - $aErrors = $oDataLoader->GetErrors(); - if (count($aErrors) > 0) - { - $oP->p('Errors ('.count($aErrors).')'); - foreach ($aErrors as $sMsg) - { - $oP->p(' * '.$sMsg); - } - } - $aWarnings = $oDataLoader->GetWarnings(); - if (count($aWarnings) > 0) - { - $oP->p('Warnings ('.count($aWarnings).')'); - foreach ($aWarnings as $sMsg) - { - $oP->p(' * '.$sMsg); - } - } - } - -} -catch(Exception $e) -{ - $oP->p("An error happened while loading the data: ".$e->getMessage()); - $oP->p("Aborting (no data written)..."); - CMDBSource::Query('ROLLBACK'); -} - -if (function_exists('memory_get_peak_usage')) -{ - $oP->p("Information: memory peak usage: ".memory_get_peak_usage()); -} - -$oP->Output(); -?> + + + +/** + * Does load data from XML files (currently used in the setup and the backoffice data loader utility) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * This page is called to load an XML file into the database + * parameters + * 'file' string Name of the file to load + */ +define('SAFE_MINIMUM_MEMORY', 256*1024*1024); + +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); + +require_once(APPROOT.'/application/startup.inc.php'); + +require_once(APPROOT.'/application/loginwebpage.class.inc.php'); +LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin) + +// required because the class xmldataloader is reporting errors in the setup.log file +require_once(APPROOT.'/setup/setuppage.class.inc.php'); +require_once(APPROOT.'/setup/xmldataloader.class.inc.php'); + + +function SetMemoryLimit($oP) +{ + $sMemoryLimit = trim(ini_get('memory_limit')); + if (empty($sMemoryLimit)) + { + // On some PHP installations, memory_limit does not exist as a PHP setting! + // (encountered on a 5.2.0 under Windows) + // In that case, ini_set will not work, let's keep track of this and proceed with the data load + $oP->p("No memory limit has been defined in this instance of PHP"); + } + else + { + // Check that the limit will allow us to load the data + // + $iMemoryLimit = utils::ConvertToBytes($sMemoryLimit); + if ($iMemoryLimit < SAFE_MINIMUM_MEMORY) + { + if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE) + { + $oP->p("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself."); + } + else + { + $oP->p("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY."."); + } + } + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// +// Main +// +//////////////////////////////////////////////////////////////////////////////// + +// Never cache this page +header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 +header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past + +/** + * Main program + */ +$sFileName = Utils::ReadParam('file', '', false, 'raw_data'); + +$oP = new WebPage("iTop - Backoffice data loader"); + + +try +{ + // Note: the data model must be loaded first + $oDataLoader = new XMLDataLoader(); + + if (empty($sFileName)) + { + throw(new Exception("Missing argument 'file'")); + } + if (!file_exists($sFileName)) + { + throw(new Exception("File $sFileName does not exist")); + } + + SetMemoryLimit($oP); + + + // The XMLDataLoader constructor has initialized the DB, let's start a transaction + CMDBSource::Query('START TRANSACTION'); + + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + $oP->p("Starting data load."); + $oDataLoader->StartSession($oChange); + + $oDataLoader->LoadFile($sFileName); + + $oP->p("Ending data load session"); + if ($oDataLoader->EndSession(true /* strict */)) + { + $iCountCreated = $oDataLoader->GetCountCreated(); + CMDBSource::Query('COMMIT'); + + $oP->p("Data successfully written into the DB: $iCountCreated objects created"); + } + else + { + CMDBSource::Query('ROLLBACK'); + $oP->p("Some issues have been encountered, changes will not be recorded, please review the source data"); + $aErrors = $oDataLoader->GetErrors(); + if (count($aErrors) > 0) + { + $oP->p('Errors ('.count($aErrors).')'); + foreach ($aErrors as $sMsg) + { + $oP->p(' * '.$sMsg); + } + } + $aWarnings = $oDataLoader->GetWarnings(); + if (count($aWarnings) > 0) + { + $oP->p('Warnings ('.count($aWarnings).')'); + foreach ($aWarnings as $sMsg) + { + $oP->p(' * '.$sMsg); + } + } + } + +} +catch(Exception $e) +{ + $oP->p("An error happened while loading the data: ".$e->getMessage()); + $oP->p("Aborting (no data written)..."); + CMDBSource::Query('ROLLBACK'); +} + +if (function_exists('memory_get_peak_usage')) +{ + $oP->p("Information: memory peak usage: ".memory_get_peak_usage()); +} + +$oP->Output(); +?> diff --git a/webservices/cron.cmd b/webservices/cron.cmd index c46fa3dbe..170e18d52 100644 --- a/webservices/cron.cmd +++ b/webservices/cron.cmd @@ -1,18 +1,18 @@ -@echo off -REM -REM To be scheduled by the following command: -REM -REM schtasks /create /tn "iTop Cron" /sc minute /tr "\"C:\Program Files\EasyPHP-5.3.6.0\www\iTop-trunk\webservices\cron.cmd\"" -REM -REM -REM PHP_PATH must point to php.exe, adjust the path to suit your own installation -SET PHP_PATH=C:\Program Files\EasyPHP-5.3.6.0\php\php.exe -REM PHP_INI must contain the full path to a PHP.ini file suitable for Command Line mode execution -SET PHP_INI=C:\Program Files\EasyPHP-5.3.6.0\php\php-cli.ini -REM The double dash (--) separates the parameters parsed by php.exe from the script's specific parameters -REM %~p0 expands to the path to this file (including the trailing backslash) -SET CRON_SCRIPT=%~p0cron.php -REM Adjust the path below if you use a param files stored in a different location -SET PARAMS_FILE=%~p0cron.params -REM Actual PHP invocation -"%PHP_PATH%" -c "%PHP_INI%" -f "%CRON_SCRIPT%" -- --param_file="%PARAMS_FILE%" --verbose=1 >> "%~p0log.txt" +@echo off +REM +REM To be scheduled by the following command: +REM +REM schtasks /create /tn "iTop Cron" /sc minute /tr "\"C:\Program Files\EasyPHP-5.3.6.0\www\iTop-trunk\webservices\cron.cmd\"" +REM +REM +REM PHP_PATH must point to php.exe, adjust the path to suit your own installation +SET PHP_PATH=C:\Program Files\EasyPHP-5.3.6.0\php\php.exe +REM PHP_INI must contain the full path to a PHP.ini file suitable for Command Line mode execution +SET PHP_INI=C:\Program Files\EasyPHP-5.3.6.0\php\php-cli.ini +REM The double dash (--) separates the parameters parsed by php.exe from the script's specific parameters +REM %~p0 expands to the path to this file (including the trailing backslash) +SET CRON_SCRIPT=%~p0cron.php +REM Adjust the path below if you use a param files stored in a different location +SET PARAMS_FILE=%~p0cron.params +REM Actual PHP invocation +"%PHP_PATH%" -c "%PHP_INI%" -f "%CRON_SCRIPT%" -- --param_file="%PARAMS_FILE%" --verbose=1 >> "%~p0log.txt" diff --git a/webservices/cron.distrib b/webservices/cron.distrib index 4f29e97f6..2e3240149 100644 --- a/webservices/cron.distrib +++ b/webservices/cron.distrib @@ -1,6 +1,6 @@ -# CRON parameters file used by cron.cmd -# -# Warning: make sure that this file is NOT readable by the web server - -auth_user=admin +# CRON parameters file used by cron.cmd +# +# Warning: make sure that this file is NOT readable by the web server + +auth_user=admin auth_pwd=admin \ No newline at end of file diff --git a/webservices/export.php b/webservices/export.php index 495522166..c2863ad40 100644 --- a/webservices/export.php +++ b/webservices/export.php @@ -1,365 +1,365 @@ - - - -/** - * Export data specified by an OQL - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); -require_once(__DIR__.'/../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); -require_once(APPROOT.'/application/nicewebpage.class.inc.php'); -require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); -require_once(APPROOT.'/application/csvpage.class.inc.php'); -require_once(APPROOT.'/application/xmlpage.class.inc.php'); -require_once(APPROOT.'/application/clipage.class.inc.php'); -require_once(APPROOT.'/application/excelexporter.class.inc.php'); - -require_once(APPROOT.'/application/startup.inc.php'); - -try -{ - // Do this before loging, in order to allow setting user credentials from within the file - utils::UseParamFile(); -} -catch(Exception $e) -{ - echo "Error: ".$e->GetMessage()."
    \n"; - exit -2; -} - -if (utils::IsModeCLI()) -{ - $sAuthUser = utils::ReadParam('auth_user', null, true /* Allow CLI */, 'raw_data'); - $sAuthPwd = utils::ReadParam('auth_pwd', null, true /* Allow CLI */, 'raw_data'); - - if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) - { - UserRights::Login($sAuthUser); // Login & set the user's language - } - else - { - $oP = new CLIPage("iTop - Export"); - $oP->p("Access restricted or wrong credentials ('$sAuthUser')"); - $oP->output(); - exit -1; - } -} -else -{ - require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(); // Check user rights and prompt if needed -} -ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker'); - - -$oAppContext = new ApplicationContext(); -$iActiveNodeId = utils::ReadParam('menu', -1); -$currentOrganization = utils::ReadParam('org_id', ''); - -if (utils::IsArchiveMode() && !UserRights::CanBrowseArchive()) -{ - $oP = new CLIPage("iTop - Export"); - $oP->p("The user account is not authorized to access the archives"); - $oP->output(); - exit -1; -} - -$bLocalize = (utils::ReadParam('no_localize', 0) != 1); -$sFileName = utils::ReadParam('filename', '', true, 'string'); - -// Main program -$sExpression = utils::ReadParam('expression', '', true /* Allow CLI */, 'raw_data'); -$sFields = trim(utils::ReadParam('fields', '', true, 'raw_data')); // CSV field list (allows to specify link set attributes, still not taken into account for XML export) -$bFieldsAdvanced = utils::ReadParam('fields_advanced', 0); - -if (strlen($sExpression) == 0) -{ - $sQueryId = trim(utils::ReadParam('query', '', true /* Allow CLI */, 'raw_data')); - if (strlen($sQueryId) > 0) - { - $oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $sQueryId)); - $oQueries = new DBObjectSet($oSearch); - if ($oQueries->Count() > 0) - { - $oQuery = $oQueries->Fetch(); - $sExpression = $oQuery->Get('oql'); - if (strlen($sFields) == 0) - { - $sFields = trim($oQuery->Get('fields')); - } - } - } -} - -$sFormat = strtolower(utils::ReadParam('format', 'html', true /* Allow CLI */)); - - -$aFields = explode(',', $sFields); -// Clean the list of columns (empty it if every string is empty) -foreach($aFields as $index => $sField) -{ - $aFields[$index] = trim($sField); - if(strlen($aFields[$index]) == 0) - { - unset($aFields[$index]); - } -} - -$oP = null; - -if (!empty($sExpression)) -{ - try - { - $oFilter = DBObjectSearch::FromOQL($sExpression); - - // Check and adjust column names - // - $aAliasToFields = array(); - foreach($aFields as $index => $sField) - { - if (preg_match('/^(.*)\.(.*)$/', $sField, $aMatches)) - { - $sClassAlias = $aMatches[1]; - $sAttCode = $aMatches[2]; - } - else - { - $sClassAlias = $oFilter->GetClassAlias(); - $sAttCode = $sField; - // Disambiguate the class alias - $aFields[$index] = $sClassAlias.'.'.$sAttCode; - } - $aAliasToFields[$sClassAlias][] = $sAttCode; - - $sClass = $oFilter->GetClassName($sClassAlias); - if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) - { - throw new CoreException("Invalid field specification $sField: $sAttCode is not a valid attribute for $sClass"); - } - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ($oAttDef instanceof AttributeSubItem) - { - $aAliasToFields[$sClassAlias][] = $oAttDef->GetParentAttCode(); - } - else if($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) - { - $aAliasToFields[$sClassAlias][] = $sKeyAttCode; - } - } - - // Read query parameters - // - $aArgs = array(); - foreach($oFilter->GetQueryParams() as $sParam => $foo) - { - $value = utils::ReadParam('arg_'.$sParam, null, true, 'raw_data'); - if (!is_null($value)) - { - $aArgs[$sParam] = $value; - } - } - $oFilter->SetInternalParams($aArgs); - foreach ($oFilter->GetSelectedClasses() as $sAlias => $sClass) - { - if ((UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_READ) && UR_ALLOWED_YES) == 0) - { - throw new Exception("The current user does not have permission for exporting data of class $sClass"); - } - } - - if ($oFilter) - { - $oSet = new CMDBObjectSet($oFilter, array(), $aArgs); - $oSet->OptimizeColumnLoad($aAliasToFields); - switch($sFormat) - { - case 'html': - $oP = new NiceWebPage("iTop - Export"); - $oP->add_style('body { overflow: auto; }'); // Show scroll bars if needed - - // Integration within MS-Excel web queries + HTTPS + IIS: - // MS-IIS set these header values with no-cache... while Excel fails to do the job if using HTTPS - // Then the fix is to force the reset of header values Pragma and Cache-control - header("Pragma:", true); - header("Cache-control:", true); - - // The HTML output is made for pages located in the /pages/ folder - // since this page is in a different folder, let's adjust the HTML 'base' attribute - // to make the relative hyperlinks in the page work - $sUrl = utils::GetAbsoluteUrlAppRoot(); - $oP->set_base($sUrl.'pages/'); - - if(count($aFields) > 0) - { - $iSearch = array_search('id', $aFields); - if ($iSearch !== false) - { - $bViewLink = true; - unset($aFields[$iSearch]); - } - else - { - $bViewLink = false; - } - $sFields = implode(',', $aFields); - $aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'display_limit' => false, 'localize_values' => $bLocalize, 'zlist' => false, 'extra_fields' => $sFields, 'view_link' => $bViewLink); - } - else - { - $aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'display_limit' => false, 'localize_values' => $bLocalize, 'zlist' => 'details'); - } - - $oResultBlock = new DisplayBlock($oFilter, 'list', false, $aExtraParams); - $oResultBlock->Display($oP, 'expresult'); - break; - - case 'csv': - $oP = new CSVPage("iTop - Export"); - $sFields = implode(',', $aFields); - $sCharset = utils::ReadParam('charset', MetaModel::GetConfig()->Get('csv_file_default_charset'), true /* Allow CLI */, 'raw_data'); - $sCSVData = cmdbAbstractObject::GetSetAsCSV($oSet, array('fields' => $sFields, 'fields_advanced' => $bFieldsAdvanced, 'localize_values' => $bLocalize), $sCharset); - if ($sCharset == 'UTF-8') - { - $sOutputData = UTF8_BOM.$sCSVData; - } - else - { - $sOutputData = $sCSVData; - } - if ($sFileName == '') - { - // Plain text => Firefox will NOT propose to download the file - $oP->add_header("Content-type: text/plain; charset=$sCharset"); - } - else - { - $oP->add_header("Content-type: text/csv; charset=$sCharset"); - } - $oP->add($sOutputData); - break; - - case 'spreadsheet': - $oP = new WebPage("iTop - Export for spreadsheet"); - - // Integration within MS-Excel web queries + HTTPS + IIS: - // MS-IIS set these header values with no-cache... while Excel fails to do the job if using HTTPS - // Then the fix is to force the reset of header values Pragma and Cache-control - header("Pragma:", true); - header("Cache-control:", true); - - $sFields = implode(',', $aFields); - $oP->add_style('table br {mso-data-placement:same-cell;}'); // Trick for Excel: keep line breaks inside the same cell ! - cmdbAbstractObject::DisplaySetAsHTMLSpreadsheet($oP, $oSet, array('fields' => $sFields, 'fields_advanced' => $bFieldsAdvanced, 'localize_values' => $bLocalize)); - break; - - case 'xml': - $oP = new XMLPage("iTop - Export", true /* passthrough */); - cmdbAbstractObject::DisplaySetAsXML($oP, $oSet, array('localize_values' => $bLocalize)); - break; - - case 'xlsx': - $oP = new ajax_page(''); - $oExporter = new ExcelExporter(); - $oExporter->SetObjectList($oFilter); - - // Run the export by chunk of 1000 objects to limit memory usage - $oExporter->SetChunkSize(1000); - do - { - $aStatus = $oExporter->Run(); // process one chunk - } - while( ($aStatus['code'] != 'done') && ($aStatus['code'] != 'error')); - - if ($aStatus['code'] == 'done') - { - $oP->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - $oP->SetContentDisposition('attachment', $oFilter->GetClass().'.xlsx'); - $oP->add(file_get_contents($oExporter->GetExcelFilePath())); - $oExporter->Cleanup(); - } - else - { - $oP->add('Error, xlsx export failed: '.$aStatus['message']); - } - break; - - default: - $oP = new WebPage("iTop - Export"); - $oP->add("Unsupported format '$sFormat'. Possible values are: html, csv, spreadsheet or xml."); - } - } - } - catch(Exception $e) - { - $oP = new WebPage("iTop - Export"); - $oP->p("Error the query can not be executed."); - if ($e instanceof CoreException) - { - $oP->p($e->GetHtmlDesc()); - } - else - { - $oP->p($e->getMessage()); - } - } -} -if (!$oP) -{ - // Display a short message about how to use this page - $bModeCLI = utils::IsModeCLI(); - if ($bModeCLI) - { - $oP = new CLIPage("iTop - Export"); - } - else - { - $oP = new WebPage("iTop - Export"); - } - $oP->p("General purpose export page."); - $oP->p("Parameters:"); - $oP->p(" * expression: an OQL expression (URL encoded if needed)"); - $oP->p(" * query: (alternative to 'expression') the id of an entry from the query phrasebook"); - if (Utils::IsModeCLI()) - { - $oP->p(" * with_archive: (optional, defaults to 0) if set to 1 then the result set will include archived objects"); - } - else - { - $oP->p(" * with_archive: (optional, defaults to the current mode) if set to 1 then the result set will include archived objects"); - } - $oP->p(" * arg_xxx: (needed if the query has parameters) the value of the parameter 'xxx'"); - $oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv', 'xlsx' or 'xml'"); - $oP->p(" * fields: (optional, no effect on XML format) list of fields (attribute codes, or alias.attcode) separated by a coma"); - $oP->p(" * fields_advanced: (optional, no effect on XML/HTML formats ; ignored is fields is specified) If set to 1, the default list of fields will include the external keys and their reconciliation keys"); - $oP->p(" * filename: (optional, no effect in CLI mode) if set then the results will be downloaded as a file"); -} - -if ($sFileName != '') -{ - $oP->add_header('Content-Disposition: attachment; filename="'.$sFileName.'"'); -} - -$oP->TrashUnexpectedOutput(); -$oP->output(); -?> + + + +/** + * Export data specified by an OQL + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); +require_once(__DIR__.'/../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); +require_once(APPROOT.'/application/nicewebpage.class.inc.php'); +require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); +require_once(APPROOT.'/application/csvpage.class.inc.php'); +require_once(APPROOT.'/application/xmlpage.class.inc.php'); +require_once(APPROOT.'/application/clipage.class.inc.php'); +require_once(APPROOT.'/application/excelexporter.class.inc.php'); + +require_once(APPROOT.'/application/startup.inc.php'); + +try +{ + // Do this before loging, in order to allow setting user credentials from within the file + utils::UseParamFile(); +} +catch(Exception $e) +{ + echo "Error: ".$e->GetMessage()."
    \n"; + exit -2; +} + +if (utils::IsModeCLI()) +{ + $sAuthUser = utils::ReadParam('auth_user', null, true /* Allow CLI */, 'raw_data'); + $sAuthPwd = utils::ReadParam('auth_pwd', null, true /* Allow CLI */, 'raw_data'); + + if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) + { + UserRights::Login($sAuthUser); // Login & set the user's language + } + else + { + $oP = new CLIPage("iTop - Export"); + $oP->p("Access restricted or wrong credentials ('$sAuthUser')"); + $oP->output(); + exit -1; + } +} +else +{ + require_once(APPROOT.'/application/loginwebpage.class.inc.php'); + LoginWebPage::DoLogin(); // Check user rights and prompt if needed +} +ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker'); + + +$oAppContext = new ApplicationContext(); +$iActiveNodeId = utils::ReadParam('menu', -1); +$currentOrganization = utils::ReadParam('org_id', ''); + +if (utils::IsArchiveMode() && !UserRights::CanBrowseArchive()) +{ + $oP = new CLIPage("iTop - Export"); + $oP->p("The user account is not authorized to access the archives"); + $oP->output(); + exit -1; +} + +$bLocalize = (utils::ReadParam('no_localize', 0) != 1); +$sFileName = utils::ReadParam('filename', '', true, 'string'); + +// Main program +$sExpression = utils::ReadParam('expression', '', true /* Allow CLI */, 'raw_data'); +$sFields = trim(utils::ReadParam('fields', '', true, 'raw_data')); // CSV field list (allows to specify link set attributes, still not taken into account for XML export) +$bFieldsAdvanced = utils::ReadParam('fields_advanced', 0); + +if (strlen($sExpression) == 0) +{ + $sQueryId = trim(utils::ReadParam('query', '', true /* Allow CLI */, 'raw_data')); + if (strlen($sQueryId) > 0) + { + $oSearch = DBObjectSearch::FromOQL('SELECT QueryOQL WHERE id = :query_id', array('query_id' => $sQueryId)); + $oQueries = new DBObjectSet($oSearch); + if ($oQueries->Count() > 0) + { + $oQuery = $oQueries->Fetch(); + $sExpression = $oQuery->Get('oql'); + if (strlen($sFields) == 0) + { + $sFields = trim($oQuery->Get('fields')); + } + } + } +} + +$sFormat = strtolower(utils::ReadParam('format', 'html', true /* Allow CLI */)); + + +$aFields = explode(',', $sFields); +// Clean the list of columns (empty it if every string is empty) +foreach($aFields as $index => $sField) +{ + $aFields[$index] = trim($sField); + if(strlen($aFields[$index]) == 0) + { + unset($aFields[$index]); + } +} + +$oP = null; + +if (!empty($sExpression)) +{ + try + { + $oFilter = DBObjectSearch::FromOQL($sExpression); + + // Check and adjust column names + // + $aAliasToFields = array(); + foreach($aFields as $index => $sField) + { + if (preg_match('/^(.*)\.(.*)$/', $sField, $aMatches)) + { + $sClassAlias = $aMatches[1]; + $sAttCode = $aMatches[2]; + } + else + { + $sClassAlias = $oFilter->GetClassAlias(); + $sAttCode = $sField; + // Disambiguate the class alias + $aFields[$index] = $sClassAlias.'.'.$sAttCode; + } + $aAliasToFields[$sClassAlias][] = $sAttCode; + + $sClass = $oFilter->GetClassName($sClassAlias); + if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + throw new CoreException("Invalid field specification $sField: $sAttCode is not a valid attribute for $sClass"); + } + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + if ($oAttDef instanceof AttributeSubItem) + { + $aAliasToFields[$sClassAlias][] = $oAttDef->GetParentAttCode(); + } + else if($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) + { + $aAliasToFields[$sClassAlias][] = $sKeyAttCode; + } + } + + // Read query parameters + // + $aArgs = array(); + foreach($oFilter->GetQueryParams() as $sParam => $foo) + { + $value = utils::ReadParam('arg_'.$sParam, null, true, 'raw_data'); + if (!is_null($value)) + { + $aArgs[$sParam] = $value; + } + } + $oFilter->SetInternalParams($aArgs); + foreach ($oFilter->GetSelectedClasses() as $sAlias => $sClass) + { + if ((UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_READ) && UR_ALLOWED_YES) == 0) + { + throw new Exception("The current user does not have permission for exporting data of class $sClass"); + } + } + + if ($oFilter) + { + $oSet = new CMDBObjectSet($oFilter, array(), $aArgs); + $oSet->OptimizeColumnLoad($aAliasToFields); + switch($sFormat) + { + case 'html': + $oP = new NiceWebPage("iTop - Export"); + $oP->add_style('body { overflow: auto; }'); // Show scroll bars if needed + + // Integration within MS-Excel web queries + HTTPS + IIS: + // MS-IIS set these header values with no-cache... while Excel fails to do the job if using HTTPS + // Then the fix is to force the reset of header values Pragma and Cache-control + header("Pragma:", true); + header("Cache-control:", true); + + // The HTML output is made for pages located in the /pages/ folder + // since this page is in a different folder, let's adjust the HTML 'base' attribute + // to make the relative hyperlinks in the page work + $sUrl = utils::GetAbsoluteUrlAppRoot(); + $oP->set_base($sUrl.'pages/'); + + if(count($aFields) > 0) + { + $iSearch = array_search('id', $aFields); + if ($iSearch !== false) + { + $bViewLink = true; + unset($aFields[$iSearch]); + } + else + { + $bViewLink = false; + } + $sFields = implode(',', $aFields); + $aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'display_limit' => false, 'localize_values' => $bLocalize, 'zlist' => false, 'extra_fields' => $sFields, 'view_link' => $bViewLink); + } + else + { + $aExtraParams = array('menu' => false, 'toolkit_menu' => false, 'display_limit' => false, 'localize_values' => $bLocalize, 'zlist' => 'details'); + } + + $oResultBlock = new DisplayBlock($oFilter, 'list', false, $aExtraParams); + $oResultBlock->Display($oP, 'expresult'); + break; + + case 'csv': + $oP = new CSVPage("iTop - Export"); + $sFields = implode(',', $aFields); + $sCharset = utils::ReadParam('charset', MetaModel::GetConfig()->Get('csv_file_default_charset'), true /* Allow CLI */, 'raw_data'); + $sCSVData = cmdbAbstractObject::GetSetAsCSV($oSet, array('fields' => $sFields, 'fields_advanced' => $bFieldsAdvanced, 'localize_values' => $bLocalize), $sCharset); + if ($sCharset == 'UTF-8') + { + $sOutputData = UTF8_BOM.$sCSVData; + } + else + { + $sOutputData = $sCSVData; + } + if ($sFileName == '') + { + // Plain text => Firefox will NOT propose to download the file + $oP->add_header("Content-type: text/plain; charset=$sCharset"); + } + else + { + $oP->add_header("Content-type: text/csv; charset=$sCharset"); + } + $oP->add($sOutputData); + break; + + case 'spreadsheet': + $oP = new WebPage("iTop - Export for spreadsheet"); + + // Integration within MS-Excel web queries + HTTPS + IIS: + // MS-IIS set these header values with no-cache... while Excel fails to do the job if using HTTPS + // Then the fix is to force the reset of header values Pragma and Cache-control + header("Pragma:", true); + header("Cache-control:", true); + + $sFields = implode(',', $aFields); + $oP->add_style('table br {mso-data-placement:same-cell;}'); // Trick for Excel: keep line breaks inside the same cell ! + cmdbAbstractObject::DisplaySetAsHTMLSpreadsheet($oP, $oSet, array('fields' => $sFields, 'fields_advanced' => $bFieldsAdvanced, 'localize_values' => $bLocalize)); + break; + + case 'xml': + $oP = new XMLPage("iTop - Export", true /* passthrough */); + cmdbAbstractObject::DisplaySetAsXML($oP, $oSet, array('localize_values' => $bLocalize)); + break; + + case 'xlsx': + $oP = new ajax_page(''); + $oExporter = new ExcelExporter(); + $oExporter->SetObjectList($oFilter); + + // Run the export by chunk of 1000 objects to limit memory usage + $oExporter->SetChunkSize(1000); + do + { + $aStatus = $oExporter->Run(); // process one chunk + } + while( ($aStatus['code'] != 'done') && ($aStatus['code'] != 'error')); + + if ($aStatus['code'] == 'done') + { + $oP->SetContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + $oP->SetContentDisposition('attachment', $oFilter->GetClass().'.xlsx'); + $oP->add(file_get_contents($oExporter->GetExcelFilePath())); + $oExporter->Cleanup(); + } + else + { + $oP->add('Error, xlsx export failed: '.$aStatus['message']); + } + break; + + default: + $oP = new WebPage("iTop - Export"); + $oP->add("Unsupported format '$sFormat'. Possible values are: html, csv, spreadsheet or xml."); + } + } + } + catch(Exception $e) + { + $oP = new WebPage("iTop - Export"); + $oP->p("Error the query can not be executed."); + if ($e instanceof CoreException) + { + $oP->p($e->GetHtmlDesc()); + } + else + { + $oP->p($e->getMessage()); + } + } +} +if (!$oP) +{ + // Display a short message about how to use this page + $bModeCLI = utils::IsModeCLI(); + if ($bModeCLI) + { + $oP = new CLIPage("iTop - Export"); + } + else + { + $oP = new WebPage("iTop - Export"); + } + $oP->p("General purpose export page."); + $oP->p("Parameters:"); + $oP->p(" * expression: an OQL expression (URL encoded if needed)"); + $oP->p(" * query: (alternative to 'expression') the id of an entry from the query phrasebook"); + if (Utils::IsModeCLI()) + { + $oP->p(" * with_archive: (optional, defaults to 0) if set to 1 then the result set will include archived objects"); + } + else + { + $oP->p(" * with_archive: (optional, defaults to the current mode) if set to 1 then the result set will include archived objects"); + } + $oP->p(" * arg_xxx: (needed if the query has parameters) the value of the parameter 'xxx'"); + $oP->p(" * format: (optional, default is html) the desired output format. Can be one of 'html', 'spreadsheet', 'csv', 'xlsx' or 'xml'"); + $oP->p(" * fields: (optional, no effect on XML format) list of fields (attribute codes, or alias.attcode) separated by a coma"); + $oP->p(" * fields_advanced: (optional, no effect on XML/HTML formats ; ignored is fields is specified) If set to 1, the default list of fields will include the external keys and their reconciliation keys"); + $oP->p(" * filename: (optional, no effect in CLI mode) if set then the results will be downloaded as a file"); +} + +if ($sFileName != '') +{ + $oP->add_header('Content-Disposition: attachment; filename="'.$sFileName.'"'); +} + +$oP->TrashUnexpectedOutput(); +$oP->output(); +?> diff --git a/webservices/import.php b/webservices/import.php index 10d70f721..e67a7a95f 100644 --- a/webservices/import.php +++ b/webservices/import.php @@ -1,909 +1,909 @@ - - - -/** - * Import web service - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -// -// Known limitations -// - reconciliation is made on the first column -// -// Known issues -// - ALMOST impossible to troubleshoot when an externl key has a wrong value -// - no character escaping in the xml output (yes !?!?!) -// - not outputing xml when a wrong input is given (class, attribute names) -// - -if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); -require_once(__DIR__.'/../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); -require_once(APPROOT.'/application/webpage.class.inc.php'); -require_once(APPROOT.'/application/csvpage.class.inc.php'); -require_once(APPROOT.'/application/clipage.class.inc.php'); - -require_once(APPROOT.'/application/startup.inc.php'); - -class BulkLoadException extends Exception -{ -} - -$aPageParams = array -( - 'auth_user' => array - ( - 'mandatory' => true, - 'modes' => 'cli', - 'default' => null, - 'description' => 'login (must have enough rights to create objects of the given class)', - ), - 'auth_pwd' => array - ( - 'mandatory' => true, - 'modes' => 'cli', - 'default' => null, - 'description' => 'password', - ), - 'class' => array - ( - 'mandatory' => true, - 'modes' => 'http,cli', - 'default' => null, - 'description' => 'class of loaded objects', - ), - 'csvdata' => array - ( - 'mandatory' => true, - 'modes' => 'http', - 'default' => null, - 'description' => 'data', - ), - 'csvfile' => array - ( - 'mandatory' => true, - 'modes' => 'cli', - 'default' => '', - 'description' => 'local data file, replaces csvdata if specified', - ), - 'charset' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '', - 'description' => 'Character set encoding of the CSV data: UTF-8, ISO-8859-1, WINDOWS-1251, WINDOWS-1252, ISO-8859-15, If blank, then the charset is set to config(csv_file_default_charset)', - ), - 'date_format' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '', - 'description' => 'Input date format (used both for dates and datetimes) - Examples: Y-m-d H:i:s, d/m/Y H:i:s (Europe) - no transformation is applied if the argument is omitted. (note: old format specification using %Y %m %d is also supported for backward compatibility)', - ), - 'separator' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => ',', - 'description' => 'column separator in CSV data (1 char, or \'tab\')', - ), - 'qualifier' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '"', - 'description' => 'test qualifier in CSV data', - ), - 'output' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => 'summary', - 'description' => '[retcode] to return the count of lines in error, [summary] to return a concise report, [details] to get a detailed report (each line listed)', - ), -/* - 'reportlevel' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => 'errors|warnings|created|changed|unchanged', - 'description' => 'combination of flags to limit the detailed output', - ), -*/ - 'reconciliationkeys' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '', - 'description' => 'name of the columns used to identify existing objects and update them, or create a new one', - ), - 'simulate' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '0', - 'description' => 'If set to 1, then the load will not be executed, but the expected report will be produced', - ), - 'comment' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '', - 'description' => 'Comment to be added into the change log', - ), - 'no_localize' => array - ( - 'mandatory' => false, - 'modes' => 'http,cli', - 'default' => '0', - 'description' => 'If set to 0, then header and values are supposed to be localized in the language of the logged in user. Set to 1 to use internal attribute codes and values (enums)', - ), -); - -function UsageAndExit($oP) -{ - global $aPageParams; - $bModeCLI = utils::IsModeCLI(); - - $oP->p("USAGE:\n"); - foreach($aPageParams as $sParam => $aParamData) - { - $aModes = explode(',', $aParamData['modes']); - if ($bModeCLI) - { - if (in_array('cli', $aModes)) - { - $sDesc = $aParamData['description'].', '.($aParamData['mandatory'] ? 'mandatory' : 'optional, defaults to ['.$aParamData['default'].']'); - $oP->p("$sParam = $sDesc"); - } - } - else - { - if (in_array('http', $aModes)) - { - $sDesc = $aParamData['description'].', '.($aParamData['mandatory'] ? 'mandatory' : 'optional, defaults to ['.$aParamData['default'].']'); - $oP->p("$sParam = $sDesc"); - } - } - } - $oP->output(); - exit; -} - - -function ReadParam($oP, $sParam, $sSanitizationFilter = 'parameter') -{ - global $aPageParams; - assert(isset($aPageParams[$sParam])); - assert(!$aPageParams[$sParam]['mandatory']); - $sValue = utils::ReadParam($sParam, $aPageParams[$sParam]['default'], true /* Allow CLI */, $sSanitizationFilter); - return trim($sValue); -} - -function ReadMandatoryParam($oP, $sParam, $sSanitizationFilter) -{ - global $aPageParams; - assert(isset($aPageParams[$sParam])); - assert($aPageParams[$sParam]['mandatory']); - - $sValue = utils::ReadParam($sParam, null, true /* Allow CLI */, $sSanitizationFilter); - if (is_null($sValue)) - { - $oP->p("ERROR: Missing argument '$sParam'\n"); - UsageAndExit($oP); - } - return trim($sValue); -} - -///////////////////////////////// -// Main program - -if (utils::IsModeCLI()) -{ - $oP = new CLIPage("iTop - Bulk import"); -} -else -{ - $oP = new CSVPage("iTop - Bulk import"); -} - -try -{ - utils::UseParamFile(); -} -catch(Exception $e) -{ - $oP->p("Error: ".$e->GetMessage()); - $oP->output(); - exit -2; -} - -if (utils::IsModeCLI()) -{ - // Next steps: - // specific arguments: 'csvfile' - // - $sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data'); - $sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data'); - $sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data'); - if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) - { - UserRights::Login($sAuthUser); // Login & set the user's language - } - else - { - $oP->p("Access restricted or wrong credentials ('$sAuthUser')"); - $oP->output(); - exit -1; - } - - if (!is_readable($sCsvFile)) - { - $oP->p("Input file could not be found or could not be read: '$sCsvFile'"); - $oP->output(); - exit -1; - } - $sCSVData = file_get_contents($sCsvFile); - -} -else -{ - $_SESSION['login_mode'] = 'basic'; - require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(); // Check user rights and prompt if needed - - $sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data'); -} - - -try -{ - $aWarnings = array(); - - ////////////////////////////////////////////////// - // - // Read parameters - // - $sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves - $sSep = ReadParam($oP, 'separator', 'raw_data'); - $sQualifier = ReadParam($oP, 'qualifier', 'raw_data'); - $sCharSet = ReadParam($oP, 'charset', 'raw_data'); - $sDateFormat = ReadParam($oP, 'date_format', 'raw_data'); - if (strpos($sDateFormat, '%') !== false) - { - $sDateFormat = utils::DateTimeFormatToPHP($sDateFormat); - } - $sOutput = ReadParam($oP, 'output', 'string'); - $sReconcKeys = ReadParam($oP, 'reconciliationkeys', 'raw_data'); - $sSimulate = ReadParam($oP, 'simulate'); - $sComment = ReadParam($oP, 'comment', 'raw_data'); - $bLocalize = (ReadParam($oP, 'no_localize') != 1); - - if (strtolower(trim($sSep)) == 'tab') - { - $sSep = "\t"; - } - - ////////////////////////////////////////////////// - // - // Check parameters format/consistency - // - if (strlen($sCSVData) == 0) - { - throw new BulkLoadException("Missing data - at least one line is expected"); - } - - if (!MetaModel::IsValidClass($sClass)) - { - throw new BulkLoadException("Unknown class: '$sClass'"); - } - - if (strlen($sSep) > 1) - { - throw new BulkLoadException("Separator is limited to one character, found '$sSep'"); - } - - if (strlen($sQualifier) > 1) - { - throw new BulkLoadException("Text qualifier is limited to one character, found '$sQualifier'"); - } - - if (!in_array($sOutput, array('retcode', 'summary', 'details'))) - { - throw new BulkLoadException("Unknown output format: '$sOutput'"); - } - - if (strlen($sDateFormat) == 0) - { - $sDateFormat = null; - } - - if ($sCharSet == '') - { - $sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset'); - } - - if ($sSimulate == '1') - { - $bSimulate = true; - } - else - { - $bSimulate = false; - } - - if (($sOutput == "summary") || ($sOutput == 'details')) - { - $oP->add_comment("Output format: ".$sOutput); - $oP->add_comment("Class: ".$sClass); - $oP->add_comment("Separator: ".$sSep); - $oP->add_comment("Qualifier: ".$sQualifier); - $oP->add_comment("Charset Encoding:".$sCharSet); - if (($sDateFormat !== null) && (strlen($sDateFormat) > 0)) - { - $oP->add_comment("Date and time format: '$sDateFormat'"); - $oDateTimeFormat = new DateTimeFormat($sDateFormat); - $sDateOnlyFormat = $oDateTimeFormat->ToDateFormat(); - $oP->add_comment("Date format: '$sDateOnlyFormat'"); - } - else - { - $oP->add_comment("Date format: "); - } - $oP->add_comment("Localize: ".($bLocalize?'yes':'no')); - $oP->add_comment("Data Size: ".strlen($sCSVData)); - } - ////////////////////////////////////////////////// - // - // Security - // - if (!UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY)) - { - throw new SecurityException(Dict::Format('UI:Error:BulkModifyNotAllowedOn_Class', $sClass)); - } - - ////////////////////////////////////////////////// - // - // Create an index of the known column names (in lower case) - // If data is localized, an array of => array of (several leads to ambiguity) - // Otherwise an array of => array of (1 element by construction) - // - // Examples (localized in french): - // 'lieu' => 'location_id' - // 'lieu->name' => 'location_id->name' - // - // Note: it may happen that an external field has the same label as the external key - // in that case, we consider that the external key has precedence - // - $aKnownColumnNames = array(); - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($bLocalize) - { - $sColName = strtolower(MetaModel::GetLabel($sClass, $sAttCode)); - } - else - { - $sColName = strtolower($sAttCode); - } - if (!$oAttDef->IsExternalField() || !array_key_exists($sColName, $aKnownColumnNames)) - { - $aKnownColumnNames[$sColName][] = $sAttCode; - } - if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE)) - { - $sRemoteClass = $oAttDef->GetTargetClass(); - foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) - { - $sAttCodeEx = $sAttCode.'->'.$sRemoteAttCode; - if ($bLocalize) - { - $sColName = strtolower(MetaModel::GetLabel($sClass, $sAttCodeEx)); - } - else - { - $sColName = strtolower($sAttCodeEx); - } - if (!array_key_exists($sColName, $aKnownColumnNames)) - { - $aKnownColumnNames[$sColName][] = $sAttCodeEx; - } - } - } - } - - //print_r($aKnownColumnNames); - //print_r(array_keys($aKnownColumnNames)); - //exit; - - ////////////////////////////////////////////////// - // - // Parse first line, check attributes, analyse the request - // - if ($sCharSet == 'UTF-8') - { - // Remove the BOM if any - if (substr($sCSVData, 0, 3) == UTF8_BOM) - { - $sCSVData = substr($sCSVData, 3); - } - // Clean the input - // Todo: warn the user if some characters are lost/substituted - $sUTF8Data = iconv('UTF-8', 'UTF-8//IGNORE//TRANSLIT', $sCSVData); - } - else - { - $sUTF8Data = iconv($sCharSet, 'UTF-8//IGNORE//TRANSLIT', $sCSVData); - } - $oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier); - - // Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute - $aRawFieldList = $oCSVParser->ListFields(); - $iColCount = count($aRawFieldList); - - // Translate into internal names - $aFieldList = array(); - foreach($aRawFieldList as $iFieldId => $sFieldName) - { - $sFieldName = trim($sFieldName); - $aMatches = array(); - if (preg_match('/^(.+)\*$/', $sFieldName, $aMatches)) - { - // Ignore any trailing "star" (*) that simply indicates a mandatory field - $sFieldName = $aMatches[1]; - } - else if (preg_match('/^(.+)\*->(.+)$/', $sFieldName, $aMatches)) - { - // Remove any trailing "star" character before the arrow (->) - // A star character at the end can be used to indicate a mandatory field - $sFieldName = $aMatches[1].'->'.$aMatches[2]; - } - if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames)) - { - $aColumns = $aKnownColumnNames[strtolower($sFieldName)]; - if (count($aColumns) > 1) - { - $aCompetitors = array(); - foreach ($aColumns as $sAttCodeEx) - { - $aCompetitors[] = $sAttCodeEx; - } - $aWarnings[] = "Input column '$sFieldName' is ambiguous. Could be related to ".implode (' or ', $aCompetitors).". The first one will be used: ".$aColumns[0]; - } - $aFieldList[$iFieldId] = $aColumns[0]; - } - else - { - // Protect against XSS injection - $sSafeName = str_replace(array('"', '<', '>'), '', $sFieldName); - throw new BulkLoadException("Unknown column: '$sSafeName'. Possible columns: ".implode(', ', array_keys($aKnownColumnNames))); - } - } - // Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->') - - $aAttList = array(); - $aExtKeys = array(); - foreach($aFieldList as $iFieldId => $sFieldName) - { - $aMatches = array(); - if (preg_match('/^(.+)->(.+)$/', trim($sFieldName), $aMatches)) - { - // The column has been specified as "extkey->attcode" - // - $sExtKeyAttCode = $aMatches[1]; - $sRemoteAttCode = $aMatches[2]; - if (!MetaModel::IsValidAttCode($sClass, $sExtKeyAttCode)) - { - // Safety net - should not happen now that column names are checked against known names - throw new BulkLoadException("Unknown attribute '$sExtKeyAttCode' (class: '$sClass')"); - } - $oAtt = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode); - if (!$oAtt->IsExternalKey()) - { - // Safety net - should not happen now that column names are checked against known names - throw new BulkLoadException("Not an external key '$sExtKeyAttCode' (class: '$sClass')"); - } - $sTargetClass = $oAtt->GetTargetClass(); - if (!MetaModel::IsValidAttCode($sTargetClass, $sRemoteAttCode)) - { - // Safety net - should not happen now that column names are checked against known names - throw new BulkLoadException("Unknown attribute '$sRemoteAttCode' (key: '$sExtKeyAttCode', class: '$sTargetClass')"); - } - $aExtKeys[$sExtKeyAttCode][$sRemoteAttCode] = $iFieldId; - } - elseif ($sFieldName == 'id') - { - $aAttList[$sFieldName] = $iFieldId; - } - else - { - // The column has been specified as "attcode" - // - if (!MetaModel::IsValidAttCode($sClass, $sFieldName)) - { - // Safety net - should not happen now that column names are checked against known names - throw new BulkLoadException("Unknown attribute '$sFieldName' (class: '$sClass')"); - } - $oAtt = MetaModel::GetAttributeDef($sClass, $sFieldName); - if ($oAtt->IsExternalKey()) - { - $aExtKeys[$sFieldName]['id'] = $iFieldId; - $aAttList[$sFieldName] = $iFieldId; - } - elseif ($oAtt->IsExternalField()) - { - $sExtKeyAttCode = $oAtt->GetKeyAttCode(); - $sRemoteAttCode = $oAtt->GetExtAttCode(); - $aExtKeys[$sExtKeyAttCode][$sRemoteAttCode] = $iFieldId; - } - else - { - $aAttList[$sFieldName] = $iFieldId; - } - } - } - - // Make sure there are some reconciliation keys - // - if (empty($sReconcKeys)) - { - $aReconcSpec = array(); - // Base reconciliation scheme on the default one - // The reconciliation attributes not present in the data will be ignored - foreach(MetaModel::GetReconcKeys($sClass) as $sReconcKeyAttCode) - { - if (in_array($sReconcKeyAttCode, $aFieldList)) - { - if ($bLocalize) - { - $aReconcSpec[] = MetaModel::GetLabel($sClass, $sReconcKeyAttCode); - } - else - { - $aReconcSpec[] = $sReconcKeyAttCode; - } - } - } - if (count($aReconcSpec) == 0) - { - throw new BulkLoadException("No reconciliation scheme could be defined, please add a column corresponding to one defined reconciliation key (class: '$sClass', reconciliation:".implode(',', MetaModel::GetReconcKeys($sClass)).")"); - } - $sReconcKeys = implode(',', $aReconcSpec); - } - - // Interpret the list of reconciliation keys - // - $aFinalReconcilKeys = array(); - $aReconcilKeysReport = array(); - foreach (explode(',', $sReconcKeys) as $sReconcKey) - { - $sReconcKey = trim($sReconcKey); - if (empty($sReconcKey)) continue; // skip empty spec - - if (array_key_exists(strtolower($sReconcKey), $aKnownColumnNames)) - { - // Translate from a translated name to codes - $aColumns = $aKnownColumnNames[strtolower($sReconcKey)]; - if (count($aColumns) > 1) - { - $aCompetitors = array(); - foreach ($aColumns as $sAttCodeEx) - { - $aCompetitors[] = $sAttCodeEx; - } - $aWarnings[] = "Reconciliation key '$sReconcKey' is ambiguous. Could be related to ".implode (' or ', $aCompetitors).". The first one will be used: ".$aColumns[0]; - } - $sReconcKey = $aColumns[0]; - } - else - { - // Protect against XSS injection - $sSafeName = str_replace(array('"', '<', '>'), '', $sReconcKey); - throw new BulkLoadException("Unknown reconciliation key: '$sSafeName'"); - } - - // Check that the reconciliation key is either a given column, or an external key - if (!in_array($sReconcKey, $aFieldList)) - { - if (!array_key_exists($sReconcKey, $aExtKeys)) - { - // Protect against XSS injection - $sSafeName = str_replace(array('"', '<', '>'), '', $sReconcKey); - throw new BulkLoadException("Reconciliation key not found in the input columns: '$sSafeName'"); - } - } - - if (preg_match('/^(.+)->(.+)$/', trim($sReconcKey), $aMatches)) - { - // The column has been specified as "extkey->attcode" - // - $sExtKeyAttCode = $aMatches[1]; - $sRemoteAttCode = $aMatches[2]; - - $aFinalReconcilKeys[] = $sExtKeyAttCode; - $aReconcilKeysReport[$sExtKeyAttCode][] = $sRemoteAttCode; - } - else - { - if (!MetaModel::IsValidAttCode($sClass, $sReconcKey)) - { - // Safety net - should not happen now that column names are checked against known names - throw new BulkLoadException("Unknown reconciliation attribute '$sReconcKey' (class: '$sClass')"); - } - $oAtt = MetaModel::GetAttributeDef($sClass, $sReconcKey); - if ($oAtt->IsExternalKey()) - { - $aFinalReconcilKeys[] = $sReconcKey; - $aReconcilKeysReport[$sReconcKey][] = 'id'; - } - elseif ($oAtt->IsExternalField()) - { - $sReconcAttCode = $oAtt->GetKeyAttCode(); - $sReconcKeyReport = "$sReconcAttCode ($sReconcKey)"; - - $aFinalReconcilKeys[] = $sReconcAttCode; - $aReconcilKeysReport[$sReconcAttCode][] = $sReconcKeyReport; - } - else - { - $aFinalReconcilKeys[] = $sReconcKey; - $aReconcilKeysReport[$sReconcKey] = array(); - } - } - } - - ////////////////////////////////////////////////// - // - // Go for parsing and interpretation - // - - $aData = $oCSVParser->ToArray(); - $iLineCount = count($aData); - - if (($sOutput == "summary") || ($sOutput == 'details')) - { - $oP->add_comment("Data Lines: ".$iLineCount); - $oP->add_comment("Simulate: ".($bSimulate ? '1' : '0')); - $oP->add_comment("Columns: ".implode(', ', $aFieldList)); - - $aReconciliationReport = array(); - foreach($aReconcilKeysReport as $sKey => $aKeyDetails) - { - if (count($aKeyDetails) > 0) - { - $aReconciliationReport[] = $sKey.' ('.implode(',', $aKeyDetails).')'; - } - else - { - $aReconciliationReport[] = $sKey; - } - } - $oP->add_comment("Reconciliation Keys: ".implode(', ', $aReconciliationReport)); - - foreach ($aWarnings as $sWarning) - { - $oP->add_comment("Warning: ".$sWarning); - } - } - - $oBulk = new BulkChange( - $sClass, - $aData, - $aAttList, - $aExtKeys, - $aFinalReconcilKeys, - null, // synchro scope - null, // on delete - $sDateFormat, - $bLocalize - ); - - if ($bSimulate) - { - $oMyChange = null; - } - else - { - if (strlen($sComment) > 0) - { - $sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV) - '.$sComment; - } - else - { - $sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV)'; - } - CMDBObject::SetTrackInfo($sMoreInfo); - CMDBObject::SetTrackOrigin('csv-import.php'); - - $oMyChange = CMDBObject::GetCurrentChange(); - } - - $aRes = $oBulk->Process($oMyChange); - - ////////////////////////////////////////////////// - // - // Compute statistics - // - $iCountErrors = 0; - $iCountWarnings = 0; - $iCountCreations = 0; - $iCountUpdates = 0; - $iCountUnchanged = 0; - foreach($aRes as $iRow => $aRowData) - { - $bWritten = false; - - $oStatus = $aRowData["__STATUS__"]; - switch(get_class($oStatus)) - { - case 'RowStatus_NoChange': - $iCountUnchanged++; - break; - case 'RowStatus_Modify': - $iCountUpdates++; - $bWritten = true; - break; - case 'RowStatus_NewObj': - $iCountCreations++; - $bWritten = true; - break; - case 'RowStatus_Issue': - $iCountErrors++; - break; - } - - if ($bWritten) - { - // Something has been done, still there may be some issues to report - foreach($aRowData as $key => $value) - { - if (!is_object($value)) continue; - - switch (get_class($value)) - { - case 'CellStatus_Void': - case 'CellStatus_Modify': - break; - case 'CellStatus_Issue': - case 'CellStatus_SearchIssue': - case 'CellStatus_NullIssue': - case 'CellStatus_Ambiguous': - $iCountWarnings++; - break; - } - } - } - } - - ////////////////////////////////////////////////// - // - // Summary of settings and results - // - if ($sOutput == 'retcode') - { - $oP->add($iCountErrors); - } - - if (($sOutput == "summary") || ($sOutput == 'details')) - { - $oP->add_comment("Change tracking comment: ".$sComment); - $oP->add_comment("Issues: ".$iCountErrors); - $oP->add_comment("Warnings: ".$iCountWarnings); - $oP->add_comment("Created: ".$iCountCreations); - $oP->add_comment("Updated: ".$iCountUpdates); - $oP->add_comment("Unchanged: ".$iCountUnchanged); - } - - - if ($sOutput == 'details') - { - // Setup result presentation - // - $aDisplayConfig = array(); - $aDisplayConfig["__LINE__"] = array("label"=>"Line", "description"=>""); - $aDisplayConfig["__STATUS__"] = array("label"=>"Status", "description"=>""); - $aDisplayConfig["__OBJECT_CLASS__"] = array("label"=>"Object Class", "description"=>""); - $aDisplayConfig["__OBJECT_ID__"] = array("label"=>"Object Id", "description"=>""); - foreach($aExtKeys as $sExtKeyAttCode => $aRemoteAtt) - { - $sLabel = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode)->GetLabel(); - $aDisplayConfig["$sExtKeyAttCode"] = array("label"=>$sExtKeyAttCode, "description"=>$sLabel." - ext key"); - } - foreach($aFinalReconcilKeys as $iCol => $sAttCode) - { - // $sLabel = MetaModel::GetAttributeDef($sClass, $sAttCode)->GetLabel(); - // $aDisplayConfig["$iCol"] = array("label"=>"$sLabel", "description"=>""); - } - foreach ($aAttList as $sAttCode => $iCol) - { - if ($sAttCode == 'id') - { - $sLabel = Dict::S('UI:CSVImport:idField'); - - $aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel); - } - else - { - $sLabel = MetaModel::GetAttributeDef($sClass, $sAttCode)->GetLabel(); - $aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel); - } - } - - $aResultDisp = array(); // to be displayed - foreach($aRes as $iRow => $aRowData) - { - $aRowDisp = array(); - $aRowDisp["__LINE__"] = $iRow; - if (is_object($aRowData["__STATUS__"])) - { - $aRowDisp["__STATUS__"] = $aRowData["__STATUS__"]->GetDescription(); - } - else - { - $aRowDisp["__STATUS__"] = "*No status available*"; - } - if (isset($aRowData["finalclass"]) && isset($aRowData["id"])) - { - $aRowDisp["__OBJECT_CLASS__"] = $aRowData["finalclass"]; - $aRowDisp["__OBJECT_ID__"] = $aRowData["id"]->GetDisplayableValue(); - } - else - { - $aRowDisp["__OBJECT_CLASS__"] = "n/a"; - $aRowDisp["__OBJECT_ID__"] = "n/a"; - } - foreach($aRowData as $key => $value) - { - $sKey = (string) $key; - - if ($sKey == '__STATUS__') continue; - if ($sKey == 'finalclass') continue; - if ($sKey == 'id') continue; - - if (is_object($value)) - { - $aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription(); - } - else - { - $aRowDisp["$sKey"] = $value; - } - } - $aResultDisp[$iRow] = $aRowDisp; - } - $oP->table($aDisplayConfig, $aResultDisp); - } -} -catch(BulkLoadException $e) -{ - $oP->add_comment($e->getMessage()); -} -catch(SecurityException $e) -{ - $oP->add_comment($e->getMessage()); -} -catch(Exception $e) -{ - $oP->add_comment((string)$e); -} - -$oP->output(); -?> + + + +/** + * Import web service + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +// +// Known limitations +// - reconciliation is made on the first column +// +// Known issues +// - ALMOST impossible to troubleshoot when an externl key has a wrong value +// - no character escaping in the xml output (yes !?!?!) +// - not outputing xml when a wrong input is given (class, attribute names) +// + +if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); +require_once(__DIR__.'/../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); +require_once(APPROOT.'/application/webpage.class.inc.php'); +require_once(APPROOT.'/application/csvpage.class.inc.php'); +require_once(APPROOT.'/application/clipage.class.inc.php'); + +require_once(APPROOT.'/application/startup.inc.php'); + +class BulkLoadException extends Exception +{ +} + +$aPageParams = array +( + 'auth_user' => array + ( + 'mandatory' => true, + 'modes' => 'cli', + 'default' => null, + 'description' => 'login (must have enough rights to create objects of the given class)', + ), + 'auth_pwd' => array + ( + 'mandatory' => true, + 'modes' => 'cli', + 'default' => null, + 'description' => 'password', + ), + 'class' => array + ( + 'mandatory' => true, + 'modes' => 'http,cli', + 'default' => null, + 'description' => 'class of loaded objects', + ), + 'csvdata' => array + ( + 'mandatory' => true, + 'modes' => 'http', + 'default' => null, + 'description' => 'data', + ), + 'csvfile' => array + ( + 'mandatory' => true, + 'modes' => 'cli', + 'default' => '', + 'description' => 'local data file, replaces csvdata if specified', + ), + 'charset' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '', + 'description' => 'Character set encoding of the CSV data: UTF-8, ISO-8859-1, WINDOWS-1251, WINDOWS-1252, ISO-8859-15, If blank, then the charset is set to config(csv_file_default_charset)', + ), + 'date_format' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '', + 'description' => 'Input date format (used both for dates and datetimes) - Examples: Y-m-d H:i:s, d/m/Y H:i:s (Europe) - no transformation is applied if the argument is omitted. (note: old format specification using %Y %m %d is also supported for backward compatibility)', + ), + 'separator' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => ',', + 'description' => 'column separator in CSV data (1 char, or \'tab\')', + ), + 'qualifier' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '"', + 'description' => 'test qualifier in CSV data', + ), + 'output' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => 'summary', + 'description' => '[retcode] to return the count of lines in error, [summary] to return a concise report, [details] to get a detailed report (each line listed)', + ), +/* + 'reportlevel' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => 'errors|warnings|created|changed|unchanged', + 'description' => 'combination of flags to limit the detailed output', + ), +*/ + 'reconciliationkeys' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '', + 'description' => 'name of the columns used to identify existing objects and update them, or create a new one', + ), + 'simulate' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '0', + 'description' => 'If set to 1, then the load will not be executed, but the expected report will be produced', + ), + 'comment' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '', + 'description' => 'Comment to be added into the change log', + ), + 'no_localize' => array + ( + 'mandatory' => false, + 'modes' => 'http,cli', + 'default' => '0', + 'description' => 'If set to 0, then header and values are supposed to be localized in the language of the logged in user. Set to 1 to use internal attribute codes and values (enums)', + ), +); + +function UsageAndExit($oP) +{ + global $aPageParams; + $bModeCLI = utils::IsModeCLI(); + + $oP->p("USAGE:\n"); + foreach($aPageParams as $sParam => $aParamData) + { + $aModes = explode(',', $aParamData['modes']); + if ($bModeCLI) + { + if (in_array('cli', $aModes)) + { + $sDesc = $aParamData['description'].', '.($aParamData['mandatory'] ? 'mandatory' : 'optional, defaults to ['.$aParamData['default'].']'); + $oP->p("$sParam = $sDesc"); + } + } + else + { + if (in_array('http', $aModes)) + { + $sDesc = $aParamData['description'].', '.($aParamData['mandatory'] ? 'mandatory' : 'optional, defaults to ['.$aParamData['default'].']'); + $oP->p("$sParam = $sDesc"); + } + } + } + $oP->output(); + exit; +} + + +function ReadParam($oP, $sParam, $sSanitizationFilter = 'parameter') +{ + global $aPageParams; + assert(isset($aPageParams[$sParam])); + assert(!$aPageParams[$sParam]['mandatory']); + $sValue = utils::ReadParam($sParam, $aPageParams[$sParam]['default'], true /* Allow CLI */, $sSanitizationFilter); + return trim($sValue); +} + +function ReadMandatoryParam($oP, $sParam, $sSanitizationFilter) +{ + global $aPageParams; + assert(isset($aPageParams[$sParam])); + assert($aPageParams[$sParam]['mandatory']); + + $sValue = utils::ReadParam($sParam, null, true /* Allow CLI */, $sSanitizationFilter); + if (is_null($sValue)) + { + $oP->p("ERROR: Missing argument '$sParam'\n"); + UsageAndExit($oP); + } + return trim($sValue); +} + +///////////////////////////////// +// Main program + +if (utils::IsModeCLI()) +{ + $oP = new CLIPage("iTop - Bulk import"); +} +else +{ + $oP = new CSVPage("iTop - Bulk import"); +} + +try +{ + utils::UseParamFile(); +} +catch(Exception $e) +{ + $oP->p("Error: ".$e->GetMessage()); + $oP->output(); + exit -2; +} + +if (utils::IsModeCLI()) +{ + // Next steps: + // specific arguments: 'csvfile' + // + $sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data'); + $sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data'); + $sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data'); + if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) + { + UserRights::Login($sAuthUser); // Login & set the user's language + } + else + { + $oP->p("Access restricted or wrong credentials ('$sAuthUser')"); + $oP->output(); + exit -1; + } + + if (!is_readable($sCsvFile)) + { + $oP->p("Input file could not be found or could not be read: '$sCsvFile'"); + $oP->output(); + exit -1; + } + $sCSVData = file_get_contents($sCsvFile); + +} +else +{ + $_SESSION['login_mode'] = 'basic'; + require_once(APPROOT.'/application/loginwebpage.class.inc.php'); + LoginWebPage::DoLogin(); // Check user rights and prompt if needed + + $sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data'); +} + + +try +{ + $aWarnings = array(); + + ////////////////////////////////////////////////// + // + // Read parameters + // + $sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves + $sSep = ReadParam($oP, 'separator', 'raw_data'); + $sQualifier = ReadParam($oP, 'qualifier', 'raw_data'); + $sCharSet = ReadParam($oP, 'charset', 'raw_data'); + $sDateFormat = ReadParam($oP, 'date_format', 'raw_data'); + if (strpos($sDateFormat, '%') !== false) + { + $sDateFormat = utils::DateTimeFormatToPHP($sDateFormat); + } + $sOutput = ReadParam($oP, 'output', 'string'); + $sReconcKeys = ReadParam($oP, 'reconciliationkeys', 'raw_data'); + $sSimulate = ReadParam($oP, 'simulate'); + $sComment = ReadParam($oP, 'comment', 'raw_data'); + $bLocalize = (ReadParam($oP, 'no_localize') != 1); + + if (strtolower(trim($sSep)) == 'tab') + { + $sSep = "\t"; + } + + ////////////////////////////////////////////////// + // + // Check parameters format/consistency + // + if (strlen($sCSVData) == 0) + { + throw new BulkLoadException("Missing data - at least one line is expected"); + } + + if (!MetaModel::IsValidClass($sClass)) + { + throw new BulkLoadException("Unknown class: '$sClass'"); + } + + if (strlen($sSep) > 1) + { + throw new BulkLoadException("Separator is limited to one character, found '$sSep'"); + } + + if (strlen($sQualifier) > 1) + { + throw new BulkLoadException("Text qualifier is limited to one character, found '$sQualifier'"); + } + + if (!in_array($sOutput, array('retcode', 'summary', 'details'))) + { + throw new BulkLoadException("Unknown output format: '$sOutput'"); + } + + if (strlen($sDateFormat) == 0) + { + $sDateFormat = null; + } + + if ($sCharSet == '') + { + $sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset'); + } + + if ($sSimulate == '1') + { + $bSimulate = true; + } + else + { + $bSimulate = false; + } + + if (($sOutput == "summary") || ($sOutput == 'details')) + { + $oP->add_comment("Output format: ".$sOutput); + $oP->add_comment("Class: ".$sClass); + $oP->add_comment("Separator: ".$sSep); + $oP->add_comment("Qualifier: ".$sQualifier); + $oP->add_comment("Charset Encoding:".$sCharSet); + if (($sDateFormat !== null) && (strlen($sDateFormat) > 0)) + { + $oP->add_comment("Date and time format: '$sDateFormat'"); + $oDateTimeFormat = new DateTimeFormat($sDateFormat); + $sDateOnlyFormat = $oDateTimeFormat->ToDateFormat(); + $oP->add_comment("Date format: '$sDateOnlyFormat'"); + } + else + { + $oP->add_comment("Date format: "); + } + $oP->add_comment("Localize: ".($bLocalize?'yes':'no')); + $oP->add_comment("Data Size: ".strlen($sCSVData)); + } + ////////////////////////////////////////////////// + // + // Security + // + if (!UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY)) + { + throw new SecurityException(Dict::Format('UI:Error:BulkModifyNotAllowedOn_Class', $sClass)); + } + + ////////////////////////////////////////////////// + // + // Create an index of the known column names (in lower case) + // If data is localized, an array of => array of (several leads to ambiguity) + // Otherwise an array of => array of (1 element by construction) + // + // Examples (localized in french): + // 'lieu' => 'location_id' + // 'lieu->name' => 'location_id->name' + // + // Note: it may happen that an external field has the same label as the external key + // in that case, we consider that the external key has precedence + // + $aKnownColumnNames = array(); + foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + { + if ($bLocalize) + { + $sColName = strtolower(MetaModel::GetLabel($sClass, $sAttCode)); + } + else + { + $sColName = strtolower($sAttCode); + } + if (!$oAttDef->IsExternalField() || !array_key_exists($sColName, $aKnownColumnNames)) + { + $aKnownColumnNames[$sColName][] = $sAttCode; + } + if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE)) + { + $sRemoteClass = $oAttDef->GetTargetClass(); + foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) + { + $sAttCodeEx = $sAttCode.'->'.$sRemoteAttCode; + if ($bLocalize) + { + $sColName = strtolower(MetaModel::GetLabel($sClass, $sAttCodeEx)); + } + else + { + $sColName = strtolower($sAttCodeEx); + } + if (!array_key_exists($sColName, $aKnownColumnNames)) + { + $aKnownColumnNames[$sColName][] = $sAttCodeEx; + } + } + } + } + + //print_r($aKnownColumnNames); + //print_r(array_keys($aKnownColumnNames)); + //exit; + + ////////////////////////////////////////////////// + // + // Parse first line, check attributes, analyse the request + // + if ($sCharSet == 'UTF-8') + { + // Remove the BOM if any + if (substr($sCSVData, 0, 3) == UTF8_BOM) + { + $sCSVData = substr($sCSVData, 3); + } + // Clean the input + // Todo: warn the user if some characters are lost/substituted + $sUTF8Data = iconv('UTF-8', 'UTF-8//IGNORE//TRANSLIT', $sCSVData); + } + else + { + $sUTF8Data = iconv($sCharSet, 'UTF-8//IGNORE//TRANSLIT', $sCSVData); + } + $oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier); + + // Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute + $aRawFieldList = $oCSVParser->ListFields(); + $iColCount = count($aRawFieldList); + + // Translate into internal names + $aFieldList = array(); + foreach($aRawFieldList as $iFieldId => $sFieldName) + { + $sFieldName = trim($sFieldName); + $aMatches = array(); + if (preg_match('/^(.+)\*$/', $sFieldName, $aMatches)) + { + // Ignore any trailing "star" (*) that simply indicates a mandatory field + $sFieldName = $aMatches[1]; + } + else if (preg_match('/^(.+)\*->(.+)$/', $sFieldName, $aMatches)) + { + // Remove any trailing "star" character before the arrow (->) + // A star character at the end can be used to indicate a mandatory field + $sFieldName = $aMatches[1].'->'.$aMatches[2]; + } + if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames)) + { + $aColumns = $aKnownColumnNames[strtolower($sFieldName)]; + if (count($aColumns) > 1) + { + $aCompetitors = array(); + foreach ($aColumns as $sAttCodeEx) + { + $aCompetitors[] = $sAttCodeEx; + } + $aWarnings[] = "Input column '$sFieldName' is ambiguous. Could be related to ".implode (' or ', $aCompetitors).". The first one will be used: ".$aColumns[0]; + } + $aFieldList[$iFieldId] = $aColumns[0]; + } + else + { + // Protect against XSS injection + $sSafeName = str_replace(array('"', '<', '>'), '', $sFieldName); + throw new BulkLoadException("Unknown column: '$sSafeName'. Possible columns: ".implode(', ', array_keys($aKnownColumnNames))); + } + } + // Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->') + + $aAttList = array(); + $aExtKeys = array(); + foreach($aFieldList as $iFieldId => $sFieldName) + { + $aMatches = array(); + if (preg_match('/^(.+)->(.+)$/', trim($sFieldName), $aMatches)) + { + // The column has been specified as "extkey->attcode" + // + $sExtKeyAttCode = $aMatches[1]; + $sRemoteAttCode = $aMatches[2]; + if (!MetaModel::IsValidAttCode($sClass, $sExtKeyAttCode)) + { + // Safety net - should not happen now that column names are checked against known names + throw new BulkLoadException("Unknown attribute '$sExtKeyAttCode' (class: '$sClass')"); + } + $oAtt = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode); + if (!$oAtt->IsExternalKey()) + { + // Safety net - should not happen now that column names are checked against known names + throw new BulkLoadException("Not an external key '$sExtKeyAttCode' (class: '$sClass')"); + } + $sTargetClass = $oAtt->GetTargetClass(); + if (!MetaModel::IsValidAttCode($sTargetClass, $sRemoteAttCode)) + { + // Safety net - should not happen now that column names are checked against known names + throw new BulkLoadException("Unknown attribute '$sRemoteAttCode' (key: '$sExtKeyAttCode', class: '$sTargetClass')"); + } + $aExtKeys[$sExtKeyAttCode][$sRemoteAttCode] = $iFieldId; + } + elseif ($sFieldName == 'id') + { + $aAttList[$sFieldName] = $iFieldId; + } + else + { + // The column has been specified as "attcode" + // + if (!MetaModel::IsValidAttCode($sClass, $sFieldName)) + { + // Safety net - should not happen now that column names are checked against known names + throw new BulkLoadException("Unknown attribute '$sFieldName' (class: '$sClass')"); + } + $oAtt = MetaModel::GetAttributeDef($sClass, $sFieldName); + if ($oAtt->IsExternalKey()) + { + $aExtKeys[$sFieldName]['id'] = $iFieldId; + $aAttList[$sFieldName] = $iFieldId; + } + elseif ($oAtt->IsExternalField()) + { + $sExtKeyAttCode = $oAtt->GetKeyAttCode(); + $sRemoteAttCode = $oAtt->GetExtAttCode(); + $aExtKeys[$sExtKeyAttCode][$sRemoteAttCode] = $iFieldId; + } + else + { + $aAttList[$sFieldName] = $iFieldId; + } + } + } + + // Make sure there are some reconciliation keys + // + if (empty($sReconcKeys)) + { + $aReconcSpec = array(); + // Base reconciliation scheme on the default one + // The reconciliation attributes not present in the data will be ignored + foreach(MetaModel::GetReconcKeys($sClass) as $sReconcKeyAttCode) + { + if (in_array($sReconcKeyAttCode, $aFieldList)) + { + if ($bLocalize) + { + $aReconcSpec[] = MetaModel::GetLabel($sClass, $sReconcKeyAttCode); + } + else + { + $aReconcSpec[] = $sReconcKeyAttCode; + } + } + } + if (count($aReconcSpec) == 0) + { + throw new BulkLoadException("No reconciliation scheme could be defined, please add a column corresponding to one defined reconciliation key (class: '$sClass', reconciliation:".implode(',', MetaModel::GetReconcKeys($sClass)).")"); + } + $sReconcKeys = implode(',', $aReconcSpec); + } + + // Interpret the list of reconciliation keys + // + $aFinalReconcilKeys = array(); + $aReconcilKeysReport = array(); + foreach (explode(',', $sReconcKeys) as $sReconcKey) + { + $sReconcKey = trim($sReconcKey); + if (empty($sReconcKey)) continue; // skip empty spec + + if (array_key_exists(strtolower($sReconcKey), $aKnownColumnNames)) + { + // Translate from a translated name to codes + $aColumns = $aKnownColumnNames[strtolower($sReconcKey)]; + if (count($aColumns) > 1) + { + $aCompetitors = array(); + foreach ($aColumns as $sAttCodeEx) + { + $aCompetitors[] = $sAttCodeEx; + } + $aWarnings[] = "Reconciliation key '$sReconcKey' is ambiguous. Could be related to ".implode (' or ', $aCompetitors).". The first one will be used: ".$aColumns[0]; + } + $sReconcKey = $aColumns[0]; + } + else + { + // Protect against XSS injection + $sSafeName = str_replace(array('"', '<', '>'), '', $sReconcKey); + throw new BulkLoadException("Unknown reconciliation key: '$sSafeName'"); + } + + // Check that the reconciliation key is either a given column, or an external key + if (!in_array($sReconcKey, $aFieldList)) + { + if (!array_key_exists($sReconcKey, $aExtKeys)) + { + // Protect against XSS injection + $sSafeName = str_replace(array('"', '<', '>'), '', $sReconcKey); + throw new BulkLoadException("Reconciliation key not found in the input columns: '$sSafeName'"); + } + } + + if (preg_match('/^(.+)->(.+)$/', trim($sReconcKey), $aMatches)) + { + // The column has been specified as "extkey->attcode" + // + $sExtKeyAttCode = $aMatches[1]; + $sRemoteAttCode = $aMatches[2]; + + $aFinalReconcilKeys[] = $sExtKeyAttCode; + $aReconcilKeysReport[$sExtKeyAttCode][] = $sRemoteAttCode; + } + else + { + if (!MetaModel::IsValidAttCode($sClass, $sReconcKey)) + { + // Safety net - should not happen now that column names are checked against known names + throw new BulkLoadException("Unknown reconciliation attribute '$sReconcKey' (class: '$sClass')"); + } + $oAtt = MetaModel::GetAttributeDef($sClass, $sReconcKey); + if ($oAtt->IsExternalKey()) + { + $aFinalReconcilKeys[] = $sReconcKey; + $aReconcilKeysReport[$sReconcKey][] = 'id'; + } + elseif ($oAtt->IsExternalField()) + { + $sReconcAttCode = $oAtt->GetKeyAttCode(); + $sReconcKeyReport = "$sReconcAttCode ($sReconcKey)"; + + $aFinalReconcilKeys[] = $sReconcAttCode; + $aReconcilKeysReport[$sReconcAttCode][] = $sReconcKeyReport; + } + else + { + $aFinalReconcilKeys[] = $sReconcKey; + $aReconcilKeysReport[$sReconcKey] = array(); + } + } + } + + ////////////////////////////////////////////////// + // + // Go for parsing and interpretation + // + + $aData = $oCSVParser->ToArray(); + $iLineCount = count($aData); + + if (($sOutput == "summary") || ($sOutput == 'details')) + { + $oP->add_comment("Data Lines: ".$iLineCount); + $oP->add_comment("Simulate: ".($bSimulate ? '1' : '0')); + $oP->add_comment("Columns: ".implode(', ', $aFieldList)); + + $aReconciliationReport = array(); + foreach($aReconcilKeysReport as $sKey => $aKeyDetails) + { + if (count($aKeyDetails) > 0) + { + $aReconciliationReport[] = $sKey.' ('.implode(',', $aKeyDetails).')'; + } + else + { + $aReconciliationReport[] = $sKey; + } + } + $oP->add_comment("Reconciliation Keys: ".implode(', ', $aReconciliationReport)); + + foreach ($aWarnings as $sWarning) + { + $oP->add_comment("Warning: ".$sWarning); + } + } + + $oBulk = new BulkChange( + $sClass, + $aData, + $aAttList, + $aExtKeys, + $aFinalReconcilKeys, + null, // synchro scope + null, // on delete + $sDateFormat, + $bLocalize + ); + + if ($bSimulate) + { + $oMyChange = null; + } + else + { + if (strlen($sComment) > 0) + { + $sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV) - '.$sComment; + } + else + { + $sMoreInfo = CMDBChange::GetCurrentUserName().', Web Service (CSV)'; + } + CMDBObject::SetTrackInfo($sMoreInfo); + CMDBObject::SetTrackOrigin('csv-import.php'); + + $oMyChange = CMDBObject::GetCurrentChange(); + } + + $aRes = $oBulk->Process($oMyChange); + + ////////////////////////////////////////////////// + // + // Compute statistics + // + $iCountErrors = 0; + $iCountWarnings = 0; + $iCountCreations = 0; + $iCountUpdates = 0; + $iCountUnchanged = 0; + foreach($aRes as $iRow => $aRowData) + { + $bWritten = false; + + $oStatus = $aRowData["__STATUS__"]; + switch(get_class($oStatus)) + { + case 'RowStatus_NoChange': + $iCountUnchanged++; + break; + case 'RowStatus_Modify': + $iCountUpdates++; + $bWritten = true; + break; + case 'RowStatus_NewObj': + $iCountCreations++; + $bWritten = true; + break; + case 'RowStatus_Issue': + $iCountErrors++; + break; + } + + if ($bWritten) + { + // Something has been done, still there may be some issues to report + foreach($aRowData as $key => $value) + { + if (!is_object($value)) continue; + + switch (get_class($value)) + { + case 'CellStatus_Void': + case 'CellStatus_Modify': + break; + case 'CellStatus_Issue': + case 'CellStatus_SearchIssue': + case 'CellStatus_NullIssue': + case 'CellStatus_Ambiguous': + $iCountWarnings++; + break; + } + } + } + } + + ////////////////////////////////////////////////// + // + // Summary of settings and results + // + if ($sOutput == 'retcode') + { + $oP->add($iCountErrors); + } + + if (($sOutput == "summary") || ($sOutput == 'details')) + { + $oP->add_comment("Change tracking comment: ".$sComment); + $oP->add_comment("Issues: ".$iCountErrors); + $oP->add_comment("Warnings: ".$iCountWarnings); + $oP->add_comment("Created: ".$iCountCreations); + $oP->add_comment("Updated: ".$iCountUpdates); + $oP->add_comment("Unchanged: ".$iCountUnchanged); + } + + + if ($sOutput == 'details') + { + // Setup result presentation + // + $aDisplayConfig = array(); + $aDisplayConfig["__LINE__"] = array("label"=>"Line", "description"=>""); + $aDisplayConfig["__STATUS__"] = array("label"=>"Status", "description"=>""); + $aDisplayConfig["__OBJECT_CLASS__"] = array("label"=>"Object Class", "description"=>""); + $aDisplayConfig["__OBJECT_ID__"] = array("label"=>"Object Id", "description"=>""); + foreach($aExtKeys as $sExtKeyAttCode => $aRemoteAtt) + { + $sLabel = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode)->GetLabel(); + $aDisplayConfig["$sExtKeyAttCode"] = array("label"=>$sExtKeyAttCode, "description"=>$sLabel." - ext key"); + } + foreach($aFinalReconcilKeys as $iCol => $sAttCode) + { + // $sLabel = MetaModel::GetAttributeDef($sClass, $sAttCode)->GetLabel(); + // $aDisplayConfig["$iCol"] = array("label"=>"$sLabel", "description"=>""); + } + foreach ($aAttList as $sAttCode => $iCol) + { + if ($sAttCode == 'id') + { + $sLabel = Dict::S('UI:CSVImport:idField'); + + $aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel); + } + else + { + $sLabel = MetaModel::GetAttributeDef($sClass, $sAttCode)->GetLabel(); + $aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel); + } + } + + $aResultDisp = array(); // to be displayed + foreach($aRes as $iRow => $aRowData) + { + $aRowDisp = array(); + $aRowDisp["__LINE__"] = $iRow; + if (is_object($aRowData["__STATUS__"])) + { + $aRowDisp["__STATUS__"] = $aRowData["__STATUS__"]->GetDescription(); + } + else + { + $aRowDisp["__STATUS__"] = "*No status available*"; + } + if (isset($aRowData["finalclass"]) && isset($aRowData["id"])) + { + $aRowDisp["__OBJECT_CLASS__"] = $aRowData["finalclass"]; + $aRowDisp["__OBJECT_ID__"] = $aRowData["id"]->GetDisplayableValue(); + } + else + { + $aRowDisp["__OBJECT_CLASS__"] = "n/a"; + $aRowDisp["__OBJECT_ID__"] = "n/a"; + } + foreach($aRowData as $key => $value) + { + $sKey = (string) $key; + + if ($sKey == '__STATUS__') continue; + if ($sKey == 'finalclass') continue; + if ($sKey == 'id') continue; + + if (is_object($value)) + { + $aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription(); + } + else + { + $aRowDisp["$sKey"] = $value; + } + } + $aResultDisp[$iRow] = $aRowDisp; + } + $oP->table($aDisplayConfig, $aResultDisp); + } +} +catch(BulkLoadException $e) +{ + $oP->add_comment($e->getMessage()); +} +catch(SecurityException $e) +{ + $oP->add_comment($e->getMessage()); +} +catch(Exception $e) +{ + $oP->add_comment((string)$e); +} + +$oP->output(); +?> diff --git a/webservices/itoprest.examples.php b/webservices/itoprest.examples.php index 4cda07991..280e4a839 100644 --- a/webservices/itoprest.examples.php +++ b/webservices/itoprest.examples.php @@ -1,320 +1,320 @@ - - - -/** - * Shows a usage of the SOAP queries - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * Helper to execute an HTTP POST request - * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl - * originaly named after do_post_request - */ -function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null) -{ - // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request. - - $sData = http_build_query($aData); - - $aParams = array('http' => array( - 'method' => 'POST', - 'content' => $sData, - 'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n", - )); - if ($sOptionnalHeaders !== null) - { - $aParams['http']['header'] .= $sOptionnalHeaders; - } - $ctx = stream_context_create($aParams); - - $fp = @fopen($sUrl, 'rb', false, $ctx); - if (!$fp) - { - global $php_errormsg; - if (isset($php_errormsg)) - { - throw new Exception("Problem with $sUrl, $php_errormsg"); - } - else - { - throw new Exception("Problem with $sUrl"); - } - } - $response = @stream_get_contents($fp); - if ($response === false) - { - throw new Exception("Problem reading data from $sUrl, $php_errormsg"); - } - return $response; -} - -// If the library curl is installed.... use this function -// -function DoPostRequest_curl($sUrl, $aData) -{ - $curl = curl_init($sUrl); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_POST, true); - curl_setopt($curl, CURLOPT_POSTFIELDS, $aData); - $response = curl_exec($curl); - curl_close($curl); - - return $response; -} - -//////////////////////////////////////////////////////////////////////////////// -// -// Main program -// -//////////////////////////////////////////////////////////////////////////////// - -// Define the operations to perform (one operation per call the rest service) -// - -$aOperations = array( - array( - 'operation' => 'list_operations', // operation code - ), - array( - 'operation' => 'core/create', // operation code - 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) - // Values for the object to create - 'fields' => array( - 'org_id' => "SELECT Organization WHERE name = 'Demo'", - 'caller_id' => array('name' => 'monet', 'first_name' => 'claude'), - 'title' => 'issue blah', - 'description' => 'something happened' - ), - ), - array( - 'operation' => 'core/update', // operation code - 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'key' => 'SELECT UserRequest WHERE id=1', - 'output_fields' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c) - // Values for the object to create - 'fields' => array( - 'title' => 'Issue #'.rand(0, 100), - 'contacts_list' => array( - array( - 'role' => 'fireman #'.rand(0, 100), - 'contact_id' => array('finalclass' => 'Person', 'name' => 'monet', 'first_name' => 'claude'), - ), - ), - ), - ), - // Rewrite the full CaseLog on an existing UserRequest with id=1, setting date and user (optional) - array( - 'operation' => 'core/update', - 'comment' => 'Synchronization from ServiceFirst', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'key' => 'SELECT UserRequest WHERE id=1', - 'output_fields' => 'id, friendlyname, title', - 'fields' => array( - 'public_log' => array( - 'items' => array( - 0 => array( - 'date' => '2001-02-01 23:59:59', //Allow to set the date of a true event, an alarm for eg. - 'user_login' => 'Alarm monitoring', //Free text - 'user_id' => 0, //0 is required for the user_login to be taken into account - 'message' => 'This is 1st entry as an HTML formatted
    text', - ), - 1 => array( - 'date' => '2001-02-02 00:00:00', //If ommitted set automatically. - 'user_login' => 'Alarm monitoring', //user=id=0 is missing so will be ignored - 'message' => 'Second entry in text format: -with new line, but format not specified, so treated as HTML!, user_id=0 missing, so user_login ignored', - ), - ), - ), - ), - ), - // Add a Text entry in the HTML CaseLog of the UserRequest with id=1, setting date and user (optional) - array( - 'operation' => 'core/update', // operation code - 'comment' => 'Synchronization from Alarm Monitoring', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'key' => 1, // object id or OQL - 'output_fields' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c) - // Example of adding an entry into a CaseLog on an existing UserRequest - 'fields' => array( - 'public_log' => array( - 'add_item' => array( - 'user_login' => 'New Entry', //Free text - 'user_id' => 0, //0 is required for the user_login to be taken into account - 'format' => 'text', //If ommitted, source is expected to be HTML - 'message' => 'This text is not HTML formatted with 3 lines: -new line -3rd and last line', - ), - ), - ), - ), - array( - 'operation' => 'core/get', // operation code - 'class' => 'UserRequest', - 'key' => 'SELECT UserRequest', - 'output_fields' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c) - ), - array( - 'operation' => 'core/delete', // operation code - 'comment' => 'Cleanup for synchro with...', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'key' => 'SELECT UserRequest WHERE org_id = 2', - 'simulate' => true, - ), - array( - 'operation' => 'core/apply_stimulus', // operation code - 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'key' => 1, - 'stimulus' => 'ev_assign', - // Values to set - 'fields' => array( - 'team_id' => 15, // Helpdesk - 'agent_id' => 9 // Jules Verne - ), - 'output_fields' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c) - ), - array( - 'operation' => 'core/get_related', // operation code - 'class' => 'Server', - 'key' => 'SELECT Server', - 'relation' => 'impacts', // relation code - 'depth' => 4, // max recursion depth - ), -); -$aOperations = array( - array( - 'operation' => 'core/create', // operation code - 'comment' => 'Automatic creation of attachment blah blah...', // comment recorded in the change tracking log - 'class' => 'Attachment', - 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) - // Values for the object to create - 'fields' => array( - 'item_class' => 'UserRequest', - 'item_id' => 1, - 'item_org_id' => 3, - 'contents' => array( - 'data' => 'iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACmSURBVChTfZHRDYMwDESzQ2fqhHx3C3ao+MkW/WlnaFxfzk7sEnE6JHJ+NgaKZN2zLHVN2ssfkae0Da7FQ5PRk/ve4Hcx19Ie6CEGuh/6vMgNhwanHVUNbt73lUDbYJ+6pg8b3+m2RehsVPdMXyvQY+OVkB+Rrv64lUjb3nq+aCA6v4leRqtfaIgimr53atBy9PlfUhoh3fFCNDmErv9FWR6ylBL5AREbmHBnFj5lAAAAAElFTkSuQmCC', - 'filename' => 'myself.png', - 'mimetype' => 'image/png' - ), - ), - ), - array( - 'operation' => 'core/get', // operation code - 'class' => 'Attachment', - 'key' => 'SELECT Attachment', - 'output_fields' => '*', - ) -); -$aOperations = array( - array( - 'operation' => 'core/update', // operation code - 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log - 'class' => 'Server', - 'key' => 'SELECT Server WHERE name="Server1"', - 'output_fields' => 'id, friendlyname, description', // list of fields to show in the results (* or a,b,c) - // Values for the object to create - 'fields' => array( - 'description' => 'Issue #'.time(), - ), - ), -); -$aOperations = array( - array( - 'operation' => 'core/create', // operation code - 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log - 'class' => 'UserRequest', - 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) - // Values for the object to create - 'fields' => array( - 'org_id' => "SELECT Organization WHERE name = 'Demo'", - 'caller_id' => array('name' => 'monet', 'first_name' => 'claude'), - 'title' => 'issue blah', - 'description' => 'something happened' - ), - ), -); -$aXXXOperations = array( - array( - 'operation' => 'core/check_credentials', // operation code - 'user' => 'admin', - 'password' => 'admin', - ), -); -$aOperations = array( - array( - 'operation' => 'core/delete', // operation code - 'comment' => 'Cleanup for synchro with...', // comment recorded in the change tracking log - 'class' => 'Server', - 'key' => 'SELECT Server', - 'simulate' => false, - ), -); - -if (false) -{ - echo "Please edit the sample script and configure the server URL"; - exit; -} -else -{ - $sUrl = "http://localhost/trunk/webservices/rest.php?version=1.1"; -} - -$aData = array(); -$aData['auth_user'] = 'no-export'; -$aData['auth_pwd'] = 'no-export'; -//$aData['auth_user'] = 'admin'; -//$aData['auth_pwd'] = 'admin'; - - -foreach ($aOperations as $iOp => $aOperation) -{ - echo "======================================\n"; - echo "Operation #$iOp: ".$aOperation['operation']."\n"; - $aData['json_data'] = json_encode($aOperation); - - echo "--------------------------------------\n"; - echo "Input:\n"; - print_r($aOperation); - - $response = DoPostRequest($sUrl, $aData); - $aResults = json_decode($response); - if ($aResults) - { - echo "--------------------------------------\n"; - echo "Reply:\n"; - print_r($aResults); - } - else - { - echo "ERROR rest.php replied:\n"; - echo $response; - } -} - + + + +/** + * Shows a usage of the SOAP queries + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * Helper to execute an HTTP POST request + * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl + * originaly named after do_post_request + */ +function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null) +{ + // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request. + + $sData = http_build_query($aData); + + $aParams = array('http' => array( + 'method' => 'POST', + 'content' => $sData, + 'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n", + )); + if ($sOptionnalHeaders !== null) + { + $aParams['http']['header'] .= $sOptionnalHeaders; + } + $ctx = stream_context_create($aParams); + + $fp = @fopen($sUrl, 'rb', false, $ctx); + if (!$fp) + { + global $php_errormsg; + if (isset($php_errormsg)) + { + throw new Exception("Problem with $sUrl, $php_errormsg"); + } + else + { + throw new Exception("Problem with $sUrl"); + } + } + $response = @stream_get_contents($fp); + if ($response === false) + { + throw new Exception("Problem reading data from $sUrl, $php_errormsg"); + } + return $response; +} + +// If the library curl is installed.... use this function +// +function DoPostRequest_curl($sUrl, $aData) +{ + $curl = curl_init($sUrl); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $aData); + $response = curl_exec($curl); + curl_close($curl); + + return $response; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Main program +// +//////////////////////////////////////////////////////////////////////////////// + +// Define the operations to perform (one operation per call the rest service) +// + +$aOperations = array( + array( + 'operation' => 'list_operations', // operation code + ), + array( + 'operation' => 'core/create', // operation code + 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) + // Values for the object to create + 'fields' => array( + 'org_id' => "SELECT Organization WHERE name = 'Demo'", + 'caller_id' => array('name' => 'monet', 'first_name' => 'claude'), + 'title' => 'issue blah', + 'description' => 'something happened' + ), + ), + array( + 'operation' => 'core/update', // operation code + 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'key' => 'SELECT UserRequest WHERE id=1', + 'output_fields' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c) + // Values for the object to create + 'fields' => array( + 'title' => 'Issue #'.rand(0, 100), + 'contacts_list' => array( + array( + 'role' => 'fireman #'.rand(0, 100), + 'contact_id' => array('finalclass' => 'Person', 'name' => 'monet', 'first_name' => 'claude'), + ), + ), + ), + ), + // Rewrite the full CaseLog on an existing UserRequest with id=1, setting date and user (optional) + array( + 'operation' => 'core/update', + 'comment' => 'Synchronization from ServiceFirst', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'key' => 'SELECT UserRequest WHERE id=1', + 'output_fields' => 'id, friendlyname, title', + 'fields' => array( + 'public_log' => array( + 'items' => array( + 0 => array( + 'date' => '2001-02-01 23:59:59', //Allow to set the date of a true event, an alarm for eg. + 'user_login' => 'Alarm monitoring', //Free text + 'user_id' => 0, //0 is required for the user_login to be taken into account + 'message' => 'This is 1st entry as an HTML formatted
    text', + ), + 1 => array( + 'date' => '2001-02-02 00:00:00', //If ommitted set automatically. + 'user_login' => 'Alarm monitoring', //user=id=0 is missing so will be ignored + 'message' => 'Second entry in text format: +with new line, but format not specified, so treated as HTML!, user_id=0 missing, so user_login ignored', + ), + ), + ), + ), + ), + // Add a Text entry in the HTML CaseLog of the UserRequest with id=1, setting date and user (optional) + array( + 'operation' => 'core/update', // operation code + 'comment' => 'Synchronization from Alarm Monitoring', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'key' => 1, // object id or OQL + 'output_fields' => 'id, friendlyname, title', // list of fields to show in the results (* or a,b,c) + // Example of adding an entry into a CaseLog on an existing UserRequest + 'fields' => array( + 'public_log' => array( + 'add_item' => array( + 'user_login' => 'New Entry', //Free text + 'user_id' => 0, //0 is required for the user_login to be taken into account + 'format' => 'text', //If ommitted, source is expected to be HTML + 'message' => 'This text is not HTML formatted with 3 lines: +new line +3rd and last line', + ), + ), + ), + ), + array( + 'operation' => 'core/get', // operation code + 'class' => 'UserRequest', + 'key' => 'SELECT UserRequest', + 'output_fields' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c) + ), + array( + 'operation' => 'core/delete', // operation code + 'comment' => 'Cleanup for synchro with...', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'key' => 'SELECT UserRequest WHERE org_id = 2', + 'simulate' => true, + ), + array( + 'operation' => 'core/apply_stimulus', // operation code + 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'key' => 1, + 'stimulus' => 'ev_assign', + // Values to set + 'fields' => array( + 'team_id' => 15, // Helpdesk + 'agent_id' => 9 // Jules Verne + ), + 'output_fields' => 'id, friendlyname, title, contacts_list', // list of fields to show in the results (* or a,b,c) + ), + array( + 'operation' => 'core/get_related', // operation code + 'class' => 'Server', + 'key' => 'SELECT Server', + 'relation' => 'impacts', // relation code + 'depth' => 4, // max recursion depth + ), +); +$aOperations = array( + array( + 'operation' => 'core/create', // operation code + 'comment' => 'Automatic creation of attachment blah blah...', // comment recorded in the change tracking log + 'class' => 'Attachment', + 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) + // Values for the object to create + 'fields' => array( + 'item_class' => 'UserRequest', + 'item_id' => 1, + 'item_org_id' => 3, + 'contents' => array( + 'data' => 'iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACmSURBVChTfZHRDYMwDESzQ2fqhHx3C3ao+MkW/WlnaFxfzk7sEnE6JHJ+NgaKZN2zLHVN2ssfkae0Da7FQ5PRk/ve4Hcx19Ie6CEGuh/6vMgNhwanHVUNbt73lUDbYJ+6pg8b3+m2RehsVPdMXyvQY+OVkB+Rrv64lUjb3nq+aCA6v4leRqtfaIgimr53atBy9PlfUhoh3fFCNDmErv9FWR6ylBL5AREbmHBnFj5lAAAAAElFTkSuQmCC', + 'filename' => 'myself.png', + 'mimetype' => 'image/png' + ), + ), + ), + array( + 'operation' => 'core/get', // operation code + 'class' => 'Attachment', + 'key' => 'SELECT Attachment', + 'output_fields' => '*', + ) +); +$aOperations = array( + array( + 'operation' => 'core/update', // operation code + 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log + 'class' => 'Server', + 'key' => 'SELECT Server WHERE name="Server1"', + 'output_fields' => 'id, friendlyname, description', // list of fields to show in the results (* or a,b,c) + // Values for the object to create + 'fields' => array( + 'description' => 'Issue #'.time(), + ), + ), +); +$aOperations = array( + array( + 'operation' => 'core/create', // operation code + 'comment' => 'Synchronization from blah...', // comment recorded in the change tracking log + 'class' => 'UserRequest', + 'output_fields' => 'id, friendlyname', // list of fields to show in the results (* or a,b,c) + // Values for the object to create + 'fields' => array( + 'org_id' => "SELECT Organization WHERE name = 'Demo'", + 'caller_id' => array('name' => 'monet', 'first_name' => 'claude'), + 'title' => 'issue blah', + 'description' => 'something happened' + ), + ), +); +$aXXXOperations = array( + array( + 'operation' => 'core/check_credentials', // operation code + 'user' => 'admin', + 'password' => 'admin', + ), +); +$aOperations = array( + array( + 'operation' => 'core/delete', // operation code + 'comment' => 'Cleanup for synchro with...', // comment recorded in the change tracking log + 'class' => 'Server', + 'key' => 'SELECT Server', + 'simulate' => false, + ), +); + +if (false) +{ + echo "Please edit the sample script and configure the server URL"; + exit; +} +else +{ + $sUrl = "http://localhost/trunk/webservices/rest.php?version=1.1"; +} + +$aData = array(); +$aData['auth_user'] = 'no-export'; +$aData['auth_pwd'] = 'no-export'; +//$aData['auth_user'] = 'admin'; +//$aData['auth_pwd'] = 'admin'; + + +foreach ($aOperations as $iOp => $aOperation) +{ + echo "======================================\n"; + echo "Operation #$iOp: ".$aOperation['operation']."\n"; + $aData['json_data'] = json_encode($aOperation); + + echo "--------------------------------------\n"; + echo "Input:\n"; + print_r($aOperation); + + $response = DoPostRequest($sUrl, $aData); + $aResults = json_decode($response); + if ($aResults) + { + echo "--------------------------------------\n"; + echo "Reply:\n"; + print_r($aResults); + } + else + { + echo "ERROR rest.php replied:\n"; + echo $response; + } +} + ?> \ No newline at end of file diff --git a/webservices/itopsoap.examples.php b/webservices/itopsoap.examples.php index e20cd494b..03812fd50 100644 --- a/webservices/itopsoap.examples.php +++ b/webservices/itopsoap.examples.php @@ -1,145 +1,145 @@ - - - -/** - * Shows a usage of the SOAP queries - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once('itopsoaptypes.class.inc.php'); -$sItopRoot = 'http'.(utils::IsConnectionSecure() ? 's' : '').'://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['SCRIPT_NAME']).'/..'; -$sWsdlUri = $sItopRoot.'/webservices/itop.wsdl.php'; -//$sWsdlUri .= '?service_category='; - -$aSOAPMapping = SOAPMapping::GetMapping(); - -ini_set("soap.wsdl_cache_enabled","0"); -$oSoapClient = new SoapClient( - $sWsdlUri, - array( - 'trace' => 1, - 'classmap' => $aSOAPMapping, // defined in itopsoaptypes.class.inc.php - ) -); - -try -{ - // The most simple service, returning a string - // - $sServerVersion = $oSoapClient->GetVersion(); - echo "

    GetVersion() returned $sServerVersion

    "; - - // More complex ones, returning a SOAPResult structure - // (run the page to know more about the returned data) - // - $oRes = $oSoapClient->CreateIncidentTicket - ( - 'admin', /* login */ - 'admin', /* password */ - 'Email server down', /* title */ - 'HW found shutdown', /* description */ - null, /* caller */ - new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'Demo'))), /* customer */ - new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'NW Management'))), /* service */ - new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'Troubleshooting'))), /* service subcategory */ - '', /* product */ - new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'NW support'))), /* workgroup */ - array( - new SOAPLinkCreationSpec( - 'Device', - array(new SOAPSearchCondition('name', 'switch01')), - array() - ), - new SOAPLinkCreationSpec( - 'Server', - array(new SOAPSearchCondition('name', 'dbserver1.demo.com')), - array() - ), - ), /* impacted cis */ - '1', /* impact */ - '1' /* urgency */ - ); - - echo "

    CreateIncidentTicket() returned:\n"; - echo "

    \n";
    -	print_r($oRes);
    -	echo "
    \n"; - echo "

    \n"; - - $oRes = $oSoapClient->SearchObjects - ( - 'admin', /* login */ - 'admin', /* password */ - 'SELECT URP_Profiles' /* oql */ - ); - - echo "

    SearchObjects() returned:\n"; - if ($oRes->status) - { - $aResults = $oRes->result; - - echo "\n"; - - // Header made after the first line - echo "\n"; - foreach ($aResults[0]->values as $aKeyValuePair) - { - echo " \n"; - } - echo "\n"; - - foreach ($aResults as $iRow => $aData) - { - echo "\n"; - foreach ($aData->values as $aKeyValuePair) - { - echo " \n"; - } - echo "\n"; - } - echo "
    ".$aKeyValuePair->key."
    ".$aKeyValuePair->value."
    \n"; - } - else - { - $aErrors = array(); - foreach ($oRes->errors->messages as $oMessage) - { - $aErrors[] = $oMessage->text; - } - $sErrorMsg = implode(', ', $aErrors); - echo "

    SearchObjects() failed with message: $sErrorMsg

    \n"; - //echo "
    \n";
    -		//print_r($oRes);
    -		//echo "
    \n"; - } - echo "

    \n"; -} -catch(SoapFault $e) -{ - echo "

    SoapFault Exception: {$e->getMessage()}

    \n"; - echo "

    Request

    \n"; - echo "
    \n"; 
    -	echo htmlspecialchars($oSoapClient->__getLastRequest())."\n"; 
    -	echo "
    "; - echo "

    Response

    "; - echo $oSoapClient->__getLastResponse()."\n"; -} -?> + + + +/** + * Shows a usage of the SOAP queries + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('itopsoaptypes.class.inc.php'); +$sItopRoot = 'http'.(utils::IsConnectionSecure() ? 's' : '').'://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['SCRIPT_NAME']).'/..'; +$sWsdlUri = $sItopRoot.'/webservices/itop.wsdl.php'; +//$sWsdlUri .= '?service_category='; + +$aSOAPMapping = SOAPMapping::GetMapping(); + +ini_set("soap.wsdl_cache_enabled","0"); +$oSoapClient = new SoapClient( + $sWsdlUri, + array( + 'trace' => 1, + 'classmap' => $aSOAPMapping, // defined in itopsoaptypes.class.inc.php + ) +); + +try +{ + // The most simple service, returning a string + // + $sServerVersion = $oSoapClient->GetVersion(); + echo "

    GetVersion() returned $sServerVersion

    "; + + // More complex ones, returning a SOAPResult structure + // (run the page to know more about the returned data) + // + $oRes = $oSoapClient->CreateIncidentTicket + ( + 'admin', /* login */ + 'admin', /* password */ + 'Email server down', /* title */ + 'HW found shutdown', /* description */ + null, /* caller */ + new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'Demo'))), /* customer */ + new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'NW Management'))), /* service */ + new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'Troubleshooting'))), /* service subcategory */ + '', /* product */ + new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'NW support'))), /* workgroup */ + array( + new SOAPLinkCreationSpec( + 'Device', + array(new SOAPSearchCondition('name', 'switch01')), + array() + ), + new SOAPLinkCreationSpec( + 'Server', + array(new SOAPSearchCondition('name', 'dbserver1.demo.com')), + array() + ), + ), /* impacted cis */ + '1', /* impact */ + '1' /* urgency */ + ); + + echo "

    CreateIncidentTicket() returned:\n"; + echo "

    \n";
    +	print_r($oRes);
    +	echo "
    \n"; + echo "

    \n"; + + $oRes = $oSoapClient->SearchObjects + ( + 'admin', /* login */ + 'admin', /* password */ + 'SELECT URP_Profiles' /* oql */ + ); + + echo "

    SearchObjects() returned:\n"; + if ($oRes->status) + { + $aResults = $oRes->result; + + echo "\n"; + + // Header made after the first line + echo "\n"; + foreach ($aResults[0]->values as $aKeyValuePair) + { + echo " \n"; + } + echo "\n"; + + foreach ($aResults as $iRow => $aData) + { + echo "\n"; + foreach ($aData->values as $aKeyValuePair) + { + echo " \n"; + } + echo "\n"; + } + echo "
    ".$aKeyValuePair->key."
    ".$aKeyValuePair->value."
    \n"; + } + else + { + $aErrors = array(); + foreach ($oRes->errors->messages as $oMessage) + { + $aErrors[] = $oMessage->text; + } + $sErrorMsg = implode(', ', $aErrors); + echo "

    SearchObjects() failed with message: $sErrorMsg

    \n"; + //echo "
    \n";
    +		//print_r($oRes);
    +		//echo "
    \n"; + } + echo "

    \n"; +} +catch(SoapFault $e) +{ + echo "

    SoapFault Exception: {$e->getMessage()}

    \n"; + echo "

    Request

    \n"; + echo "
    \n"; 
    +	echo htmlspecialchars($oSoapClient->__getLastRequest())."\n"; 
    +	echo "
    "; + echo "

    Response

    "; + echo $oSoapClient->__getLastResponse()."\n"; +} +?> diff --git a/webservices/itopsoaptypes.class.inc.php b/webservices/itopsoaptypes.class.inc.php index 852763311..442bff575 100644 --- a/webservices/itopsoaptypes.class.inc.php +++ b/webservices/itopsoaptypes.class.inc.php @@ -1,188 +1,188 @@ - - - -/** - * Declarations required for the WSDL - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -// Note: the attributes must have the same names (case sensitive) as in the WSDL specification -// - -class SOAPSearchCondition -{ - public $attcode; // string - public $value; // mixed - - public function __construct($sAttCode, $value) - { - $this->attcode = $sAttCode; - $this->value = $value; - } -} - - -class SOAPExternalKeySearch -{ - public $conditions; // array of SOAPSearchCondition - - public function __construct($aConditions = null) - { - $this->conditions = $aConditions; - } - - public function IsVoid() - { - if (is_null($this->conditions)) return true; - if (count($this->conditions) == 0) return true; - } -} - - -class SOAPAttributeValue -{ - public $attcode; // string - public $value; // mixed - - public function __construct($sAttCode, $value) - { - $this->attcode = $sAttCode; - $this->value = $value; - } -} - - -class SOAPLinkCreationSpec -{ - public $class; - public $conditions; // array of SOAPSearchCondition - public $attributes; // array of SOAPAttributeValue - - public function __construct($sClass, $aConditions, $aAttributes) - { - $this->class = $sClass; - $this->conditions = $aConditions; - $this->attributes = $aAttributes; - } -} - - -class SOAPLogMessage -{ - public $text; // string - - public function __construct($sText) - { - $this->text = $sText; - } -} - - -class SOAPResultLog -{ - public $messages; // array of SOAPLogMessage - - public function __construct($aMessages) - { - $this->messages = $aMessages; - } -} - - -class SOAPKeyValue -{ - public $key; // string - public $value; // string - - public function __construct($sKey, $sValue) - { - $this->key = $sKey; - $this->value = $sValue; - } -} - -class SOAPResultMessage -{ - public $label; // string - public $values; // array of SOAPKeyValue - - public function __construct($sLabel, $aValues) - { - $this->label = $sLabel; - $this->values = $aValues; - } -} - - -class SOAPResult -{ - public $status; // boolean - public $result; // array of SOAPResultMessage - public $errors; // array of SOAPResultLog - public $warnings; // array of SOAPResultLog - public $infos; // array of SOAPResultLog - - public function __construct($bStatus, $aResult, $aErrors, $aWarnings, $aInfos) - { - $this->status = $bStatus; - $this->result = $aResult; - $this->errors = $aErrors; - $this->warnings = $aWarnings; - $this->infos = $aInfos; - } -} - -class SOAPSimpleResult -{ - public $status; // boolean - public $message; // string - - public function __construct($bStatus, $sMessage) - { - $this->status = $bStatus; - $this->message = $sMessage; - } -} - - -class SOAPMapping -{ - static function GetMapping() - { - $aSOAPMapping = array( - 'SearchCondition' => 'SOAPSearchCondition', - 'ExternalKeySearch' => 'SOAPExternalKeySearch', - 'AttributeValue' => 'SOAPAttributeValue', - 'LinkCreationSpec' => 'SOAPLinkCreationSpec', - 'KeyValue' => 'SOAPKeyValue', - 'LogMessage' => 'SOAPLogMessage', - 'ResultLog' => 'SOAPResultLog', - 'ResultData' => 'SOAPKeyValue', - 'ResultMessage' => 'SOAPResultMessage', - 'Result' => 'SOAPResult', - 'SimpleResult' => 'SOAPSimpleResult', - ); - return $aSOAPMapping; - } -} - -?> + + + +/** + * Declarations required for the WSDL + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +// Note: the attributes must have the same names (case sensitive) as in the WSDL specification +// + +class SOAPSearchCondition +{ + public $attcode; // string + public $value; // mixed + + public function __construct($sAttCode, $value) + { + $this->attcode = $sAttCode; + $this->value = $value; + } +} + + +class SOAPExternalKeySearch +{ + public $conditions; // array of SOAPSearchCondition + + public function __construct($aConditions = null) + { + $this->conditions = $aConditions; + } + + public function IsVoid() + { + if (is_null($this->conditions)) return true; + if (count($this->conditions) == 0) return true; + } +} + + +class SOAPAttributeValue +{ + public $attcode; // string + public $value; // mixed + + public function __construct($sAttCode, $value) + { + $this->attcode = $sAttCode; + $this->value = $value; + } +} + + +class SOAPLinkCreationSpec +{ + public $class; + public $conditions; // array of SOAPSearchCondition + public $attributes; // array of SOAPAttributeValue + + public function __construct($sClass, $aConditions, $aAttributes) + { + $this->class = $sClass; + $this->conditions = $aConditions; + $this->attributes = $aAttributes; + } +} + + +class SOAPLogMessage +{ + public $text; // string + + public function __construct($sText) + { + $this->text = $sText; + } +} + + +class SOAPResultLog +{ + public $messages; // array of SOAPLogMessage + + public function __construct($aMessages) + { + $this->messages = $aMessages; + } +} + + +class SOAPKeyValue +{ + public $key; // string + public $value; // string + + public function __construct($sKey, $sValue) + { + $this->key = $sKey; + $this->value = $sValue; + } +} + +class SOAPResultMessage +{ + public $label; // string + public $values; // array of SOAPKeyValue + + public function __construct($sLabel, $aValues) + { + $this->label = $sLabel; + $this->values = $aValues; + } +} + + +class SOAPResult +{ + public $status; // boolean + public $result; // array of SOAPResultMessage + public $errors; // array of SOAPResultLog + public $warnings; // array of SOAPResultLog + public $infos; // array of SOAPResultLog + + public function __construct($bStatus, $aResult, $aErrors, $aWarnings, $aInfos) + { + $this->status = $bStatus; + $this->result = $aResult; + $this->errors = $aErrors; + $this->warnings = $aWarnings; + $this->infos = $aInfos; + } +} + +class SOAPSimpleResult +{ + public $status; // boolean + public $message; // string + + public function __construct($bStatus, $sMessage) + { + $this->status = $bStatus; + $this->message = $sMessage; + } +} + + +class SOAPMapping +{ + static function GetMapping() + { + $aSOAPMapping = array( + 'SearchCondition' => 'SOAPSearchCondition', + 'ExternalKeySearch' => 'SOAPExternalKeySearch', + 'AttributeValue' => 'SOAPAttributeValue', + 'LinkCreationSpec' => 'SOAPLinkCreationSpec', + 'KeyValue' => 'SOAPKeyValue', + 'LogMessage' => 'SOAPLogMessage', + 'ResultLog' => 'SOAPResultLog', + 'ResultData' => 'SOAPKeyValue', + 'ResultMessage' => 'SOAPResultMessage', + 'Result' => 'SOAPResult', + 'SimpleResult' => 'SOAPSimpleResult', + ); + return $aSOAPMapping; + } +} + +?> diff --git a/webservices/rest.php b/webservices/rest.php index 73e43e776..aab2958c6 100644 --- a/webservices/rest.php +++ b/webservices/rest.php @@ -1,295 +1,295 @@ - - -/** - * Entry point for all the REST services - * - * -------------------------------------------------- - * Create an object - * -------------------------------------------------- - * POST itop/webservices/rest.php - * { - * operation: 'object_create', - * comment: 'Synchronization from blah...', - * class: 'UserRequest', - * results: 'id, friendlyname', - * fields: - * { - * org_id: 'SELECT Organization WHERE name = "Demo"', - * caller_id: - * { - * name: 'monet', - * first_name: 'claude', - * } - * title: 'Houston, got a problem!', - * description: 'The fridge is empty' - * contacts_list: - * [ - * { - * role: 'pizza delivery', - * contact_id: - * { - * finalclass: 'Person', - * name: 'monet', - * first_name: 'claude' - * } - * } - * ] - * } - * } - * - * - * @copyright Copyright (C) 2010-2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @link https://www.itophub.io/wiki/page?id=2_5_0%3Aadvancedtopics%3Arest_json REST service documentation - * @example https://www.itophub.io/wiki/page?id=2_5_0%3Aadvancedtopics%3Arest_json_playground - */ - -if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); -require_once(__DIR__.'/../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); -require_once(APPROOT.'/application/loginwebpage.class.inc.php'); -require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); -require_once(APPROOT.'/application/startup.inc.php'); - -require_once(APPROOT.'core/restservices.class.inc.php'); - - -/** - * Result structure that is specific to the hardcoded verb 'list_operations' - */ -class RestResultListOperations extends RestResult -{ - public $version; - public $operations; - - public function AddOperation($sVerb, $sDescription, $sServiceProviderClass) - { - $this->operations[] = array( - 'verb' => $sVerb, - 'description' => $sDescription, - 'extension' => $sServiceProviderClass - ); - } -} - -if (!function_exists('json_last_error_msg')) { - function json_last_error_msg() { - static $ERRORS = array( - JSON_ERROR_NONE => 'No error', - JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', - JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)', - JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', - JSON_ERROR_SYNTAX => 'Syntax error', - JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' - ); - - $error = json_last_error(); - return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error'; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// -// Main -// -$oP = new ajax_page('rest'); -$oCtx = new ContextTag('REST/JSON'); - -$sVersion = utils::ReadParam('version', null, false, 'raw_data'); -$sOperation = utils::ReadParam('operation', null); -$sJsonString = utils::ReadParam('json_data', null, false, 'raw_data'); -$sProvider = ''; -try -{ - utils::UseParamFile(); - - $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); // Starting with iTop 2.2.0 portal users are no longer allowed to access the REST/JSON API - if ($iRet == LoginWebPage::EXIT_CODE_OK) - { - // Extra validation of the profile - if ((MetaModel::GetConfig()->Get('secure_rest_services') == true) && !UserRights::HasProfile('REST Services User')) - { - // Web services access is limited to the users with the profile REST Web Services - $iRet = LoginWebPage::EXIT_CODE_NOTAUTHORIZED; - } - } - if ($iRet != LoginWebPage::EXIT_CODE_OK) - { - switch($iRet) - { - case LoginWebPage::EXIT_CODE_MISSINGLOGIN: - throw new Exception("Missing parameter 'auth_user'", RestResult::MISSING_AUTH_USER); - break; - - case LoginWebPage::EXIT_CODE_MISSINGPASSWORD: - throw new Exception("Missing parameter 'auth_pwd'", RestResult::MISSING_AUTH_PWD); - break; - - case LoginWebPage::EXIT_CODE_WRONGCREDENTIALS: - throw new Exception("Invalid login", RestResult::UNAUTHORIZED); - break; - - case LoginWebPage::EXIT_CODE_PORTALUSERNOTAUTHORIZED: - throw new Exception("Portal user is not allowed", RestResult::UNAUTHORIZED); - break; - - case LoginWebPage::EXIT_CODE_NOTAUTHORIZED: - throw new Exception("This user is not authorized to use the web services. (The profile REST Services User is required to access the REST web services)", RestResult::UNAUTHORIZED); - break; - - default: - throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED); - } - } - - if ($sVersion == null) - { - throw new Exception("Missing parameter 'version' (e.g. '1.0')", RestResult::MISSING_VERSION); - } - - if ($sJsonString == null) - { - throw new Exception("Missing parameter 'json_data", RestResult::MISSING_JSON); - } - $aJsonData = @json_decode($sJsonString); - if ($aJsonData == null) - { - throw new Exception("Parameter json_data is not a valid JSON structure", RestResult::INVALID_JSON); - } - - - /** @var iRestServiceProvider[] $aProviders */ - $aProviders = array(); - foreach(get_declared_classes() as $sPHPClass) - { - $oRefClass = new ReflectionClass($sPHPClass); - if ($oRefClass->implementsInterface('iRestServiceProvider')) - { - $aProviders[] = new $sPHPClass; - } - } - - $aOpToRestService = array(); // verb => $oRestServiceProvider - foreach ($aProviders as $oRestSP) - { - $aOperations = $oRestSP->ListOperations($sVersion); - foreach ($aOperations as $aOpData) - { - $aOpToRestService[$aOpData['verb']] = array - ( - 'service_provider' => $oRestSP, - 'description' => $aOpData['description'], - ); - } - } - - if (count($aOpToRestService) == 0) - { - throw new Exception("There is no service available for version '$sVersion'", RestResult::UNSUPPORTED_VERSION); - } - - - $sOperation = RestUtils::GetMandatoryParam($aJsonData, 'operation'); - if ($sOperation == 'list_operations') - { - $oResult = new RestResultListOperations(); - $oResult->message = "Operations: ".count($aOpToRestService); - $oResult->version = $sVersion; - foreach ($aOpToRestService as $sVerb => $aOpData) - { - $oResult->AddOperation($sVerb, $aOpData['description'], get_class($aOpData['service_provider'])); - } - } - else - { - if (!array_key_exists($sOperation, $aOpToRestService)) - { - throw new Exception("Unknown verb '$sOperation' in version '$sVersion'", RestResult::UNKNOWN_OPERATION); - } - /** @var iRestServiceProvider $oRS */ - $oRS = $aOpToRestService[$sOperation]['service_provider']; - $sProvider = get_class($oRS); - - CMDBObject::SetTrackOrigin('webservice-rest'); - $oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData); - } -} -catch(Exception $e) -{ - $oResult = new RestResult(); - if ($e->GetCode() == 0) - { - $oResult->code = RestResult::INTERNAL_ERROR; - } - else - { - $oResult->code = $e->GetCode(); - } - $oResult->message = "Error: ".$e->GetMessage(); -} - -// Output the results -// -$sResponse = json_encode($oResult); - -if ($sResponse === false) -{ - $oJsonIssue = new RestResult(); - $oJsonIssue->code = RestResult::INTERNAL_ERROR; - $oJsonIssue->message = 'json encoding failed with message: '.json_last_error_msg().'. Full response structure for debugging purposes (print_r+bin2hex): '.bin2hex(print_r($oResult, true)); - $sResponse = json_encode($oJsonIssue); -} - -$oP->add_header('Access-Control-Allow-Origin: *'); - -$sCallback = utils::ReadParam('callback', null); -if ($sCallback == null) -{ - $oP->SetContentType('application/json'); - $oP->add($sResponse); -} -else -{ - $oP->SetContentType('application/javascript'); - $oP->add($sCallback.'('.$sResponse.')'); -} -$oP->Output(); - -// Log usage -// -if (MetaModel::GetConfig()->Get('log_rest_service')) -{ - $oLog = new EventRestService(); - $oLog->SetTrim('userinfo', UserRights::GetUser()); - $oLog->Set('version', $sVersion); - $oLog->Set('operation', $sOperation); - $oLog->SetTrim('json_input', $sJsonString); - - $oLog->Set('provider', $sProvider); - $sMessage = $oResult->message; - if (empty($oResult->message)) - { - $sMessage = 'Ok'; - } - $oLog->SetTrim('message', $sMessage); - $oLog->Set('code', $oResult->code); - $oLog->SetTrim('json_output', $sResponse); - - $oLog->DBInsertNoReload(); -} + + +/** + * Entry point for all the REST services + * + * -------------------------------------------------- + * Create an object + * -------------------------------------------------- + * POST itop/webservices/rest.php + * { + * operation: 'object_create', + * comment: 'Synchronization from blah...', + * class: 'UserRequest', + * results: 'id, friendlyname', + * fields: + * { + * org_id: 'SELECT Organization WHERE name = "Demo"', + * caller_id: + * { + * name: 'monet', + * first_name: 'claude', + * } + * title: 'Houston, got a problem!', + * description: 'The fridge is empty' + * contacts_list: + * [ + * { + * role: 'pizza delivery', + * contact_id: + * { + * finalclass: 'Person', + * name: 'monet', + * first_name: 'claude' + * } + * } + * ] + * } + * } + * + * + * @copyright Copyright (C) 2010-2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @link https://www.itophub.io/wiki/page?id=2_5_0%3Aadvancedtopics%3Arest_json REST service documentation + * @example https://www.itophub.io/wiki/page?id=2_5_0%3Aadvancedtopics%3Arest_json_playground + */ + +if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); +require_once(__DIR__.'/../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); +require_once(APPROOT.'/application/loginwebpage.class.inc.php'); +require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); +require_once(APPROOT.'/application/startup.inc.php'); + +require_once(APPROOT.'core/restservices.class.inc.php'); + + +/** + * Result structure that is specific to the hardcoded verb 'list_operations' + */ +class RestResultListOperations extends RestResult +{ + public $version; + public $operations; + + public function AddOperation($sVerb, $sDescription, $sServiceProviderClass) + { + $this->operations[] = array( + 'verb' => $sVerb, + 'description' => $sDescription, + 'extension' => $sServiceProviderClass + ); + } +} + +if (!function_exists('json_last_error_msg')) { + function json_last_error_msg() { + static $ERRORS = array( + JSON_ERROR_NONE => 'No error', + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)', + JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', + JSON_ERROR_SYNTAX => 'Syntax error', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' + ); + + $error = json_last_error(); + return isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error'; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Main +// +$oP = new ajax_page('rest'); +$oCtx = new ContextTag('REST/JSON'); + +$sVersion = utils::ReadParam('version', null, false, 'raw_data'); +$sOperation = utils::ReadParam('operation', null); +$sJsonString = utils::ReadParam('json_data', null, false, 'raw_data'); +$sProvider = ''; +try +{ + utils::UseParamFile(); + + $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); // Starting with iTop 2.2.0 portal users are no longer allowed to access the REST/JSON API + if ($iRet == LoginWebPage::EXIT_CODE_OK) + { + // Extra validation of the profile + if ((MetaModel::GetConfig()->Get('secure_rest_services') == true) && !UserRights::HasProfile('REST Services User')) + { + // Web services access is limited to the users with the profile REST Web Services + $iRet = LoginWebPage::EXIT_CODE_NOTAUTHORIZED; + } + } + if ($iRet != LoginWebPage::EXIT_CODE_OK) + { + switch($iRet) + { + case LoginWebPage::EXIT_CODE_MISSINGLOGIN: + throw new Exception("Missing parameter 'auth_user'", RestResult::MISSING_AUTH_USER); + break; + + case LoginWebPage::EXIT_CODE_MISSINGPASSWORD: + throw new Exception("Missing parameter 'auth_pwd'", RestResult::MISSING_AUTH_PWD); + break; + + case LoginWebPage::EXIT_CODE_WRONGCREDENTIALS: + throw new Exception("Invalid login", RestResult::UNAUTHORIZED); + break; + + case LoginWebPage::EXIT_CODE_PORTALUSERNOTAUTHORIZED: + throw new Exception("Portal user is not allowed", RestResult::UNAUTHORIZED); + break; + + case LoginWebPage::EXIT_CODE_NOTAUTHORIZED: + throw new Exception("This user is not authorized to use the web services. (The profile REST Services User is required to access the REST web services)", RestResult::UNAUTHORIZED); + break; + + default: + throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED); + } + } + + if ($sVersion == null) + { + throw new Exception("Missing parameter 'version' (e.g. '1.0')", RestResult::MISSING_VERSION); + } + + if ($sJsonString == null) + { + throw new Exception("Missing parameter 'json_data", RestResult::MISSING_JSON); + } + $aJsonData = @json_decode($sJsonString); + if ($aJsonData == null) + { + throw new Exception("Parameter json_data is not a valid JSON structure", RestResult::INVALID_JSON); + } + + + /** @var iRestServiceProvider[] $aProviders */ + $aProviders = array(); + foreach(get_declared_classes() as $sPHPClass) + { + $oRefClass = new ReflectionClass($sPHPClass); + if ($oRefClass->implementsInterface('iRestServiceProvider')) + { + $aProviders[] = new $sPHPClass; + } + } + + $aOpToRestService = array(); // verb => $oRestServiceProvider + foreach ($aProviders as $oRestSP) + { + $aOperations = $oRestSP->ListOperations($sVersion); + foreach ($aOperations as $aOpData) + { + $aOpToRestService[$aOpData['verb']] = array + ( + 'service_provider' => $oRestSP, + 'description' => $aOpData['description'], + ); + } + } + + if (count($aOpToRestService) == 0) + { + throw new Exception("There is no service available for version '$sVersion'", RestResult::UNSUPPORTED_VERSION); + } + + + $sOperation = RestUtils::GetMandatoryParam($aJsonData, 'operation'); + if ($sOperation == 'list_operations') + { + $oResult = new RestResultListOperations(); + $oResult->message = "Operations: ".count($aOpToRestService); + $oResult->version = $sVersion; + foreach ($aOpToRestService as $sVerb => $aOpData) + { + $oResult->AddOperation($sVerb, $aOpData['description'], get_class($aOpData['service_provider'])); + } + } + else + { + if (!array_key_exists($sOperation, $aOpToRestService)) + { + throw new Exception("Unknown verb '$sOperation' in version '$sVersion'", RestResult::UNKNOWN_OPERATION); + } + /** @var iRestServiceProvider $oRS */ + $oRS = $aOpToRestService[$sOperation]['service_provider']; + $sProvider = get_class($oRS); + + CMDBObject::SetTrackOrigin('webservice-rest'); + $oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData); + } +} +catch(Exception $e) +{ + $oResult = new RestResult(); + if ($e->GetCode() == 0) + { + $oResult->code = RestResult::INTERNAL_ERROR; + } + else + { + $oResult->code = $e->GetCode(); + } + $oResult->message = "Error: ".$e->GetMessage(); +} + +// Output the results +// +$sResponse = json_encode($oResult); + +if ($sResponse === false) +{ + $oJsonIssue = new RestResult(); + $oJsonIssue->code = RestResult::INTERNAL_ERROR; + $oJsonIssue->message = 'json encoding failed with message: '.json_last_error_msg().'. Full response structure for debugging purposes (print_r+bin2hex): '.bin2hex(print_r($oResult, true)); + $sResponse = json_encode($oJsonIssue); +} + +$oP->add_header('Access-Control-Allow-Origin: *'); + +$sCallback = utils::ReadParam('callback', null); +if ($sCallback == null) +{ + $oP->SetContentType('application/json'); + $oP->add($sResponse); +} +else +{ + $oP->SetContentType('application/javascript'); + $oP->add($sCallback.'('.$sResponse.')'); +} +$oP->Output(); + +// Log usage +// +if (MetaModel::GetConfig()->Get('log_rest_service')) +{ + $oLog = new EventRestService(); + $oLog->SetTrim('userinfo', UserRights::GetUser()); + $oLog->Set('version', $sVersion); + $oLog->Set('operation', $sOperation); + $oLog->SetTrim('json_input', $sJsonString); + + $oLog->Set('provider', $sProvider); + $sMessage = $oResult->message; + if (empty($oResult->message)) + { + $sMessage = 'Ok'; + } + $oLog->SetTrim('message', $sMessage); + $oLog->Set('code', $oResult->code); + $oLog->SetTrim('json_output', $sResponse); + + $oLog->DBInsertNoReload(); +} diff --git a/webservices/soapserver.php b/webservices/soapserver.php index 2b294ec49..72bea3cbc 100644 --- a/webservices/soapserver.php +++ b/webservices/soapserver.php @@ -1,109 +1,109 @@ - - - -/** - * Handling of SOAP queries - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -// Important note: if some required includes are missing, this might result -// in the error "looks like we got no XML document"... - -require_once('../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); -require_once(APPROOT.'/application/startup.inc.php'); - -// this file is generated dynamically with location = here -$sWsdlUri = utils::GetAbsoluteUrlAppRoot().'webservices/itop.wsdl.php'; -if (isset($_REQUEST['service_category']) && (!empty($_REQUEST['service_category']))) -{ - $sWsdlUri .= "?service_category=".$_REQUEST['service_category']; -} - - -ini_set("soap.wsdl_cache_enabled","0"); - -$aSOAPMapping = SOAPMapping::GetMapping(); -$oSoapServer = new SoapServer -( - $sWsdlUri, - array( - 'classmap' => $aSOAPMapping - ) -); -// $oSoapServer->setPersistence(SOAP_PERSISTENCE_SESSION); -if (isset($_REQUEST['service_category']) && (!empty($_REQUEST['service_category']))) -{ - $sServiceClass = $_REQUEST['service_category']; - if (!class_exists($sServiceClass)) - { - // not a valid class name (not a PHP class at all) - throw new SoapFault("iTop SOAP server", "Invalid argument service_category: '$sServiceClass' is not a PHP class"); - } - elseif (!is_subclass_of($sServiceClass, 'WebServicesBase')) - { - // not a valid class name (not deriving from WebServicesBase) - throw new SoapFault("iTop SOAP server", "Invalid argument service_category: '$sServiceClass' is not derived from WebServicesBase"); - } - else - { - $oSoapServer->setClass($sServiceClass, null); - } -} -else -{ - $oSoapServer->setClass('BasicServices', null); -} - -if ($_SERVER["REQUEST_METHOD"] == "POST") -{ - CMDBObject::SetTrackOrigin('webservice-soap'); - $oSoapServer->handle(); -} -else -{ - echo "This SOAP server can handle the following functions: "; - $aFunctions = $oSoapServer->getFunctions(); - echo "
      \n"; - foreach($aFunctions as $sFunc) - { - if ($sFunc == 'GetWSDLContents') continue; - - echo "
    • $sFunc
    • \n"; - } - echo "
    \n"; - echo "

    Here the WSDL file

    "; - - echo "You may also want to try the following service categories: "; - echo "

      \n"; - foreach(get_declared_classes() as $sPHPClass) - { - if (is_subclass_of($sPHPClass, 'WebServicesBase')) - { - $sServiceCategory = $sPHPClass; - $sSoapServerUri = utils::GetAbsoluteUrlAppRoot().'webservices/soapserver.php'; - $sSoapServerUri .= "?service_category=$sServiceCategory"; - echo "
    • $sServiceCategory
    • \n"; - } - } - echo "
    \n"; -} -?> + + + +/** + * Handling of SOAP queries + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +// Important note: if some required includes are missing, this might result +// in the error "looks like we got no XML document"... + +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); +require_once(APPROOT.'/application/startup.inc.php'); + +// this file is generated dynamically with location = here +$sWsdlUri = utils::GetAbsoluteUrlAppRoot().'webservices/itop.wsdl.php'; +if (isset($_REQUEST['service_category']) && (!empty($_REQUEST['service_category']))) +{ + $sWsdlUri .= "?service_category=".$_REQUEST['service_category']; +} + + +ini_set("soap.wsdl_cache_enabled","0"); + +$aSOAPMapping = SOAPMapping::GetMapping(); +$oSoapServer = new SoapServer +( + $sWsdlUri, + array( + 'classmap' => $aSOAPMapping + ) +); +// $oSoapServer->setPersistence(SOAP_PERSISTENCE_SESSION); +if (isset($_REQUEST['service_category']) && (!empty($_REQUEST['service_category']))) +{ + $sServiceClass = $_REQUEST['service_category']; + if (!class_exists($sServiceClass)) + { + // not a valid class name (not a PHP class at all) + throw new SoapFault("iTop SOAP server", "Invalid argument service_category: '$sServiceClass' is not a PHP class"); + } + elseif (!is_subclass_of($sServiceClass, 'WebServicesBase')) + { + // not a valid class name (not deriving from WebServicesBase) + throw new SoapFault("iTop SOAP server", "Invalid argument service_category: '$sServiceClass' is not derived from WebServicesBase"); + } + else + { + $oSoapServer->setClass($sServiceClass, null); + } +} +else +{ + $oSoapServer->setClass('BasicServices', null); +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") +{ + CMDBObject::SetTrackOrigin('webservice-soap'); + $oSoapServer->handle(); +} +else +{ + echo "This SOAP server can handle the following functions: "; + $aFunctions = $oSoapServer->getFunctions(); + echo "
      \n"; + foreach($aFunctions as $sFunc) + { + if ($sFunc == 'GetWSDLContents') continue; + + echo "
    • $sFunc
    • \n"; + } + echo "
    \n"; + echo "

    Here the WSDL file

    "; + + echo "You may also want to try the following service categories: "; + echo "

      \n"; + foreach(get_declared_classes() as $sPHPClass) + { + if (is_subclass_of($sPHPClass, 'WebServicesBase')) + { + $sServiceCategory = $sPHPClass; + $sSoapServerUri = utils::GetAbsoluteUrlAppRoot().'webservices/soapserver.php'; + $sSoapServerUri .= "?service_category=$sServiceCategory"; + echo "
    • $sServiceCategory
    • \n"; + } + } + echo "
    \n"; +} +?> diff --git a/webservices/webservices.basic.php b/webservices/webservices.basic.php index dd6a2eb73..af469aa9d 100644 --- a/webservices/webservices.basic.php +++ b/webservices/webservices.basic.php @@ -1,295 +1,295 @@ - - - -/** - * Implementation of iTop SOAP services - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/webservices/webservices.class.inc.php'); - - -class BasicServices extends WebServicesBase -{ - static protected function GetWSDLFilePath() - { - return APPROOT.'/webservices/itop.wsdl.tpl'; - } - - /** - * Get the server version (TODO: get it dynamically, where ?) - * - * @return WebServiceResult - */ - static public function GetVersion() - { - if (ITOP_REVISION == '$WCREV$') - { - $sVersionString = ITOP_VERSION.' [dev]'; - } - else - { - // This is a build made from SVN, let display the full information - $sVersionString = ITOP_VERSION."-".ITOP_REVISION." ".ITOP_BUILD_DATE; - } - - return $sVersionString; - } - - public function CreateRequestTicket($sLogin, $sPassword, $sTitle, $sDescription, $oCallerDesc, $oCustomerDesc, $oServiceDesc, $oServiceSubcategoryDesc, $sProduct, $oWorkgroupDesc, $aSOAPImpactedCIs, $sImpact, $sUrgency) - { - if (!UserRights::CheckCredentials($sLogin, $sPassword)) - { - $oRes = new WebServiceResultFailedLogin($sLogin); - $this->LogUsage(__FUNCTION__, $oRes); - - return $oRes->ToSoapStructure(); - } - UserRights::Login($sLogin); - - $aCallerDesc = self::SoapStructToExternalKeySearch($oCallerDesc); - $aCustomerDesc = self::SoapStructToExternalKeySearch($oCustomerDesc); - $aServiceDesc = self::SoapStructToExternalKeySearch($oServiceDesc); - $aServiceSubcategoryDesc = self::SoapStructToExternalKeySearch($oServiceSubcategoryDesc); - $aWorkgroupDesc = self::SoapStructToExternalKeySearch($oWorkgroupDesc); - - $aImpactedCIs = array(); - if (is_null($aSOAPImpactedCIs)) $aSOAPImpactedCIs = array(); - foreach($aSOAPImpactedCIs as $oImpactedCIs) - { - $aImpactedCIs[] = self::SoapStructToLinkCreationSpec($oImpactedCIs); - } - - $oRes = $this->_CreateResponseTicket - ( - 'UserRequest', - $sTitle, - $sDescription, - $aCallerDesc, - $aCustomerDesc, - $aServiceDesc, - $aServiceSubcategoryDesc, - $sProduct, - $aWorkgroupDesc, - $aImpactedCIs, - $sImpact, - $sUrgency - ); - return $oRes->ToSoapStructure(); - } - - public function CreateIncidentTicket($sLogin, $sPassword, $sTitle, $sDescription, $oCallerDesc, $oCustomerDesc, $oServiceDesc, $oServiceSubcategoryDesc, $sProduct, $oWorkgroupDesc, $aSOAPImpactedCIs, $sImpact, $sUrgency) - { - if (!UserRights::CheckCredentials($sLogin, $sPassword)) - { - $oRes = new WebServiceResultFailedLogin($sLogin); - $this->LogUsage(__FUNCTION__, $oRes); - - return $oRes->ToSoapStructure(); - } - UserRights::Login($sLogin); - - - if (!class_exists('Incident')) - { - $oRes = new WebServiceResult(); - $oRes->LogError("The class Incident does not exist. Did you install the Incident Management (ITIL) module ?"); - return $oRes->ToSoapStructure(); - } - - $aCallerDesc = self::SoapStructToExternalKeySearch($oCallerDesc); - $aCustomerDesc = self::SoapStructToExternalKeySearch($oCustomerDesc); - $aServiceDesc = self::SoapStructToExternalKeySearch($oServiceDesc); - $aServiceSubcategoryDesc = self::SoapStructToExternalKeySearch($oServiceSubcategoryDesc); - $aWorkgroupDesc = self::SoapStructToExternalKeySearch($oWorkgroupDesc); - - $aImpactedCIs = array(); - if (is_null($aSOAPImpactedCIs)) $aSOAPImpactedCIs = array(); - foreach($aSOAPImpactedCIs as $oImpactedCIs) - { - $aImpactedCIs[] = self::SoapStructToLinkCreationSpec($oImpactedCIs); - } - - $oRes = $this->_CreateResponseTicket - ( - 'Incident', - $sTitle, - $sDescription, - $aCallerDesc, - $aCustomerDesc, - $aServiceDesc, - $aServiceSubcategoryDesc, - $sProduct, - $aWorkgroupDesc, - $aImpactedCIs, - $sImpact, - $sUrgency - ); - return $oRes->ToSoapStructure(); - } - - /** - * Create an ResponseTicket (Incident or UserRequest) from an external system - * Some CIs might be specified (by their name/IP) - * - * @param string sClass The class of the ticket: Incident or UserRequest - * @param string sTitle - * @param string sDescription - * @param array aCallerDesc - * @param array aCustomerDesc - * @param array aServiceDesc - * @param array aServiceSubcategoryDesc - * @param string sProduct - * @param array aWorkgroupDesc - * @param array aImpactedCIs - * @param string sImpact - * @param string sUrgency - * - * @return WebServiceResult - */ - protected function _CreateResponseTicket($sClass, $sTitle, $sDescription, $aCallerDesc, $aCustomerDesc, $aServiceDesc, $aServiceSubcategoryDesc, $sProduct, $aWorkgroupDesc, $aImpactedCIs, $sImpact, $sUrgency) - { - - $oRes = new WebServiceResult(); - - try - { - $oMyChange = MetaModel::NewObject("CMDBChange"); - $oMyChange->Set("date", time()); - $oMyChange->Set("userinfo", "Administrator"); - $iChangeId = $oMyChange->DBInsertNoReload(); - - $oNewTicket = MetaModel::NewObject($sClass); - $this->MyObjectSetScalar('title', 'title', $sTitle, $oNewTicket, $oRes); - $this->MyObjectSetScalar('description', 'description', $sDescription, $oNewTicket, $oRes); - - $this->MyObjectSetExternalKey('org_id', 'customer', $aCustomerDesc, $oNewTicket, $oRes); - $this->MyObjectSetExternalKey('caller_id', 'caller', $aCallerDesc, $oNewTicket, $oRes); - - $this->MyObjectSetExternalKey('service_id', 'service', $aServiceDesc, $oNewTicket, $oRes); - if (!array_key_exists('service_id', $aServiceSubcategoryDesc)) - { - $aServiceSubcategoryDesc['service_id'] = $oNewTicket->Get('service_id'); - } - $this->MyObjectSetExternalKey('servicesubcategory_id', 'servicesubcategory', $aServiceSubcategoryDesc, $oNewTicket, $oRes); - if (MetaModel::IsValidAttCode($sClass, 'product')) - { - // 1.x data models - $this->MyObjectSetScalar('product', 'product', $sProduct, $oNewTicket, $oRes); - } - - if (MetaModel::IsValidAttCode($sClass, 'workgroup_id')) - { - // 1.x data models - $this->MyObjectSetExternalKey('workgroup_id', 'workgroup', $aWorkgroupDesc, $oNewTicket, $oRes); - } - else if (MetaModel::IsValidAttCode($sClass, 'team_id')) - { - // 2.x data models - $this->MyObjectSetExternalKey('team_id', 'workgroup', $aWorkgroupDesc, $oNewTicket, $oRes); - } - - - if (MetaModel::IsValidAttCode($sClass, 'ci_list')) - { - // 1.x data models - $aDevicesNotFound = $this->AddLinkedObjects('ci_list', 'impacted_cis', 'FunctionalCI', $aImpactedCIs, $oNewTicket, $oRes); - } - else if (MetaModel::IsValidAttCode($sClass, 'functionalcis_list')) - { - // 2.x data models - $aDevicesNotFound = $this->AddLinkedObjects('functionalcis_list', 'impacted_cis', 'FunctionalCI', $aImpactedCIs, $oNewTicket, $oRes); - } - - if (count($aDevicesNotFound) > 0) - { - $this->MyObjectSetScalar('description', 'n/a', $sDescription.' - Related CIs: '.implode(', ', $aDevicesNotFound), $oNewTicket, $oRes); - } - else - { - $this->MyObjectSetScalar('description', 'n/a', $sDescription, $oNewTicket, $oRes); - } - - $this->MyObjectSetScalar('impact', 'impact', $sImpact, $oNewTicket, $oRes); - $this->MyObjectSetScalar('urgency', 'urgency', $sUrgency, $oNewTicket, $oRes); - - $this->MyObjectInsert($oNewTicket, 'created', $oMyChange, $oRes); - } - catch (CoreException $e) - { - $oRes->LogError($e->getMessage()); - } - catch (Exception $e) - { - $oRes->LogError($e->getMessage()); - } - - $this->LogUsage(__FUNCTION__, $oRes); - return $oRes; - } - - /** - * Given an OQL, returns a set of objects (several objects could be on the same row) - * - * @param string sOQL - */ - public function SearchObjects($sLogin, $sPassword, $sOQL) - { - if (!UserRights::CheckCredentials($sLogin, $sPassword)) - { - $oRes = new WebServiceResultFailedLogin($sLogin); - $this->LogUsage(__FUNCTION__, $oRes); - - return $oRes->ToSoapStructure(); - } - UserRights::Login($sLogin); - - $oRes = $this->_SearchObjects($sOQL); - return $oRes->ToSoapStructure(); - } - - protected function _SearchObjects($sOQL) - { - $oRes = new WebServiceResult(); - try - { - $oSearch = DBObjectSearch::FromOQL($sOQL); - $oSet = new DBObjectSet($oSearch); - $aData = $oSet->ToArrayOfValues(); - foreach($aData as $iRow => $aRow) - { - $oRes->AddResultRow("row_$iRow", $aRow); - } - } - catch (CoreException $e) - { - $oRes->LogError($e->getMessage()); - } - catch (Exception $e) - { - $oRes->LogError($e->getMessage()); - } - - $this->LogUsage(__FUNCTION__, $oRes); - return $oRes; - } -} -?> + + + +/** + * Implementation of iTop SOAP services + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/webservices/webservices.class.inc.php'); + + +class BasicServices extends WebServicesBase +{ + static protected function GetWSDLFilePath() + { + return APPROOT.'/webservices/itop.wsdl.tpl'; + } + + /** + * Get the server version (TODO: get it dynamically, where ?) + * + * @return WebServiceResult + */ + static public function GetVersion() + { + if (ITOP_REVISION == '$WCREV$') + { + $sVersionString = ITOP_VERSION.' [dev]'; + } + else + { + // This is a build made from SVN, let display the full information + $sVersionString = ITOP_VERSION."-".ITOP_REVISION." ".ITOP_BUILD_DATE; + } + + return $sVersionString; + } + + public function CreateRequestTicket($sLogin, $sPassword, $sTitle, $sDescription, $oCallerDesc, $oCustomerDesc, $oServiceDesc, $oServiceSubcategoryDesc, $sProduct, $oWorkgroupDesc, $aSOAPImpactedCIs, $sImpact, $sUrgency) + { + if (!UserRights::CheckCredentials($sLogin, $sPassword)) + { + $oRes = new WebServiceResultFailedLogin($sLogin); + $this->LogUsage(__FUNCTION__, $oRes); + + return $oRes->ToSoapStructure(); + } + UserRights::Login($sLogin); + + $aCallerDesc = self::SoapStructToExternalKeySearch($oCallerDesc); + $aCustomerDesc = self::SoapStructToExternalKeySearch($oCustomerDesc); + $aServiceDesc = self::SoapStructToExternalKeySearch($oServiceDesc); + $aServiceSubcategoryDesc = self::SoapStructToExternalKeySearch($oServiceSubcategoryDesc); + $aWorkgroupDesc = self::SoapStructToExternalKeySearch($oWorkgroupDesc); + + $aImpactedCIs = array(); + if (is_null($aSOAPImpactedCIs)) $aSOAPImpactedCIs = array(); + foreach($aSOAPImpactedCIs as $oImpactedCIs) + { + $aImpactedCIs[] = self::SoapStructToLinkCreationSpec($oImpactedCIs); + } + + $oRes = $this->_CreateResponseTicket + ( + 'UserRequest', + $sTitle, + $sDescription, + $aCallerDesc, + $aCustomerDesc, + $aServiceDesc, + $aServiceSubcategoryDesc, + $sProduct, + $aWorkgroupDesc, + $aImpactedCIs, + $sImpact, + $sUrgency + ); + return $oRes->ToSoapStructure(); + } + + public function CreateIncidentTicket($sLogin, $sPassword, $sTitle, $sDescription, $oCallerDesc, $oCustomerDesc, $oServiceDesc, $oServiceSubcategoryDesc, $sProduct, $oWorkgroupDesc, $aSOAPImpactedCIs, $sImpact, $sUrgency) + { + if (!UserRights::CheckCredentials($sLogin, $sPassword)) + { + $oRes = new WebServiceResultFailedLogin($sLogin); + $this->LogUsage(__FUNCTION__, $oRes); + + return $oRes->ToSoapStructure(); + } + UserRights::Login($sLogin); + + + if (!class_exists('Incident')) + { + $oRes = new WebServiceResult(); + $oRes->LogError("The class Incident does not exist. Did you install the Incident Management (ITIL) module ?"); + return $oRes->ToSoapStructure(); + } + + $aCallerDesc = self::SoapStructToExternalKeySearch($oCallerDesc); + $aCustomerDesc = self::SoapStructToExternalKeySearch($oCustomerDesc); + $aServiceDesc = self::SoapStructToExternalKeySearch($oServiceDesc); + $aServiceSubcategoryDesc = self::SoapStructToExternalKeySearch($oServiceSubcategoryDesc); + $aWorkgroupDesc = self::SoapStructToExternalKeySearch($oWorkgroupDesc); + + $aImpactedCIs = array(); + if (is_null($aSOAPImpactedCIs)) $aSOAPImpactedCIs = array(); + foreach($aSOAPImpactedCIs as $oImpactedCIs) + { + $aImpactedCIs[] = self::SoapStructToLinkCreationSpec($oImpactedCIs); + } + + $oRes = $this->_CreateResponseTicket + ( + 'Incident', + $sTitle, + $sDescription, + $aCallerDesc, + $aCustomerDesc, + $aServiceDesc, + $aServiceSubcategoryDesc, + $sProduct, + $aWorkgroupDesc, + $aImpactedCIs, + $sImpact, + $sUrgency + ); + return $oRes->ToSoapStructure(); + } + + /** + * Create an ResponseTicket (Incident or UserRequest) from an external system + * Some CIs might be specified (by their name/IP) + * + * @param string sClass The class of the ticket: Incident or UserRequest + * @param string sTitle + * @param string sDescription + * @param array aCallerDesc + * @param array aCustomerDesc + * @param array aServiceDesc + * @param array aServiceSubcategoryDesc + * @param string sProduct + * @param array aWorkgroupDesc + * @param array aImpactedCIs + * @param string sImpact + * @param string sUrgency + * + * @return WebServiceResult + */ + protected function _CreateResponseTicket($sClass, $sTitle, $sDescription, $aCallerDesc, $aCustomerDesc, $aServiceDesc, $aServiceSubcategoryDesc, $sProduct, $aWorkgroupDesc, $aImpactedCIs, $sImpact, $sUrgency) + { + + $oRes = new WebServiceResult(); + + try + { + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $oMyChange->Set("userinfo", "Administrator"); + $iChangeId = $oMyChange->DBInsertNoReload(); + + $oNewTicket = MetaModel::NewObject($sClass); + $this->MyObjectSetScalar('title', 'title', $sTitle, $oNewTicket, $oRes); + $this->MyObjectSetScalar('description', 'description', $sDescription, $oNewTicket, $oRes); + + $this->MyObjectSetExternalKey('org_id', 'customer', $aCustomerDesc, $oNewTicket, $oRes); + $this->MyObjectSetExternalKey('caller_id', 'caller', $aCallerDesc, $oNewTicket, $oRes); + + $this->MyObjectSetExternalKey('service_id', 'service', $aServiceDesc, $oNewTicket, $oRes); + if (!array_key_exists('service_id', $aServiceSubcategoryDesc)) + { + $aServiceSubcategoryDesc['service_id'] = $oNewTicket->Get('service_id'); + } + $this->MyObjectSetExternalKey('servicesubcategory_id', 'servicesubcategory', $aServiceSubcategoryDesc, $oNewTicket, $oRes); + if (MetaModel::IsValidAttCode($sClass, 'product')) + { + // 1.x data models + $this->MyObjectSetScalar('product', 'product', $sProduct, $oNewTicket, $oRes); + } + + if (MetaModel::IsValidAttCode($sClass, 'workgroup_id')) + { + // 1.x data models + $this->MyObjectSetExternalKey('workgroup_id', 'workgroup', $aWorkgroupDesc, $oNewTicket, $oRes); + } + else if (MetaModel::IsValidAttCode($sClass, 'team_id')) + { + // 2.x data models + $this->MyObjectSetExternalKey('team_id', 'workgroup', $aWorkgroupDesc, $oNewTicket, $oRes); + } + + + if (MetaModel::IsValidAttCode($sClass, 'ci_list')) + { + // 1.x data models + $aDevicesNotFound = $this->AddLinkedObjects('ci_list', 'impacted_cis', 'FunctionalCI', $aImpactedCIs, $oNewTicket, $oRes); + } + else if (MetaModel::IsValidAttCode($sClass, 'functionalcis_list')) + { + // 2.x data models + $aDevicesNotFound = $this->AddLinkedObjects('functionalcis_list', 'impacted_cis', 'FunctionalCI', $aImpactedCIs, $oNewTicket, $oRes); + } + + if (count($aDevicesNotFound) > 0) + { + $this->MyObjectSetScalar('description', 'n/a', $sDescription.' - Related CIs: '.implode(', ', $aDevicesNotFound), $oNewTicket, $oRes); + } + else + { + $this->MyObjectSetScalar('description', 'n/a', $sDescription, $oNewTicket, $oRes); + } + + $this->MyObjectSetScalar('impact', 'impact', $sImpact, $oNewTicket, $oRes); + $this->MyObjectSetScalar('urgency', 'urgency', $sUrgency, $oNewTicket, $oRes); + + $this->MyObjectInsert($oNewTicket, 'created', $oMyChange, $oRes); + } + catch (CoreException $e) + { + $oRes->LogError($e->getMessage()); + } + catch (Exception $e) + { + $oRes->LogError($e->getMessage()); + } + + $this->LogUsage(__FUNCTION__, $oRes); + return $oRes; + } + + /** + * Given an OQL, returns a set of objects (several objects could be on the same row) + * + * @param string sOQL + */ + public function SearchObjects($sLogin, $sPassword, $sOQL) + { + if (!UserRights::CheckCredentials($sLogin, $sPassword)) + { + $oRes = new WebServiceResultFailedLogin($sLogin); + $this->LogUsage(__FUNCTION__, $oRes); + + return $oRes->ToSoapStructure(); + } + UserRights::Login($sLogin); + + $oRes = $this->_SearchObjects($sOQL); + return $oRes->ToSoapStructure(); + } + + protected function _SearchObjects($sOQL) + { + $oRes = new WebServiceResult(); + try + { + $oSearch = DBObjectSearch::FromOQL($sOQL); + $oSet = new DBObjectSet($oSearch); + $aData = $oSet->ToArrayOfValues(); + foreach($aData as $iRow => $aRow) + { + $oRes->AddResultRow("row_$iRow", $aRow); + } + } + catch (CoreException $e) + { + $oRes->LogError($e->getMessage()); + } + catch (Exception $e) + { + $oRes->LogError($e->getMessage()); + } + + $this->LogUsage(__FUNCTION__, $oRes); + return $oRes; + } +} +?> diff --git a/webservices/webservices.class.inc.php b/webservices/webservices.class.inc.php index ef7f9611f..33008e11b 100644 --- a/webservices/webservices.class.inc.php +++ b/webservices/webservices.class.inc.php @@ -1,594 +1,594 @@ - - - -/** - * Implementation of iTop SOAP services - * - * @copyright Copyright (C) 2010-2015 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -require_once(APPROOT.'/webservices/itopsoaptypes.class.inc.php'); - -/** - * Generic response of iTop SOAP services - * - * @package iTopORM - */ -class WebServiceResult -{ - - /** - * Overall status - * - * @var m_bStatus - */ - public $m_bStatus; - - /** - * Error log - * - * @var m_aErrors - */ - public $m_aErrors; - - /** - * Warning log - * - * @var m_aWarnings - */ - public $m_aWarnings; - - /** - * Information log - * - * @var m_aInfos - */ - public $m_aInfos; - - /** - * Constructor - * - * @param status $bStatus - */ - public function __construct() - { - $this->m_bStatus = true; - $this->m_aResult = array(); - $this->m_aErrors = array(); - $this->m_aWarnings = array(); - $this->m_aInfos = array(); - } - - public function ToSoapStructure() - { - $aResults = array(); - foreach($this->m_aResult as $sLabel => $aData) - { - $aValues = array(); - foreach($aData as $sKey => $value) - { - $aValues[] = new SOAPKeyValue($sKey, $value); - } - $aResults[] = new SoapResultMessage($sLabel, $aValues); - } - $aInfos = array(); - foreach($this->m_aInfos as $sMessage) - { - $aInfos[] = new SoapLogMessage($sMessage); - } - $aWarnings = array(); - foreach($this->m_aWarnings as $sMessage) - { - $aWarnings[] = new SoapLogMessage($sMessage); - } - $aErrors = array(); - foreach($this->m_aErrors as $sMessage) - { - $aErrors[] = new SoapLogMessage($sMessage); - } - - $oRet = new SOAPResult( - $this->m_bStatus, - $aResults, - new SOAPResultLog($aErrors), - new SOAPResultLog($aWarnings), - new SOAPResultLog($aInfos) - ); - - return $oRet; - } - - /** - * Did the current processing encounter a stopper issue ? - * - * @return bool - */ - public function IsOk() - { - return $this->m_bStatus; - } - - /** - * Add result details - object reference - * - * @param string sLabel - * @param object oObject - */ - public function AddResultObject($sLabel, $oObject) - { - $oAppContext = new ApplicationContext(); - $this->m_aResult[$sLabel] = array( - 'id' => $oObject->GetKey(), - 'name' => $oObject->GetRawName(), - 'url' => $oAppContext->MakeObjectUrl(get_class($oObject), $oObject->GetKey(), null, false), // Raw URL without HTML tags - ); - } - - /** - * Add result details - a table row - * - * @param string sLabel - * @param object oObject - */ - public function AddResultRow($sLabel, $aRow) - { - $this->m_aResult[$sLabel] = $aRow; - } - - /** - * Log an error - * - * @param string sDescription - */ - public function LogError($sDescription) - { - $this->m_aErrors[] = $sDescription; - // Note: SOAP do transform false into null - $this->m_bStatus = 0; - } - - /** - * Log a warning - * - * @param string sDescription - */ - public function LogWarning($sDescription) - { - $this->m_aWarnings[] = $sDescription; - } - - /** - * Log an error or a warning - * - * @param string sDescription - * @param boolean bIsStopper - */ - public function LogIssue($sDescription, $bIsStopper = true) - { - if ($bIsStopper) $this->LogError($sDescription); - else $this->LogWarning($sDescription); - } - - /** - * Log operation details - * - * @param description $sDescription - */ - public function LogInfo($sDescription) - { - $this->m_aInfos[] = $sDescription; - } - - protected static function LogToText($aLog) - { - return implode("\n", $aLog); - } - - public function GetInfoAsText() - { - return self::LogToText($this->m_aInfos); - } - - public function GetWarningsAsText() - { - return self::LogToText($this->m_aWarnings); - } - - public function GetErrorsAsText() - { - return self::LogToText($this->m_aErrors); - } - - public function GetReturnedDataAsText() - { - $sRet = ''; - foreach ($this->m_aResult as $sKey => $value) - { - $sRet .= "===== $sKey =====\n"; - $sRet .= print_r($value, true); - } - return $sRet; - } -} - - -/** - * Generic response of iTop SOAP services - failed login - * - * @package iTopORM - */ -class WebServiceResultFailedLogin extends WebServiceResult -{ - public function __construct($sLogin) - { - parent::__construct(); - $this->LogError("Wrong credentials: '$sLogin'"); - } -} - -/** - * Implementation of the Services - * - * @package iTopORM - */ -abstract class WebServicesBase -{ - static public function GetWSDLContents($sServiceCategory = '') - { - if ($sServiceCategory == '') - { - $sServiceCategory = 'BasicServices'; - } - $sWsdlFilePath = call_user_func(array($sServiceCategory, 'GetWSDLFilePath')); - return file_get_contents($sWsdlFilePath); - } - - /** - * Helper to log a service delivery - * - * @param string sVerb - * @param array aArgs - * @param WebServiceResult oRes - * - */ - protected function LogUsage($sVerb, $oRes) - { - if (!MetaModel::IsLogEnabledWebService()) return; - - $oLog = new EventWebService(); - if ($oRes->IsOk()) - { - $oLog->Set('message', $sVerb.' was successfully invoked'); - } - else - { - $oLog->Set('message', $sVerb.' returned errors'); - } - $oLog->Set('userinfo', UserRights::GetUser()); - $oLog->Set('verb', $sVerb); - $oLog->Set('result', $oRes->IsOk()); - $this->TrimAndSetValue($oLog, 'log_info', (string)$oRes->GetInfoAsText()); - $this->TrimAndSetValue($oLog, 'log_warning', (string)$oRes->GetWarningsAsText()); - $this->TrimAndSetValue($oLog, 'log_error', (string)$oRes->GetErrorsAsText()); - $this->TrimAndSetValue($oLog, 'data', (string)$oRes->GetReturnedDataAsText()); - $oLog->DBInsertNoReload(); - } - - protected function TrimAndSetValue($oLog, $sAttCode, $sValue) - { - $oAttDef = MetaModel::GetAttributeDef(get_class($oLog), $sAttCode); - if (is_object($oAttDef)) - { - $iMaxSize = $oAttDef->GetMaxSize(); - if ($iMaxSize && (strlen($sValue) > $iMaxSize)) - { - $sValue = substr($sValue, 0, $iMaxSize); - } - $oLog->Set($sAttCode, $sValue); - } - } - - /** - * Helper to set a scalar attribute - * - * @param string sAttCode - * @param scalar value - * @param DBObject oTargetObj - * @param WebServiceResult oRes - * - */ - protected function MyObjectSetScalar($sAttCode, $sParamName, $value, &$oTargetObj, &$oRes) - { - $res = $oTargetObj->CheckValue($sAttCode, $value); - if ($res === true) - { - $oTargetObj->Set($sAttCode, $value); - } - else - { - // $res contains the error description - $oRes->LogError("Unexpected value for parameter $sParamName: $res"); - } - } - - /** - * Helper to set an external key - * - * @param string sAttCode - * @param array aExtKeyDesc - * @param DBObject oTargetObj - * @param WebServiceResult oRes - * - */ - protected function MyObjectSetExternalKey($sAttCode, $sParamName, $aExtKeyDesc, &$oTargetObj, &$oRes) - { - $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); - - $bIsMandatory = !$oExtKey->IsNullAllowed(); - - if (is_null($aExtKeyDesc)) - { - if ($bIsMandatory) - { - $oRes->LogError("Parameter $sParamName: found null for a mandatory key"); - } - else - { - // skip silently - return; - } - } - - if (count($aExtKeyDesc) == 0) - { - $oRes->LogIssue("Parameter $sParamName: no search condition has been specified", $bIsMandatory); - return; - } - - $sKeyClass = $oExtKey->GetTargetClass(); - $oReconFilter = new DBObjectSearch($sKeyClass); - foreach ($aExtKeyDesc as $sForeignAttCode => $value) - { - if (!MetaModel::IsValidFilterCode($sKeyClass, $sForeignAttCode)) - { - $aCodes = array_keys(MetaModel::GetClassFilterDefs($sKeyClass)); - $sMsg = "Parameter $sParamName: '$sForeignAttCode' is not a valid filter code for class '$sKeyClass', expecting a value in {".implode(', ', $aCodes)."}"; - $oRes->LogIssue($sMsg, $bIsMandatory); - } - // The foreign attribute is one of our reconciliation key - $oReconFilter->AddCondition($sForeignAttCode, $value, '='); - } - $oExtObjects = new CMDBObjectSet($oReconFilter); - switch($oExtObjects->Count()) - { - case 0: - $sMsg = "Parameter $sParamName: no match (searched: '".$oReconFilter->ToOQL(true)."')"; - $oRes->LogIssue($sMsg, $bIsMandatory); - break; - case 1: - // Do change the external key attribute - $oForeignObj = $oExtObjects->Fetch(); - $oTargetObj->Set($sAttCode, $oForeignObj->GetKey()); - - // Report it (no need to report if the object already had this value - if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) - { - $oRes->LogInfo("Parameter $sParamName: found match ".get_class($oForeignObj)."::".$oForeignObj->GetKey()." '".$oForeignObj->GetName()."'"); - } - break; - default: - $sMsg = "Parameter $sParamName: Found ".$oExtObjects->Count()." matches (searched: '".$oReconFilter->ToOQL(true)."')"; - $oRes->LogIssue($sMsg, $bIsMandatory); - } - } - - /** - * Helper to link objects - * - * @param string sLinkAttCode - * @param string sLinkedClass - * @param array $aLinkList - * @param DBObject oTargetObj - * @param WebServiceResult oRes - * - * @return array List of objects that could not be found - */ - protected function AddLinkedObjects($sLinkAttCode, $sParamName, $sLinkedClass, $aLinkList, &$oTargetObj, &$oRes) - { - $oLinkAtt = MetaModel::GetAttributeDef(get_class($oTargetObj), $sLinkAttCode); - $sLinkClass = $oLinkAtt->GetLinkedClass(); - $sExtKeyToItem = $oLinkAtt->GetExtKeyToRemote(); - - $aItemsFound = array(); - $aItemsNotFound = array(); - - if (is_null($aLinkList)) - { - return $aItemsNotFound; - } - - foreach ($aLinkList as $aItemData) - { - if (!array_key_exists('class', $aItemData)) - { - $oRes->LogWarning("Parameter $sParamName: missing 'class' specification"); - continue; // skip - } - $sTargetClass = $aItemData['class']; - if (!MetaModel::IsValidClass($sTargetClass)) - { - $oRes->LogError("Parameter $sParamName: invalid class '$sTargetClass'"); - continue; // skip - } - if (!MetaModel::IsParentClass($sLinkedClass, $sTargetClass)) - { - $oRes->LogError("Parameter $sParamName: '$sTargetClass' is not a child class of '$sLinkedClass'"); - continue; // skip - } - $oReconFilter = new DBObjectSearch($sTargetClass); - $aCIStringDesc = array(); - foreach ($aItemData['search'] as $sAttCode => $value) - { - if (!MetaModel::IsValidFilterCode($sTargetClass, $sAttCode)) - { - $aCodes = array_keys(MetaModel::GetClassFilterDefs($sTargetClass)); - $oRes->LogError("Parameter $sParamName: '$sAttCode' is not a valid filter code for class '$sTargetClass', expecting a value in {".implode(', ', $aCodes)."}"); - continue 2; // skip the entire item - } - $aCIStringDesc[] = "$sAttCode: $value"; - - // The attribute is one of our reconciliation key - $oReconFilter->AddCondition($sAttCode, $value, '='); - } - if (count($aCIStringDesc) == 1) - { - // take the last and unique value to describe the object - $sItemDesc = $value; - } - else - { - // describe the object by the given keys - $sItemDesc = $sTargetClass.'('.implode('/', $aCIStringDesc).')'; - } - - $oExtObjects = new CMDBObjectSet($oReconFilter); - switch($oExtObjects->Count()) - { - case 0: - $oRes->LogWarning("Parameter $sParamName: object to link $sLinkedClass / $sItemDesc could not be found (searched: '".$oReconFilter->ToOQL(true)."')"); - $aItemsNotFound[] = $sItemDesc; - break; - case 1: - $aItemsFound[] = array ( - 'object' => $oExtObjects->Fetch(), - 'link_values' => @$aItemData['link_values'], - 'desc' => $sItemDesc, - ); - break; - default: - $oRes->LogWarning("Parameter $sParamName: Found ".$oExtObjects->Count()." matches for item '$sItemDesc' (searched: '".$oReconFilter->ToOQL(true)."')"); - $aItemsNotFound[] = $sItemDesc; - } - } - - if (count($aItemsFound) > 0) - { - $aLinks = array(); - foreach($aItemsFound as $aItemData) - { - $oLink = MetaModel::NewObject($sLinkClass); - $oLink->Set($sExtKeyToItem, $aItemData['object']->GetKey()); - foreach($aItemData['link_values'] as $sKey => $value) - { - if(!MetaModel::IsValidAttCode($sLinkClass, $sKey)) - { - $oRes->LogWarning("Parameter $sParamName: Attaching item '".$aItemData['desc']."', the attribute code '$sKey' is not valid ; check the class '$sLinkClass'"); - } - else - { - $oLink->Set($sKey, $value); - } - } - $aLinks[] = $oLink; - } - $oImpactedInfraSet = DBObjectSet::FromArray($sLinkClass, $aLinks); - $oTargetObj->Set($sLinkAttCode, $oImpactedInfraSet); - } - - return $aItemsNotFound; - } - - protected function MyObjectInsert($oTargetObj, $sResultLabel, $oChange, &$oRes) - { - if ($oRes->IsOk()) - { - list($bRes, $aIssues) = $oTargetObj->CheckToWrite(); - if ($bRes) - { - $iId = $oTargetObj->DBInsertTrackedNoReload($oChange); - $oRes->LogInfo("Created object ".get_class($oTargetObj)."::$iId"); - $oRes->AddResultObject($sResultLabel, $oTargetObj); - } - else - { - $oRes->LogError("The ticket could not be created due to forbidden values (or inconsistent values)"); - foreach($aIssues as $iIssue => $sIssue) - { - $oRes->LogError("Issue #$iIssue: $sIssue"); - } - } - } - } - - - static protected function SoapStructToExternalKeySearch($oExternalKeySearch) - { - if (is_null($oExternalKeySearch)) return null; - if ($oExternalKeySearch->IsVoid()) return null; - - $aRes = array(); - foreach($oExternalKeySearch->conditions as $oSearchCondition) - { - $aRes[$oSearchCondition->attcode] = $oSearchCondition->value; - } - return $aRes; - } - - static protected function SoapStructToLinkCreationSpec(SoapLinkCreationSpec $oLinkCreationSpec) - { - $aRes = array - ( - 'class' => $oLinkCreationSpec->class, - 'search' => array(), - 'link_values' => array(), - ); - - foreach($oLinkCreationSpec->conditions as $oSearchCondition) - { - $aRes['search'][$oSearchCondition->attcode] = $oSearchCondition->value; - } - - foreach($oLinkCreationSpec->attributes as $oAttributeValue) - { - $aRes['link_values'][$oAttributeValue->attcode] = $oAttributeValue->value; - } - - return $aRes; - } - - static protected function SoapStructToAssociativeArray($aArrayOfAssocArray) - { - if (is_null($aArrayOfAssocArray)) return array(); - - $aRes = array(); - foreach($aArrayOfAssocArray as $aAssocArray) - { - $aRow = array(); - foreach ($aAssocArray as $oKeyValuePair) - { - $aRow[$oKeyValuePair->key] = $oKeyValuePair->value; - } - $aRes[] = $aRow; - } - return $aRes; - } -} -?> + + + +/** + * Implementation of iTop SOAP services + * + * @copyright Copyright (C) 2010-2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +require_once(APPROOT.'/webservices/itopsoaptypes.class.inc.php'); + +/** + * Generic response of iTop SOAP services + * + * @package iTopORM + */ +class WebServiceResult +{ + + /** + * Overall status + * + * @var m_bStatus + */ + public $m_bStatus; + + /** + * Error log + * + * @var m_aErrors + */ + public $m_aErrors; + + /** + * Warning log + * + * @var m_aWarnings + */ + public $m_aWarnings; + + /** + * Information log + * + * @var m_aInfos + */ + public $m_aInfos; + + /** + * Constructor + * + * @param status $bStatus + */ + public function __construct() + { + $this->m_bStatus = true; + $this->m_aResult = array(); + $this->m_aErrors = array(); + $this->m_aWarnings = array(); + $this->m_aInfos = array(); + } + + public function ToSoapStructure() + { + $aResults = array(); + foreach($this->m_aResult as $sLabel => $aData) + { + $aValues = array(); + foreach($aData as $sKey => $value) + { + $aValues[] = new SOAPKeyValue($sKey, $value); + } + $aResults[] = new SoapResultMessage($sLabel, $aValues); + } + $aInfos = array(); + foreach($this->m_aInfos as $sMessage) + { + $aInfos[] = new SoapLogMessage($sMessage); + } + $aWarnings = array(); + foreach($this->m_aWarnings as $sMessage) + { + $aWarnings[] = new SoapLogMessage($sMessage); + } + $aErrors = array(); + foreach($this->m_aErrors as $sMessage) + { + $aErrors[] = new SoapLogMessage($sMessage); + } + + $oRet = new SOAPResult( + $this->m_bStatus, + $aResults, + new SOAPResultLog($aErrors), + new SOAPResultLog($aWarnings), + new SOAPResultLog($aInfos) + ); + + return $oRet; + } + + /** + * Did the current processing encounter a stopper issue ? + * + * @return bool + */ + public function IsOk() + { + return $this->m_bStatus; + } + + /** + * Add result details - object reference + * + * @param string sLabel + * @param object oObject + */ + public function AddResultObject($sLabel, $oObject) + { + $oAppContext = new ApplicationContext(); + $this->m_aResult[$sLabel] = array( + 'id' => $oObject->GetKey(), + 'name' => $oObject->GetRawName(), + 'url' => $oAppContext->MakeObjectUrl(get_class($oObject), $oObject->GetKey(), null, false), // Raw URL without HTML tags + ); + } + + /** + * Add result details - a table row + * + * @param string sLabel + * @param object oObject + */ + public function AddResultRow($sLabel, $aRow) + { + $this->m_aResult[$sLabel] = $aRow; + } + + /** + * Log an error + * + * @param string sDescription + */ + public function LogError($sDescription) + { + $this->m_aErrors[] = $sDescription; + // Note: SOAP do transform false into null + $this->m_bStatus = 0; + } + + /** + * Log a warning + * + * @param string sDescription + */ + public function LogWarning($sDescription) + { + $this->m_aWarnings[] = $sDescription; + } + + /** + * Log an error or a warning + * + * @param string sDescription + * @param boolean bIsStopper + */ + public function LogIssue($sDescription, $bIsStopper = true) + { + if ($bIsStopper) $this->LogError($sDescription); + else $this->LogWarning($sDescription); + } + + /** + * Log operation details + * + * @param description $sDescription + */ + public function LogInfo($sDescription) + { + $this->m_aInfos[] = $sDescription; + } + + protected static function LogToText($aLog) + { + return implode("\n", $aLog); + } + + public function GetInfoAsText() + { + return self::LogToText($this->m_aInfos); + } + + public function GetWarningsAsText() + { + return self::LogToText($this->m_aWarnings); + } + + public function GetErrorsAsText() + { + return self::LogToText($this->m_aErrors); + } + + public function GetReturnedDataAsText() + { + $sRet = ''; + foreach ($this->m_aResult as $sKey => $value) + { + $sRet .= "===== $sKey =====\n"; + $sRet .= print_r($value, true); + } + return $sRet; + } +} + + +/** + * Generic response of iTop SOAP services - failed login + * + * @package iTopORM + */ +class WebServiceResultFailedLogin extends WebServiceResult +{ + public function __construct($sLogin) + { + parent::__construct(); + $this->LogError("Wrong credentials: '$sLogin'"); + } +} + +/** + * Implementation of the Services + * + * @package iTopORM + */ +abstract class WebServicesBase +{ + static public function GetWSDLContents($sServiceCategory = '') + { + if ($sServiceCategory == '') + { + $sServiceCategory = 'BasicServices'; + } + $sWsdlFilePath = call_user_func(array($sServiceCategory, 'GetWSDLFilePath')); + return file_get_contents($sWsdlFilePath); + } + + /** + * Helper to log a service delivery + * + * @param string sVerb + * @param array aArgs + * @param WebServiceResult oRes + * + */ + protected function LogUsage($sVerb, $oRes) + { + if (!MetaModel::IsLogEnabledWebService()) return; + + $oLog = new EventWebService(); + if ($oRes->IsOk()) + { + $oLog->Set('message', $sVerb.' was successfully invoked'); + } + else + { + $oLog->Set('message', $sVerb.' returned errors'); + } + $oLog->Set('userinfo', UserRights::GetUser()); + $oLog->Set('verb', $sVerb); + $oLog->Set('result', $oRes->IsOk()); + $this->TrimAndSetValue($oLog, 'log_info', (string)$oRes->GetInfoAsText()); + $this->TrimAndSetValue($oLog, 'log_warning', (string)$oRes->GetWarningsAsText()); + $this->TrimAndSetValue($oLog, 'log_error', (string)$oRes->GetErrorsAsText()); + $this->TrimAndSetValue($oLog, 'data', (string)$oRes->GetReturnedDataAsText()); + $oLog->DBInsertNoReload(); + } + + protected function TrimAndSetValue($oLog, $sAttCode, $sValue) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($oLog), $sAttCode); + if (is_object($oAttDef)) + { + $iMaxSize = $oAttDef->GetMaxSize(); + if ($iMaxSize && (strlen($sValue) > $iMaxSize)) + { + $sValue = substr($sValue, 0, $iMaxSize); + } + $oLog->Set($sAttCode, $sValue); + } + } + + /** + * Helper to set a scalar attribute + * + * @param string sAttCode + * @param scalar value + * @param DBObject oTargetObj + * @param WebServiceResult oRes + * + */ + protected function MyObjectSetScalar($sAttCode, $sParamName, $value, &$oTargetObj, &$oRes) + { + $res = $oTargetObj->CheckValue($sAttCode, $value); + if ($res === true) + { + $oTargetObj->Set($sAttCode, $value); + } + else + { + // $res contains the error description + $oRes->LogError("Unexpected value for parameter $sParamName: $res"); + } + } + + /** + * Helper to set an external key + * + * @param string sAttCode + * @param array aExtKeyDesc + * @param DBObject oTargetObj + * @param WebServiceResult oRes + * + */ + protected function MyObjectSetExternalKey($sAttCode, $sParamName, $aExtKeyDesc, &$oTargetObj, &$oRes) + { + $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode); + + $bIsMandatory = !$oExtKey->IsNullAllowed(); + + if (is_null($aExtKeyDesc)) + { + if ($bIsMandatory) + { + $oRes->LogError("Parameter $sParamName: found null for a mandatory key"); + } + else + { + // skip silently + return; + } + } + + if (count($aExtKeyDesc) == 0) + { + $oRes->LogIssue("Parameter $sParamName: no search condition has been specified", $bIsMandatory); + return; + } + + $sKeyClass = $oExtKey->GetTargetClass(); + $oReconFilter = new DBObjectSearch($sKeyClass); + foreach ($aExtKeyDesc as $sForeignAttCode => $value) + { + if (!MetaModel::IsValidFilterCode($sKeyClass, $sForeignAttCode)) + { + $aCodes = array_keys(MetaModel::GetClassFilterDefs($sKeyClass)); + $sMsg = "Parameter $sParamName: '$sForeignAttCode' is not a valid filter code for class '$sKeyClass', expecting a value in {".implode(', ', $aCodes)."}"; + $oRes->LogIssue($sMsg, $bIsMandatory); + } + // The foreign attribute is one of our reconciliation key + $oReconFilter->AddCondition($sForeignAttCode, $value, '='); + } + $oExtObjects = new CMDBObjectSet($oReconFilter); + switch($oExtObjects->Count()) + { + case 0: + $sMsg = "Parameter $sParamName: no match (searched: '".$oReconFilter->ToOQL(true)."')"; + $oRes->LogIssue($sMsg, $bIsMandatory); + break; + case 1: + // Do change the external key attribute + $oForeignObj = $oExtObjects->Fetch(); + $oTargetObj->Set($sAttCode, $oForeignObj->GetKey()); + + // Report it (no need to report if the object already had this value + if (array_key_exists($sAttCode, $oTargetObj->ListChanges())) + { + $oRes->LogInfo("Parameter $sParamName: found match ".get_class($oForeignObj)."::".$oForeignObj->GetKey()." '".$oForeignObj->GetName()."'"); + } + break; + default: + $sMsg = "Parameter $sParamName: Found ".$oExtObjects->Count()." matches (searched: '".$oReconFilter->ToOQL(true)."')"; + $oRes->LogIssue($sMsg, $bIsMandatory); + } + } + + /** + * Helper to link objects + * + * @param string sLinkAttCode + * @param string sLinkedClass + * @param array $aLinkList + * @param DBObject oTargetObj + * @param WebServiceResult oRes + * + * @return array List of objects that could not be found + */ + protected function AddLinkedObjects($sLinkAttCode, $sParamName, $sLinkedClass, $aLinkList, &$oTargetObj, &$oRes) + { + $oLinkAtt = MetaModel::GetAttributeDef(get_class($oTargetObj), $sLinkAttCode); + $sLinkClass = $oLinkAtt->GetLinkedClass(); + $sExtKeyToItem = $oLinkAtt->GetExtKeyToRemote(); + + $aItemsFound = array(); + $aItemsNotFound = array(); + + if (is_null($aLinkList)) + { + return $aItemsNotFound; + } + + foreach ($aLinkList as $aItemData) + { + if (!array_key_exists('class', $aItemData)) + { + $oRes->LogWarning("Parameter $sParamName: missing 'class' specification"); + continue; // skip + } + $sTargetClass = $aItemData['class']; + if (!MetaModel::IsValidClass($sTargetClass)) + { + $oRes->LogError("Parameter $sParamName: invalid class '$sTargetClass'"); + continue; // skip + } + if (!MetaModel::IsParentClass($sLinkedClass, $sTargetClass)) + { + $oRes->LogError("Parameter $sParamName: '$sTargetClass' is not a child class of '$sLinkedClass'"); + continue; // skip + } + $oReconFilter = new DBObjectSearch($sTargetClass); + $aCIStringDesc = array(); + foreach ($aItemData['search'] as $sAttCode => $value) + { + if (!MetaModel::IsValidFilterCode($sTargetClass, $sAttCode)) + { + $aCodes = array_keys(MetaModel::GetClassFilterDefs($sTargetClass)); + $oRes->LogError("Parameter $sParamName: '$sAttCode' is not a valid filter code for class '$sTargetClass', expecting a value in {".implode(', ', $aCodes)."}"); + continue 2; // skip the entire item + } + $aCIStringDesc[] = "$sAttCode: $value"; + + // The attribute is one of our reconciliation key + $oReconFilter->AddCondition($sAttCode, $value, '='); + } + if (count($aCIStringDesc) == 1) + { + // take the last and unique value to describe the object + $sItemDesc = $value; + } + else + { + // describe the object by the given keys + $sItemDesc = $sTargetClass.'('.implode('/', $aCIStringDesc).')'; + } + + $oExtObjects = new CMDBObjectSet($oReconFilter); + switch($oExtObjects->Count()) + { + case 0: + $oRes->LogWarning("Parameter $sParamName: object to link $sLinkedClass / $sItemDesc could not be found (searched: '".$oReconFilter->ToOQL(true)."')"); + $aItemsNotFound[] = $sItemDesc; + break; + case 1: + $aItemsFound[] = array ( + 'object' => $oExtObjects->Fetch(), + 'link_values' => @$aItemData['link_values'], + 'desc' => $sItemDesc, + ); + break; + default: + $oRes->LogWarning("Parameter $sParamName: Found ".$oExtObjects->Count()." matches for item '$sItemDesc' (searched: '".$oReconFilter->ToOQL(true)."')"); + $aItemsNotFound[] = $sItemDesc; + } + } + + if (count($aItemsFound) > 0) + { + $aLinks = array(); + foreach($aItemsFound as $aItemData) + { + $oLink = MetaModel::NewObject($sLinkClass); + $oLink->Set($sExtKeyToItem, $aItemData['object']->GetKey()); + foreach($aItemData['link_values'] as $sKey => $value) + { + if(!MetaModel::IsValidAttCode($sLinkClass, $sKey)) + { + $oRes->LogWarning("Parameter $sParamName: Attaching item '".$aItemData['desc']."', the attribute code '$sKey' is not valid ; check the class '$sLinkClass'"); + } + else + { + $oLink->Set($sKey, $value); + } + } + $aLinks[] = $oLink; + } + $oImpactedInfraSet = DBObjectSet::FromArray($sLinkClass, $aLinks); + $oTargetObj->Set($sLinkAttCode, $oImpactedInfraSet); + } + + return $aItemsNotFound; + } + + protected function MyObjectInsert($oTargetObj, $sResultLabel, $oChange, &$oRes) + { + if ($oRes->IsOk()) + { + list($bRes, $aIssues) = $oTargetObj->CheckToWrite(); + if ($bRes) + { + $iId = $oTargetObj->DBInsertTrackedNoReload($oChange); + $oRes->LogInfo("Created object ".get_class($oTargetObj)."::$iId"); + $oRes->AddResultObject($sResultLabel, $oTargetObj); + } + else + { + $oRes->LogError("The ticket could not be created due to forbidden values (or inconsistent values)"); + foreach($aIssues as $iIssue => $sIssue) + { + $oRes->LogError("Issue #$iIssue: $sIssue"); + } + } + } + } + + + static protected function SoapStructToExternalKeySearch($oExternalKeySearch) + { + if (is_null($oExternalKeySearch)) return null; + if ($oExternalKeySearch->IsVoid()) return null; + + $aRes = array(); + foreach($oExternalKeySearch->conditions as $oSearchCondition) + { + $aRes[$oSearchCondition->attcode] = $oSearchCondition->value; + } + return $aRes; + } + + static protected function SoapStructToLinkCreationSpec(SoapLinkCreationSpec $oLinkCreationSpec) + { + $aRes = array + ( + 'class' => $oLinkCreationSpec->class, + 'search' => array(), + 'link_values' => array(), + ); + + foreach($oLinkCreationSpec->conditions as $oSearchCondition) + { + $aRes['search'][$oSearchCondition->attcode] = $oSearchCondition->value; + } + + foreach($oLinkCreationSpec->attributes as $oAttributeValue) + { + $aRes['link_values'][$oAttributeValue->attcode] = $oAttributeValue->value; + } + + return $aRes; + } + + static protected function SoapStructToAssociativeArray($aArrayOfAssocArray) + { + if (is_null($aArrayOfAssocArray)) return array(); + + $aRes = array(); + foreach($aArrayOfAssocArray as $aAssocArray) + { + $aRow = array(); + foreach ($aAssocArray as $oKeyValuePair) + { + $aRow[$oKeyValuePair->key] = $oKeyValuePair->value; + } + $aRes[] = $aRow; + } + return $aRes; + } +} +?>