From 1fed66fff3da0a8070ca0bca60455e084e901f4d Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Fri, 27 Oct 2017 08:56:45 +0000 Subject: [PATCH] N.1117 some PHPDoc modifications SVN:trunk[5104] --- core/dbsearch.class.php | 1691 +++++++++--------- pages/UI.php | 3581 ++++++++++++++++++++------------------- 2 files changed, 2640 insertions(+), 2632 deletions(-) diff --git a/core/dbsearch.class.php b/core/dbsearch.class.php index 6ee33e931..6c8d6fba1 100644 --- a/core/dbsearch.class.php +++ b/core/dbsearch.class.php @@ -1,843 +1,848 @@ - - - -require_once('dbobjectsearch.class.php'); -require_once('dbunionsearch.class.php'); - -/** - * An object search - * - * Note: in the ancient times of iTop, a search was named after DBObjectSearch. - * When the UNION has been introduced, it has been decided to: - * - declare a hierarchy of search classes, with two leafs : - * - one class to cope with a single query (A JOIN B... WHERE...) - * - and the other to cope with several queries (query1 UNION query2) - * - in order to preserve forward/backward compatibility of the existing modules - * - keep the name of DBObjectSearch even if it a little bit confusing - * - do not provide a type-hint for function parameters defined in the modules - * - leave the statements DBObjectSearch::FromOQL in the modules, though DBSearch is more relevant - * - * @copyright Copyright (C) 2015-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -abstract class DBSearch -{ - const JOIN_POINTING_TO = 0; - const JOIN_REFERENCED_BY = 1; - - protected $m_bNoContextParameters = false; - protected $m_aModifierProperties = array(); - protected $m_bArchiveMode = false; - protected $m_bShowObsoleteData = true; - - public function __construct() - { - $this->m_bArchiveMode = utils::IsArchiveMode(); - } - - /** - * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects) - **/ - public function DeepClone() - { - return unserialize(serialize($this)); // Beware this serializes/unserializes the search and its parameters as well - } - - abstract public function AllowAllData(); - abstract public function IsAllDataAllowed(); - - public function SetArchiveMode($bEnable) - { - $this->m_bArchiveMode = $bEnable; - } - public function GetArchiveMode() - { - return $this->m_bArchiveMode; - } - - public function SetShowObsoleteData($bShow) - { - $this->m_bShowObsoleteData = $bShow; - } - public function GetShowObsoleteData() - { - if ($this->m_bArchiveMode || $this->IsAllDataAllowed()) - { - // Enable obsolete data too! - $bRet = true; - } - else - { - $bRet = $this->m_bShowObsoleteData; - } - return $bRet; - } - - public function NoContextParameters() {$this->m_bNoContextParameters = true;} - public function HasContextParameters() {return $this->m_bNoContextParameters;} - - 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(); - } - } - - abstract public function GetClassName($sAlias); - abstract public function GetClass(); - abstract public function GetClassAlias(); - - /** - * 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 - */ - abstract public function ChangeClass($sNewClass, $sAlias = null); - abstract public function GetSelectedClasses(); - - /** - * @param array $aSelectedClasses array of aliases - * @throws CoreException - */ - abstract public function SetSelectedClasses($aSelectedClasses); - - /** - * Change any alias of the query tree - * - * @param $sOldName - * @param $sNewName - * @return bool True if the alias has been found and changed - */ - abstract public function RenameAlias($sOldName, $sNewName); - - abstract public function IsAny(); - - public function Describe(){return 'deprecated - use ToOQL() instead';} - public function DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo){return 'deprecated - use ToOQL() instead';} - public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode){return 'deprecated - use ToOQL() instead';} - public function DescribeConditionRelTo($aRelInfo){return 'deprecated - use ToOQL() instead';} - public function DescribeConditions(){return 'deprecated - use ToOQL() instead';} - public function __DescribeHTML(){return 'deprecated - use ToOQL() instead';} - - abstract public function ResetCondition(); - abstract public function MergeConditionExpression($oExpression); - abstract public function AddConditionExpression($oExpression); - abstract public function AddNameCondition($sName); - abstract public function AddCondition($sFilterCode, $value, $sOpCode = null); - /** - * 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 - */ - abstract public function AddConditionAdvanced($sAttSpec, $value); - abstract public function 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 - */ - abstract public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null); - - /** - * @param DBObjectSearch $oFilter - * @param $sForeignExtKeyAttCode - * @param int $iOperatorCode - * @param null $aRealiasingMap array of => , for each alias that has changed - */ - abstract public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null); - - abstract public function Intersect(DBSearch $oFilter); - - /** - * @param DBSearch $oFilter - * @param integer $iDirection - * @param string $sExtKeyAttCode - * @param integer $iOperatorCode - * @param array &$RealisasingMap Map of aliases from the attached query, that could have been renamed by the optimization process - * @return DBSearch - */ - public function Join(DBSearch $oFilter, $iDirection, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) - { - $oSourceFilter = $this->DeepClone(); - $oRet = null; - - if ($oFilter instanceof DBUnionSearch) - { - $aSearches = array(); - foreach ($oFilter->GetSearches() as $oSearch) - { - $aSearches[] = $oSourceFilter->Join($oSearch, $iDirection, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); - } - $oRet = new DBUnionSearch($aSearches); - } - else - { - if ($iDirection === static::JOIN_POINTING_TO) - { - $oSourceFilter->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); - } - else - { - if ($iOperatorCode !== TREE_OPERATOR_EQUALS) - { - throw new Exception('Only TREE_OPERATOR_EQUALS operator code is supported yet for AddCondition_ReferencedBy.'); - } - $oSourceFilter->AddCondition_ReferencedBy($oFilter, $sExtKeyAttCode, TREE_OPERATOR_EQUALS, $aRealiasingMap); - } - $oRet = $oSourceFilter; - } - - return $oRet; - } - - abstract public function SetInternalParams($aParams); - abstract public function GetInternalParams(); - abstract public function GetQueryParams($bExcludeMagicParams = true); - abstract public function ListConstantFields(); - - /** - * Turn the parameters (:xxx) into scalar values in order to easily - * serialize a search - */ - abstract public function ApplyParameters($aArgs); - - public function serialize($bDevelopParams = false, $aContextParams = null) - { - $sOql = $this->ToOql($bDevelopParams, $aContextParams); - return base64_encode(serialize(array($sOql, $this->GetInternalParams(), $this->m_aModifierProperties))); - } - - static public function unserialize($sValue) - { - $aData = unserialize(base64_decode($sValue)); - $sOql = $aData[0]; - $aParams = $aData[1]; - // We've tried to use gzcompress/gzuncompress, but for some specific queries - // it was not working at all (See Trac #193) - // gzuncompress was issuing a warning "data error" and the return object was null - $oRetFilter = self::FromOQL($sOql, $aParams); - $oRetFilter->m_aModifierProperties = $aData[2]; - return $oRetFilter; - } - - /** - * Create a new DBObjectSearch from $oSearch with a new alias $sAlias - * - * Note : This has not be tested with UNION queries. - * - * @param DBSearch $oSearch - * @param string $sAlias - * @return DBObjectSearch - */ - static public function CloneWithAlias(DBSearch $oSearch, $sAlias) - { - $oSearchWithAlias = new DBObjectSearch($oSearch->GetClass(), $sAlias); - $oSearchWithAlias = $oSearchWithAlias->Intersect($oSearch); - return $oSearchWithAlias; - } - - abstract public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false); - - static protected $m_aOQLQueries = array(); - - // Do not filter out depending on user rights - // In particular when we are currently in the process of evaluating the user rights... - static public function FromOQL_AllData($sQuery, $aParams = null) - { - $oRes = self::FromOQL($sQuery, $aParams); - $oRes->AllowAllData(); - return $oRes; - } - - /** - * @param string $sQuery - * @param array $aParams - * @return DBSearch - * @throws OQLException - */ - static public function FromOQL($sQuery, $aParams = null) - { - if (empty($sQuery)) return null; - - // Query caching - $sQueryId = md5($sQuery); - $bOQLCacheEnabled = true; - if ($bOQLCacheEnabled) - { - if (array_key_exists($sQueryId, self::$m_aOQLQueries)) - { - // hit! - $oResultFilter = self::$m_aOQLQueries[$sQueryId]->DeepClone(); - } - elseif (self::$m_bUseAPCCache) - { - // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter - // - $sAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-dbsearch-cache-'.$sQueryId; - $oKPI = new ExecutionKPI(); - $result = apc_fetch($sAPCCacheId); - $oKPI->ComputeStats('Search APC (fetch)', $sQuery); - - if (is_object($result)) - { - $oResultFilter = $result; - self::$m_aOQLQueries[$sQueryId] = $oResultFilter->DeepClone(); - } - } - } - - if (!isset($oResultFilter)) - { - $oKPI = new ExecutionKPI(); - - $oOql = new OqlInterpreter($sQuery); - $oOqlQuery = $oOql->ParseQuery(); - - $oMetaModel = new ModelReflectionRuntime(); - $oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue - - $oResultFilter = $oOqlQuery->ToDBSearch($sQuery); - - $oKPI->ComputeStats('Parse OQL', $sQuery); - - if ($bOQLCacheEnabled) - { - self::$m_aOQLQueries[$sQueryId] = $oResultFilter->DeepClone(); - - if (self::$m_bUseAPCCache) - { - $oKPI = new ExecutionKPI(); - apc_store($sAPCCacheId, $oResultFilter, self::$m_iQueryCacheTTL); - $oKPI->ComputeStats('Search APC (store)', $sQueryId); - } - } - } - - if (!is_null($aParams)) - { - $oResultFilter->SetInternalParams($aParams); - } - return $oResultFilter; - } - - // Alternative to object mapping: the data are transfered directly into an array - // This is 10 times faster than creating a set of objects, and makes sense when optimization is required - /** - * @param hash $aOrderBy Array of '[.]attcode' => bAscending - */ - public function ToDataArray($aColumns = array(), $aOrderBy = array(), $aArgs = array()) - { - $sSQL = $this->MakeSelectQuery($aOrderBy, $aArgs); - $resQuery = CMDBSource::Query($sSQL); - if (!$resQuery) return; - - if (count($aColumns) == 0) - { - $aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass())); - // Add the standard id (as first column) - array_unshift($aColumns, 'id'); - } - - $aQueryCols = CMDBSource::GetColumns($resQuery); - - $sClassAlias = $this->GetClassAlias(); - $aColMap = array(); - foreach ($aColumns as $sAttCode) - { - $sColName = $sClassAlias.$sAttCode; - if (in_array($sColName, $aQueryCols)) - { - $aColMap[$sAttCode] = $sColName; - } - } - - $aRes = array(); - while ($aRow = CMDBSource::FetchArray($resQuery)) - { - $aMappedRow = array(); - foreach ($aColMap as $sAttCode => $sColName) - { - $aMappedRow[$sAttCode] = $aRow[$sColName]; - } - $aRes[] = $aMappedRow; - } - CMDBSource::FreeResult($resQuery); - return $aRes; - } - - //////////////////////////////////////////////////////////////////////////// - // - // Construction of the SQL queries - // - //////////////////////////////////////////////////////////////////////////// - protected static $m_aQueryStructCache = array(); - - - public function MakeGroupByQuery($aArgs, $aGroupByExpr, $bExcludeNullValues = false) - { - if ($bExcludeNullValues) - { - // Null values are not handled (though external keys set to 0 are allowed) - $oQueryFilter = $this->DeepClone(); - foreach ($aGroupByExpr as $oGroupByExp) - { - $oNull = new FunctionExpression('ISNULL', array($oGroupByExp)); - $oNotNull = new BinaryExpression($oNull, '!=', new TrueExpression()); - $oQueryFilter->AddConditionExpression($oNotNull); - } - } - else - { - $oQueryFilter = $this; - } - - $aAttToLoad = array(); - $oSQLQuery = $oQueryFilter->GetSQLQuery(array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr); - - $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); - try - { - $bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries; - $sRes = $oSQLQuery->RenderGroupBy($aScalarArgs, $bBeautifulSQL); - } - catch (MissingQueryArgument $e) - { - // Add some information... - $e->addInfo('OQL', $this->ToOQL()); - throw $e; - } - $this->AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sRes); - return $sRes; - } - - - /** - * @param array|hash $aOrderBy Array of '[.]attcode' => bAscending - * @param array $aArgs - * @param null $aAttToLoad - * @param null $aExtendedDataSpec - * @param int $iLimitCount - * @param int $iLimitStart - * @param bool $bGetCount - * @return string - * @throws CoreException - * @throws Exception - * @throws MissingQueryArgument - */ - public function MakeSelectQuery($aOrderBy = array(), $aArgs = array(), $aAttToLoad = null, $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false) - { - // Check the order by specification, and prefix with the class alias - // and make sure that the ordering columns are going to be selected - // - $sClass = $this->GetClass(); - $sClassAlias = $this->GetClassAlias(); - $aOrderSpec = array(); - foreach ($aOrderBy as $sFieldAlias => $bAscending) - { - if (!is_bool($bAscending)) - { - throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); - } - - $iDotPos = strpos($sFieldAlias, '.'); - if ($iDotPos === false) - { - $sAttClass = $sClass; - $sAttClassAlias = $sClassAlias; - $sAttCode = $sFieldAlias; - } - else - { - $sAttClassAlias = substr($sFieldAlias, 0, $iDotPos); - $sAttClass = $this->GetClassName($sAttClassAlias); - $sAttCode = substr($sFieldAlias, $iDotPos + 1); - } - - if ($sAttCode != 'id') - { - MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sAttCode, MetaModel::GetAttributesList($sAttClass)); - - $oAttDef = MetaModel::GetAttributeDef($sAttClass, $sAttCode); - foreach($oAttDef->GetOrderBySQLExpressions($sAttClassAlias) as $sSQLExpression) - { - $aOrderSpec[$sSQLExpression] = $bAscending; - } - } - else - { - $aOrderSpec['`'.$sAttClassAlias.$sAttCode.'`'] = $bAscending; - } - - // Make sure that the columns used for sorting are present in the loaded columns - if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sAttClassAlias][$sAttCode])) - { - $aAttToLoad[$sAttClassAlias][$sAttCode] = MetaModel::GetAttributeDef($sAttClass, $sAttCode); - } - } - - $oSQLQuery = $this->GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount); - - if ($this->m_bNoContextParameters) - { - // Only internal parameters - $aScalarArgs = $this->GetInternalParams(); - } - else - { - // The complete list of arguments will include magic arguments (e.g. current_user->attcode) - $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); - } - try - { - $bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries; - $sRes = $oSQLQuery->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount, $bBeautifulSQL); - if ($sClassAlias == '_itop_') - { - IssueLog::Info('SQL Query (_itop_): '.$sRes); - } - } - catch (MissingQueryArgument $e) - { - // Add some information... - $e->addInfo('OQL', $this->ToOQL()); - throw $e; - } - $this->AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sRes); - return $sRes; - } - - - protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null) - { - $oSQLQuery = $this->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr); - $oSQLQuery->SetSourceOQL($this->ToOQL()); - - // Join to an additional table, if required... - // - if ($aExtendedDataSpec != null) - { - $sTableAlias = '_extended_data_'; - $aExtendedFields = array(); - foreach($aExtendedDataSpec['fields'] as $sColumn) - { - $sColRef = $this->GetClassAlias().'_extdata_'.$sColumn; - $aExtendedFields[$sColRef] = new FieldExpressionResolved($sColumn, $sTableAlias); - } - $oSQLQueryExt = new SQLObjectQuery($aExtendedDataSpec['table'], $sTableAlias, $aExtendedFields); - $oSQLQuery->AddInnerJoin($oSQLQueryExt, 'id', $aExtendedDataSpec['join_key'] /*, $sTableAlias*/); - } - - return $oSQLQuery; - } - - //////////////////////////////////////////////////////////////////////////// - // - // Cache/Trace/Log queries - // - //////////////////////////////////////////////////////////////////////////// - protected static $m_bDebugQuery = false; - protected static $m_aQueriesLog = array(); - protected static $m_bQueryCacheEnabled = false; - protected static $m_bUseAPCCache = false; - protected static $m_iQueryCacheTTL = 3600; - protected static $m_bTraceQueries = false; - protected static $m_bIndentQueries = false; - protected static $m_bOptimizeQueries = false; - - public static function StartDebugQuery() - { - $aBacktrace = debug_backtrace(); - self::$m_bDebugQuery = true; - } - public static function StopDebugQuery() - { - self::$m_bDebugQuery = false; - } - - public static function EnableQueryCache($bEnabled, $bUseAPC, $iTimeToLive = 3600) - { - self::$m_bQueryCacheEnabled = $bEnabled; - self::$m_bUseAPCCache = $bUseAPC; - self::$m_iQueryCacheTTL = $iTimeToLive; - } - public static function EnableQueryTrace($bEnabled) - { - self::$m_bTraceQueries = $bEnabled; - } - public static function EnableQueryIndentation($bEnabled) - { - self::$m_bIndentQueries = $bEnabled; - } - public static function EnableOptimizeQuery($bEnabled) - { - self::$m_bOptimizeQueries = $bEnabled; - } - - - protected function AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sSql) - { - if (self::$m_bTraceQueries) - { - $aQueryData = array( - 'type' => 'select', - 'filter' => $this, - 'order_by' => $aOrderBy, - 'args' => $aArgs, - 'att_to_load' => $aAttToLoad, - 'extended_data_spec' => $aExtendedDataSpec, - 'limit_count' => $iLimitCount, - 'limit_start' => $iLimitStart, - 'is_count' => $bGetCount - ); - $sOql = $this->ToOQL(true, $aArgs); - self::AddQueryTrace($aQueryData, $sOql, $sSql); - } - } - - protected function AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sSql) - { - if (self::$m_bTraceQueries) - { - $aQueryData = array( - 'type' => 'group_by', - 'filter' => $this, - 'args' => $aArgs, - 'group_by_expr' => $aGroupByExpr - ); - $sOql = $this->ToOQL(true, $aArgs); - self::AddQueryTrace($aQueryData, $sOql, $sSql); - } - } - - protected static function AddQueryTrace($aQueryData, $sOql, $sSql) - { - if (self::$m_bTraceQueries) - { - $sQueryId = md5(serialize($aQueryData)); - $sMySQLQueryId = md5($sSql); - if(!isset(self::$m_aQueriesLog[$sQueryId])) - { - self::$m_aQueriesLog[$sQueryId]['data'] = serialize($aQueryData); - self::$m_aQueriesLog[$sQueryId]['oql'] = $sOql; - self::$m_aQueriesLog[$sQueryId]['hits'] = 1; - } - else - { - self::$m_aQueriesLog[$sQueryId]['hits']++; - } - if(!isset(self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId])) - { - self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['sql'] = $sSql; - self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count'] = 1; - $iTableCount = count(CMDBSource::ExplainQuery($sSql)); - self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['table_count'] = $iTableCount; - } - else - { - self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count']++; - } - } - } - - public static function RecordQueryTrace() - { - if (!self::$m_bTraceQueries) return; - - $iOqlCount = count(self::$m_aQueriesLog); - $iSqlCount = 0; - foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData) - { - $iSqlCount += $aOqlData['hits']; - } - $sHtml = "

Stats on SELECT queries: OQL=$iOqlCount, SQL=$iSqlCount

\n"; - foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData) - { - $sOql = $aOqlData['oql']; - $sHits = $aOqlData['hits']; - - $sHtml .= "

$sHits hits for OQL query: $sOql

\n"; - $sHtml .= "
    \n"; - foreach($aOqlData['queries'] as $aSqlData) - { - $sQuery = $aSqlData['sql']; - $sSqlHits = $aSqlData['count']; - $iTableCount = $aSqlData['table_count']; - $sHtml .= "
  • $sSqlHits hits for SQL ($iTableCount tables):
    $sQuery
  • \n"; - } - $sHtml .= "
\n"; - } - - $sLogFile = 'queries.latest'; - file_put_contents(APPROOT.'data/'.$sLogFile.'.html', $sHtml); - - $sLog = " $aOqlData) - { - if (!array_key_exists($sQueryId, $aQueriesLog)) - { - $aQueriesLog[$sQueryId] = $aOqlData; - } - } - } - else - { - $aQueriesLog = self::$m_aQueriesLog; - } - $sLog = "".$aBacktrace[1]["function"].""; - - if (is_string($value)) - { - echo "$sIndent$sFunction: $value
\n"; - } - else if (is_object($value)) - { - echo "$sIndent$sFunction:\n
\n";
-			print_r($value);
-			echo "
\n"; - } - else - { - echo "$sIndent$sFunction: $value
\n"; - } - } - - /** - * Experimental! - * todo: implement the change tracking - * - * @param $bArchive - * @throws Exception - */ - function DBBulkWriteArchiveFlag($bArchive) - { - $sClass = $this->GetClass(); - if (!MetaModel::IsArchivable($sClass)) - { - throw new Exception($sClass.' is not an archivable class'); - } - - $iFlag = $bArchive ? 1 : 0; - - $oSet = new DBObjectSet($this); - if (MetaModel::IsStandaloneClass($sClass)) - { - $oSet->OptimizeColumnLoad(array($this->GetClassAlias() => array(''))); - $aIds = array($sClass => $oSet->GetColumnAsArray('id')); - } - else - { - $oSet->OptimizeColumnLoad(array($this->GetClassAlias() => array('finalclass'))); - $aTemp = $oSet->GetColumnAsArray('finalclass'); - $aIds = array(); - foreach ($aTemp as $iObjectId => $sObjectClass) - { - $aIds[$sObjectClass][$iObjectId] = $iObjectId; - } - } - foreach ($aIds as $sFinalClass => $aObjectIds) - { - $sIds = implode(', ', $aObjectIds); - - $sArchiveRoot = MetaModel::GetAttributeOrigin($sFinalClass, 'archive_flag'); - $sRootTable = MetaModel::DBGetTable($sArchiveRoot); - $sRootKey = MetaModel::DBGetKey($sArchiveRoot); - $aJoins = array("`$sRootTable`"); - $aUpdates = array(); - foreach (MetaModel::EnumParentClasses($sFinalClass, 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) - { - // Set the date (do not change it) - $sDate = '"'.date(AttributeDate::GetSQLFormat()).'"'; - $aUpdates[] = "`$sTable`.`archive_date` = coalesce(`$sTable`.`archive_date`, $sDate)"; - } - else - { - // Reset the date - $aUpdates[] = "`$sTable`.`archive_date` = null"; - } - } - 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` IN ($sIds)"; - CMDBSource::Query($sUpdateQuery); - } - } - - public function UpdateContextFromUser() - { - $this->SetShowObsoleteData(utils::ShowObsoleteData()); - } -} + + + +require_once('dbobjectsearch.class.php'); +require_once('dbunionsearch.class.php'); + +/** + * An object search + * + * Note: in the ancient times of iTop, a search was named after DBObjectSearch. + * When the UNION has been introduced, it has been decided to: + * - declare a hierarchy of search classes, with two leafs : + * - one class to cope with a single query (A JOIN B... WHERE...) + * - and the other to cope with several queries (query1 UNION query2) + * - in order to preserve forward/backward compatibility of the existing modules + * - keep the name of DBObjectSearch even if it a little bit confusing + * - do not provide a type-hint for function parameters defined in the modules + * - leave the statements DBObjectSearch::FromOQL in the modules, though DBSearch is more relevant + * + * @copyright Copyright (C) 2015-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +abstract class DBSearch +{ + const JOIN_POINTING_TO = 0; + const JOIN_REFERENCED_BY = 1; + + protected $m_bNoContextParameters = false; + protected $m_aModifierProperties = array(); + protected $m_bArchiveMode = false; + protected $m_bShowObsoleteData = true; + + public function __construct() + { + $this->m_bArchiveMode = utils::IsArchiveMode(); + } + + /** + * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects) + **/ + public function DeepClone() + { + return unserialize(serialize($this)); // Beware this serializes/unserializes the search and its parameters as well + } + + abstract public function AllowAllData(); + abstract public function IsAllDataAllowed(); + + public function SetArchiveMode($bEnable) + { + $this->m_bArchiveMode = $bEnable; + } + public function GetArchiveMode() + { + return $this->m_bArchiveMode; + } + + public function SetShowObsoleteData($bShow) + { + $this->m_bShowObsoleteData = $bShow; + } + public function GetShowObsoleteData() + { + if ($this->m_bArchiveMode || $this->IsAllDataAllowed()) + { + // Enable obsolete data too! + $bRet = true; + } + else + { + $bRet = $this->m_bShowObsoleteData; + } + return $bRet; + } + + public function NoContextParameters() {$this->m_bNoContextParameters = true;} + public function HasContextParameters() {return $this->m_bNoContextParameters;} + + 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(); + } + } + + abstract public function GetClassName($sAlias); + abstract public function GetClass(); + abstract public function GetClassAlias(); + + /** + * 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 + */ + abstract public function ChangeClass($sNewClass, $sAlias = null); + abstract public function GetSelectedClasses(); + + /** + * @param array $aSelectedClasses array of aliases + * @throws CoreException + */ + abstract public function SetSelectedClasses($aSelectedClasses); + + /** + * Change any alias of the query tree + * + * @param $sOldName + * @param $sNewName + * @return bool True if the alias has been found and changed + */ + abstract public function RenameAlias($sOldName, $sNewName); + + abstract public function IsAny(); + + public function Describe(){return 'deprecated - use ToOQL() instead';} + public function DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo){return 'deprecated - use ToOQL() instead';} + public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode){return 'deprecated - use ToOQL() instead';} + public function DescribeConditionRelTo($aRelInfo){return 'deprecated - use ToOQL() instead';} + public function DescribeConditions(){return 'deprecated - use ToOQL() instead';} + public function __DescribeHTML(){return 'deprecated - use ToOQL() instead';} + + abstract public function ResetCondition(); + abstract public function MergeConditionExpression($oExpression); + abstract public function AddConditionExpression($oExpression); + abstract public function AddNameCondition($sName); + abstract public function AddCondition($sFilterCode, $value, $sOpCode = null); + /** + * 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 + */ + abstract public function AddConditionAdvanced($sAttSpec, $value); + abstract public function 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 + */ + abstract public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null); + + /** + * @param DBObjectSearch $oFilter + * @param $sForeignExtKeyAttCode + * @param int $iOperatorCode + * @param null $aRealiasingMap array of => , for each alias that has changed + */ + abstract public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null); + + abstract public function Intersect(DBSearch $oFilter); + + /** + * @param DBSearch $oFilter + * @param integer $iDirection + * @param string $sExtKeyAttCode + * @param integer $iOperatorCode + * @param array &$RealisasingMap Map of aliases from the attached query, that could have been renamed by the optimization process + * @return DBSearch + */ + public function Join(DBSearch $oFilter, $iDirection, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) + { + $oSourceFilter = $this->DeepClone(); + $oRet = null; + + if ($oFilter instanceof DBUnionSearch) + { + $aSearches = array(); + foreach ($oFilter->GetSearches() as $oSearch) + { + $aSearches[] = $oSourceFilter->Join($oSearch, $iDirection, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); + } + $oRet = new DBUnionSearch($aSearches); + } + else + { + if ($iDirection === static::JOIN_POINTING_TO) + { + $oSourceFilter->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); + } + else + { + if ($iOperatorCode !== TREE_OPERATOR_EQUALS) + { + throw new Exception('Only TREE_OPERATOR_EQUALS operator code is supported yet for AddCondition_ReferencedBy.'); + } + $oSourceFilter->AddCondition_ReferencedBy($oFilter, $sExtKeyAttCode, TREE_OPERATOR_EQUALS, $aRealiasingMap); + } + $oRet = $oSourceFilter; + } + + return $oRet; + } + + abstract public function SetInternalParams($aParams); + abstract public function GetInternalParams(); + abstract public function GetQueryParams($bExcludeMagicParams = true); + abstract public function ListConstantFields(); + + /** + * Turn the parameters (:xxx) into scalar values in order to easily + * serialize a search + */ + abstract public function ApplyParameters($aArgs); + + public function serialize($bDevelopParams = false, $aContextParams = null) + { + $sOql = $this->ToOql($bDevelopParams, $aContextParams); + return base64_encode(serialize(array($sOql, $this->GetInternalParams(), $this->m_aModifierProperties))); + } + + /** + * @param string $sValue Serialized OQL query + * + * @return \DBSearch + */ + static public function unserialize($sValue) + { + $aData = unserialize(base64_decode($sValue)); + $sOql = $aData[0]; + $aParams = $aData[1]; + // We've tried to use gzcompress/gzuncompress, but for some specific queries + // it was not working at all (See Trac #193) + // gzuncompress was issuing a warning "data error" and the return object was null + $oRetFilter = self::FromOQL($sOql, $aParams); + $oRetFilter->m_aModifierProperties = $aData[2]; + return $oRetFilter; + } + + /** + * Create a new DBObjectSearch from $oSearch with a new alias $sAlias + * + * Note : This has not be tested with UNION queries. + * + * @param DBSearch $oSearch + * @param string $sAlias + * @return DBObjectSearch + */ + static public function CloneWithAlias(DBSearch $oSearch, $sAlias) + { + $oSearchWithAlias = new DBObjectSearch($oSearch->GetClass(), $sAlias); + $oSearchWithAlias = $oSearchWithAlias->Intersect($oSearch); + return $oSearchWithAlias; + } + + abstract public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false); + + static protected $m_aOQLQueries = array(); + + // Do not filter out depending on user rights + // In particular when we are currently in the process of evaluating the user rights... + static public function FromOQL_AllData($sQuery, $aParams = null) + { + $oRes = self::FromOQL($sQuery, $aParams); + $oRes->AllowAllData(); + return $oRes; + } + + /** + * @param string $sQuery + * @param array $aParams + * @return DBSearch + * @throws OQLException + */ + static public function FromOQL($sQuery, $aParams = null) + { + if (empty($sQuery)) return null; + + // Query caching + $sQueryId = md5($sQuery); + $bOQLCacheEnabled = true; + if ($bOQLCacheEnabled) + { + if (array_key_exists($sQueryId, self::$m_aOQLQueries)) + { + // hit! + $oResultFilter = self::$m_aOQLQueries[$sQueryId]->DeepClone(); + } + elseif (self::$m_bUseAPCCache) + { + // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter + // + $sAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-dbsearch-cache-'.$sQueryId; + $oKPI = new ExecutionKPI(); + $result = apc_fetch($sAPCCacheId); + $oKPI->ComputeStats('Search APC (fetch)', $sQuery); + + if (is_object($result)) + { + $oResultFilter = $result; + self::$m_aOQLQueries[$sQueryId] = $oResultFilter->DeepClone(); + } + } + } + + if (!isset($oResultFilter)) + { + $oKPI = new ExecutionKPI(); + + $oOql = new OqlInterpreter($sQuery); + $oOqlQuery = $oOql->ParseQuery(); + + $oMetaModel = new ModelReflectionRuntime(); + $oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue + + $oResultFilter = $oOqlQuery->ToDBSearch($sQuery); + + $oKPI->ComputeStats('Parse OQL', $sQuery); + + if ($bOQLCacheEnabled) + { + self::$m_aOQLQueries[$sQueryId] = $oResultFilter->DeepClone(); + + if (self::$m_bUseAPCCache) + { + $oKPI = new ExecutionKPI(); + apc_store($sAPCCacheId, $oResultFilter, self::$m_iQueryCacheTTL); + $oKPI->ComputeStats('Search APC (store)', $sQueryId); + } + } + } + + if (!is_null($aParams)) + { + $oResultFilter->SetInternalParams($aParams); + } + return $oResultFilter; + } + + // Alternative to object mapping: the data are transfered directly into an array + // This is 10 times faster than creating a set of objects, and makes sense when optimization is required + /** + * @param hash $aOrderBy Array of '[.]attcode' => bAscending + */ + public function ToDataArray($aColumns = array(), $aOrderBy = array(), $aArgs = array()) + { + $sSQL = $this->MakeSelectQuery($aOrderBy, $aArgs); + $resQuery = CMDBSource::Query($sSQL); + if (!$resQuery) return; + + if (count($aColumns) == 0) + { + $aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass())); + // Add the standard id (as first column) + array_unshift($aColumns, 'id'); + } + + $aQueryCols = CMDBSource::GetColumns($resQuery); + + $sClassAlias = $this->GetClassAlias(); + $aColMap = array(); + foreach ($aColumns as $sAttCode) + { + $sColName = $sClassAlias.$sAttCode; + if (in_array($sColName, $aQueryCols)) + { + $aColMap[$sAttCode] = $sColName; + } + } + + $aRes = array(); + while ($aRow = CMDBSource::FetchArray($resQuery)) + { + $aMappedRow = array(); + foreach ($aColMap as $sAttCode => $sColName) + { + $aMappedRow[$sAttCode] = $aRow[$sColName]; + } + $aRes[] = $aMappedRow; + } + CMDBSource::FreeResult($resQuery); + return $aRes; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Construction of the SQL queries + // + //////////////////////////////////////////////////////////////////////////// + protected static $m_aQueryStructCache = array(); + + + public function MakeGroupByQuery($aArgs, $aGroupByExpr, $bExcludeNullValues = false) + { + if ($bExcludeNullValues) + { + // Null values are not handled (though external keys set to 0 are allowed) + $oQueryFilter = $this->DeepClone(); + foreach ($aGroupByExpr as $oGroupByExp) + { + $oNull = new FunctionExpression('ISNULL', array($oGroupByExp)); + $oNotNull = new BinaryExpression($oNull, '!=', new TrueExpression()); + $oQueryFilter->AddConditionExpression($oNotNull); + } + } + else + { + $oQueryFilter = $this; + } + + $aAttToLoad = array(); + $oSQLQuery = $oQueryFilter->GetSQLQuery(array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr); + + $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); + try + { + $bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries; + $sRes = $oSQLQuery->RenderGroupBy($aScalarArgs, $bBeautifulSQL); + } + catch (MissingQueryArgument $e) + { + // Add some information... + $e->addInfo('OQL', $this->ToOQL()); + throw $e; + } + $this->AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sRes); + return $sRes; + } + + + /** + * @param array|hash $aOrderBy Array of '[.]attcode' => bAscending + * @param array $aArgs + * @param null $aAttToLoad + * @param null $aExtendedDataSpec + * @param int $iLimitCount + * @param int $iLimitStart + * @param bool $bGetCount + * @return string + * @throws CoreException + * @throws Exception + * @throws MissingQueryArgument + */ + public function MakeSelectQuery($aOrderBy = array(), $aArgs = array(), $aAttToLoad = null, $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false) + { + // Check the order by specification, and prefix with the class alias + // and make sure that the ordering columns are going to be selected + // + $sClass = $this->GetClass(); + $sClassAlias = $this->GetClassAlias(); + $aOrderSpec = array(); + foreach ($aOrderBy as $sFieldAlias => $bAscending) + { + if (!is_bool($bAscending)) + { + throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); + } + + $iDotPos = strpos($sFieldAlias, '.'); + if ($iDotPos === false) + { + $sAttClass = $sClass; + $sAttClassAlias = $sClassAlias; + $sAttCode = $sFieldAlias; + } + else + { + $sAttClassAlias = substr($sFieldAlias, 0, $iDotPos); + $sAttClass = $this->GetClassName($sAttClassAlias); + $sAttCode = substr($sFieldAlias, $iDotPos + 1); + } + + if ($sAttCode != 'id') + { + MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sAttCode, MetaModel::GetAttributesList($sAttClass)); + + $oAttDef = MetaModel::GetAttributeDef($sAttClass, $sAttCode); + foreach($oAttDef->GetOrderBySQLExpressions($sAttClassAlias) as $sSQLExpression) + { + $aOrderSpec[$sSQLExpression] = $bAscending; + } + } + else + { + $aOrderSpec['`'.$sAttClassAlias.$sAttCode.'`'] = $bAscending; + } + + // Make sure that the columns used for sorting are present in the loaded columns + if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sAttClassAlias][$sAttCode])) + { + $aAttToLoad[$sAttClassAlias][$sAttCode] = MetaModel::GetAttributeDef($sAttClass, $sAttCode); + } + } + + $oSQLQuery = $this->GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount); + + if ($this->m_bNoContextParameters) + { + // Only internal parameters + $aScalarArgs = $this->GetInternalParams(); + } + else + { + // The complete list of arguments will include magic arguments (e.g. current_user->attcode) + $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); + } + try + { + $bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries; + $sRes = $oSQLQuery->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount, $bBeautifulSQL); + if ($sClassAlias == '_itop_') + { + IssueLog::Info('SQL Query (_itop_): '.$sRes); + } + } + catch (MissingQueryArgument $e) + { + // Add some information... + $e->addInfo('OQL', $this->ToOQL()); + throw $e; + } + $this->AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sRes); + return $sRes; + } + + + protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null) + { + $oSQLQuery = $this->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr); + $oSQLQuery->SetSourceOQL($this->ToOQL()); + + // Join to an additional table, if required... + // + if ($aExtendedDataSpec != null) + { + $sTableAlias = '_extended_data_'; + $aExtendedFields = array(); + foreach($aExtendedDataSpec['fields'] as $sColumn) + { + $sColRef = $this->GetClassAlias().'_extdata_'.$sColumn; + $aExtendedFields[$sColRef] = new FieldExpressionResolved($sColumn, $sTableAlias); + } + $oSQLQueryExt = new SQLObjectQuery($aExtendedDataSpec['table'], $sTableAlias, $aExtendedFields); + $oSQLQuery->AddInnerJoin($oSQLQueryExt, 'id', $aExtendedDataSpec['join_key'] /*, $sTableAlias*/); + } + + return $oSQLQuery; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Cache/Trace/Log queries + // + //////////////////////////////////////////////////////////////////////////// + protected static $m_bDebugQuery = false; + protected static $m_aQueriesLog = array(); + protected static $m_bQueryCacheEnabled = false; + protected static $m_bUseAPCCache = false; + protected static $m_iQueryCacheTTL = 3600; + protected static $m_bTraceQueries = false; + protected static $m_bIndentQueries = false; + protected static $m_bOptimizeQueries = false; + + public static function StartDebugQuery() + { + $aBacktrace = debug_backtrace(); + self::$m_bDebugQuery = true; + } + public static function StopDebugQuery() + { + self::$m_bDebugQuery = false; + } + + public static function EnableQueryCache($bEnabled, $bUseAPC, $iTimeToLive = 3600) + { + self::$m_bQueryCacheEnabled = $bEnabled; + self::$m_bUseAPCCache = $bUseAPC; + self::$m_iQueryCacheTTL = $iTimeToLive; + } + public static function EnableQueryTrace($bEnabled) + { + self::$m_bTraceQueries = $bEnabled; + } + public static function EnableQueryIndentation($bEnabled) + { + self::$m_bIndentQueries = $bEnabled; + } + public static function EnableOptimizeQuery($bEnabled) + { + self::$m_bOptimizeQueries = $bEnabled; + } + + + protected function AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sSql) + { + if (self::$m_bTraceQueries) + { + $aQueryData = array( + 'type' => 'select', + 'filter' => $this, + 'order_by' => $aOrderBy, + 'args' => $aArgs, + 'att_to_load' => $aAttToLoad, + 'extended_data_spec' => $aExtendedDataSpec, + 'limit_count' => $iLimitCount, + 'limit_start' => $iLimitStart, + 'is_count' => $bGetCount + ); + $sOql = $this->ToOQL(true, $aArgs); + self::AddQueryTrace($aQueryData, $sOql, $sSql); + } + } + + protected function AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sSql) + { + if (self::$m_bTraceQueries) + { + $aQueryData = array( + 'type' => 'group_by', + 'filter' => $this, + 'args' => $aArgs, + 'group_by_expr' => $aGroupByExpr + ); + $sOql = $this->ToOQL(true, $aArgs); + self::AddQueryTrace($aQueryData, $sOql, $sSql); + } + } + + protected static function AddQueryTrace($aQueryData, $sOql, $sSql) + { + if (self::$m_bTraceQueries) + { + $sQueryId = md5(serialize($aQueryData)); + $sMySQLQueryId = md5($sSql); + if(!isset(self::$m_aQueriesLog[$sQueryId])) + { + self::$m_aQueriesLog[$sQueryId]['data'] = serialize($aQueryData); + self::$m_aQueriesLog[$sQueryId]['oql'] = $sOql; + self::$m_aQueriesLog[$sQueryId]['hits'] = 1; + } + else + { + self::$m_aQueriesLog[$sQueryId]['hits']++; + } + if(!isset(self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId])) + { + self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['sql'] = $sSql; + self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count'] = 1; + $iTableCount = count(CMDBSource::ExplainQuery($sSql)); + self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['table_count'] = $iTableCount; + } + else + { + self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count']++; + } + } + } + + public static function RecordQueryTrace() + { + if (!self::$m_bTraceQueries) return; + + $iOqlCount = count(self::$m_aQueriesLog); + $iSqlCount = 0; + foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData) + { + $iSqlCount += $aOqlData['hits']; + } + $sHtml = "

Stats on SELECT queries: OQL=$iOqlCount, SQL=$iSqlCount

\n"; + foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData) + { + $sOql = $aOqlData['oql']; + $sHits = $aOqlData['hits']; + + $sHtml .= "

$sHits hits for OQL query: $sOql

\n"; + $sHtml .= "
    \n"; + foreach($aOqlData['queries'] as $aSqlData) + { + $sQuery = $aSqlData['sql']; + $sSqlHits = $aSqlData['count']; + $iTableCount = $aSqlData['table_count']; + $sHtml .= "
  • $sSqlHits hits for SQL ($iTableCount tables):
    $sQuery
  • \n"; + } + $sHtml .= "
\n"; + } + + $sLogFile = 'queries.latest'; + file_put_contents(APPROOT.'data/'.$sLogFile.'.html', $sHtml); + + $sLog = " $aOqlData) + { + if (!array_key_exists($sQueryId, $aQueriesLog)) + { + $aQueriesLog[$sQueryId] = $aOqlData; + } + } + } + else + { + $aQueriesLog = self::$m_aQueriesLog; + } + $sLog = "".$aBacktrace[1]["function"].""; + + if (is_string($value)) + { + echo "$sIndent$sFunction: $value
\n"; + } + else if (is_object($value)) + { + echo "$sIndent$sFunction:\n
\n";
+			print_r($value);
+			echo "
\n"; + } + else + { + echo "$sIndent$sFunction: $value
\n"; + } + } + + /** + * Experimental! + * todo: implement the change tracking + * + * @param $bArchive + * @throws Exception + */ + function DBBulkWriteArchiveFlag($bArchive) + { + $sClass = $this->GetClass(); + if (!MetaModel::IsArchivable($sClass)) + { + throw new Exception($sClass.' is not an archivable class'); + } + + $iFlag = $bArchive ? 1 : 0; + + $oSet = new DBObjectSet($this); + if (MetaModel::IsStandaloneClass($sClass)) + { + $oSet->OptimizeColumnLoad(array($this->GetClassAlias() => array(''))); + $aIds = array($sClass => $oSet->GetColumnAsArray('id')); + } + else + { + $oSet->OptimizeColumnLoad(array($this->GetClassAlias() => array('finalclass'))); + $aTemp = $oSet->GetColumnAsArray('finalclass'); + $aIds = array(); + foreach ($aTemp as $iObjectId => $sObjectClass) + { + $aIds[$sObjectClass][$iObjectId] = $iObjectId; + } + } + foreach ($aIds as $sFinalClass => $aObjectIds) + { + $sIds = implode(', ', $aObjectIds); + + $sArchiveRoot = MetaModel::GetAttributeOrigin($sFinalClass, 'archive_flag'); + $sRootTable = MetaModel::DBGetTable($sArchiveRoot); + $sRootKey = MetaModel::DBGetKey($sArchiveRoot); + $aJoins = array("`$sRootTable`"); + $aUpdates = array(); + foreach (MetaModel::EnumParentClasses($sFinalClass, 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) + { + // Set the date (do not change it) + $sDate = '"'.date(AttributeDate::GetSQLFormat()).'"'; + $aUpdates[] = "`$sTable`.`archive_date` = coalesce(`$sTable`.`archive_date`, $sDate)"; + } + else + { + // Reset the date + $aUpdates[] = "`$sTable`.`archive_date` = null"; + } + } + 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` IN ($sIds)"; + CMDBSource::Query($sUpdateQuery); + } + } + + public function UpdateContextFromUser() + { + $this->SetShowObsoleteData(utils::ShowObsoleteData()); + } +} diff --git a/pages/UI.php b/pages/UI.php index b57dfed2f..ec5a1a352 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -1,1790 +1,1793 @@ - - - -/** - * Main page of iTop - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -/** - * Displays a popup welcome message, once per session at maximum - * until the user unchecks the "Display welcome at startup" - * @param WebPage $oP The current web page for the display - * @return void - */ -function DisplayWelcomePopup(WebPage $oP) -{ - if (!isset($_SESSION['welcome'])) - { - // Check, only once per session, if the popup should be displayed... - // If the user did not already ask for hiding it forever - $bPopup = appUserPreferences::GetPref('welcome_popup', true); - if ($bPopup) - { - $sTemplate = @file_get_contents('../application/templates/welcome_popup.html'); - if ($sTemplate !== false) - { - $oTemplate = new DisplayTemplate($sTemplate); - $oP->add("
"); - $oTemplate->Render($oP, array()); - $oP->add("

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

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

\n"); - $sTitle = addslashes(Dict::S('UI:WelcomeMenu:Title')); - $oP->add_ready_script( -<< ($(window).height()-70)) - { - $('#welcome_popup').height($(window).height()-70); - } -EOF -); - $_SESSION['welcome'] = 'ok'; - } - } - } -} - -/** - * Apply the 'next-action' to the given object or redirect to the page that prompts for additional information if needed - * @param $oP WebPage The page for the output - * @param $oObj CMDBObject The object to process - * @param $sNextAction string The code of the stimulus for the 'action' (i.e. Transition) to apply - */ -function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction) -{ - // Here handle the apply stimulus - $aTransitions = $oObj->EnumTransitions(); - $aStimuli = MetaModel::EnumStimuli(get_class($oObj)); - if (!isset($aTransitions[$sNextAction])) - { - // Invalid stimulus - throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sNextAction, $oObj->GetName(), $oObj->GetStateLabel())); - } - // Get the list of missing mandatory fields for the target state, considering only the changes from the previous form (i.e don't prompt twice) - $aExpectedAttributes = $oObj->GetTransitionAttributes($sNextAction); - - if (count($aExpectedAttributes) == 0) - { - // If all the mandatory fields are already present, just apply the transition silently... - if ($oObj->ApplyStimulus($sNextAction)) - { - $oObj->DBUpdate(); - } - ReloadAndDisplay($oP, $oObj); - } - else - { - // redirect to the 'stimulus' action - $oAppContext = new ApplicationContext(); -//echo "

Missing Attributes

".print_r($aExpectedAttributes, true)."

\n"; - - $oP->add_header('Location: '.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=stimulus&class='.get_class($oObj).'&stimulus='.$sNextAction.'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink()); - } -} - -function ReloadAndDisplay($oPage, $oObj, $sMessageId = '', $sMessage = '', $sSeverity = null) -{ - $oAppContext = new ApplicationContext(); - if ($sMessageId != '') - { - cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), $sMessageId, $sMessage, $sSeverity, 0, true /* must not exist */); - } - $oPage->add_header('Location: '.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class='.get_class($oObj).'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink()); -} -/** - * Displays the details of an object - * @param $oP WebPage Page for the output - * @param $sClass string The name of the class of the object - * @param $oObj DBObject The object to display - * @param $id mixed Identifier of the object (name or ID) - */ -function DisplayDetails($oP, $sClass, $oObj, $id) -{ - $sClassLabel = MetaModel::GetName($sClass); - $oSearch = new DBObjectSearch($sClass); - $oBlock = new DisplayBlock($oSearch, 'search', false); - $oBlock->Display($oP, 0); - - // The object could be listed, check if it is actually allowed to view it - $oSet = CMDBObjectSet::FromObject($oObj); - if (UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) == UR_ALLOWED_NO) - { - throw new SecurityException('User not allowed to view this object', array('class' => $sClass, 'id' => $id)); - } - $oP->set_title(Dict::Format('UI:DetailsPageTitle', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding - $oObj->DisplayDetails($oP); -} - -/** - * Display the session messages relative to the object identified by its "message key" (class::id) - * @param string $sMessageKey - * @param WebPage $oPage - */ -function DisplayMessages($sMessageKey, WebPage $oPage) -{ - if (array_key_exists('obj_messages', $_SESSION) && array_key_exists($sMessageKey, $_SESSION['obj_messages'])) - { - $aMessages = array(); - $aRanks = array(); - foreach ($_SESSION['obj_messages'][$sMessageKey] as $sMessageId => $aMessageData) - { - $sMsgClass = 'message_'.$aMessageData['severity']; - $aMessages[] = "
".$aMessageData['message']."
"; - $aRanks[] = $aMessageData['rank']; - } - unset($_SESSION['obj_messages'][$sMessageKey]); - array_multisort($aRanks, $aMessages); - foreach ($aMessages as $sMessage) - { - $oPage->add($sMessage); - } - } -} - -/** - * Helper to update the breadrumb for the current object - * @param DBObject $oObj - * @param WebPage $oPage - */ -function SetObjectBreadCrumbEntry(DBObject $oObj, WebPage $oPage) -{ - $sClass = get_class($oObj); // get the leaf class - $sIcon = MetaModel::GetClassIcon($sClass, false); - if ($sIcon == '') - { - $sIcon = utils::GetAbsoluteUrlAppRoot().'images/breadcrumb_object.png'; - } - $oPage->SetBreadCrumbEntry("ui-details-$sClass-".$oObj->GetKey(), $oObj->Get('friendlyname'), MetaModel::GetName($sClass).': '.$oObj->Get('friendlyname'), '', $sIcon); -} - -/** - * Displays the result of a search request - * @param $oP WebPage Web page for the output - * @param $oFilter DBSearch The search of objects to display - * @param $bSearchForm boolean Whether or not to display the search form at the top the page - * @param $sBaseClass string The base class for the search (can be different from the actual class of the results) - * @param $sFormat string The format to use for the output: csv or html - * @param $bDoSearch bool True to display the search results below the search form - * @param $bSearchFormOpen bool True to display the search form fully expanded (only if $bSearchForm of course) - */ -function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '', $bDoSearch = true, $bSearchFormOpen = false) -{ - if ($bSearchForm) - { - $aParams = array('open' => $bSearchFormOpen); - if (!empty($sBaseClass)) - { - $aParams['baseClass'] = $sBaseClass; - } - $oBlock = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams); - $oBlock->Display($oP, 0); - } - if ($bDoSearch) - { - if (strtolower($sFormat) == 'csv') - { - $oBlock = new DisplayBlock($oFilter, 'csv', false); - $oBlock->Display($oP, 1); - // Adjust the size of the Textarea containing the CSV to fit almost all the remaining space - $oP->add_ready_script(" $('#1>textarea').height($('#1').parent().height() - $('#0').outerHeight() - 30).width( $('#1').parent().width() - 20);"); // adjust the size of the block - } - else - { - $oBlock = new DisplayBlock($oFilter, 'list', false); - $oBlock->Display($oP, 1); - - // Breadcrumb - //$iCount = $oBlock->GetDisplayedCount(); - $sPageId = "ui-search-".$oFilter->GetClass(); - $sLabel = MetaModel::GetName($oFilter->GetClass()); - $oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', '../images/breadcrumb-search.png'); - } - } -} - -/** - * Displays a form (checkboxes) to select the objects for which to apply a given action - * Only the objects for which the action is valid can be checked. By default all valid objects are checked - * @param $oP WebPage The page for output - * @param $oFilter DBSearch The filter that defines the list of objects - * @param $sNextOperation string The next operation (code) to be executed when the form is submitted - * @param $oChecker ActionChecker The helper class/instance used to check for which object the action is valid - * @return none - */ -function DisplayMultipleSelectionForm($oP, $oFilter, $sNextOperation, $oChecker, $aExtraFormParams = array()) -{ - $oAppContext = new ApplicationContext(); - $iBulkActionAllowed = $oChecker->IsAllowed(); - $sClass = $oFilter->GetClass(); - $aExtraParams = array('selection_type' => 'multiple', 'selection_mode' => true, 'display_limit' => false, 'menu' => false); - if ($iBulkActionAllowed == UR_ALLOWED_DEPENDS) - { - $aAllowed = array(); - $aExtraParams['selection_enabled'] = $oChecker->GetAllowedIDs(); - } - else if(UR_ALLOWED_NO) - { - throw new ApplicationException(Dict::Format('UI:ActionNotAllowed')); - } - - $oBlock = new DisplayBlock($oFilter, 'list', false); - $oP->add("
\n"); - $oP->add("\n"); - $oP->add("GetClass()."\">\n"); - $oP->add("Serialize()."\">\n"); - $oP->add("\n"); - foreach($aExtraFormParams as $sName => $sValue) - { - $oP->add("\n"); - } - $oP->add($oAppContext->GetForForm()); - $oBlock->Display($oP, 1, $aExtraParams); - $oP->add("  \n"); - $oP->add("
\n"); - $oP->add_ready_script("$('#1 table.listResults').trigger('check_all');"); -} - -function DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj) -{ - $oP->SetCurrentTab(Dict::S('UI:RelationshipList')); - $oP->add("
"); - $sOldRelation = $sRelation; - if (($sRelation == 'impacts') && ($sDirection == 'up')) - { - $sOldRelation = 'depends on'; - } - $oP->add("

".MetaModel::GetRelationDescription($sOldRelation).' '.$oObj->GetName()."

\n"); - $oP->add("
"); - $oP->add(''); - /* - * Content is rendered asynchronously via pages/ajax.render.php?operation=relation_lists - */ - /* - $iBlock = 1; // Zero is not a valid blockid - foreach($aResults as $sListClass => $aObjects) - { - $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); - $oP->add("
\n"); - $oP->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aObjects), Metamodel::GetName($sListClass))."

\n"); - $oP->add("
\n"); - $oBlock = DisplayBlock::FromObjectSet($oSet, 'list'); - $oBlock->Display($oP, $iBlock++, array('table_id' => get_class($oObj).'_'.$sRelation.'_'.$sDirection.'_'.$sListClass)); - $oP->P(' '); // Some space ? - } - */ - $oP->add("
"); - $oP->add("
"); -} - -function DisplayNavigatorGroupTab($oP) -{ - $oP->SetCurrentTab(Dict::S('UI:RelationGroups')); - $oP->add("
"); - $oP->add(''); - /* - * Content is rendered asynchronously via pages/ajax.render.php?operation=relation_groups - */ - $oP->add("
"); -} - -/*********************************************************************************** - * - * Main user interface page starts here - * - ***********************************************************************************/ -require_once('../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); -require_once(APPROOT.'/application/itopwebpage.class.inc.php'); -require_once(APPROOT.'/application/wizardhelper.class.inc.php'); - -require_once(APPROOT.'/application/startup.inc.php'); - -try -{ - $operation = utils::ReadParam('operation', ''); - $bPrintable = (utils::ReadParam('printable', 0) == '1'); - - $oKPI = new ExecutionKPI(); - $oKPI->ComputeAndReport('Data model loaded'); - - $oKPI = new ExecutionKPI(); - - require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - $sLoginMessage = LoginWebPage::DoLogin(); // Check user rights and prompt if needed - $oAppContext = new ApplicationContext(); - - $oKPI->ComputeAndReport('User login'); - - $oP = new iTopWebPage(Dict::S('UI:WelcomeToITop'), $bPrintable); - $oP->SetMessage($sLoginMessage); - - - // All the following actions use advanced forms that require more javascript to be loaded - switch($operation) - { - case 'new': // Form to create a new object - case 'modify': // Form to modify an object - case 'apply_new': // Creation of a new object - case 'apply_modify': // Applying the modifications to an existing object - case 'form_for_modify_all': // Form to modify multiple objects (bulk modify) - case 'bulk_stimulus': // For to apply a stimulus to multiple objects - case 'stimulus': // Form displayed when applying a stimulus (state change) - case 'apply_stimulus': // Form displayed when applying a stimulus (state change) - $oP->add_linked_script("../js/json.js"); - $oP->add_linked_script("../js/forms-json-utils.js"); - $oP->add_linked_script("../js/wizardhelper.js"); - $oP->add_linked_script("../js/wizard.utils.js"); - $oP->add_linked_script("../js/linkswidget.js"); - $oP->add_linked_script("../js/linksdirectwidget.js"); - $oP->add_linked_script("../js/extkeywidget.js"); - $oP->add_linked_script("../js/jquery.blockUI.js"); - break; - } - - switch($operation) - { - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'details': // Details of an object - $sClass = utils::ReadParam('class', ''); - $id = utils::ReadParam('id', ''); - if ( empty($sClass) || empty($id)) - { - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); - } - - if (is_numeric($id)) - { - $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); - } - else - { - $oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */); - } - if (is_null($oObj)) - { - // Check anyhow if there is a message for this object (like you've just created it) - $sMessageKey = $sClass.'::'.$id; - DisplayMessages($sMessageKey, $oP); - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - - // Attempt to load the object in archive mode - utils::PushArchiveMode(true); - if (is_numeric($id)) - { - $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); - } - else - { - $oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */); - } - utils::PopArchiveMode(); - if (is_null($oObj)) - { - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - } - else - { - SetObjectBreadCrumbEntry($oObj, $oP); - $oP->P(Dict::S('UI:ObjectArchived')); - } - } - else - { - try - { - $oObj->Reload(); - } - catch(Exception $e) - { - // Probably not allowed to see this instance of a derived class - - // Check anyhow if there is a message for this object (like you've just created it) - $sMessageKey = $sClass.'::'.$id; - DisplayMessages($sMessageKey, $oP); - - $oObj = null; - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - } - if (!is_null($oObj)) - { - SetObjectBreadCrumbEntry($oObj, $oP); - DisplayDetails($oP, $sClass, $oObj, $id); - } - } - break; - - case 'release_lock_and_details': - $oP->DisableBreadCrumb(); - $sClass = utils::ReadParam('class', ''); - $id = utils::ReadParam('id', ''); - $oObj = MetaModel::GetObject($sClass, $id); - $sToken = utils::ReadParam('token', ''); - if ($sToken != '') - { - iTopOwnershipLock::ReleaseLock($sClass, $id, $sToken); - } - cmdbAbstractObject::ReloadAndDisplay($oP, $oObj, array('operation' => 'details')); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'search_oql': // OQL query - $sOQLClass = utils::ReadParam('oql_class', '', false, 'class'); - $sBaseClass = utils::ReadParam('base_class', $sOQLClass, false, 'class'); - $sOQLClause = utils::ReadParam('oql_clause', '', false, 'raw_data'); - $sFormat = utils::ReadParam('format', ''); - $bSearchForm = utils::ReadParam('search_form', true); - $sTitle = utils::ReadParam('title', 'UI:SearchResultsPageTitle'); - if (empty($sOQLClass)) - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'oql_class')); - } - $oP->set_title(Dict::S($sTitle)); - $oP->add('

'.Dict::S($sTitle).'

'); - $sOQL = "SELECT $sOQLClass $sOQLClause"; - try - { - $oFilter = DBObjectSearch::FromOQL($sOQL); - DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat); - } - catch(CoreException $e) - { - $oFilter = new DBObjectSearch($sOQLClass); - $oSet = new DBObjectSet($oFilter); - if ($bSearchForm) - { - $oBlock = new DisplayBlock($oFilter, 'search', false); - $oBlock->Display($oP, 0); - } - $oP->P(''.Dict::Format('UI:Error:IncorrectOQLQuery_Message', $e->getHtmlDesc()).''); - } - catch(Exception $e) - { - $oP->P(''.Dict::Format('UI:Error:AnErrorOccuredWhileRunningTheQuery_Message', $e->getMessage()).''); - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'search_form': // Search form - $sClass = utils::ReadParam('class', '', false, 'class'); - $sFormat = utils::ReadParam('format', 'html'); - $bSearchForm = utils::ReadParam('search_form', true); - $bDoSearch = utils::ReadParam('do_search', true); - if (empty($sClass)) - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); - } - $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); - $oFilter = new DBObjectSearch($sClass); - DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch, true /* Search Form Expanded */); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'search': // Serialized DBSearch - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - $sFormat = utils::ReadParam('format', ''); - $bSearchForm = utils::ReadParam('search_form', true); - if (empty($sFilter)) - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); - } - $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); - $oFilter = DBSearch::unserialize($sFilter); // TO DO : check that the filter is valid - $oFilter->UpdateContextFromUser(); - DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'full_text': // Global "google-like" search - $oP->DisableBreadCrumb(); - $sFullText = trim(utils::ReadParam('text', '', false, 'raw_data')); - $iTune = utils::ReadParam('tune', 0); - if (empty($sFullText)) - { - $oP->p(Dict::S('UI:Search:NoSearch')); - } - else - { - $iErrors = 0; - - // Check if a class name/label is supplied to limit the search - $sClassName = ''; - if (preg_match('/^([^\"]+):(.+)$/', $sFullText, $aMatches)) - { - $sClassName = $aMatches[1]; - if (MetaModel::IsValidClass($sClassName)) - { - $sFullText = trim($aMatches[2]); - } - elseif ($sClassName = MetaModel::GetClassFromLabel($sClassName, false /* => not case sensitive */)) - { - $sFullText = trim($aMatches[2]); - } - } - - if (preg_match('/^"(.*)"$/', $sFullText, $aMatches)) - { - // The text is surrounded by double-quotes, remove the quotes and treat it as one single expression - $aFullTextNeedles = array($aMatches[1]); - } - else - { - // Split the text on the blanks and treat this as a search for AND AND - $aFullTextNeedles = explode(' ', $sFullText); - } - - // Check the needle length - $iMinLenth = MetaModel::GetConfig()->Get('full_text_needle_min'); - foreach ($aFullTextNeedles as $sNeedle) - { - if (strlen($sNeedle) < $iMinLenth) - { - $oP->p(Dict::Format('UI:Search:NeedleTooShort', $sNeedle, $iMinLenth)); - $iErrors++; - } - } - - // Sanity check of the accelerators - $aAccelerators = MetaModel::GetConfig()->Get('full_text_accelerators'); - foreach ($aAccelerators as $sClass => $aAccelerator) - { - try - { - $bSkip = array_key_exists('skip', $aAccelerator) ? $aAccelerator['skip'] : false; - if (!$bSkip) - { - $oSearch = DBObjectSearch::FromOQL($aAccelerator['query']); - if ($sClass != $oSearch->GetClass()) - { - $oP->p("Full text accelerator for class '$sClass': searched class mismatch (".$oSearch->GetClass().")"); - $iErrors++; - } - } - } - catch (OqlException $e) - { - $oP->p("Full text accelerator for class '$sClass': ".$e->getHtmlDesc()); - $iErrors++; - } - } - - if ($iErrors == 0) - { - $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); - $sPageId = "ui-global-search"; - $sLabel = Dict::S('UI:SearchResultsTitle'); - $sDescription = Dict::S('UI:SearchResultsTitle+'); - $oP->SetBreadCrumbEntry($sPageId, $sLabel, $sDescription, '', utils::GetAbsoluteUrlAppRoot().'images/search.png'); - $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - $oP->add("
\n"); - $oP->add("
\n"); - $oP->add(' '.Dict::Format('UI:Search:Ongoing', htmlentities($sFullText, ENT_QUOTES, 'UTF-8')).''); - $oP->add("
\n"); - $oP->add("
\n"); - $oP->add("
 
\n"); - $oP->add("

".Dict::Format('UI:FullTextSearchTitle_Text', htmlentities($sFullText, ENT_QUOTES, 'UTF-8'))."

"); - $oP->add("
\n"); - $oP->add("
\n"); - $sJSClass = addslashes($sClassName); - $sJSNeedles = json_encode($aFullTextNeedles); - $oP->add_ready_script( -<< 0) - { - $oP->add_script("var oTimeStatistics = {};"); - } - } - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'modify': // Form to modify an object - $oP->DisableBreadCrumb(); - $sClass = utils::ReadParam('class', '', false, 'class'); - $id = utils::ReadParam('id', ''); - if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); - } - // Check if the user can modify this object - $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); - if (is_null($oObj)) - { - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - } - else - { - // The object could be read - check if it is allowed to modify it - $oSet = CMDBObjectSet::FromObject($oObj); - if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_NO) - { - throw new SecurityException('User not allowed to modify this object', array('class' => $sClass, 'id' => $id)); - } - // Note: code duplicated to the case 'apply_modify' when a data integrity issue has been found - $oObj->DisplayModifyForm($oP, array('wizard_container' => 1)); // wizard_container: Display the blue borders and the title above the form - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'select_for_modify_all': // Select the list of objects to be modified (bulk modify) - $oP->DisableBreadCrumb(); - $oP->set_title(Dict::S('UI:ModifyAllPageTitle')); - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - if (empty($sFilter)) - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); - } - $oFilter = DBObjectSearch::unserialize($sFilter); // TO DO : check that the filter is valid - // Add user filter - $oFilter->UpdateContextFromUser(); - $sClass = $oFilter->GetClass(); - $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_MODIFY); - $oP->add("

".Dict::S('UI:ModifyAllPageTitle')."

\n"); - - DisplayMultipleSelectionForm($oP, $oFilter, 'form_for_modify_all', $oChecker); - break; - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'form_for_modify_all': // Form to modify multiple objects (bulk modify) - $oP->DisableBreadCrumb(); - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - $sClass = utils::ReadParam('class', '', false, 'class'); - $oFullSetFilter = DBObjectSearch::unserialize($sFilter); - // Add user filter - $oFullSetFilter->UpdateContextFromUser(); - $aSelectedObj = utils::ReadMultipleSelection($oFullSetFilter); - $sCancelUrl = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); - $aContext = array('filter' => $sFilter); - cmdbAbstractObject::DisplayBulkModifyForm($oP, $sClass, $aSelectedObj, 'preview_or_modify_all', $sCancelUrl, array(), $aContext); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'preview_or_modify_all': // Preview or apply bulk modify - $oP->DisableBreadCrumb(); - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - $oFilter = DBObjectSearch::unserialize($sFilter); // TO DO : check that the filter is valid - // Add user filter - $oFilter->UpdateContextFromUser(); - $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_MODIFY); - - $sClass = utils::ReadParam('class', '', false, 'class'); - $bPreview = utils::ReadParam('preview_mode', ''); - $sSelectedObj = utils::ReadParam('selectObj', '', false, 'raw_data'); - if ( empty($sClass) || empty($sSelectedObj)) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'selectObj')); - } - $aSelectedObj = explode(',', $sSelectedObj); - $sCancelUrl = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); - $aContext = array( - 'filter' => $sFilter, - 'selectObj' => $sSelectedObj, - ); - cmdbAbstractObject::DoBulkModify($oP, $sClass, $aSelectedObj, 'preview_or_modify_all', $bPreview, $sCancelUrl, $aContext); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'new': // Form to create a new object - $oP->DisableBreadCrumb(); - $sClass = utils::ReadParam('class', '', false, 'class'); - $sStateCode = utils::ReadParam('state', ''); - $bCheckSubClass = utils::ReadParam('checkSubclass', true); - if ( empty($sClass) ) - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); - } - -/* - $aArgs = utils::ReadParam('default', array(), false, 'raw_data'); - $aContext = $oAppContext->GetAsHash(); - foreach( $oAppContext->GetNames() as $key) - { - $aArgs[$key] = $oAppContext->GetCurrentValue($key); - } -*/ - // If the specified class has subclasses, ask the user an instance of which class to create - $aSubClasses = MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself - $aPossibleClasses = array(); - $sRealClass = ''; - if ($bCheckSubClass) - { - foreach($aSubClasses as $sCandidateClass) - { - if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) - { - $aPossibleClasses[$sCandidateClass] = MetaModel::GetName($sCandidateClass); - } - } - // Only one of the subclasses can be instantiated... - if (count($aPossibleClasses) == 1) - { - $aKeys = array_keys($aPossibleClasses); - $sRealClass = $aKeys[0]; - } - } - else - { - $sRealClass = $sClass; - } - - if (!empty($sRealClass)) - { - // Display the creation form - $sClassLabel = MetaModel::GetName($sRealClass); - // Note: some code has been duplicated to the case 'apply_new' when a data integrity issue has been found - $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); - $oP->add("

".MetaModel::GetClassIcon($sRealClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); - $oP->add("
\n"); - - // Set all the default values in an object and clone this "default" object - $oObjToClone = MetaModel::NewObject($sRealClass); - - // 1st - set context values - $oAppContext->InitObjectFromContext($oObjToClone); - - // 2nd - set values from the page argument 'default' - $oObjToClone->UpdateObjectFromArg('default'); - - cmdbAbstractObject::DisplayCreationForm($oP, $sRealClass, $oObjToClone, array()); - $oP->add("
\n"); - } - else - { - // Select the derived class to create - $sClassLabel = MetaModel::GetName($sClass); - $oP->add("

".MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); - $oP->add("
\n"); - $oP->add('
'); - $oP->add('

'.Dict::Format('UI:SelectTheTypeOf_Class_ToCreate', $sClassLabel)); - $aDefaults = utils::ReadParam('default', array(), false, 'raw_data'); - $oP->add($oAppContext->GetForForm()); - $oP->add("\n"); - $oP->add("\n"); - $oP->add("\n"); - foreach($aDefaults as $key => $value) - { - if (is_array($value)) - { - foreach($value as $key2 => $value2) - { - if (is_array($value2)) - { - foreach($value2 as $key3 => $value3) - { - $sValue = htmlentities($value3, ENT_QUOTES, 'UTF-8'); - $oP->add("\n"); - } - } - else - { - $sValue = htmlentities($value2, ENT_QUOTES, 'UTF-8'); - $oP->add("\n"); - } - } - } - else - { - $sValue = htmlentities($value, ENT_QUOTES, 'UTF-8'); - $oP->add("\n"); - } - } - $oP->add(''); - $oP->add(" 

"); - $oP->add('
'); - $oP->add("
\n"); - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'apply_modify': // Applying the modifications to an existing object - $oP->DisableBreadCrumb(); - $sClass = utils::ReadPostedParam('class', ''); - $sClassLabel = MetaModel::GetName($sClass); - $id = utils::ReadPostedParam('id', ''); - $sTransactionId = utils::ReadPostedParam('transaction_id', ''); - if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); - } - $bDisplayDetails = true; - $oObj = MetaModel::GetObject($sClass, $id, false); - if ($oObj == null) - { - $bDisplayDetails = false; - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - } - elseif (!utils::IsTransactionValid($sTransactionId, false)) - { - $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding - $oP->p("".Dict::S('UI:Error:ObjectAlreadyUpdated')."\n"); - } - else - { - $oObj->UpdateObjectFromPostedForm(); - $sMessage = ''; - $sSeverity = 'ok'; - - if (!$oObj->IsModified()) - { - $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding - $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'info'; - } - else - { - list($bRes, $aIssues) = $oObj->CheckToWrite(); - if ($bRes) - { - try - { - CMDBSource::Query('START TRANSACTION'); - $oObj->DBUpdate(); - CMDBSource::Query('COMMIT'); - $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'ok'; - } - catch(DeleteException $e) - { - CMDBSource::Query('ROLLBACK'); - // Say two things: 1) Don't be afraid nothing was modified - $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'info'; - cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage, $sSeverity, 0, true /* must not exist */); - // 2) Ok, there was some trouble indeed - $sMessage = $e->getMessage(); - $sSeverity = 'error'; - $bDisplayDetails = true; - } - utils::RemoveTransaction($sTransactionId); - - } - else - { - $bDisplayDetails = false; - // Found issues, explain and give the user a second chance - // - $oObj->DisplayModifyForm($oP, array('wizard_container' => true)); // wizard_container: display the wizard border and the title - $sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten', implode(', ', $aIssues)); - $oP->add_ready_script("alert('".addslashes($sIssueDesc)."');"); - } - } - } - if ($bDisplayDetails) - { - $oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); //Workaround: reload the object so that the linkedset are displayed properly - $sNextAction = utils::ReadPostedParam('next_action', ''); - if (!empty($sNextAction)) - { - ApplyNextAction($oP, $oObj, $sNextAction); - } - else - { - // Nothing more to do - ReloadAndDisplay($oP, $oObj, 'update', $sMessage, $sSeverity); - } - - $bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled'); - if ($bLockEnabled) - { - // Release the concurrent lock, if any - $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data'); - if ($sOwnershipToken !== null) - { - // We're done, let's release the lock - iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); - } - } - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'select_for_deletion': // Select multiple objects for deletion - $oP->DisableBreadCrumb(); - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - if (empty($sFilter)) - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); - } - $oP->set_title(Dict::S('UI:BulkDeletePageTitle')); - $oP->add("

".Dict::S('UI:BulkDeleteTitle')."

\n"); - $oFilter = DBSearch::unserialize($sFilter); // TO DO : check that the filter is valid - $oFilter->UpdateContextFromUser(); - $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_DELETE); - DisplayMultipleSelectionForm($oP, $oFilter, 'bulk_delete', $oChecker); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'bulk_delete_confirmed': // Confirm bulk deletion of objects - $oP->DisableBreadCrumb(); - $sTransactionId = utils::ReadPostedParam('transaction_id', ''); - if (!utils::IsTransactionValid($sTransactionId)) - { - throw new ApplicationException(Dict::S('UI:Error:ObjectsAlreadyDeleted')); - } - // Fall through - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'delete': - case 'bulk_delete': // Actual bulk deletion (if confirmed) - $oP->DisableBreadCrumb(); - $sClass = utils::ReadParam('class', '', false, 'class'); - $sClassLabel = MetaModel::GetName($sClass); - $aObjects = array(); - if ($operation == 'delete') - { - // Single object - $id = utils::ReadParam('id', ''); - $oObj = MetaModel::GetObject($sClass, $id); - $aObjects[] = $oObj; - if (!UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, DBObjectSet::FromObject($oObj))) - { - throw new SecurityException(Dict::Format('UI:Error:DeleteNotAllowedOn_Class', $sClassLabel)); - } - } - else - { - // Several objects - $sFilter = utils::ReadPostedParam('filter', ''); - $oFullSetFilter = DBObjectSearch::unserialize($sFilter); - // Add user filter - $oFullSetFilter->UpdateContextFromUser(); - $aSelectObject = utils::ReadMultipleSelection($oFullSetFilter); - if ( empty($sClass) || empty($aSelectObject)) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'selectObject[]')); - } - foreach($aSelectObject as $iId) - { - $aObjects[] = MetaModel::GetObject($sClass, $iId); - } - if (count($aObjects) == 1) - { - if (!UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, DBObjectSet::FromArray($sClass, $aObjects))) - { - throw new SecurityException(Dict::Format('UI:Error:BulkDeleteNotAllowedOn_Class', $sClassLabel)); - } - } - else - { - if (!UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, DBObjectSet::FromArray($sClass, $aObjects))) - { - throw new SecurityException(Dict::Format('UI:Error:BulkDeleteNotAllowedOn_Class', $sClassLabel)); - } - $oP->set_title(Dict::S('UI:BulkDeletePageTitle')); - } - } - // Go for the common part... (delete single, delete bulk, delete confirmed) - cmdbAbstractObject::DeleteObjects($oP, $sClass, $aObjects, ($operation != 'bulk_delete_confirmed'), 'bulk_delete_confirmed'); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'apply_new': // Creation of a new object - $oP->DisableBreadCrumb(); - $sClass = utils::ReadPostedParam('class', '', 'class'); - $sClassLabel = MetaModel::GetName($sClass); - $sTransactionId = utils::ReadPostedParam('transaction_id', ''); - if ( empty($sClass) ) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); - } - if (!utils::IsTransactionValid($sTransactionId, false)) - { - $oP->p("".Dict::S('UI:Error:ObjectAlreadyCreated')."\n"); - } - else - { - $oObj = MetaModel::NewObject($sClass); - $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); - if (!empty($sStateAttCode)) - { - $sTargetState = utils::ReadPostedParam('obj_state', ''); - if ($sTargetState != '') - { - $oObj->Set($sStateAttCode, $sTargetState); - } - } - $oObj->UpdateObjectFromPostedForm(); - } - if (isset($oObj) && is_object($oObj)) - { - $sClass = get_class($oObj); - $sClassLabel = MetaModel::GetName($sClass); - - list($bRes, $aIssues) = $oObj->CheckToWrite(); - if ($bRes) - { - $oObj->DBInsertNoReload(); // No need to reload - utils::RemoveTransaction($sTransactionId); - $oP->set_title(Dict::S('UI:PageTitle:ObjectCreated')); - - // Compute the name, by reloading the object, even if it disappeared from the silo - $oObj = MetaModel::GetObject($sClass, $oObj->GetKey(), true /* Must be found */, true /* Allow All Data*/); - $sName = $oObj->GetName(); - $sMessage = Dict::Format('UI:Title:Object_Of_Class_Created', $sName, $sClassLabel); - - $sNextAction = utils::ReadPostedParam('next_action', ''); - if (!empty($sNextAction)) - { - $oP->add("

$sMessage

"); - ApplyNextAction($oP, $oObj, $sNextAction); - } - else - { - // Nothing more to do - ReloadAndDisplay($oP, $oObj, 'create', $sMessage, 'ok'); - } - } - else - { - // Found issues, explain and give the user a second chance - // - $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); - $oP->add("

".MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); - $oP->add("
\n"); - cmdbAbstractObject::DisplayCreationForm($oP, $sClass, $oObj); - $oP->add("
\n"); - $sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten', implode(', ', $aIssues)); - $oP->add_ready_script("alert('".addslashes($sIssueDesc)."');"); - } - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'select_bulk_stimulus': // Form displayed when applying a stimulus to many objects - $oP->DisableBreadCrumb(); - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - $sStimulus = utils::ReadParam('stimulus', ''); - $sState = utils::ReadParam('state', ''); - if (empty($sFilter) || empty($sStimulus) || empty($sState)) - { - throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); - } - $oFilter = DBObjectSearch::unserialize($sFilter); - $oFilter->UpdateContextFromUser(); - $sClass = $oFilter->GetClass(); - $aStimuli = MetaModel::EnumStimuli($sClass); - $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); - $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); - $oP->set_title($sActionLabel); - $oP->add(''); - - $oChecker = new StimulusChecker($oFilter, $sState, $sStimulus); - $aExtraFormParams = array('stimulus' => $sStimulus, 'state' => $sState); - DisplayMultipleSelectionForm($oP, $oFilter, 'bulk_stimulus', $oChecker, $aExtraFormParams); - break; - - case 'bulk_stimulus': - $oP->DisableBreadCrumb(); - $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); - $sStimulus = utils::ReadParam('stimulus', ''); - $sState = utils::ReadParam('state', ''); - if (empty($sFilter) || empty($sStimulus) || empty($sState)) - { - throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); - } - $oFilter = DBObjectSearch::unserialize($sFilter); - // Add user filter - $oFilter->UpdateContextFromUser(); - $sClass = $oFilter->GetClass(); - $aSelectObject = utils::ReadMultipleSelection($oFilter); - if (count($aSelectObject) == 0) - { - // Nothing to do, no object was selected ! - throw new ApplicationException(Dict::S('UI:BulkAction:NoObjectSelected')); - } - else - { - $aTransitions = MetaModel::EnumTransitions($sClass, $sState); - $aStimuli = MetaModel::EnumStimuli($sClass); - - $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); - $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); - $sTargetState = $aTransitions[$sStimulus]['target_state']; - $aStates = MetaModel::EnumStates($sClass); - $aTargetStateDef = $aStates[$sTargetState]; - - $oP->set_title(Dict::Format('UI:StimulusModify_N_ObjectsOf_Class', $sActionLabel, count($aSelectObject), $sClass)); - $oP->add(''); - - $aExpectedAttributes = MetaModel::GetTransitionAttributes($sClass, $sStimulus, $sState); - $aDetails = array(); - $iFieldIndex = 0; - $aFieldsMap = array(); - $aValues = array(); - $aObjects = array(); - foreach($aSelectObject as $iId) - { - $aObjects[] = MetaModel::GetObject($sClass, $iId); - } - $oSet = DBObjectSet::FromArray($sClass, $aObjects); - $oObj = $oSet->ComputeCommonObject($aValues); - $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); - $oObj->Set($sStateAttCode,$sTargetState); - $sReadyScript = ''; - foreach($aExpectedAttributes as $sAttCode => $iExpectCode) - { - // Prompt for an attribute if - // - the attribute must be changed or must be displayed to the user for confirmation - // - or the field is mandatory and currently empty - if ( ($iExpectCode & (OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) || - (($iExpectCode & OPT_ATT_MANDATORY) && ($oObj->Get($sAttCode) == '')) ) - { - $aAttributesDef = MetaModel::ListAttributeDefs($sClass); - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $aPrerequisites = MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one - if (count($aPrerequisites) > 0) - { - // When 'enabling' a field, all its prerequisites must be enabled too - $sFieldList = "['".implode("','", $aPrerequisites)."']"; - $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n"); - } - $aDependents = MetaModel::GetDependentAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one - if (count($aDependents) > 0) - { - // When 'disabling' a field, all its dependent fields must be disabled too - $sFieldList = "['".implode("','", $aDependents)."']"; - $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); - } - $aArgs = array('this' => $oObj); - $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sAttCode, '', $iExpectCode, $aArgs); - $sComments = ''; - if (!isset($aValues[$sAttCode])) - { - $aValues[$sAttCode] = array(); - } - if (count($aValues[$sAttCode]) == 1) - { - $sComments .= '
1
'; - } - else - { - // Non-homogenous value - $iMaxCount = 5; - $sTip = "

".Dict::Format('UI:BulkModify_Count_DistinctValues', count($aValues[$sAttCode]))."

    "; - $index = 0; - foreach($aValues[$sAttCode] as $sCurrValue => $aVal) - { - $sDisplayValue = empty($aVal['display']) ? ''.Dict::S('Enum:Undefined').'' : str_replace(array("\n", "\r"), " ", $aVal['display']); - $sTip .= "
  • ".Dict::Format('UI:BulkModify:Value_Exists_N_Times', $sDisplayValue, $aVal['count'])."
  • "; - $index++; - if ($iMaxCount == $index) - { - $sTip .= "
  • ".Dict::Format('UI:BulkModify:N_MoreValues', count($aValues[$sAttCode]) - $iMaxCount)."
  • "; - break; - } - } - $sTip .= "

"; - $sTip = addslashes($sTip); - $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n"; - $sComments .= '
'.count($aValues[$sAttCode]).'
'; - } - $aDetails[] = array('label' => ''.$oAttDef->GetLabel().'', 'value' => "$sHTMLValue", 'comments' => $sComments); - $aFieldsMap[$sAttCode] = $sAttCode; - $iFieldIndex++; - } - } - $sButtonsPosition = MetaModel::GetConfig()->Get('buttons_position'); - if ($sButtonsPosition == 'bottom') - { - // bottom: Displays the ticket details BEFORE the actions - $oP->add('
'); - $oObj->DisplayBareProperties($oP); - $oP->add('
'); - } - $oP->add("
\n"); - $oP->add("
\n"); - $oP->add("
\n"); - $oP->details($aDetails); - $oP->add("
\n"); - $oP->add("\n"); - $oP->add("\n"); - $oP->add("\n"); - $oP->add("\n"); - $oP->add("\n"); - $oP->add("\n"); - $oP->add("\n"); - $oP->add($oAppContext->GetForForm()); - $oP->add("\n"); - $sURL = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); - $oP->add("    \n"); - $oP->add("\n"); - $oP->add("
\n"); - $oP->add("
\n"); - if ($sButtonsPosition != 'bottom') - { - // top or both: Displays the ticket details AFTER the actions - $oP->add('
'); - $oObj->DisplayBareProperties($oP); - $oP->add('
'); - } - $iFieldsCount = count($aFieldsMap); - $sJsonFieldsMap = json_encode($aFieldsMap); - - $oP->add_script( -<<add_ready_script( -<<DisableBreadCrumb(); - $bPreviewMode = utils::ReadPostedParam('preview_mode', false); - $sFilter = utils::ReadPostedParam('filter', '', false, 'raw_data'); - $sStimulus = utils::ReadPostedParam('stimulus', ''); - $sState = utils::ReadPostedParam('state', ''); - $sSelectObject = utils::ReadPostedParam('selectObject', '', false, 'raw_data'); - $aSelectObject = explode(',', $sSelectObject); - - if (empty($sFilter) || empty($sStimulus) || empty($sState)) - { - throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); - } - $sTransactionId = utils::ReadPostedParam('transaction_id', ''); - if (!utils::IsTransactionValid($sTransactionId)) - { - $oP->p(Dict::S('UI:Error:ObjectAlreadyUpdated')); - } - else - { - // For archiving the modification - $oFilter = DBObjectSearch::unserialize($sFilter); - // Add user filter - $oFilter->UpdateContextFromUser(); - $sClass = $oFilter->GetClass(); - $aObjects = array(); - foreach($aSelectObject as $iId) - { - $aObjects[] = MetaModel::GetObject($sClass, $iId); - } - - $aTransitions = MetaModel::EnumTransitions($sClass, $sState); - $aStimuli = MetaModel::EnumStimuli($sClass); - - $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); - $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); - - $oP->set_title(Dict::Format('UI:StimulusModify_N_ObjectsOf_Class', $sActionLabel, count($aObjects), $sClass)); - $oP->add(''); - - $oSet = DBObjectSet::FromArray($sClass, $aObjects); - - // For reporting - $aHeaders = array( - 'object' => array('label' => MetaModel::GetName($sClass), 'description' => Dict::S('UI:ModifiedObject')), - 'status' => array('label' => Dict::S('UI:BulkModifyStatus'), 'description' => Dict::S('UI:BulkModifyStatus+')), - 'errors' => array('label' => Dict::S('UI:BulkModifyErrors'), 'description' => Dict::S('UI:BulkModifyErrors+')), - ); - $aRows = array(); - while ($oObj = $oSet->Fetch()) - { - $sError = Dict::S('UI:BulkModifyStatusOk'); - try - { - $aTransitions = $oObj->EnumTransitions(); - $aStimuli = MetaModel::EnumStimuli($sClass); - if (!isset($aTransitions[$sStimulus])) - { - throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus, $oObj->GetName(), $oObj->GetStateLabel())); - } - else - { - $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); - $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); - $sTargetState = $aTransitions[$sStimulus]['target_state']; - $aExpectedAttributes = $oObj->GetTransitionAttributes($sStimulus /* cureent state */); - $aDetails = array(); - $aErrors = array(); - foreach($aExpectedAttributes as $sAttCode => $iExpectCode) - { - $iFlags = $oObj->GetTransitionFlags($sAttCode, $sStimulus); - if (($iExpectCode & (OPT_ATT_MUSTCHANGE|OPT_ATT_MUSTPROMPT)) || ($oObj->Get($sAttCode) == '') ) - { - $paramValue = utils::ReadPostedParam("attr_$sAttCode", '', 'raw_data'); - if ( ($iFlags & OPT_ATT_SLAVE) && ($paramValue != $oObj->Get($sAttCode)) ) - { - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $aErrors[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $oAttDef->GetLabel()); - unset($aExpectedAttributes[$sAttCode]); - } - } - } - - $oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes); - - if (count($aErrors) == 0) - { - if ($oObj->ApplyStimulus($sStimulus)) - { - list($bResult, $aErrors) = $oObj->CheckToWrite(); - $sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped'); - if ($bResult) - { - $oObj->DBUpdate(); - } - else - { - $sError = '

'.implode('

',$aErrors)."

\n"; - } - } - else - { - $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); - $sError = '

'.Dict::S('UI:FailedToApplyStimuli')."

\n"; - } - } - else - { - $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); - $sError = '

'.implode('

',$aErrors)."

\n"; - } - } - } - catch(Exception $e) - { - $sError = $e->getMessage(); - $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); - } - $aRows[] = array( - 'object' => $oObj->GetHyperlink(), - 'status' => $sStatus, - 'errors' => $sError, - ); - } - $oP->Table($aHeaders, $aRows); - // Back to the list - $sURL = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); - $oP->add(''); - } - break; - - case 'stimulus': // Form displayed when applying a stimulus (state change) - $oP->DisableBreadCrumb(); - $sClass = utils::ReadParam('class', '', false, 'class'); - $id = utils::ReadParam('id', ''); - $sStimulus = utils::ReadParam('stimulus', ''); - if ( empty($sClass) || empty($id) || empty($sStimulus) ) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')); - } - $oObj = MetaModel::GetObject($sClass, $id, false); - if ($oObj != null) - { - $oObj->DisplayStimulusForm($oP, $sStimulus); - } - else - { - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'apply_stimulus': // Actual state change - $oP->DisableBreadCrumb(); - $sClass = utils::ReadPostedParam('class', ''); - $id = utils::ReadPostedParam('id', ''); - $sTransactionId = utils::ReadPostedParam('transaction_id', ''); - $sStimulus = utils::ReadPostedParam('stimulus', ''); - if ( empty($sClass) || empty($id) || empty($sStimulus) ) // TO DO: check that the class name is valid ! - { - throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')); - } - $oObj = MetaModel::GetObject($sClass, $id, false); - if ($oObj != null) - { - $aTransitions = $oObj->EnumTransitions(); - $aStimuli = MetaModel::EnumStimuli($sClass); - $sMessage = ''; - $sSeverity = 'ok'; - $bDisplayDetails = true; - if (!isset($aTransitions[$sStimulus])) - { - throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus, $oObj->GetName(), $oObj->GetStateLabel())); - } - if (!utils::IsTransactionValid($sTransactionId)) - { - $sMessage = Dict::S('UI:Error:ObjectAlreadyUpdated'); - $sSeverity = 'info'; - } - else - { - $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); - $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); - $sTargetState = $aTransitions[$sStimulus]['target_state']; - $aExpectedAttributes = $oObj->GetTransitionAttributes($sStimulus /*, current state*/); - $aDetails = array(); - $aErrors = array(); - foreach($aExpectedAttributes as $sAttCode => $iExpectCode) - { - $iFlags = $oObj->GetTransitionFlags($sAttCode, $sStimulus); - if (($iExpectCode & (OPT_ATT_MUSTCHANGE|OPT_ATT_MUSTPROMPT)) || ($oObj->Get($sAttCode) == '') ) - { - $paramValue = utils::ReadPostedParam("attr_$sAttCode", '', 'raw_data'); - if ( ($iFlags & OPT_ATT_SLAVE) && ($paramValue != $oObj->Get($sAttCode))) - { - $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - $aErrors[] = Dict::Format('UI:AttemptingToChangeASlaveAttribute_Name', $oAttDef->GetLabel()); - unset($aExpectedAttributes[$sAttCode]); - } - } - } - - $oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes); - - if (count($aErrors) == 0) - { - $sIssues = ''; - $bApplyStimulus = true; - list($bRes, $aIssues) = $oObj->CheckToWrite(); // Check before trying to write the object - if ($bRes) - { - try - { - $bApplyStimulus = $oObj->ApplyStimulus($sStimulus); // will write the object in the DB - } - catch(CoreException $e) - { - // Rollback to the previous state... by reloading the object from the database and applying the modifications again - $oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); - $oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes); - $sIssues = $e->getMessage(); - } - } - else - { - $sIssues = implode(' ', $aIssues); - } - - if (!$bApplyStimulus) - { - $sMessage = Dict::S('UI:FailedToApplyStimuli'); - $sSeverity = 'error'; - } - else if ($sIssues != '') - { - - $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data'); - if ($sOwnershipToken !== null) - { - // Release the concurrent lock, if any, a new lock will be re-acquired by DisplayStimulusForm below - iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); - } - - $bDisplayDetails = false; - // Found issues, explain and give the user a second chance - // - $oObj->DisplayStimulusForm($oP, $sStimulus); - $sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten',$sIssues); - $oP->add_ready_script("alert('".addslashes($sIssueDesc)."');"); - } - else - { - $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'ok'; - utils::RemoveTransaction($sTransactionId); - $bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled'); - if ($bLockEnabled) - { - // Release the concurrent lock, if any - $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data'); - if ($sOwnershipToken !== null) - { - // We're done, let's release the lock - iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); - } - } - } - } - else - { - $sMessage = implode('

', $aErrors); - $sSeverity = 'error'; - } - } - if ($bDisplayDetails) - { - ReloadAndDisplay($oP, $oObj, 'apply_stimulus', $sMessage, $sSeverity); - } - } - else - { - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - } - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'swf_navigator': // Graphical display of the relations "impact" / "depends on" - require_once(APPROOT.'core/simplegraph.class.inc.php'); - require_once(APPROOT.'core/relationgraph.class.inc.php'); - require_once(APPROOT.'core/displayablegraph.class.inc.php'); - $sClass = utils::ReadParam('class', '', false, 'class'); - $id = utils::ReadParam('id', 0); - $sRelation = utils::ReadParam('relation', 'impact'); - $sDirection = utils::ReadParam('direction', 'down'); - $iGroupingThreshold = utils::ReadParam('g', 5); - - $oObj = MetaModel::GetObject($sClass, $id); - $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); - $aSourceObjects = array($oObj); - - $oP->set_title(MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName()); - - $sPageId = "ui-relation-graph-".$sClass.'::'.$id; - $sLabel = $oObj->GetName().' '.MetaModel::GetRelationLabel($sRelation); - $sDescription = MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName(); - $oP->SetBreadCrumbEntry($sPageId, $sLabel, $sDescription); - - if ($sRelation == 'depends on') - { - $sRelation = 'impacts'; - $sDirection = 'up'; - } - if ($sDirection == 'up') - { - $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth); - } - else - { - $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth); - } - - - $aResults = $oRelGraph->GetObjectsByClass(); - $oDisplayGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); - - $oP->AddTabContainer('Navigator'); - $oP->SetCurrentTabContainer('Navigator'); - - $sFirstTab = MetaModel::GetConfig()->Get('impact_analysis_first_tab'); - $sContextKey = "itop-config-mgmt/relation_context/$sClass/$sRelation/$sDirection"; - - // Check if the current object supports Attachments, similar to AttachmentPlugin::IsTargetObject - $sClassForAttachment = null; - $iIdForAttachment = null; - if (class_exists('Attachment')) - { - $aAllowedClasses = MetaModel::GetModuleSetting('itop-attachments', 'allowed_classes', array('Ticket')); - foreach($aAllowedClasses as $sAllowedClass) - { - if ($oObj instanceof $sAllowedClass) - { - $iIdForAttachment = $id; - $sClassForAttachment = $sClass; - } - } - } - $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - - // Display the tabs - if ($sFirstTab == 'list') - { - DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj); - $oP->SetCurrentTab(Dict::S('UI:RelationshipGraph')); - $oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, array(), $sClassForAttachment, $iIdForAttachment, $sContextKey, array('this' => $oObj)); - DisplayNavigatorGroupTab($oP); - } - else - { - $oP->SetCurrentTab(Dict::S('UI:RelationshipGraph')); - $oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, array(), $sClassForAttachment, $iIdForAttachment, $sContextKey, array('this' => $oObj)); - DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj); - DisplayNavigatorGroupTab($oP); - } - - $oP->SetCurrentTab(''); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'kill_lock': - $oP->DisableBreadCrumb(); - $sClass = utils::ReadParam('class', ''); - $id = utils::ReadParam('id', ''); - iTopOwnershipLock::KillLock($sClass, $id); - $oObj = MetaModel::GetObject($sClass, $id); - ReloadAndDisplay($oP, $oObj, 'concurrent_lock_killed', Dict::S('UI:ConcurrentLockKilled'), 'info'); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'cancel': // An action was cancelled - $oP->DisableBreadCrumb(); - $oP->set_title(Dict::S('UI:OperationCancelled')); - $oP->add('

'.Dict::S('UI:OperationCancelled').'

'); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - default: // Menu node rendering (templates) - ApplicationMenu::LoadAdditionalMenus(); - $oMenuNode = ApplicationMenu::GetMenuNode(ApplicationMenu::GetMenuIndexById(ApplicationMenu::GetActiveNodeId())); - if (is_object($oMenuNode)) - { - $oMenuNode->RenderContent($oP, $oAppContext->GetAsHash()); - $oP->set_title($oMenuNode->GetLabel()); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - - } - DisplayWelcomePopup($oP); - $oP->output(); -} -catch(CoreException $e) -{ - require_once(APPROOT.'/setup/setuppage.class.inc.php'); - $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); - if ($e instanceof SecurityException) - { - $oP->add("

".Dict::S('UI:SystemIntrusion')."

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

".Dict::S('UI:FatalErrorMessage')."

\n"); - } - $oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc())); - $oP->output(); - - if (MetaModel::IsLogEnabledIssue()) - { - if (MetaModel::IsValidClass('EventIssue')) - { - try - { - $oLog = new EventIssue(); - - $oLog->Set('message', $e->getMessage()); - $oLog->Set('userinfo', ''); - $oLog->Set('issue', $e->GetIssue()); - $oLog->Set('impact', 'Page could not be displayed'); - $oLog->Set('callstack', $e->getTrace()); - $oLog->Set('data', $e->getContextData()); - $oLog->DBInsertNoReload(); - } - catch(Exception $e) - { - IssueLog::Error("Failed to log issue into the DB"); - } - } - - IssueLog::Error($e->getMessage()); - } - - // For debugging only - //throw $e; -} -catch(Exception $e) -{ - require_once(APPROOT.'/setup/setuppage.class.inc.php'); - $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); - $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); - $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); - $oP->output(); - - if (MetaModel::IsLogEnabledIssue()) - { - if (MetaModel::IsValidClass('EventIssue')) - { - try - { - $oLog = new EventIssue(); - - $oLog->Set('message', $e->getMessage()); - $oLog->Set('userinfo', ''); - $oLog->Set('issue', 'PHP Exception'); - $oLog->Set('impact', 'Page could not be displayed'); - $oLog->Set('callstack', $e->getTrace()); - $oLog->Set('data', array()); - $oLog->DBInsertNoReload(); - } - catch(Exception $e) - { - IssueLog::Error("Failed to log issue into the DB"); - } - } - - IssueLog::Error($e->getMessage()); - } + + + +/** + * Main page of iTop + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + + +/** + * Displays a popup welcome message, once per session at maximum + * until the user unchecks the "Display welcome at startup" + * @param WebPage $oP The current web page for the display + * @return void + */ +function DisplayWelcomePopup(WebPage $oP) +{ + if (!isset($_SESSION['welcome'])) + { + // Check, only once per session, if the popup should be displayed... + // If the user did not already ask for hiding it forever + $bPopup = appUserPreferences::GetPref('welcome_popup', true); + if ($bPopup) + { + $sTemplate = @file_get_contents('../application/templates/welcome_popup.html'); + if ($sTemplate !== false) + { + $oTemplate = new DisplayTemplate($sTemplate); + $oP->add("
"); + $oTemplate->Render($oP, array()); + $oP->add("

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

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

\n"); + $sTitle = addslashes(Dict::S('UI:WelcomeMenu:Title')); + $oP->add_ready_script( +<< ($(window).height()-70)) + { + $('#welcome_popup').height($(window).height()-70); + } +EOF +); + $_SESSION['welcome'] = 'ok'; + } + } + } +} + +/** + * Apply the 'next-action' to the given object or redirect to the page that prompts for additional information if needed + * @param $oP WebPage The page for the output + * @param $oObj CMDBObject The object to process + * @param $sNextAction string The code of the stimulus for the 'action' (i.e. Transition) to apply + */ +function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction) +{ + // Here handle the apply stimulus + $aTransitions = $oObj->EnumTransitions(); + $aStimuli = MetaModel::EnumStimuli(get_class($oObj)); + if (!isset($aTransitions[$sNextAction])) + { + // Invalid stimulus + throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sNextAction, $oObj->GetName(), $oObj->GetStateLabel())); + } + // Get the list of missing mandatory fields for the target state, considering only the changes from the previous form (i.e don't prompt twice) + $aExpectedAttributes = $oObj->GetTransitionAttributes($sNextAction); + + if (count($aExpectedAttributes) == 0) + { + // If all the mandatory fields are already present, just apply the transition silently... + if ($oObj->ApplyStimulus($sNextAction)) + { + $oObj->DBUpdate(); + } + ReloadAndDisplay($oP, $oObj); + } + else + { + // redirect to the 'stimulus' action + $oAppContext = new ApplicationContext(); +//echo "

Missing Attributes

".print_r($aExpectedAttributes, true)."

\n"; + + $oP->add_header('Location: '.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=stimulus&class='.get_class($oObj).'&stimulus='.$sNextAction.'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink()); + } +} + +function ReloadAndDisplay($oPage, $oObj, $sMessageId = '', $sMessage = '', $sSeverity = null) +{ + $oAppContext = new ApplicationContext(); + if ($sMessageId != '') + { + cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), $sMessageId, $sMessage, $sSeverity, 0, true /* must not exist */); + } + $oPage->add_header('Location: '.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class='.get_class($oObj).'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink()); +} +/** + * Displays the details of an object + * @param $oP WebPage Page for the output + * @param $sClass string The name of the class of the object + * @param $oObj DBObject The object to display + * @param $id mixed Identifier of the object (name or ID) + */ +function DisplayDetails($oP, $sClass, $oObj, $id) +{ + $sClassLabel = MetaModel::GetName($sClass); + $oSearch = new DBObjectSearch($sClass); + $oBlock = new DisplayBlock($oSearch, 'search', false); + $oBlock->Display($oP, 0); + + // The object could be listed, check if it is actually allowed to view it + $oSet = CMDBObjectSet::FromObject($oObj); + if (UserRights::IsActionAllowed($sClass, UR_ACTION_READ, $oSet) == UR_ALLOWED_NO) + { + throw new SecurityException('User not allowed to view this object', array('class' => $sClass, 'id' => $id)); + } + $oP->set_title(Dict::Format('UI:DetailsPageTitle', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding + $oObj->DisplayDetails($oP); +} + +/** + * Display the session messages relative to the object identified by its "message key" (class::id) + * @param string $sMessageKey + * @param WebPage $oPage + */ +function DisplayMessages($sMessageKey, WebPage $oPage) +{ + if (array_key_exists('obj_messages', $_SESSION) && array_key_exists($sMessageKey, $_SESSION['obj_messages'])) + { + $aMessages = array(); + $aRanks = array(); + foreach ($_SESSION['obj_messages'][$sMessageKey] as $sMessageId => $aMessageData) + { + $sMsgClass = 'message_'.$aMessageData['severity']; + $aMessages[] = "
".$aMessageData['message']."
"; + $aRanks[] = $aMessageData['rank']; + } + unset($_SESSION['obj_messages'][$sMessageKey]); + array_multisort($aRanks, $aMessages); + foreach ($aMessages as $sMessage) + { + $oPage->add($sMessage); + } + } +} + +/** + * Helper to update the breadrumb for the current object + * @param DBObject $oObj + * @param WebPage $oPage + */ +function SetObjectBreadCrumbEntry(DBObject $oObj, WebPage $oPage) +{ + $sClass = get_class($oObj); // get the leaf class + $sIcon = MetaModel::GetClassIcon($sClass, false); + if ($sIcon == '') + { + $sIcon = utils::GetAbsoluteUrlAppRoot().'images/breadcrumb_object.png'; + } + $oPage->SetBreadCrumbEntry("ui-details-$sClass-".$oObj->GetKey(), $oObj->Get('friendlyname'), MetaModel::GetName($sClass).': '.$oObj->Get('friendlyname'), '', $sIcon); +} + +/** + * Displays the result of a search request + * @param $oP WebPage Web page for the output + * @param $oFilter DBSearch The search of objects to display + * @param $bSearchForm boolean Whether or not to display the search form at the top the page + * @param $sBaseClass string The base class for the search (can be different from the actual class of the results) + * @param $sFormat string The format to use for the output: csv or html + * @param $bDoSearch bool True to display the search results below the search form + * @param $bSearchFormOpen bool True to display the search form fully expanded (only if $bSearchForm of course) + */ +function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '', $sFormat = '', $bDoSearch = true, $bSearchFormOpen = false) +{ + if ($bSearchForm) + { + $aParams = array('open' => $bSearchFormOpen); + if (!empty($sBaseClass)) + { + $aParams['baseClass'] = $sBaseClass; + } + $oBlock = new DisplayBlock($oFilter, 'search', false /* Asynchronous */, $aParams); + $oBlock->Display($oP, 0); + } + if ($bDoSearch) + { + if (strtolower($sFormat) == 'csv') + { + $oBlock = new DisplayBlock($oFilter, 'csv', false); + $oBlock->Display($oP, 1); + // Adjust the size of the Textarea containing the CSV to fit almost all the remaining space + $oP->add_ready_script(" $('#1>textarea').height($('#1').parent().height() - $('#0').outerHeight() - 30).width( $('#1').parent().width() - 20);"); // adjust the size of the block + } + else + { + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oBlock->Display($oP, 1); + + // Breadcrumb + //$iCount = $oBlock->GetDisplayedCount(); + $sPageId = "ui-search-".$oFilter->GetClass(); + $sLabel = MetaModel::GetName($oFilter->GetClass()); + $oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', '../images/breadcrumb-search.png'); + } + } +} + +/** + * Displays a form (checkboxes) to select the objects for which to apply a given action + * Only the objects for which the action is valid can be checked. By default all valid objects are checked + * + * @param \WebPage $oP WebPage The page for output + * @param \DBSearch $oFilter DBSearch The filter that defines the list of objects + * @param string $sNextOperation string The next operation (code) to be executed when the form is submitted + * @param ActionChecker $oChecker ActionChecker The helper class/instance used to check for which object the action is valid + * @param array $aExtraFormParams + * + * @throws \ApplicationException + */ +function DisplayMultipleSelectionForm($oP, $oFilter, $sNextOperation, $oChecker, $aExtraFormParams = array()) +{ + $oAppContext = new ApplicationContext(); + $iBulkActionAllowed = $oChecker->IsAllowed(); + $sClass = $oFilter->GetClass(); + $aExtraParams = array('selection_type' => 'multiple', 'selection_mode' => true, 'display_limit' => false, 'menu' => false); + if ($iBulkActionAllowed == UR_ALLOWED_DEPENDS) + { + $aAllowed = array(); + $aExtraParams['selection_enabled'] = $oChecker->GetAllowedIDs(); + } + else if(UR_ALLOWED_NO) + { + throw new ApplicationException(Dict::Format('UI:ActionNotAllowed')); + } + + $oBlock = new DisplayBlock($oFilter, 'list', false); + $oP->add("
\n"); + $oP->add("\n"); + $oP->add("GetClass()."\">\n"); + $oP->add("Serialize()."\">\n"); + $oP->add("\n"); + foreach($aExtraFormParams as $sName => $sValue) + { + $oP->add("\n"); + } + $oP->add($oAppContext->GetForForm()); + $oBlock->Display($oP, 1, $aExtraParams); + $oP->add("  \n"); + $oP->add("
\n"); + $oP->add_ready_script("$('#1 table.listResults').trigger('check_all');"); +} + +function DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj) +{ + $oP->SetCurrentTab(Dict::S('UI:RelationshipList')); + $oP->add("
"); + $sOldRelation = $sRelation; + if (($sRelation == 'impacts') && ($sDirection == 'up')) + { + $sOldRelation = 'depends on'; + } + $oP->add("

".MetaModel::GetRelationDescription($sOldRelation).' '.$oObj->GetName()."

\n"); + $oP->add("
"); + $oP->add(''); + /* + * Content is rendered asynchronously via pages/ajax.render.php?operation=relation_lists + */ + /* + $iBlock = 1; // Zero is not a valid blockid + foreach($aResults as $sListClass => $aObjects) + { + $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); + $oP->add("
\n"); + $oP->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aObjects), Metamodel::GetName($sListClass))."

\n"); + $oP->add("
\n"); + $oBlock = DisplayBlock::FromObjectSet($oSet, 'list'); + $oBlock->Display($oP, $iBlock++, array('table_id' => get_class($oObj).'_'.$sRelation.'_'.$sDirection.'_'.$sListClass)); + $oP->P(' '); // Some space ? + } + */ + $oP->add("
"); + $oP->add("
"); +} + +function DisplayNavigatorGroupTab($oP) +{ + $oP->SetCurrentTab(Dict::S('UI:RelationGroups')); + $oP->add("
"); + $oP->add(''); + /* + * Content is rendered asynchronously via pages/ajax.render.php?operation=relation_groups + */ + $oP->add("
"); +} + +/*********************************************************************************** + * + * Main user interface page starts here + * + ***********************************************************************************/ +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); +require_once(APPROOT.'/application/itopwebpage.class.inc.php'); +require_once(APPROOT.'/application/wizardhelper.class.inc.php'); + +require_once(APPROOT.'/application/startup.inc.php'); + +try +{ + $operation = utils::ReadParam('operation', ''); + $bPrintable = (utils::ReadParam('printable', 0) == '1'); + + $oKPI = new ExecutionKPI(); + $oKPI->ComputeAndReport('Data model loaded'); + + $oKPI = new ExecutionKPI(); + + require_once(APPROOT.'/application/loginwebpage.class.inc.php'); + $sLoginMessage = LoginWebPage::DoLogin(); // Check user rights and prompt if needed + $oAppContext = new ApplicationContext(); + + $oKPI->ComputeAndReport('User login'); + + $oP = new iTopWebPage(Dict::S('UI:WelcomeToITop'), $bPrintable); + $oP->SetMessage($sLoginMessage); + + + // All the following actions use advanced forms that require more javascript to be loaded + switch($operation) + { + case 'new': // Form to create a new object + case 'modify': // Form to modify an object + case 'apply_new': // Creation of a new object + case 'apply_modify': // Applying the modifications to an existing object + case 'form_for_modify_all': // Form to modify multiple objects (bulk modify) + case 'bulk_stimulus': // For to apply a stimulus to multiple objects + case 'stimulus': // Form displayed when applying a stimulus (state change) + case 'apply_stimulus': // Form displayed when applying a stimulus (state change) + $oP->add_linked_script("../js/json.js"); + $oP->add_linked_script("../js/forms-json-utils.js"); + $oP->add_linked_script("../js/wizardhelper.js"); + $oP->add_linked_script("../js/wizard.utils.js"); + $oP->add_linked_script("../js/linkswidget.js"); + $oP->add_linked_script("../js/linksdirectwidget.js"); + $oP->add_linked_script("../js/extkeywidget.js"); + $oP->add_linked_script("../js/jquery.blockUI.js"); + break; + } + + switch($operation) + { + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'details': // Details of an object + $sClass = utils::ReadParam('class', ''); + $id = utils::ReadParam('id', ''); + if ( empty($sClass) || empty($id)) + { + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + } + + if (is_numeric($id)) + { + $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); + } + else + { + $oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */); + } + if (is_null($oObj)) + { + // Check anyhow if there is a message for this object (like you've just created it) + $sMessageKey = $sClass.'::'.$id; + DisplayMessages($sMessageKey, $oP); + $oP->set_title(Dict::S('UI:ErrorPageTitle')); + + // Attempt to load the object in archive mode + utils::PushArchiveMode(true); + if (is_numeric($id)) + { + $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); + } + else + { + $oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */); + } + utils::PopArchiveMode(); + if (is_null($oObj)) + { + $oP->P(Dict::S('UI:ObjectDoesNotExist')); + } + else + { + SetObjectBreadCrumbEntry($oObj, $oP); + $oP->P(Dict::S('UI:ObjectArchived')); + } + } + else + { + try + { + $oObj->Reload(); + } + catch(Exception $e) + { + // Probably not allowed to see this instance of a derived class + + // Check anyhow if there is a message for this object (like you've just created it) + $sMessageKey = $sClass.'::'.$id; + DisplayMessages($sMessageKey, $oP); + + $oObj = null; + $oP->set_title(Dict::S('UI:ErrorPageTitle')); + $oP->P(Dict::S('UI:ObjectDoesNotExist')); + } + if (!is_null($oObj)) + { + SetObjectBreadCrumbEntry($oObj, $oP); + DisplayDetails($oP, $sClass, $oObj, $id); + } + } + break; + + case 'release_lock_and_details': + $oP->DisableBreadCrumb(); + $sClass = utils::ReadParam('class', ''); + $id = utils::ReadParam('id', ''); + $oObj = MetaModel::GetObject($sClass, $id); + $sToken = utils::ReadParam('token', ''); + if ($sToken != '') + { + iTopOwnershipLock::ReleaseLock($sClass, $id, $sToken); + } + cmdbAbstractObject::ReloadAndDisplay($oP, $oObj, array('operation' => 'details')); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'search_oql': // OQL query + $sOQLClass = utils::ReadParam('oql_class', '', false, 'class'); + $sBaseClass = utils::ReadParam('base_class', $sOQLClass, false, 'class'); + $sOQLClause = utils::ReadParam('oql_clause', '', false, 'raw_data'); + $sFormat = utils::ReadParam('format', ''); + $bSearchForm = utils::ReadParam('search_form', true); + $sTitle = utils::ReadParam('title', 'UI:SearchResultsPageTitle'); + if (empty($sOQLClass)) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'oql_class')); + } + $oP->set_title(Dict::S($sTitle)); + $oP->add('

'.Dict::S($sTitle).'

'); + $sOQL = "SELECT $sOQLClass $sOQLClause"; + try + { + $oFilter = DBObjectSearch::FromOQL($sOQL); + DisplaySearchSet($oP, $oFilter, $bSearchForm, $sBaseClass, $sFormat); + } + catch(CoreException $e) + { + $oFilter = new DBObjectSearch($sOQLClass); + $oSet = new DBObjectSet($oFilter); + if ($bSearchForm) + { + $oBlock = new DisplayBlock($oFilter, 'search', false); + $oBlock->Display($oP, 0); + } + $oP->P(''.Dict::Format('UI:Error:IncorrectOQLQuery_Message', $e->getHtmlDesc()).''); + } + catch(Exception $e) + { + $oP->P(''.Dict::Format('UI:Error:AnErrorOccuredWhileRunningTheQuery_Message', $e->getMessage()).''); + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'search_form': // Search form + $sClass = utils::ReadParam('class', '', false, 'class'); + $sFormat = utils::ReadParam('format', 'html'); + $bSearchForm = utils::ReadParam('search_form', true); + $bDoSearch = utils::ReadParam('do_search', true); + if (empty($sClass)) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); + } + $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); + $oFilter = new DBObjectSearch($sClass); + DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch, true /* Search Form Expanded */); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'search': // Serialized DBSearch + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + $sFormat = utils::ReadParam('format', ''); + $bSearchForm = utils::ReadParam('search_form', true); + if (empty($sFilter)) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); + } + $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); + $oFilter = DBSearch::unserialize($sFilter); // TO DO : check that the filter is valid + $oFilter->UpdateContextFromUser(); + DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'full_text': // Global "google-like" search + $oP->DisableBreadCrumb(); + $sFullText = trim(utils::ReadParam('text', '', false, 'raw_data')); + $iTune = utils::ReadParam('tune', 0); + if (empty($sFullText)) + { + $oP->p(Dict::S('UI:Search:NoSearch')); + } + else + { + $iErrors = 0; + + // Check if a class name/label is supplied to limit the search + $sClassName = ''; + if (preg_match('/^([^\"]+):(.+)$/', $sFullText, $aMatches)) + { + $sClassName = $aMatches[1]; + if (MetaModel::IsValidClass($sClassName)) + { + $sFullText = trim($aMatches[2]); + } + elseif ($sClassName = MetaModel::GetClassFromLabel($sClassName, false /* => not case sensitive */)) + { + $sFullText = trim($aMatches[2]); + } + } + + if (preg_match('/^"(.*)"$/', $sFullText, $aMatches)) + { + // The text is surrounded by double-quotes, remove the quotes and treat it as one single expression + $aFullTextNeedles = array($aMatches[1]); + } + else + { + // Split the text on the blanks and treat this as a search for AND AND + $aFullTextNeedles = explode(' ', $sFullText); + } + + // Check the needle length + $iMinLenth = MetaModel::GetConfig()->Get('full_text_needle_min'); + foreach ($aFullTextNeedles as $sNeedle) + { + if (strlen($sNeedle) < $iMinLenth) + { + $oP->p(Dict::Format('UI:Search:NeedleTooShort', $sNeedle, $iMinLenth)); + $iErrors++; + } + } + + // Sanity check of the accelerators + $aAccelerators = MetaModel::GetConfig()->Get('full_text_accelerators'); + foreach ($aAccelerators as $sClass => $aAccelerator) + { + try + { + $bSkip = array_key_exists('skip', $aAccelerator) ? $aAccelerator['skip'] : false; + if (!$bSkip) + { + $oSearch = DBObjectSearch::FromOQL($aAccelerator['query']); + if ($sClass != $oSearch->GetClass()) + { + $oP->p("Full text accelerator for class '$sClass': searched class mismatch (".$oSearch->GetClass().")"); + $iErrors++; + } + } + } + catch (OqlException $e) + { + $oP->p("Full text accelerator for class '$sClass': ".$e->getHtmlDesc()); + $iErrors++; + } + } + + if ($iErrors == 0) + { + $oP->set_title(Dict::S('UI:SearchResultsPageTitle')); + $sPageId = "ui-global-search"; + $sLabel = Dict::S('UI:SearchResultsTitle'); + $sDescription = Dict::S('UI:SearchResultsTitle+'); + $oP->SetBreadCrumbEntry($sPageId, $sLabel, $sDescription, '', utils::GetAbsoluteUrlAppRoot().'images/search.png'); + $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + $oP->add("
\n"); + $oP->add("
\n"); + $oP->add(' '.Dict::Format('UI:Search:Ongoing', htmlentities($sFullText, ENT_QUOTES, 'UTF-8')).''); + $oP->add("
\n"); + $oP->add("
\n"); + $oP->add("
 
\n"); + $oP->add("

".Dict::Format('UI:FullTextSearchTitle_Text', htmlentities($sFullText, ENT_QUOTES, 'UTF-8'))."

"); + $oP->add("
\n"); + $oP->add("
\n"); + $sJSClass = addslashes($sClassName); + $sJSNeedles = json_encode($aFullTextNeedles); + $oP->add_ready_script( +<< 0) + { + $oP->add_script("var oTimeStatistics = {};"); + } + } + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'modify': // Form to modify an object + $oP->DisableBreadCrumb(); + $sClass = utils::ReadParam('class', '', false, 'class'); + $id = utils::ReadParam('id', ''); + if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + } + // Check if the user can modify this object + $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); + if (is_null($oObj)) + { + $oP->set_title(Dict::S('UI:ErrorPageTitle')); + $oP->P(Dict::S('UI:ObjectDoesNotExist')); + } + else + { + // The object could be read - check if it is allowed to modify it + $oSet = CMDBObjectSet::FromObject($oObj); + if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_NO) + { + throw new SecurityException('User not allowed to modify this object', array('class' => $sClass, 'id' => $id)); + } + // Note: code duplicated to the case 'apply_modify' when a data integrity issue has been found + $oObj->DisplayModifyForm($oP, array('wizard_container' => 1)); // wizard_container: Display the blue borders and the title above the form + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'select_for_modify_all': // Select the list of objects to be modified (bulk modify) + $oP->DisableBreadCrumb(); + $oP->set_title(Dict::S('UI:ModifyAllPageTitle')); + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + if (empty($sFilter)) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); + } + $oFilter = DBObjectSearch::unserialize($sFilter); // TO DO : check that the filter is valid + // Add user filter + $oFilter->UpdateContextFromUser(); + $sClass = $oFilter->GetClass(); + $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_MODIFY); + $oP->add("

".Dict::S('UI:ModifyAllPageTitle')."

\n"); + + DisplayMultipleSelectionForm($oP, $oFilter, 'form_for_modify_all', $oChecker); + break; + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'form_for_modify_all': // Form to modify multiple objects (bulk modify) + $oP->DisableBreadCrumb(); + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + $sClass = utils::ReadParam('class', '', false, 'class'); + $oFullSetFilter = DBObjectSearch::unserialize($sFilter); + // Add user filter + $oFullSetFilter->UpdateContextFromUser(); + $aSelectedObj = utils::ReadMultipleSelection($oFullSetFilter); + $sCancelUrl = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); + $aContext = array('filter' => $sFilter); + cmdbAbstractObject::DisplayBulkModifyForm($oP, $sClass, $aSelectedObj, 'preview_or_modify_all', $sCancelUrl, array(), $aContext); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'preview_or_modify_all': // Preview or apply bulk modify + $oP->DisableBreadCrumb(); + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + $oFilter = DBObjectSearch::unserialize($sFilter); // TO DO : check that the filter is valid + // Add user filter + $oFilter->UpdateContextFromUser(); + $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_MODIFY); + + $sClass = utils::ReadParam('class', '', false, 'class'); + $bPreview = utils::ReadParam('preview_mode', ''); + $sSelectedObj = utils::ReadParam('selectObj', '', false, 'raw_data'); + if ( empty($sClass) || empty($sSelectedObj)) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'selectObj')); + } + $aSelectedObj = explode(',', $sSelectedObj); + $sCancelUrl = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); + $aContext = array( + 'filter' => $sFilter, + 'selectObj' => $sSelectedObj, + ); + cmdbAbstractObject::DoBulkModify($oP, $sClass, $aSelectedObj, 'preview_or_modify_all', $bPreview, $sCancelUrl, $aContext); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'new': // Form to create a new object + $oP->DisableBreadCrumb(); + $sClass = utils::ReadParam('class', '', false, 'class'); + $sStateCode = utils::ReadParam('state', ''); + $bCheckSubClass = utils::ReadParam('checkSubclass', true); + if ( empty($sClass) ) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); + } + +/* + $aArgs = utils::ReadParam('default', array(), false, 'raw_data'); + $aContext = $oAppContext->GetAsHash(); + foreach( $oAppContext->GetNames() as $key) + { + $aArgs[$key] = $oAppContext->GetCurrentValue($key); + } +*/ + // If the specified class has subclasses, ask the user an instance of which class to create + $aSubClasses = MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself + $aPossibleClasses = array(); + $sRealClass = ''; + if ($bCheckSubClass) + { + foreach($aSubClasses as $sCandidateClass) + { + if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) + { + $aPossibleClasses[$sCandidateClass] = MetaModel::GetName($sCandidateClass); + } + } + // Only one of the subclasses can be instantiated... + if (count($aPossibleClasses) == 1) + { + $aKeys = array_keys($aPossibleClasses); + $sRealClass = $aKeys[0]; + } + } + else + { + $sRealClass = $sClass; + } + + if (!empty($sRealClass)) + { + // Display the creation form + $sClassLabel = MetaModel::GetName($sRealClass); + // Note: some code has been duplicated to the case 'apply_new' when a data integrity issue has been found + $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); + $oP->add("

".MetaModel::GetClassIcon($sRealClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); + $oP->add("
\n"); + + // Set all the default values in an object and clone this "default" object + $oObjToClone = MetaModel::NewObject($sRealClass); + + // 1st - set context values + $oAppContext->InitObjectFromContext($oObjToClone); + + // 2nd - set values from the page argument 'default' + $oObjToClone->UpdateObjectFromArg('default'); + + cmdbAbstractObject::DisplayCreationForm($oP, $sRealClass, $oObjToClone, array()); + $oP->add("
\n"); + } + else + { + // Select the derived class to create + $sClassLabel = MetaModel::GetName($sClass); + $oP->add("

".MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); + $oP->add("
\n"); + $oP->add('
'); + $oP->add('

'.Dict::Format('UI:SelectTheTypeOf_Class_ToCreate', $sClassLabel)); + $aDefaults = utils::ReadParam('default', array(), false, 'raw_data'); + $oP->add($oAppContext->GetForForm()); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + foreach($aDefaults as $key => $value) + { + if (is_array($value)) + { + foreach($value as $key2 => $value2) + { + if (is_array($value2)) + { + foreach($value2 as $key3 => $value3) + { + $sValue = htmlentities($value3, ENT_QUOTES, 'UTF-8'); + $oP->add("\n"); + } + } + else + { + $sValue = htmlentities($value2, ENT_QUOTES, 'UTF-8'); + $oP->add("\n"); + } + } + } + else + { + $sValue = htmlentities($value, ENT_QUOTES, 'UTF-8'); + $oP->add("\n"); + } + } + $oP->add(''); + $oP->add(" 

"); + $oP->add('
'); + $oP->add("
\n"); + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'apply_modify': // Applying the modifications to an existing object + $oP->DisableBreadCrumb(); + $sClass = utils::ReadPostedParam('class', ''); + $sClassLabel = MetaModel::GetName($sClass); + $id = utils::ReadPostedParam('id', ''); + $sTransactionId = utils::ReadPostedParam('transaction_id', ''); + if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + } + $bDisplayDetails = true; + $oObj = MetaModel::GetObject($sClass, $id, false); + if ($oObj == null) + { + $bDisplayDetails = false; + $oP->set_title(Dict::S('UI:ErrorPageTitle')); + $oP->P(Dict::S('UI:ObjectDoesNotExist')); + } + elseif (!utils::IsTransactionValid($sTransactionId, false)) + { + $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding + $oP->p("".Dict::S('UI:Error:ObjectAlreadyUpdated')."\n"); + } + else + { + $oObj->UpdateObjectFromPostedForm(); + $sMessage = ''; + $sSeverity = 'ok'; + + if (!$oObj->IsModified()) + { + $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding + $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'info'; + } + else + { + list($bRes, $aIssues) = $oObj->CheckToWrite(); + if ($bRes) + { + try + { + CMDBSource::Query('START TRANSACTION'); + $oObj->DBUpdate(); + CMDBSource::Query('COMMIT'); + $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'ok'; + } + catch(DeleteException $e) + { + CMDBSource::Query('ROLLBACK'); + // Say two things: 1) Don't be afraid nothing was modified + $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'info'; + cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage, $sSeverity, 0, true /* must not exist */); + // 2) Ok, there was some trouble indeed + $sMessage = $e->getMessage(); + $sSeverity = 'error'; + $bDisplayDetails = true; + } + utils::RemoveTransaction($sTransactionId); + + } + else + { + $bDisplayDetails = false; + // Found issues, explain and give the user a second chance + // + $oObj->DisplayModifyForm($oP, array('wizard_container' => true)); // wizard_container: display the wizard border and the title + $sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten', implode(', ', $aIssues)); + $oP->add_ready_script("alert('".addslashes($sIssueDesc)."');"); + } + } + } + if ($bDisplayDetails) + { + $oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); //Workaround: reload the object so that the linkedset are displayed properly + $sNextAction = utils::ReadPostedParam('next_action', ''); + if (!empty($sNextAction)) + { + ApplyNextAction($oP, $oObj, $sNextAction); + } + else + { + // Nothing more to do + ReloadAndDisplay($oP, $oObj, 'update', $sMessage, $sSeverity); + } + + $bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled'); + if ($bLockEnabled) + { + // Release the concurrent lock, if any + $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data'); + if ($sOwnershipToken !== null) + { + // We're done, let's release the lock + iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); + } + } + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'select_for_deletion': // Select multiple objects for deletion + $oP->DisableBreadCrumb(); + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + if (empty($sFilter)) + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'filter')); + } + $oP->set_title(Dict::S('UI:BulkDeletePageTitle')); + $oP->add("

".Dict::S('UI:BulkDeleteTitle')."

\n"); + $oFilter = DBSearch::unserialize($sFilter); // TO DO : check that the filter is valid + $oFilter->UpdateContextFromUser(); + $oChecker = new ActionChecker($oFilter, UR_ACTION_BULK_DELETE); + DisplayMultipleSelectionForm($oP, $oFilter, 'bulk_delete', $oChecker); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'bulk_delete_confirmed': // Confirm bulk deletion of objects + $oP->DisableBreadCrumb(); + $sTransactionId = utils::ReadPostedParam('transaction_id', ''); + if (!utils::IsTransactionValid($sTransactionId)) + { + throw new ApplicationException(Dict::S('UI:Error:ObjectsAlreadyDeleted')); + } + // Fall through + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'delete': + case 'bulk_delete': // Actual bulk deletion (if confirmed) + $oP->DisableBreadCrumb(); + $sClass = utils::ReadParam('class', '', false, 'class'); + $sClassLabel = MetaModel::GetName($sClass); + $aObjects = array(); + if ($operation == 'delete') + { + // Single object + $id = utils::ReadParam('id', ''); + $oObj = MetaModel::GetObject($sClass, $id); + $aObjects[] = $oObj; + if (!UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, DBObjectSet::FromObject($oObj))) + { + throw new SecurityException(Dict::Format('UI:Error:DeleteNotAllowedOn_Class', $sClassLabel)); + } + } + else + { + // Several objects + $sFilter = utils::ReadPostedParam('filter', ''); + $oFullSetFilter = DBObjectSearch::unserialize($sFilter); + // Add user filter + $oFullSetFilter->UpdateContextFromUser(); + $aSelectObject = utils::ReadMultipleSelection($oFullSetFilter); + if ( empty($sClass) || empty($aSelectObject)) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'selectObject[]')); + } + foreach($aSelectObject as $iId) + { + $aObjects[] = MetaModel::GetObject($sClass, $iId); + } + if (count($aObjects) == 1) + { + if (!UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, DBObjectSet::FromArray($sClass, $aObjects))) + { + throw new SecurityException(Dict::Format('UI:Error:BulkDeleteNotAllowedOn_Class', $sClassLabel)); + } + } + else + { + if (!UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, DBObjectSet::FromArray($sClass, $aObjects))) + { + throw new SecurityException(Dict::Format('UI:Error:BulkDeleteNotAllowedOn_Class', $sClassLabel)); + } + $oP->set_title(Dict::S('UI:BulkDeletePageTitle')); + } + } + // Go for the common part... (delete single, delete bulk, delete confirmed) + cmdbAbstractObject::DeleteObjects($oP, $sClass, $aObjects, ($operation != 'bulk_delete_confirmed'), 'bulk_delete_confirmed'); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'apply_new': // Creation of a new object + $oP->DisableBreadCrumb(); + $sClass = utils::ReadPostedParam('class', '', 'class'); + $sClassLabel = MetaModel::GetName($sClass); + $sTransactionId = utils::ReadPostedParam('transaction_id', ''); + if ( empty($sClass) ) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); + } + if (!utils::IsTransactionValid($sTransactionId, false)) + { + $oP->p("".Dict::S('UI:Error:ObjectAlreadyCreated')."\n"); + } + else + { + $oObj = MetaModel::NewObject($sClass); + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + if (!empty($sStateAttCode)) + { + $sTargetState = utils::ReadPostedParam('obj_state', ''); + if ($sTargetState != '') + { + $oObj->Set($sStateAttCode, $sTargetState); + } + } + $oObj->UpdateObjectFromPostedForm(); + } + if (isset($oObj) && is_object($oObj)) + { + $sClass = get_class($oObj); + $sClassLabel = MetaModel::GetName($sClass); + + list($bRes, $aIssues) = $oObj->CheckToWrite(); + if ($bRes) + { + $oObj->DBInsertNoReload(); // No need to reload + utils::RemoveTransaction($sTransactionId); + $oP->set_title(Dict::S('UI:PageTitle:ObjectCreated')); + + // Compute the name, by reloading the object, even if it disappeared from the silo + $oObj = MetaModel::GetObject($sClass, $oObj->GetKey(), true /* Must be found */, true /* Allow All Data*/); + $sName = $oObj->GetName(); + $sMessage = Dict::Format('UI:Title:Object_Of_Class_Created', $sName, $sClassLabel); + + $sNextAction = utils::ReadPostedParam('next_action', ''); + if (!empty($sNextAction)) + { + $oP->add("

$sMessage

"); + ApplyNextAction($oP, $oObj, $sNextAction); + } + else + { + // Nothing more to do + ReloadAndDisplay($oP, $oObj, 'create', $sMessage, 'ok'); + } + } + else + { + // Found issues, explain and give the user a second chance + // + $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); + $oP->add("

".MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); + $oP->add("
\n"); + cmdbAbstractObject::DisplayCreationForm($oP, $sClass, $oObj); + $oP->add("
\n"); + $sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten', implode(', ', $aIssues)); + $oP->add_ready_script("alert('".addslashes($sIssueDesc)."');"); + } + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'select_bulk_stimulus': // Form displayed when applying a stimulus to many objects + $oP->DisableBreadCrumb(); + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + $sStimulus = utils::ReadParam('stimulus', ''); + $sState = utils::ReadParam('state', ''); + if (empty($sFilter) || empty($sStimulus) || empty($sState)) + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); + } + $oFilter = DBObjectSearch::unserialize($sFilter); + $oFilter->UpdateContextFromUser(); + $sClass = $oFilter->GetClass(); + $aStimuli = MetaModel::EnumStimuli($sClass); + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $oP->set_title($sActionLabel); + $oP->add(''); + + $oChecker = new StimulusChecker($oFilter, $sState, $sStimulus); + $aExtraFormParams = array('stimulus' => $sStimulus, 'state' => $sState); + DisplayMultipleSelectionForm($oP, $oFilter, 'bulk_stimulus', $oChecker, $aExtraFormParams); + break; + + case 'bulk_stimulus': + $oP->DisableBreadCrumb(); + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); + $sStimulus = utils::ReadParam('stimulus', ''); + $sState = utils::ReadParam('state', ''); + if (empty($sFilter) || empty($sStimulus) || empty($sState)) + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); + } + $oFilter = DBObjectSearch::unserialize($sFilter); + // Add user filter + $oFilter->UpdateContextFromUser(); + $sClass = $oFilter->GetClass(); + $aSelectObject = utils::ReadMultipleSelection($oFilter); + if (count($aSelectObject) == 0) + { + // Nothing to do, no object was selected ! + throw new ApplicationException(Dict::S('UI:BulkAction:NoObjectSelected')); + } + else + { + $aTransitions = MetaModel::EnumTransitions($sClass, $sState); + $aStimuli = MetaModel::EnumStimuli($sClass); + + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $sTargetState = $aTransitions[$sStimulus]['target_state']; + $aStates = MetaModel::EnumStates($sClass); + $aTargetStateDef = $aStates[$sTargetState]; + + $oP->set_title(Dict::Format('UI:StimulusModify_N_ObjectsOf_Class', $sActionLabel, count($aSelectObject), $sClass)); + $oP->add(''); + + $aExpectedAttributes = MetaModel::GetTransitionAttributes($sClass, $sStimulus, $sState); + $aDetails = array(); + $iFieldIndex = 0; + $aFieldsMap = array(); + $aValues = array(); + $aObjects = array(); + foreach($aSelectObject as $iId) + { + $aObjects[] = MetaModel::GetObject($sClass, $iId); + } + $oSet = DBObjectSet::FromArray($sClass, $aObjects); + $oObj = $oSet->ComputeCommonObject($aValues); + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + $oObj->Set($sStateAttCode,$sTargetState); + $sReadyScript = ''; + foreach($aExpectedAttributes as $sAttCode => $iExpectCode) + { + // Prompt for an attribute if + // - the attribute must be changed or must be displayed to the user for confirmation + // - or the field is mandatory and currently empty + if ( ($iExpectCode & (OPT_ATT_MUSTCHANGE | OPT_ATT_MUSTPROMPT)) || + (($iExpectCode & OPT_ATT_MANDATORY) && ($oObj->Get($sAttCode) == '')) ) + { + $aAttributesDef = MetaModel::ListAttributeDefs($sClass); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aPrerequisites = MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one + if (count($aPrerequisites) > 0) + { + // When 'enabling' a field, all its prerequisites must be enabled too + $sFieldList = "['".implode("','", $aPrerequisites)."']"; + $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n"); + } + $aDependents = MetaModel::GetDependentAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one + if (count($aDependents) > 0) + { + // When 'disabling' a field, all its dependent fields must be disabled too + $sFieldList = "['".implode("','", $aDependents)."']"; + $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); + } + $aArgs = array('this' => $oObj); + $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sAttCode, '', $iExpectCode, $aArgs); + $sComments = ''; + if (!isset($aValues[$sAttCode])) + { + $aValues[$sAttCode] = array(); + } + if (count($aValues[$sAttCode]) == 1) + { + $sComments .= '
1
'; + } + else + { + // Non-homogenous value + $iMaxCount = 5; + $sTip = "

".Dict::Format('UI:BulkModify_Count_DistinctValues', count($aValues[$sAttCode]))."

    "; + $index = 0; + foreach($aValues[$sAttCode] as $sCurrValue => $aVal) + { + $sDisplayValue = empty($aVal['display']) ? ''.Dict::S('Enum:Undefined').'' : str_replace(array("\n", "\r"), " ", $aVal['display']); + $sTip .= "
  • ".Dict::Format('UI:BulkModify:Value_Exists_N_Times', $sDisplayValue, $aVal['count'])."
  • "; + $index++; + if ($iMaxCount == $index) + { + $sTip .= "
  • ".Dict::Format('UI:BulkModify:N_MoreValues', count($aValues[$sAttCode]) - $iMaxCount)."
  • "; + break; + } + } + $sTip .= "

"; + $sTip = addslashes($sTip); + $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n"; + $sComments .= '
'.count($aValues[$sAttCode]).'
'; + } + $aDetails[] = array('label' => ''.$oAttDef->GetLabel().'', 'value' => "$sHTMLValue", 'comments' => $sComments); + $aFieldsMap[$sAttCode] = $sAttCode; + $iFieldIndex++; + } + } + $sButtonsPosition = MetaModel::GetConfig()->Get('buttons_position'); + if ($sButtonsPosition == 'bottom') + { + // bottom: Displays the ticket details BEFORE the actions + $oP->add('
'); + $oObj->DisplayBareProperties($oP); + $oP->add('
'); + } + $oP->add("
\n"); + $oP->add("
\n"); + $oP->add("
\n"); + $oP->details($aDetails); + $oP->add("
\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add("\n"); + $oP->add($oAppContext->GetForForm()); + $oP->add("\n"); + $sURL = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); + $oP->add("    \n"); + $oP->add("\n"); + $oP->add("
\n"); + $oP->add("
\n"); + if ($sButtonsPosition != 'bottom') + { + // top or both: Displays the ticket details AFTER the actions + $oP->add('
'); + $oObj->DisplayBareProperties($oP); + $oP->add('
'); + } + $iFieldsCount = count($aFieldsMap); + $sJsonFieldsMap = json_encode($aFieldsMap); + + $oP->add_script( +<<add_ready_script( +<<DisableBreadCrumb(); + $bPreviewMode = utils::ReadPostedParam('preview_mode', false); + $sFilter = utils::ReadPostedParam('filter', '', false, 'raw_data'); + $sStimulus = utils::ReadPostedParam('stimulus', ''); + $sState = utils::ReadPostedParam('state', ''); + $sSelectObject = utils::ReadPostedParam('selectObject', '', false, 'raw_data'); + $aSelectObject = explode(',', $sSelectObject); + + if (empty($sFilter) || empty($sStimulus) || empty($sState)) + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'filter', 'stimulus', 'state')); + } + $sTransactionId = utils::ReadPostedParam('transaction_id', ''); + if (!utils::IsTransactionValid($sTransactionId)) + { + $oP->p(Dict::S('UI:Error:ObjectAlreadyUpdated')); + } + else + { + // For archiving the modification + $oFilter = DBObjectSearch::unserialize($sFilter); + // Add user filter + $oFilter->UpdateContextFromUser(); + $sClass = $oFilter->GetClass(); + $aObjects = array(); + foreach($aSelectObject as $iId) + { + $aObjects[] = MetaModel::GetObject($sClass, $iId); + } + + $aTransitions = MetaModel::EnumTransitions($sClass, $sState); + $aStimuli = MetaModel::EnumStimuli($sClass); + + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + + $oP->set_title(Dict::Format('UI:StimulusModify_N_ObjectsOf_Class', $sActionLabel, count($aObjects), $sClass)); + $oP->add(''); + + $oSet = DBObjectSet::FromArray($sClass, $aObjects); + + // For reporting + $aHeaders = array( + 'object' => array('label' => MetaModel::GetName($sClass), 'description' => Dict::S('UI:ModifiedObject')), + 'status' => array('label' => Dict::S('UI:BulkModifyStatus'), 'description' => Dict::S('UI:BulkModifyStatus+')), + 'errors' => array('label' => Dict::S('UI:BulkModifyErrors'), 'description' => Dict::S('UI:BulkModifyErrors+')), + ); + $aRows = array(); + while ($oObj = $oSet->Fetch()) + { + $sError = Dict::S('UI:BulkModifyStatusOk'); + try + { + $aTransitions = $oObj->EnumTransitions(); + $aStimuli = MetaModel::EnumStimuli($sClass); + if (!isset($aTransitions[$sStimulus])) + { + throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus, $oObj->GetName(), $oObj->GetStateLabel())); + } + else + { + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $sTargetState = $aTransitions[$sStimulus]['target_state']; + $aExpectedAttributes = $oObj->GetTransitionAttributes($sStimulus /* cureent state */); + $aDetails = array(); + $aErrors = array(); + foreach($aExpectedAttributes as $sAttCode => $iExpectCode) + { + $iFlags = $oObj->GetTransitionFlags($sAttCode, $sStimulus); + if (($iExpectCode & (OPT_ATT_MUSTCHANGE|OPT_ATT_MUSTPROMPT)) || ($oObj->Get($sAttCode) == '') ) + { + $paramValue = utils::ReadPostedParam("attr_$sAttCode", '', 'raw_data'); + if ( ($iFlags & OPT_ATT_SLAVE) && ($paramValue != $oObj->Get($sAttCode)) ) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aErrors[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $oAttDef->GetLabel()); + unset($aExpectedAttributes[$sAttCode]); + } + } + } + + $oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes); + + if (count($aErrors) == 0) + { + if ($oObj->ApplyStimulus($sStimulus)) + { + list($bResult, $aErrors) = $oObj->CheckToWrite(); + $sStatus = $bResult ? Dict::S('UI:BulkModifyStatusModified') : Dict::S('UI:BulkModifyStatusSkipped'); + if ($bResult) + { + $oObj->DBUpdate(); + } + else + { + $sError = '

'.implode('

',$aErrors)."

\n"; + } + } + else + { + $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); + $sError = '

'.Dict::S('UI:FailedToApplyStimuli')."

\n"; + } + } + else + { + $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); + $sError = '

'.implode('

',$aErrors)."

\n"; + } + } + } + catch(Exception $e) + { + $sError = $e->getMessage(); + $sStatus = Dict::S('UI:BulkModifyStatusSkipped'); + } + $aRows[] = array( + 'object' => $oObj->GetHyperlink(), + 'status' => $sStatus, + 'errors' => $sError, + ); + } + $oP->Table($aHeaders, $aRows); + // Back to the list + $sURL = "./UI.php?operation=search&filter=".urlencode($sFilter)."&".$oAppContext->GetForLink(); + $oP->add(''); + } + break; + + case 'stimulus': // Form displayed when applying a stimulus (state change) + $oP->DisableBreadCrumb(); + $sClass = utils::ReadParam('class', '', false, 'class'); + $id = utils::ReadParam('id', ''); + $sStimulus = utils::ReadParam('stimulus', ''); + if ( empty($sClass) || empty($id) || empty($sStimulus) ) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')); + } + $oObj = MetaModel::GetObject($sClass, $id, false); + if ($oObj != null) + { + $oObj->DisplayStimulusForm($oP, $sStimulus); + } + else + { + $oP->set_title(Dict::S('UI:ErrorPageTitle')); + $oP->P(Dict::S('UI:ObjectDoesNotExist')); + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'apply_stimulus': // Actual state change + $oP->DisableBreadCrumb(); + $sClass = utils::ReadPostedParam('class', ''); + $id = utils::ReadPostedParam('id', ''); + $sTransactionId = utils::ReadPostedParam('transaction_id', ''); + $sStimulus = utils::ReadPostedParam('stimulus', ''); + if ( empty($sClass) || empty($id) || empty($sStimulus) ) // TO DO: check that the class name is valid ! + { + throw new ApplicationException(Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')); + } + $oObj = MetaModel::GetObject($sClass, $id, false); + if ($oObj != null) + { + $aTransitions = $oObj->EnumTransitions(); + $aStimuli = MetaModel::EnumStimuli($sClass); + $sMessage = ''; + $sSeverity = 'ok'; + $bDisplayDetails = true; + if (!isset($aTransitions[$sStimulus])) + { + throw new ApplicationException(Dict::Format('UI:Error:Invalid_Stimulus_On_Object_In_State', $sStimulus, $oObj->GetName(), $oObj->GetStateLabel())); + } + if (!utils::IsTransactionValid($sTransactionId)) + { + $sMessage = Dict::S('UI:Error:ObjectAlreadyUpdated'); + $sSeverity = 'info'; + } + else + { + $sActionLabel = $aStimuli[$sStimulus]->GetLabel(); + $sActionDetails = $aStimuli[$sStimulus]->GetDescription(); + $sTargetState = $aTransitions[$sStimulus]['target_state']; + $aExpectedAttributes = $oObj->GetTransitionAttributes($sStimulus /*, current state*/); + $aDetails = array(); + $aErrors = array(); + foreach($aExpectedAttributes as $sAttCode => $iExpectCode) + { + $iFlags = $oObj->GetTransitionFlags($sAttCode, $sStimulus); + if (($iExpectCode & (OPT_ATT_MUSTCHANGE|OPT_ATT_MUSTPROMPT)) || ($oObj->Get($sAttCode) == '') ) + { + $paramValue = utils::ReadPostedParam("attr_$sAttCode", '', 'raw_data'); + if ( ($iFlags & OPT_ATT_SLAVE) && ($paramValue != $oObj->Get($sAttCode))) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $aErrors[] = Dict::Format('UI:AttemptingToChangeASlaveAttribute_Name', $oAttDef->GetLabel()); + unset($aExpectedAttributes[$sAttCode]); + } + } + } + + $oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes); + + if (count($aErrors) == 0) + { + $sIssues = ''; + $bApplyStimulus = true; + list($bRes, $aIssues) = $oObj->CheckToWrite(); // Check before trying to write the object + if ($bRes) + { + try + { + $bApplyStimulus = $oObj->ApplyStimulus($sStimulus); // will write the object in the DB + } + catch(CoreException $e) + { + // Rollback to the previous state... by reloading the object from the database and applying the modifications again + $oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); + $oObj->UpdateObjectFromPostedForm('', array_keys($aExpectedAttributes), $aExpectedAttributes); + $sIssues = $e->getMessage(); + } + } + else + { + $sIssues = implode(' ', $aIssues); + } + + if (!$bApplyStimulus) + { + $sMessage = Dict::S('UI:FailedToApplyStimuli'); + $sSeverity = 'error'; + } + else if ($sIssues != '') + { + + $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data'); + if ($sOwnershipToken !== null) + { + // Release the concurrent lock, if any, a new lock will be re-acquired by DisplayStimulusForm below + iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); + } + + $bDisplayDetails = false; + // Found issues, explain and give the user a second chance + // + $oObj->DisplayStimulusForm($oP, $sStimulus); + $sIssueDesc = Dict::Format('UI:ObjectCouldNotBeWritten',$sIssues); + $oP->add_ready_script("alert('".addslashes($sIssueDesc)."');"); + } + else + { + $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'ok'; + utils::RemoveTransaction($sTransactionId); + $bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled'); + if ($bLockEnabled) + { + // Release the concurrent lock, if any + $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, false, 'raw_data'); + if ($sOwnershipToken !== null) + { + // We're done, let's release the lock + iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); + } + } + } + } + else + { + $sMessage = implode('

', $aErrors); + $sSeverity = 'error'; + } + } + if ($bDisplayDetails) + { + ReloadAndDisplay($oP, $oObj, 'apply_stimulus', $sMessage, $sSeverity); + } + } + else + { + $oP->set_title(Dict::S('UI:ErrorPageTitle')); + $oP->P(Dict::S('UI:ObjectDoesNotExist')); + } + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'swf_navigator': // Graphical display of the relations "impact" / "depends on" + require_once(APPROOT.'core/simplegraph.class.inc.php'); + require_once(APPROOT.'core/relationgraph.class.inc.php'); + require_once(APPROOT.'core/displayablegraph.class.inc.php'); + $sClass = utils::ReadParam('class', '', false, 'class'); + $id = utils::ReadParam('id', 0); + $sRelation = utils::ReadParam('relation', 'impact'); + $sDirection = utils::ReadParam('direction', 'down'); + $iGroupingThreshold = utils::ReadParam('g', 5); + + $oObj = MetaModel::GetObject($sClass, $id); + $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20); + $aSourceObjects = array($oObj); + + $oP->set_title(MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName()); + + $sPageId = "ui-relation-graph-".$sClass.'::'.$id; + $sLabel = $oObj->GetName().' '.MetaModel::GetRelationLabel($sRelation); + $sDescription = MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName(); + $oP->SetBreadCrumbEntry($sPageId, $sLabel, $sDescription); + + if ($sRelation == 'depends on') + { + $sRelation = 'impacts'; + $sDirection = 'up'; + } + if ($sDirection == 'up') + { + $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth); + } + else + { + $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth); + } + + + $aResults = $oRelGraph->GetObjectsByClass(); + $oDisplayGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down')); + + $oP->AddTabContainer('Navigator'); + $oP->SetCurrentTabContainer('Navigator'); + + $sFirstTab = MetaModel::GetConfig()->Get('impact_analysis_first_tab'); + $sContextKey = "itop-config-mgmt/relation_context/$sClass/$sRelation/$sDirection"; + + // Check if the current object supports Attachments, similar to AttachmentPlugin::IsTargetObject + $sClassForAttachment = null; + $iIdForAttachment = null; + if (class_exists('Attachment')) + { + $aAllowedClasses = MetaModel::GetModuleSetting('itop-attachments', 'allowed_classes', array('Ticket')); + foreach($aAllowedClasses as $sAllowedClass) + { + if ($oObj instanceof $sAllowedClass) + { + $iIdForAttachment = $id; + $sClassForAttachment = $sClass; + } + } + } + $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + + // Display the tabs + if ($sFirstTab == 'list') + { + DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj); + $oP->SetCurrentTab(Dict::S('UI:RelationshipGraph')); + $oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, array(), $sClassForAttachment, $iIdForAttachment, $sContextKey, array('this' => $oObj)); + DisplayNavigatorGroupTab($oP); + } + else + { + $oP->SetCurrentTab(Dict::S('UI:RelationshipGraph')); + $oDisplayGraph->Display($oP, $aResults, $sRelation, $oAppContext, array(), $sClassForAttachment, $iIdForAttachment, $sContextKey, array('this' => $oObj)); + DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj); + DisplayNavigatorGroupTab($oP); + } + + $oP->SetCurrentTab(''); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'kill_lock': + $oP->DisableBreadCrumb(); + $sClass = utils::ReadParam('class', ''); + $id = utils::ReadParam('id', ''); + iTopOwnershipLock::KillLock($sClass, $id); + $oObj = MetaModel::GetObject($sClass, $id); + ReloadAndDisplay($oP, $oObj, 'concurrent_lock_killed', Dict::S('UI:ConcurrentLockKilled'), 'info'); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + case 'cancel': // An action was cancelled + $oP->DisableBreadCrumb(); + $oP->set_title(Dict::S('UI:OperationCancelled')); + $oP->add('

'.Dict::S('UI:OperationCancelled').'

'); + break; + + /////////////////////////////////////////////////////////////////////////////////////////// + + default: // Menu node rendering (templates) + ApplicationMenu::LoadAdditionalMenus(); + $oMenuNode = ApplicationMenu::GetMenuNode(ApplicationMenu::GetMenuIndexById(ApplicationMenu::GetActiveNodeId())); + if (is_object($oMenuNode)) + { + $oMenuNode->RenderContent($oP, $oAppContext->GetAsHash()); + $oP->set_title($oMenuNode->GetLabel()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + + } + DisplayWelcomePopup($oP); + $oP->output(); +} +catch(CoreException $e) +{ + require_once(APPROOT.'/setup/setuppage.class.inc.php'); + $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); + if ($e instanceof SecurityException) + { + $oP->add("

".Dict::S('UI:SystemIntrusion')."

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

".Dict::S('UI:FatalErrorMessage')."

\n"); + } + $oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc())); + $oP->output(); + + if (MetaModel::IsLogEnabledIssue()) + { + if (MetaModel::IsValidClass('EventIssue')) + { + try + { + $oLog = new EventIssue(); + + $oLog->Set('message', $e->getMessage()); + $oLog->Set('userinfo', ''); + $oLog->Set('issue', $e->GetIssue()); + $oLog->Set('impact', 'Page could not be displayed'); + $oLog->Set('callstack', $e->getTrace()); + $oLog->Set('data', $e->getContextData()); + $oLog->DBInsertNoReload(); + } + catch(Exception $e) + { + IssueLog::Error("Failed to log issue into the DB"); + } + } + + IssueLog::Error($e->getMessage()); + } + + // For debugging only + //throw $e; +} +catch(Exception $e) +{ + require_once(APPROOT.'/setup/setuppage.class.inc.php'); + $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); + $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); + $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); + $oP->output(); + + if (MetaModel::IsLogEnabledIssue()) + { + if (MetaModel::IsValidClass('EventIssue')) + { + try + { + $oLog = new EventIssue(); + + $oLog->Set('message', $e->getMessage()); + $oLog->Set('userinfo', ''); + $oLog->Set('issue', 'PHP Exception'); + $oLog->Set('impact', 'Page could not be displayed'); + $oLog->Set('callstack', $e->getTrace()); + $oLog->Set('data', array()); + $oLog->DBInsertNoReload(); + } + catch(Exception $e) + { + IssueLog::Error("Failed to log issue into the DB"); + } + } + + IssueLog::Error($e->getMessage()); + } } \ No newline at end of file