1<?php
2// Copyright (C) 2015-2017 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19
20require_once('dbobjectsearch.class.php');
21require_once('dbunionsearch.class.php');
22
23/**
24 * An object search
25 *
26 * Note: in the ancient times of iTop, a search was named after DBObjectSearch.
27 *  When the UNION has been introduced, it has been decided to:
28 *  - declare a hierarchy of search classes, with two leafs :
29 *    - one class to cope with a single query (A JOIN B... WHERE...)
30 *    - and the other to cope with several queries (query1 UNION query2)
31 *  - in order to preserve forward/backward compatibility of the existing modules
32 *    - keep the name of DBObjectSearch even if it a little bit confusing
33 *    - do not provide a type-hint for function parameters defined in the modules
34 *    - leave the statements DBObjectSearch::FromOQL in the modules, though DBSearch is more relevant
35 *
36 * @copyright   Copyright (C) 2015-2017 Combodo SARL
37 * @license     http://opensource.org/licenses/AGPL-3.0
38 */
39
40abstract class DBSearch
41{
42	const JOIN_POINTING_TO = 0;
43	const JOIN_REFERENCED_BY = 1;
44
45	protected $m_bNoContextParameters = false;
46	protected $m_aModifierProperties = array();
47	protected $m_bArchiveMode = false;
48	protected $m_bShowObsoleteData = true;
49
50	public function __construct()
51	{
52		$this->Init();
53	}
54
55	protected function Init()
56	{
57		// Set the obsolete and archive modes to the default ones
58		$this->m_bArchiveMode = utils::IsArchiveMode();
59		$this->m_bShowObsoleteData = true;
60	}
61
62	/**
63	 * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
64	 *
65	 * @return \DBSearch
66	 **/
67	public function DeepClone()
68	{
69		return unserialize(serialize($this)); // Beware this serializes/unserializes the search and its parameters as well
70	}
71
72	abstract public function AllowAllData();
73	abstract public function IsAllDataAllowed();
74
75	public function SetArchiveMode($bEnable)
76	{
77		$this->m_bArchiveMode = $bEnable;
78	}
79	public function GetArchiveMode()
80	{
81		return $this->m_bArchiveMode;
82	}
83
84	public function SetShowObsoleteData($bShow)
85	{
86		$this->m_bShowObsoleteData = $bShow;
87	}
88	public function GetShowObsoleteData()
89	{
90		if ($this->m_bArchiveMode || $this->IsAllDataAllowed())
91		{
92			// Enable obsolete data too!
93			$bRet = true;
94		}
95		else
96		{
97			$bRet = $this->m_bShowObsoleteData;
98		}
99		return $bRet;
100	}
101
102	public function NoContextParameters() {$this->m_bNoContextParameters = true;}
103	public function HasContextParameters() {return $this->m_bNoContextParameters;}
104
105	public function SetModifierProperty($sPluginClass, $sProperty, $value)
106	{
107		$this->m_aModifierProperties[$sPluginClass][$sProperty] = $value;
108	}
109
110	public function GetModifierProperties($sPluginClass)
111	{
112		if (array_key_exists($sPluginClass, $this->m_aModifierProperties))
113		{
114			return $this->m_aModifierProperties[$sPluginClass];
115		}
116		else
117		{
118			return array();
119		}
120	}
121
122	abstract public function GetClassName($sAlias);
123	abstract public function GetClass();
124	abstract public function GetClassAlias();
125
126	/**
127	 * Change the class (only subclasses are supported as of now, because the conditions must fit the new class)
128	 * Defaults to the first selected class (most of the time it is also the first joined class
129	 */
130	abstract public function ChangeClass($sNewClass, $sAlias = null);
131	abstract public function GetSelectedClasses();
132
133	/**
134	 * @param array $aSelectedClasses array of aliases
135	 * @throws CoreException
136	 */
137	abstract public function SetSelectedClasses($aSelectedClasses);
138
139	/**
140	 * Change any alias of the query tree
141	 *
142	 * @param $sOldName
143	 * @param $sNewName
144	 * @return bool True if the alias has been found and changed
145	 */
146	abstract public function RenameAlias($sOldName, $sNewName);
147
148	abstract public function IsAny();
149
150	public function Describe(){return 'deprecated - use ToOQL() instead';}
151	public function DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo){return 'deprecated - use ToOQL() instead';}
152	public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode){return 'deprecated - use ToOQL() instead';}
153	public function DescribeConditionRelTo($aRelInfo){return 'deprecated - use ToOQL() instead';}
154	public function DescribeConditions(){return 'deprecated - use ToOQL() instead';}
155	public function __DescribeHTML(){return 'deprecated - use ToOQL() instead';}
156
157	abstract public function ResetCondition();
158	abstract public function MergeConditionExpression($oExpression);
159	abstract public function AddConditionExpression($oExpression);
160  	abstract public function AddNameCondition($sName);
161	abstract public function AddCondition($sFilterCode, $value, $sOpCode = null);
162	/**
163	 * Specify a condition on external keys or link sets
164	 * @param sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively
165	 *                 Example: infra_list->ci_id->location_id->country
166	 * @param value The value to match (can be an array => IN(val1, val2...)
167	 * @return void
168	 */
169	abstract public function AddConditionAdvanced($sAttSpec, $value);
170	abstract public function AddCondition_FullText($sFullText);
171
172	/**
173	 * @param DBObjectSearch $oFilter
174	 * @param $sExtKeyAttCode
175	 * @param int $iOperatorCode
176	 * @param null $aRealiasingMap array of <old-alias> => <new-alias>, for each alias that has changed
177	 * @throws CoreException
178	 * @throws CoreWarning
179	 */
180	abstract public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null);
181
182	/**
183	 * @param DBObjectSearch $oFilter
184	 * @param $sForeignExtKeyAttCode
185	 * @param int $iOperatorCode
186	 * @param null $aRealiasingMap array of <old-alias> => <new-alias>, for each alias that has changed
187	 */
188	abstract public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null);
189
190	abstract public function Intersect(DBSearch $oFilter);
191
192	/**
193	 * @param DBSearch $oFilter
194	 * @param integer $iDirection
195	 * @param string $sExtKeyAttCode
196	 * @param integer $iOperatorCode
197	 * @param array &$RealisasingMap  Map of aliases from the attached query, that could have been renamed by the optimization process
198	 * @return DBSearch
199	 */
200	public function Join(DBSearch $oFilter, $iDirection, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null)
201	{
202		$oSourceFilter = $this->DeepClone();
203		$oRet = null;
204
205		if ($oFilter instanceof DBUnionSearch)
206		{
207			$aSearches = array();
208			foreach ($oFilter->GetSearches() as $oSearch)
209			{
210				$aSearches[] = $oSourceFilter->Join($oSearch, $iDirection, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap);
211			}
212			$oRet = new DBUnionSearch($aSearches);
213		}
214		else
215		{
216			if ($iDirection === static::JOIN_POINTING_TO)
217			{
218				$oSourceFilter->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap);
219			}
220			else
221			{
222				if ($iOperatorCode !== TREE_OPERATOR_EQUALS)
223				{
224					throw new Exception('Only TREE_OPERATOR_EQUALS  operator code is supported yet for AddCondition_ReferencedBy.');
225				}
226				$oSourceFilter->AddCondition_ReferencedBy($oFilter, $sExtKeyAttCode, TREE_OPERATOR_EQUALS, $aRealiasingMap);
227			}
228			$oRet = $oSourceFilter;
229		}
230
231		return $oRet;
232	}
233
234	abstract public function SetInternalParams($aParams);
235	abstract public function GetInternalParams();
236	abstract public function GetQueryParams($bExcludeMagicParams = true);
237	abstract public function ListConstantFields();
238
239	/**
240	 * Turn the parameters (:xxx) into scalar values in order to easily
241	 * serialize a search
242	 *
243	 * @param array $aArgs
244	 *
245	 * @return string
246	 */
247	abstract public function ApplyParameters($aArgs);
248
249    public function serialize($bDevelopParams = false, $aContextParams = array())
250	{
251		$aQueryParams = $this->GetQueryParams();
252
253		$aContextParams = array_merge($this->GetInternalParams(), $aContextParams);
254
255		foreach($aQueryParams as $sParam => $sValue)
256		{
257			if (isset($aContextParams[$sParam]))
258			{
259				$aQueryParams[$sParam] = $aContextParams[$sParam];
260			}
261			elseif (($iPos = strpos($sParam, '->')) !== false)
262			{
263				$sParamName = substr($sParam, 0, $iPos);
264				if (isset($aContextParams[$sParamName.'->object()']))
265				{
266					$sAttCode = substr($sParam, $iPos + 2);
267					/** @var \DBObject $oObj */
268					$oObj = $aContextParams[$sParamName.'->object()'];
269					if ($oObj->IsModified())
270					{
271						if ($sAttCode == 'id')
272						{
273							$aQueryParams[$sParam] = $oObj->GetKey();
274						}
275						else
276						{
277							$aQueryParams[$sParam] = $oObj->Get($sAttCode);
278						}
279					}
280					else
281					{
282						unset($aQueryParams[$sParam]);
283						// For database objects, serialize only class, key
284						$aQueryParams[$sParamName.'->id'] = $oObj->GetKey();
285						$aQueryParams[$sParamName.'->class'] = get_class($oObj);
286					}
287				}
288			}
289		}
290
291		$sOql = $this->ToOql($bDevelopParams, $aContextParams);
292		return json_encode(array($sOql, $aQueryParams, $this->m_aModifierProperties));
293	}
294
295	/**
296	 * @param string $sValue Serialized OQL query
297	 *
298	 * @return \DBSearch
299	 * @throws \ArchivedObjectException
300	 * @throws \CoreException
301	 * @throws \OQLException
302	 */
303	static public function unserialize($sValue)
304	{
305		$aData = json_decode($sValue, true);
306		if (is_null($aData))
307		{
308			throw new CoreException("Invalid filter parameter");
309		}
310		$sOql = $aData[0];
311		$aParams = $aData[1];
312		$aExtraParams = array();
313		foreach($aParams as $sParam => $sValue)
314		{
315			if (($iPos = strpos($sParam, '->class')) !== false)
316			{
317				$sParamName = substr($sParam, 0, $iPos);
318				if (isset($aParams[$sParamName.'->id']))
319				{
320					$sClass = $aParams[$sParamName.'->class'];
321					$iKey = $aParams[$sParamName.'->id'];
322					$oObj = MetaModel::GetObject($sClass, $iKey);
323					$aExtraParams[$sParamName.'->object()'] = $oObj;
324				}
325			}
326		}
327		$aParams = array_merge($aExtraParams, $aParams);
328		// We've tried to use gzcompress/gzuncompress, but for some specific queries
329		// it was not working at all (See Trac #193)
330		// gzuncompress was issuing a warning "data error" and the return object was null
331		$oRetFilter = self::FromOQL($sOql, $aParams);
332		$oRetFilter->m_aModifierProperties = $aData[2];
333		return $oRetFilter;
334	}
335
336    /**
337     * Create a new DBObjectSearch from $oSearch with a new alias $sAlias
338     *
339     * Note : This has not be tested with UNION queries.
340     *
341     * @param DBSearch $oSearch
342     * @param string $sAlias
343     * @return DBObjectSearch
344     */
345    static public function CloneWithAlias(DBSearch $oSearch, $sAlias)
346    {
347        $oSearchWithAlias = new DBObjectSearch($oSearch->GetClass(), $sAlias);
348        $oSearchWithAlias = $oSearchWithAlias->Intersect($oSearch);
349        return $oSearchWithAlias;
350    }
351
352    abstract public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false);
353
354	static protected $m_aOQLQueries = array();
355
356	// Do not filter out depending on user rights
357	// In particular when we are currently in the process of evaluating the user rights...
358	static public function FromOQL_AllData($sQuery, $aParams = null)
359	{
360		$oRes = self::FromOQL($sQuery, $aParams);
361		$oRes->AllowAllData();
362		return $oRes;
363	}
364
365	/**
366	 * @param string $sQuery
367	 * @param array $aParams
368	 * @return self
369	 * @throws OQLException
370	 */
371	static public function FromOQL($sQuery, $aParams = null)
372	{
373		if (empty($sQuery))
374		{
375			return null;
376		}
377
378		// Query caching
379		$sQueryId = md5($sQuery);
380		$bOQLCacheEnabled = true;
381		if ($bOQLCacheEnabled)
382		{
383			if (array_key_exists($sQueryId, self::$m_aOQLQueries))
384			{
385				// hit!
386				$oResultFilter = self::$m_aOQLQueries[$sQueryId]->DeepClone();
387			}
388			elseif (self::$m_bUseAPCCache)
389			{
390				// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
391				//
392				$sAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-dbsearch-cache-'.$sQueryId;
393				$oKPI = new ExecutionKPI();
394				$result = apc_fetch($sAPCCacheId);
395				$oKPI->ComputeStats('Search APC (fetch)', $sQuery);
396
397				if (is_object($result))
398				{
399					$oResultFilter = $result;
400					self::$m_aOQLQueries[$sQueryId] = $oResultFilter->DeepClone();
401				}
402			}
403		}
404
405		/** @var DBObjectSearch | null $oResultFilter */
406		if (!isset($oResultFilter))
407		{
408			$oKPI = new ExecutionKPI();
409
410			$oOql = new OqlInterpreter($sQuery);
411			$oOqlQuery = $oOql->ParseQuery();
412
413			$oMetaModel = new ModelReflectionRuntime();
414			$oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue
415
416			$oResultFilter = $oOqlQuery->ToDBSearch($sQuery);
417
418			$oKPI->ComputeStats('Parse OQL', $sQuery);
419
420			if ($bOQLCacheEnabled)
421			{
422				self::$m_aOQLQueries[$sQueryId] = $oResultFilter->DeepClone();
423
424				if (self::$m_bUseAPCCache)
425				{
426					$oKPI = new ExecutionKPI();
427					apc_store($sAPCCacheId, $oResultFilter, self::$m_iQueryCacheTTL);
428					$oKPI->ComputeStats('Search APC (store)', $sQueryId);
429				}
430			}
431		}
432
433		if (!is_null($aParams))
434		{
435			$oResultFilter->SetInternalParams($aParams);
436		}
437
438		// Set the default fields
439		$oResultFilter->Init();
440
441		return $oResultFilter;
442	}
443
444	/**
445	 * Alternative to object mapping: the data are transfered directly into an array
446	 * This is 10 times faster than creating a set of objects, and makes sense when optimization is required
447	 *
448	 * @param array $aColumns
449	 * @param array $aOrderBy Array of '[<classalias>.]attcode' => bAscending
450	 * @param array $aArgs
451	 *
452	 * @return array|void
453	 * @throws \CoreException
454	 * @throws \MissingQueryArgument
455	 * @throws \MySQLException
456	 * @throws \MySQLHasGoneAwayException
457	 */
458	public function ToDataArray($aColumns = array(), $aOrderBy = array(), $aArgs = array())
459	{
460		$sSQL = $this->MakeSelectQuery($aOrderBy, $aArgs);
461		$resQuery = CMDBSource::Query($sSQL);
462		if (!$resQuery)
463		{
464			return;
465		}
466
467		if (count($aColumns) == 0)
468		{
469			$aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass()));
470			// Add the standard id (as first column)
471			array_unshift($aColumns, 'id');
472		}
473
474		$aQueryCols = CMDBSource::GetColumns($resQuery, $sSQL);
475
476		$sClassAlias = $this->GetClassAlias();
477		$aColMap = array();
478		foreach ($aColumns as $sAttCode)
479		{
480			$sColName = $sClassAlias.$sAttCode;
481			if (in_array($sColName, $aQueryCols))
482			{
483				$aColMap[$sAttCode] = $sColName;
484			}
485		}
486
487		$aRes = array();
488		while ($aRow = CMDBSource::FetchArray($resQuery))
489		{
490			$aMappedRow = array();
491			foreach ($aColMap as $sAttCode => $sColName)
492			{
493				$aMappedRow[$sAttCode] = $aRow[$sColName];
494			}
495			$aRes[] = $aMappedRow;
496		}
497		CMDBSource::FreeResult($resQuery);
498		return $aRes;
499	}
500
501	////////////////////////////////////////////////////////////////////////////
502	//
503	// Construction of the SQL queries
504	//
505	////////////////////////////////////////////////////////////////////////////
506	protected static $m_aQueryStructCache = array();
507
508
509	/** Generate a Group By SQL request from a search
510	 * @param array $aArgs
511	 * @param array $aGroupByExpr array('alias' => Expression)
512	 * @param bool $bExcludeNullValues
513	 * @param array $aSelectExpr array('alias' => Expression) Additional expressions added to the request
514	 * @param array $aOrderBy array('alias' => bool) true = ASC false = DESC
515	 * @param int $iLimitCount
516	 * @param int $iLimitStart
517	 * @return string SQL query generated
518	 * @throws Exception
519	 */
520	public function MakeGroupByQuery($aArgs, $aGroupByExpr, $bExcludeNullValues = false, $aSelectExpr = array(), $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0)
521	{
522		// Sanity check
523		foreach($aGroupByExpr as $sAlias => $oExpr)
524		{
525			if (!($oExpr instanceof Expression))
526			{
527				throw new CoreException("Wrong parameter for 'Group By' for [$sAlias] (an array('alias' => Expression) is awaited)");
528			}
529		}
530		foreach($aSelectExpr as $sAlias => $oExpr)
531		{
532			if (array_key_exists($sAlias, $aGroupByExpr))
533			{
534				throw new CoreException("Alias collision between 'Group By' and 'Select Expressions' [$sAlias]");
535			}
536			if (!($oExpr instanceof Expression))
537			{
538				throw new CoreException("Wrong parameter for 'Select Expressions' for [$sAlias] (an array('alias' => Expression) is awaited)");
539			}
540		}
541		foreach($aOrderBy as $sAlias => $bAscending)
542		{
543			if (!array_key_exists($sAlias, $aGroupByExpr) && !array_key_exists($sAlias, $aSelectExpr) && ($sAlias != '_itop_count_'))
544			{
545				$aAllowedAliases = array_keys($aSelectExpr);
546				$aAllowedAliases = array_merge($aAllowedAliases,  array_keys($aGroupByExpr));
547				$aAllowedAliases[] = '_itop_count_';
548				throw new CoreException("Wrong alias [$sAlias] for 'Order By'. Allowed values are: ", null, implode(", ", $aAllowedAliases));
549			}
550			if (!is_bool($bAscending))
551			{
552				throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value for '$sAlias''");
553			}
554		}
555
556		if ($bExcludeNullValues)
557		{
558			// Null values are not handled (though external keys set to 0 are allowed)
559			$oQueryFilter = $this->DeepClone();
560			foreach ($aGroupByExpr as $oGroupByExp)
561			{
562				$oNull = new FunctionExpression('ISNULL', array($oGroupByExp));
563				$oNotNull = new BinaryExpression($oNull, '!=', new TrueExpression());
564				$oQueryFilter->AddConditionExpression($oNotNull);
565			}
566		}
567		else
568		{
569			$oQueryFilter = $this;
570		}
571
572		$aAttToLoad = array();
573		$oSQLQuery = $oQueryFilter->GetSQLQuery(array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr, $aSelectExpr);
574
575		$aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams());
576		try
577		{
578			$bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries;
579			$sRes = $oSQLQuery->RenderGroupBy($aScalarArgs, $bBeautifulSQL, $aOrderBy, $iLimitCount, $iLimitStart);
580		}
581		catch (Exception $e)
582		{
583			// Add some information...
584			$e->addInfo('OQL', $this->ToOQL());
585			throw $e;
586		}
587		$this->AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sRes);
588		return $sRes;
589	}
590
591
592	/**
593	 * @param array|hash $aOrderBy Array of '[<classalias>.]attcode' => bAscending
594	 * @param array $aArgs
595	 * @param null $aAttToLoad
596	 * @param null $aExtendedDataSpec
597	 * @param int $iLimitCount
598	 * @param int $iLimitStart
599	 * @param bool $bGetCount
600	 * @return string
601	 * @throws CoreException
602	 * @throws Exception
603	 * @throws MissingQueryArgument
604	 */
605	public function MakeSelectQuery($aOrderBy = array(), $aArgs = array(), $aAttToLoad = null, $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false)
606	{
607		// Check the order by specification, and prefix with the class alias
608		// and make sure that the ordering columns are going to be selected
609		//
610		$sClass = $this->GetClass();
611		$sClassAlias = $this->GetClassAlias();
612		$aOrderSpec = array();
613		foreach ($aOrderBy as $sFieldAlias => $bAscending)
614		{
615			if (!is_bool($bAscending))
616			{
617				throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value");
618			}
619
620			$iDotPos = strpos($sFieldAlias, '.');
621			if ($iDotPos === false)
622			{
623				$sAttClass = $sClass;
624				$sAttClassAlias = $sClassAlias;
625				$sAttCode = $sFieldAlias;
626			}
627			else
628			{
629				$sAttClassAlias = substr($sFieldAlias, 0, $iDotPos);
630				$sAttClass = $this->GetClassName($sAttClassAlias);
631				$sAttCode = substr($sFieldAlias, $iDotPos + 1);
632			}
633
634			if ($sAttCode != 'id')
635			{
636				MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sAttCode, MetaModel::GetAttributesList($sAttClass));
637
638				$oAttDef = MetaModel::GetAttributeDef($sAttClass, $sAttCode);
639				foreach($oAttDef->GetOrderBySQLExpressions($sAttClassAlias) as $sSQLExpression)
640				{
641					$aOrderSpec[$sSQLExpression] = $bAscending;
642				}
643			}
644			else
645			{
646				$aOrderSpec['`'.$sAttClassAlias.$sAttCode.'`'] = $bAscending;
647			}
648
649			// Make sure that the columns used for sorting are present in the loaded columns
650			if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sAttClassAlias][$sAttCode]))
651			{
652				$aAttToLoad[$sAttClassAlias][$sAttCode] = MetaModel::GetAttributeDef($sAttClass, $sAttCode);
653			}
654		}
655
656		$oSQLQuery = $this->GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount);
657
658		if ($this->m_bNoContextParameters)
659		{
660			// Only internal parameters
661			$aScalarArgs = $this->GetInternalParams();
662		}
663		else
664		{
665			// The complete list of arguments will include magic arguments (e.g. current_user->attcode)
666			$aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams());
667		}
668		try
669		{
670			$bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries;
671			$sRes = $oSQLQuery->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount, $bBeautifulSQL);
672			if ($sClassAlias == '_itop_')
673			{
674				IssueLog::Info('SQL Query (_itop_): '.$sRes);
675			}
676		}
677		catch (MissingQueryArgument $e)
678		{
679			// Add some information...
680			$e->addInfo('OQL', $this->ToOQL());
681			throw $e;
682		}
683		$this->AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sRes);
684		return $sRes;
685	}
686
687	protected abstract function IsDataFiltered();
688	protected abstract function SetDataFiltered();
689
690	protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null, $aSelectExpr = null)
691	{
692		$oSearch = $this;
693		if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered())
694		{
695			$oVisibleObjects = UserRights::GetSelectFilter($this->GetClass(), $this->GetModifierProperties('UserRightsGetSelectFilter'));
696			if ($oVisibleObjects === false)
697			{
698				// Make sure this is a valid search object, saying NO for all
699				$oVisibleObjects = DBObjectSearch::FromEmptySet($this->GetClass());
700			}
701			if (is_object($oVisibleObjects))
702			{
703				$oVisibleObjects->AllowAllData();
704				$oSearch = $this->Intersect($oVisibleObjects);
705				/** @var DBSearch $oSearch */
706				$oSearch->SetDataFiltered();
707			}
708		}
709		$oSQLQuery = $oSearch->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, null, $aSelectExpr);
710		$oSQLQuery->SetSourceOQL($oSearch->ToOQL());
711
712		// Join to an additional table, if required...
713		//
714		if ($aExtendedDataSpec != null)
715		{
716			$sTableAlias = '_extended_data_';
717			$aExtendedFields = array();
718			foreach($aExtendedDataSpec['fields'] as $sColumn)
719			{
720				$sColRef = $this->GetClassAlias().'_extdata_'.$sColumn;
721				$aExtendedFields[$sColRef] = new FieldExpressionResolved($sColumn, $sTableAlias);
722			}
723			$oSQLQueryExt = new SQLObjectQuery($aExtendedDataSpec['table'], $sTableAlias, $aExtendedFields);
724			$oSQLQuery->AddInnerJoin($oSQLQueryExt, 'id', $aExtendedDataSpec['join_key'] /*, $sTableAlias*/);
725		}
726
727		return $oSQLQuery;
728	}
729
730	public abstract function GetSQLQueryStructure(
731		$aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null
732	);
733
734	/**
735	 * @return \Expression
736	 */
737	public abstract function GetCriteria();
738
739	public abstract function AddConditionForInOperatorUsingParam($sFilterCode, $aValues, $bPositiveMatch = true);
740
741	/**
742	 * @return string a unique param name
743	 */
744	protected function GenerateUniqueParamName() {
745		return str_replace('.', '', 'param_'.microtime(true).rand(0,100));
746	}
747
748	////////////////////////////////////////////////////////////////////////////
749	//
750	// Cache/Trace/Log queries
751	//
752	////////////////////////////////////////////////////////////////////////////
753	protected static $m_bDebugQuery = false;
754	protected static $m_aQueriesLog = array();
755	protected static $m_bQueryCacheEnabled = false;
756	protected static $m_bUseAPCCache = false;
757	protected static $m_iQueryCacheTTL = 3600;
758	protected static $m_bTraceQueries = false;
759	protected static $m_bIndentQueries = false;
760	protected static $m_bOptimizeQueries = false;
761
762	public static function StartDebugQuery()
763	{
764		$aBacktrace = debug_backtrace();
765		self::$m_bDebugQuery = true;
766	}
767	public static function StopDebugQuery()
768	{
769		self::$m_bDebugQuery = false;
770	}
771
772	public static function EnableQueryCache($bEnabled, $bUseAPC, $iTimeToLive = 3600)
773	{
774		self::$m_bQueryCacheEnabled = $bEnabled;
775		self::$m_bUseAPCCache = $bUseAPC;
776		self::$m_iQueryCacheTTL = $iTimeToLive;
777	}
778	public static function EnableQueryTrace($bEnabled)
779	{
780		self::$m_bTraceQueries = $bEnabled;
781	}
782	public static function EnableQueryIndentation($bEnabled)
783	{
784		self::$m_bIndentQueries = $bEnabled;
785	}
786	public static function EnableOptimizeQuery($bEnabled)
787	{
788		self::$m_bOptimizeQueries = $bEnabled;
789	}
790
791
792	protected function AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sSql)
793	{
794		if (self::$m_bTraceQueries)
795		{
796			$aQueryData = array(
797				'type' => 'select',
798				'filter' => $this,
799				'order_by' => $aOrderBy,
800				'args' => $aArgs,
801				'att_to_load' => $aAttToLoad,
802				'extended_data_spec' => $aExtendedDataSpec,
803				'limit_count' => $iLimitCount,
804				'limit_start' => $iLimitStart,
805				'is_count' => $bGetCount
806			);
807			$sOql = $this->ToOQL(true, $aArgs);
808			self::AddQueryTrace($aQueryData, $sOql, $sSql);
809		}
810	}
811
812	protected function AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sSql)
813	{
814		if (self::$m_bTraceQueries)
815		{
816			$aQueryData = array(
817				'type' => 'group_by',
818				'filter' => $this,
819				'args' => $aArgs,
820				'group_by_expr' => $aGroupByExpr
821			);
822			$sOql = $this->ToOQL(true, $aArgs);
823			self::AddQueryTrace($aQueryData, $sOql, $sSql);
824		}
825	}
826
827	protected static function AddQueryTrace($aQueryData, $sOql, $sSql)
828	{
829		if (self::$m_bTraceQueries)
830		{
831			$sQueryId = md5(serialize($aQueryData));
832			$sMySQLQueryId = md5($sSql);
833			if(!isset(self::$m_aQueriesLog[$sQueryId]))
834			{
835				self::$m_aQueriesLog[$sQueryId]['data'] = serialize($aQueryData);
836				self::$m_aQueriesLog[$sQueryId]['oql'] = $sOql;
837				self::$m_aQueriesLog[$sQueryId]['hits'] = 1;
838			}
839			else
840			{
841				self::$m_aQueriesLog[$sQueryId]['hits']++;
842			}
843			if(!isset(self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]))
844			{
845				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['sql'] = $sSql;
846				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count'] = 1;
847				$iTableCount = count(CMDBSource::ExplainQuery($sSql));
848				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['table_count'] = $iTableCount;
849			}
850			else
851			{
852				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count']++;
853			}
854		}
855	}
856
857	public static function RecordQueryTrace()
858	{
859		if (!self::$m_bTraceQueries)
860		{
861			return;
862		}
863
864		$iOqlCount = count(self::$m_aQueriesLog);
865		$iSqlCount = 0;
866		foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData)
867		{
868			$iSqlCount += $aOqlData['hits'];
869		}
870		$sHtml = "<h2>Stats on SELECT queries: OQL=$iOqlCount, SQL=$iSqlCount</h2>\n";
871		foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData)
872		{
873			$sOql = $aOqlData['oql'];
874			$sHits = $aOqlData['hits'];
875
876			$sHtml .= "<p><b>$sHits</b> hits for OQL query: $sOql</p>\n";
877			$sHtml .= "<ul id=\"ClassesRelationships\" class=\"treeview\">\n";
878			foreach($aOqlData['queries'] as $aSqlData)
879			{
880				$sQuery = $aSqlData['sql'];
881				$sSqlHits = $aSqlData['count'];
882				$iTableCount = $aSqlData['table_count'];
883				$sHtml .= "<li><b>$sSqlHits</b> hits for SQL ($iTableCount tables): <pre style=\"font-size:60%\">$sQuery</pre></li>\n";
884			}
885			$sHtml .= "</ul>\n";
886		}
887
888		$sLogFile = 'queries.latest';
889		file_put_contents(APPROOT.'data/'.$sLogFile.'.html', $sHtml);
890
891		$sLog = "<?php\n\$aQueriesLog = ".var_export(self::$m_aQueriesLog, true).";";
892		file_put_contents(APPROOT.'data/'.$sLogFile.'.log', $sLog);
893
894		// Cumulate the queries
895		$sAllQueries = APPROOT.'data/queries.log';
896		if (file_exists($sAllQueries))
897		{
898			// Merge the new queries into the existing log
899			include($sAllQueries);
900			$aQueriesLog = array();
901			foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData)
902			{
903				if (!array_key_exists($sQueryId, $aQueriesLog))
904				{
905					$aQueriesLog[$sQueryId] = $aOqlData;
906				}
907			}
908		}
909		else
910		{
911			$aQueriesLog = self::$m_aQueriesLog;
912		}
913		$sLog = "<?php\n\$aQueriesLog = ".var_export($aQueriesLog, true).";";
914		file_put_contents($sAllQueries, $sLog);
915	}
916
917	protected static function DbgTrace($value)
918	{
919		if (!self::$m_bDebugQuery)
920		{
921			return;
922		}
923		$aBacktrace = debug_backtrace();
924		$iCallStackPos = count($aBacktrace) - self::$m_bDebugQuery;
925		$sIndent = "";
926		for ($i = 0 ; $i < $iCallStackPos ; $i++)
927		{
928			$sIndent .= " .-=^=-. ";
929		}
930		$aCallers = array();
931		foreach($aBacktrace as $aStackInfo)
932		{
933			$aCallers[] = $aStackInfo["function"];
934		}
935		$sCallers = "Callstack: ".implode(', ', $aCallers);
936		$sFunction = "<b title=\"$sCallers\">".$aBacktrace[1]["function"]."</b>";
937
938		if (is_object($value))
939		{
940			echo "$sIndent$sFunction:\n<pre>\n";
941			print_r($value);
942			echo "</pre>\n";
943		}
944		else
945		{
946			echo "$sIndent$sFunction: $value<br/>\n";
947		}
948	}
949
950	/**
951	 * Experimental!
952	 * todo: implement the change tracking
953	 *
954	 * @param $bArchive
955	 * @throws Exception
956	 */
957	function DBBulkWriteArchiveFlag($bArchive)
958	{
959		$sClass = $this->GetClass();
960		if (!MetaModel::IsArchivable($sClass))
961		{
962			throw new Exception($sClass.' is not an archivable class');
963		}
964
965		$iFlag = $bArchive ? 1 : 0;
966
967		$oSet = new DBObjectSet($this);
968		if (MetaModel::IsStandaloneClass($sClass))
969		{
970			$oSet->OptimizeColumnLoad(array($this->GetClassAlias() => array('')));
971			$aIds = array($sClass => $oSet->GetColumnAsArray('id'));
972		}
973		else
974		{
975			$oSet->OptimizeColumnLoad(array($this->GetClassAlias() => array('finalclass')));
976			$aTemp = $oSet->GetColumnAsArray('finalclass');
977			$aIds = array();
978			foreach ($aTemp as $iObjectId => $sObjectClass)
979			{
980				$aIds[$sObjectClass][$iObjectId] = $iObjectId;
981			}
982		}
983		foreach ($aIds as $sFinalClass => $aObjectIds)
984		{
985			$sIds = implode(', ', $aObjectIds);
986
987			$sArchiveRoot = MetaModel::GetAttributeOrigin($sFinalClass, 'archive_flag');
988			$sRootTable = MetaModel::DBGetTable($sArchiveRoot);
989			$sRootKey = MetaModel::DBGetKey($sArchiveRoot);
990			$aJoins = array("`$sRootTable`");
991			$aUpdates = array();
992			foreach (MetaModel::EnumParentClasses($sFinalClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass)
993			{
994				if (!MetaModel::IsValidAttCode($sParentClass, 'archive_flag'))
995				{
996					continue;
997				}
998
999				$sTable = MetaModel::DBGetTable($sParentClass);
1000				$aUpdates[] = "`$sTable`.`archive_flag` = $iFlag";
1001				if ($sParentClass == $sArchiveRoot)
1002				{
1003					if ($bArchive)
1004					{
1005						// Set the date (do not change it)
1006						$sDate = '"'.date(AttributeDate::GetSQLFormat()).'"';
1007						$aUpdates[] = "`$sTable`.`archive_date` = coalesce(`$sTable`.`archive_date`, $sDate)";
1008					}
1009					else
1010					{
1011						// Reset the date
1012						$aUpdates[] = "`$sTable`.`archive_date` = null";
1013					}
1014				}
1015				else
1016				{
1017					$sKey = MetaModel::DBGetKey($sParentClass);
1018					$aJoins[] = "`$sTable` ON `$sTable`.`$sKey` = `$sRootTable`.`$sRootKey`";
1019				}
1020			}
1021			$sJoins = implode(' INNER JOIN ', $aJoins);
1022			$sValues = implode(', ', $aUpdates);
1023			$sUpdateQuery = "UPDATE $sJoins SET $sValues WHERE `$sRootTable`.`$sRootKey` IN ($sIds)";
1024			CMDBSource::Query($sUpdateQuery);
1025		}
1026	}
1027
1028	public function UpdateContextFromUser()
1029	{
1030		$this->SetShowObsoleteData(utils::ShowObsoleteData());
1031	}
1032}
1033