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