1<?php 2// Copyright (C) 2010-2018 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/** 20 * All objects to be displayed in the application (either as a list or as details) 21 * must implement this interface. 22 */ 23interface iDisplay 24{ 25 26 /** 27 * Maps the given context parameter name to the appropriate filter/search code for this class 28 * @param string $sContextParam Name of the context parameter, i.e. 'org_id' 29 * @return string Filter code, i.e. 'customer_id' 30 */ 31 public static function MapContextParam($sContextParam); 32 /** 33 * This function returns a 'hilight' CSS class, used to hilight a given row in a table 34 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL, 35 * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE 36 * To Be overridden by derived classes 37 * @param void 38 * @return String The desired higlight class for the object/row 39 */ 40 public function GetHilightClass(); 41 /** 42 * Returns the relative path to the page that handles the display of the object 43 * @return string 44 */ 45 public static function GetUIPage(); 46 /** 47 * Displays the details of the object 48 */ 49 public function DisplayDetails(WebPage $oPage, $bEditMode = false); 50} 51 52/** 53 * Class dbObject: the root of persistent classes 54 * 55 * @copyright Copyright (C) 2010-2016 Combodo SARL 56 * @license http://opensource.org/licenses/AGPL-3.0 57 */ 58 59require_once('metamodel.class.php'); 60require_once('deletionplan.class.inc.php'); 61require_once('mutex.class.inc.php'); 62 63 64/** 65 * A persistent object, as defined by the metamodel 66 * 67 * @package iTopORM 68 */ 69abstract class DBObject implements iDisplay 70{ 71 private static $m_aMemoryObjectsByClass = array(); 72 73 /** @var array class => array of ('table' => array of (array of <sql_value>)) */ 74 private static $m_aBulkInsertItems = array(); 75 /** @var array class => array of ('table' => array of <sql_column>) */ 76 private static $m_aBulkInsertCols = array(); 77 private static $m_bBulkInsert = false; 78 79 /** @var bool true IIF the object is mapped to a DB record */ 80 protected $m_bIsInDB = false; 81 protected $m_iKey = null; 82 private $m_aCurrValues = array(); 83 protected $m_aOrigValues = array(); 84 85 protected $m_aExtendedData = null; 86 87 private $m_bDirty = false; // Means: "a modification is ongoing" 88 // The object may have incorrect external keys, then any attempt of reload must be avoided 89 /** 90 * @var boolean|null true if the object has been verified and is consistent with integrity rules 91 * if null, then the check has to be performed again to know the status 92 * @see CheckToWrite() 93 */ 94 private $m_bCheckStatus = null; 95 /** 96 * @var null|boolean true if cannot be saved because of security reason 97 * @see CheckToWrite() 98 */ 99 protected $m_bSecurityIssue = null; 100 /** 101 * @var null|string[] list of issues preventing object save 102 * @see CheckToWrite() 103 */ 104 protected $m_aCheckIssues = null; 105 /** 106 * @var null|string[] list of warnings throws during object save 107 * @see CheckToWrite() 108 * @since 2.6 N°659 uniqueness constraints 109 */ 110 protected $m_aCheckWarnings = null; 111 protected $m_aDeleteIssues = null; 112 113 private $m_bFullyLoaded = false; // Compound objects can be partially loaded 114 private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode 115 protected $m_aTouchedAtt = array(); // list of (potentially) modified sAttCodes 116 protected $m_aModifiedAtt = array(); // real modification status: for each attCode can be: unset => don't know, true => modified, false => not modified (the same value as the original value was set) 117 protected $m_aSynchroData = null; // Set of Synch data related to this object 118 protected $m_sHighlightCode = null; 119 protected $m_aCallbacks = array(); 120 121 // Use the MetaModel::NewObject to build an object (do we have to force it?) 122 public function __construct($aRow = null, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) 123 { 124 if (!empty($aRow)) 125 { 126 $this->FromRow($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec); 127 $this->m_bFullyLoaded = $this->IsFullyLoaded(); 128 $this->m_aTouchedAtt = array(); 129 $this->m_aModifiedAtt = array(); 130 return; 131 } 132 // Creation of a brand new object 133 // 134 135 $this->m_iKey = self::GetNextTempId(get_class($this)); 136 137 // set default values 138 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) 139 { 140 $this->m_aCurrValues[$sAttCode] = $this->GetDefaultValue($sAttCode); 141 $this->m_aOrigValues[$sAttCode] = null; 142 if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName)) 143 { 144 // This field has to be read from the DB 145 // Leave the flag unset (optimization) 146 } 147 else 148 { 149 // No need to trigger a reload for that attribute 150 // Let's consider it as being already fully loaded 151 $this->m_aLoadedAtt[$sAttCode] = true; 152 } 153 } 154 155 $this->UpdateMetaAttributes(); 156 } 157 158 /** 159 * Update meta-attributes depending on the given attribute list 160 * 161 * @param array|null $aAttCodes List of att codes 162 * 163 * @throws \CoreException 164 */ 165 protected function UpdateMetaAttributes($aAttCodes = null) 166 { 167 if (is_null($aAttCodes)) 168 { 169 $aAttCodes = MetaModel::GetAttributesList(get_class($this)); 170 } 171 foreach ($aAttCodes as $sAttCode) 172 { 173 foreach (MetaModel::ListMetaAttributes(get_class($this), $sAttCode) as $sMetaAttCode => $oMetaAttDef) 174 { 175 /** @var \AttributeMetaEnum $oMetaAttDef */ 176 $this->_Set($sMetaAttCode, $oMetaAttDef->MapValue($this)); 177 } 178 } 179 } 180 181 // Read-only <=> Written once (archive) 182 public function RegisterAsDirty() 183 { 184 // While the object may be written to the DB, it is NOT possible to reload it 185 // or at least not possible to reload it the same way 186 $this->m_bDirty = true; 187 } 188 189 public function IsNew() 190 { 191 return (!$this->m_bIsInDB); 192 } 193 194 // Returns an Id for memory objects 195 static protected function GetNextTempId($sClass) 196 { 197 $sRootClass = MetaModel::GetRootClass($sClass); 198 if (!array_key_exists($sRootClass, self::$m_aMemoryObjectsByClass)) 199 { 200 self::$m_aMemoryObjectsByClass[$sRootClass] = 0; 201 } 202 self::$m_aMemoryObjectsByClass[$sRootClass]++; 203 return (- self::$m_aMemoryObjectsByClass[$sRootClass]); 204 } 205 206 public function __toString() 207 { 208 $sRet = ''; 209 $sClass = get_class($this); 210 $sRootClass = MetaModel::GetRootClass($sClass); 211 $iPKey = $this->GetKey(); 212 $sFriendlyname = $this->Get('friendlyname'); 213 $sRet .= "<b title=\"$sRootClass\">$sClass</b>::$iPKey ($sFriendlyname)<br/>\n"; 214 return $sRet; 215 } 216 217 // Restore initial values... mmmm, to be discussed 218 public function DBRevert() 219 { 220 $this->Reload(); 221 } 222 223 protected function IsFullyLoaded() 224 { 225 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) 226 { 227 if (!$oAttDef->LoadInObject()) continue; 228 if (!isset($this->m_aLoadedAtt[$sAttCode]) || !$this->m_aLoadedAtt[$sAttCode]) 229 { 230 return false; 231 } 232 } 233 return true; 234 } 235 236 /** 237 * @param bool $bAllowAllData DEPRECATED: the reload must never fail! 238 * 239 * @throws CoreException 240 * @internal 241 */ 242 public function Reload($bAllowAllData = false) 243 { 244 assert($this->m_bIsInDB); 245 $aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey, false /* must be found */, true /* AllowAllData */); 246 if (empty($aRow)) 247 { 248 throw new CoreException("Failed to reload object of class '".get_class($this)."', id = ".$this->m_iKey); 249 } 250 $this->FromRow($aRow); 251 252 // Process linked set attributes 253 // 254 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef) 255 { 256 if (!$oAttDef->IsLinkSet()) continue; 257 258 $this->m_aCurrValues[$sAttCode] = $oAttDef->GetDefaultValue($this); 259 $this->m_aOrigValues[$sAttCode] = clone $this->m_aCurrValues[$sAttCode]; 260 $this->m_aLoadedAtt[$sAttCode] = true; 261 } 262 263 $this->m_bFullyLoaded = true; 264 $this->m_aTouchedAtt = array(); 265 $this->m_aModifiedAtt = array(); 266 } 267 268 protected function FromRow($aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) 269 { 270 if (strlen($sClassAlias) == 0) 271 { 272 // Default to the current class 273 $sClassAlias = get_class($this); 274 } 275 276 $this->m_iKey = null; 277 $this->m_bIsInDB = true; 278 $this->m_aCurrValues = array(); 279 $this->m_aOrigValues = array(); 280 $this->m_aLoadedAtt = array(); 281 $this->m_bCheckStatus = true; 282 283 // Get the key 284 // 285 $sKeyField = $sClassAlias."id"; 286 if (!array_key_exists($sKeyField, $aRow)) 287 { 288 // #@# Bug ? 289 throw new CoreException("Missing key for class '".get_class($this)."'"); 290 } 291 292 $iPKey = $aRow[$sKeyField]; 293 if (!self::IsValidPKey($iPKey)) 294 { 295 if (is_null($iPKey)) 296 { 297 throw new CoreException("Missing object id in query result (found null)"); 298 } 299 else 300 { 301 throw new CoreException("An object id must be an integer value ($iPKey)"); 302 } 303 } 304 $this->m_iKey = $iPKey; 305 306 // Build the object from an array of "attCode"=>"value") 307 // 308 $bFullyLoaded = true; // ... set to false if any attribute is not found 309 if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad)) 310 { 311 $aAttList = MetaModel::ListAttributeDefs(get_class($this)); 312 } 313 else 314 { 315 $aAttList = $aAttToLoad[$sClassAlias]; 316 } 317 318 foreach($aAttList as $sAttCode=>$oAttDef) 319 { 320 // Skip links (could not be loaded by the mean of this query) 321 if ($oAttDef->IsLinkSet()) continue; 322 323 if (!$oAttDef->LoadInObject()) continue; 324 325 unset($value); 326 $bIsDefined = false; 327 if ($oAttDef->LoadFromDB()) 328 { 329 // Note: we assume that, for a given attribute, if it can be loaded, 330 // then one column will be found with an empty suffix, the others have a suffix 331 // Take care: the function isset will return false in case the value is null, 332 // which is something that could happen on open joins 333 $sAttRef = $sClassAlias.$sAttCode; 334 335 if (array_key_exists($sAttRef, $aRow)) 336 { 337 $value = $oAttDef->FromSQLToValue($aRow, $sAttRef); 338 $bIsDefined = true; 339 } 340 } 341 else 342 { 343 /** @var \AttributeCustomFields $oAttDef */ 344 $value = $oAttDef->ReadValue($this); 345 $bIsDefined = true; 346 } 347 348 if ($bIsDefined) 349 { 350 $this->m_aCurrValues[$sAttCode] = $value; 351 if (is_object($value)) 352 { 353 $this->m_aOrigValues[$sAttCode] = clone $value; 354 } 355 else 356 { 357 $this->m_aOrigValues[$sAttCode] = $value; 358 } 359 $this->m_aLoadedAtt[$sAttCode] = true; 360 } 361 else 362 { 363 // This attribute was expected and not found in the query columns 364 $bFullyLoaded = false; 365 } 366 } 367 368 // Load extended data 369 if ($aExtendedDataSpec != null) 370 { 371 $aExtendedDataSpec['table']; 372 foreach($aExtendedDataSpec['fields'] as $sColumn) 373 { 374 $sColRef = $sClassAlias.'_extdata_'.$sColumn; 375 if (array_key_exists($sColRef, $aRow)) 376 { 377 $this->m_aExtendedData[$sColumn] = $aRow[$sColRef]; 378 } 379 } 380 } 381 return $bFullyLoaded; 382 } 383 384 protected function _Set($sAttCode, $value) 385 { 386 $this->m_aCurrValues[$sAttCode] = $value; 387 $this->m_aTouchedAtt[$sAttCode] = true; 388 unset($this->m_aModifiedAtt[$sAttCode]); 389 } 390 391 public function Set($sAttCode, $value) 392 { 393 if ($sAttCode == 'finalclass') 394 { 395 // Ignore it - this attribute is set upon object creation and that's it 396 return false; 397 } 398 399 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 400 401 if (!$oAttDef->IsWritable()) 402 { 403 $sClass = get_class($this); 404 throw new Exception("Attempting to set the value on the read-only attribute $sClass::$sAttCode"); 405 } 406 407 if ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty) 408 { 409 // First time Set is called... ensure that the object gets fully loaded 410 // Otherwise we would lose the values on a further Reload 411 // + consistency does not make sense ! 412 $this->Reload(); 413 } 414 415 if ($oAttDef->IsExternalKey()) 416 { 417 if (is_object($value)) 418 { 419 // Setting an external key with a whole object (instead of just an ID) 420 // let's initialize also the external fields that depend on it 421 // (useful when building objects in memory and not from a query) 422 /** @var \AttributeExternalKey $oAttDef */ 423 if ( (get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass()))) 424 { 425 throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored"); 426 } 427 428 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) 429 { 430 /** @var \AttributeExternalField $oDef */ 431 if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) 432 { 433 /** @var \DBObject $value */ 434 $this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode()); 435 $this->m_aLoadedAtt[$sCode] = true; 436 } 437 } 438 } 439 else if ($this->m_aCurrValues[$sAttCode] != $value) 440 { 441 // Setting an external key, but no any other information is available... 442 // Invalidate the corresponding fields so that they get reloaded in case they are needed (See Get()) 443 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) 444 { 445 /** @var \AttributeExternalKey $oDef */ 446 if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode)) 447 { 448 $this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode); 449 unset($this->m_aLoadedAtt[$sCode]); 450 } 451 } 452 } 453 } 454 if ($oAttDef->IsLinkSet() && ($value != null)) 455 { 456 $realvalue = clone $this->m_aCurrValues[$sAttCode]; 457 $realvalue->UpdateFromCompleteList($value); 458 } 459 else 460 { 461 $realvalue = $oAttDef->MakeRealValue($value, $this); 462 } 463 $this->_Set($sAttCode, $realvalue); 464 465 $this->UpdateMetaAttributes(array($sAttCode)); 466 467 // The object has changed, reset caches 468 $this->m_bCheckStatus = null; 469 470 // Make sure we do not reload it anymore... before saving it 471 $this->RegisterAsDirty(); 472 473 // This function is eligible as a lifecycle action: returning true upon success is a must 474 return true; 475 } 476 477 /** 478 * @param string $sAttCode 479 * @param mixed $value 480 * 481 * @throws \CoreException 482 * @throws \CoreUnexpectedValue 483 * @throws \Exception 484 * @since 2.6 485 */ 486 public function SetIfNull($sAttCode, $value) 487 { 488 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 489 $oCurrentValue = $this->Get($sAttCode); 490 if ($oAttDef->IsNull($oCurrentValue)) 491 { 492 $this->Set($sAttCode, $value); 493 } 494 } 495 496 public function SetTrim($sAttCode, $sValue) 497 { 498 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 499 $iMaxSize = $oAttDef->GetMaxSize(); 500 if ($iMaxSize && (strlen($sValue) > $iMaxSize)) 501 { 502 $sValue = substr($sValue, 0, $iMaxSize); 503 } 504 $this->Set($sAttCode, $sValue); 505 } 506 507 public function GetLabel($sAttCode) 508 { 509 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 510 return $oAttDef->GetLabel(); 511 } 512 513 public function Get($sAttCode) 514 { 515 if (($iPos = strpos($sAttCode, '->')) === false) 516 { 517 return $this->GetStrict($sAttCode); 518 } 519 else 520 { 521 $sExtKeyAttCode = substr($sAttCode, 0, $iPos); 522 $sRemoteAttCode = substr($sAttCode, $iPos + 2); 523 if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) 524 { 525 throw new CoreException("Unknown external key '$sExtKeyAttCode' for the class ".get_class($this)); 526 } 527 528 $oExtFieldAtt = MetaModel::FindExternalField(get_class($this), $sExtKeyAttCode, $sRemoteAttCode); 529 if (!is_null($oExtFieldAtt)) 530 { 531 /** @var \AttributeExternalField $oExtFieldAtt */ 532 return $this->GetStrict($oExtFieldAtt->GetCode()); 533 } 534 else 535 { 536 $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); 537 /** @var \AttributeExternalKey $oKeyAttDef */ 538 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 539 $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); 540 if (is_null($oRemoteObj)) 541 { 542 return ''; 543 } 544 else 545 { 546 return $oRemoteObj->Get($sRemoteAttCode); 547 } 548 } 549 } 550 } 551 552 public function GetStrict($sAttCode) 553 { 554 if ($sAttCode == 'id') 555 { 556 return $this->m_iKey; 557 } 558 559 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 560 561 if (!$oAttDef->LoadInObject()) 562 { 563 $value = $oAttDef->GetValue($this); 564 } 565 else 566 { 567 if (isset($this->m_aLoadedAtt[$sAttCode])) 568 { 569 // Standard case... we have the information directly 570 } 571 elseif ($this->m_bIsInDB && !$this->m_bDirty) 572 { 573 // Lazy load (polymorphism): complete by reloading the entire object 574 // #@# non-scalar attributes.... handle that differently? 575 $oKPI = new ExecutionKPI(); 576 $this->Reload(); 577 $oKPI->ComputeStats('Reload', get_class($this).'/'.$sAttCode); 578 } 579 elseif ($sAttCode == 'friendlyname') 580 { 581 // The friendly name is not computed and the object is dirty 582 // Todo: implement the computation of the friendly name based on sprintf() 583 // 584 $this->m_aCurrValues[$sAttCode] = ''; 585 } 586 else 587 { 588 // Not loaded... is it related to an external key? 589 if ($oAttDef->IsExternalField()) 590 { 591 // Let's get the object and compute all of the corresponding attributes 592 // (i.e not only the requested attribute) 593 // 594 /** @var \AttributeExternalField $oAttDef */ 595 $sExtKeyAttCode = $oAttDef->GetKeyAttCode(); 596 597 if (($iRemote = $this->Get($sExtKeyAttCode)) && ($iRemote > 0)) // Objects in memory have negative IDs 598 { 599 $oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); 600 // Note: "allow all data" must be enabled because the external fields are always visible 601 // to the current user even if this is not the case for the remote object 602 // This is consistent with the behavior of the lists 603 /** @var \AttributeExternalKey $oExtKeyAttDef */ 604 $oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote, true, true); 605 } 606 else 607 { 608 $oRemote = null; 609 } 610 611 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef) 612 { 613 /** @var \AttributeExternalField $oDef */ 614 if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sExtKeyAttCode)) 615 { 616 if ($oRemote) 617 { 618 $this->m_aCurrValues[$sCode] = $oRemote->Get($oDef->GetExtAttCode()); 619 } 620 else 621 { 622 $this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode); 623 } 624 $this->m_aLoadedAtt[$sCode] = true; 625 } 626 } 627 } 628 } 629 $value = $this->m_aCurrValues[$sAttCode]; 630 } 631 632 if ($value instanceof ormLinkSet) 633 { 634 $value->Rewind(); 635 } 636 return $value; 637 } 638 639 public function GetOriginal($sAttCode) 640 { 641 if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this)))) 642 { 643 throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this)); 644 } 645 $aOrigValues = $this->m_aOrigValues; 646 return isset($aOrigValues[$sAttCode]) ? $aOrigValues[$sAttCode] : null; 647 } 648 649 /** 650 * Returns the default value of the $sAttCode. By default, returns the default value of the AttributeDefinition. 651 * Overridable. 652 * 653 * @param $sAttCode 654 * @return mixed 655 */ 656 public function GetDefaultValue($sAttCode) 657 { 658 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 659 return $oAttDef->GetDefaultValue($this); 660 } 661 662 /** 663 * Returns data loaded by the mean of a dynamic and explicit JOIN 664 */ 665 public function GetExtendedData() 666 { 667 return $this->m_aExtendedData; 668 } 669 670 /** 671 * Set the HighlightCode if the given code has a greater rank than the current HilightCode 672 * @param string $sCode 673 * @return void 674 */ 675 protected function SetHighlightCode($sCode) 676 { 677 $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); 678 $fCurrentRank = 0.0; 679 if (($this->m_sHighlightCode !== null) && array_key_exists($this->m_sHighlightCode, $aHighlightScale)) 680 { 681 $fCurrentRank = $aHighlightScale[$this->m_sHighlightCode]['rank']; 682 } 683 684 if (array_key_exists($sCode, $aHighlightScale)) 685 { 686 $fRank = $aHighlightScale[$sCode]['rank']; 687 if ($fRank > $fCurrentRank) 688 { 689 $this->m_sHighlightCode = $sCode; 690 } 691 } 692 } 693 694 /** 695 * Get the current HighlightCode 696 * @return string The Hightlight code (null if none set, meaning rank = 0) 697 */ 698 protected function GetHighlightCode() 699 { 700 return $this->m_sHighlightCode; 701 } 702 703 protected function ComputeHighlightCode() 704 { 705 // First if the state defines a HiglightCode, apply it 706 $sState = $this->GetState(); 707 if ($sState != '') 708 { 709 $sCode = MetaModel::GetHighlightCode(get_class($this), $sState); 710 $this->SetHighlightCode($sCode); 711 } 712 // The check for each StopWatch if a HighlightCode is effective 713 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 714 { 715 if ($oAttDef instanceof AttributeStopWatch) 716 { 717 $oStopWatch = $this->Get($sAttCode); 718 $sCode = $oStopWatch->GetHighlightCode(); 719 if ($sCode !== '') 720 { 721 $this->SetHighlightCode($sCode); 722 } 723 } 724 } 725 return $this->GetHighlightCode(); 726 } 727 728 /** 729 * Updates the value of an external field by (re)loading the object 730 * corresponding to the external key and getting the value from it 731 * 732 * UNUSED ? 733 * 734 * @param string $sAttCode Attribute code of the external field to update 735 * @return void 736 */ 737 protected function UpdateExternalField($sAttCode) 738 { 739 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 740 if ($oAttDef->IsExternalField()) 741 { 742 /** @var \AttributeExternalField $oAttDef */ 743 $sTargetClass = $oAttDef->GetTargetClass(); 744 $objkey = $this->Get($oAttDef->GetKeyAttCode()); 745 // Note: "allow all data" must be enabled because the external fields are always visible 746 // to the current user even if this is not the case for the remote object 747 // This is consistent with the behavior of the lists 748 $oObj = MetaModel::GetObject($sTargetClass, $objkey, true, true); 749 if (is_object($oObj)) 750 { 751 $value = $oObj->Get($oAttDef->GetExtAttCode()); 752 $this->Set($sAttCode, $value); 753 } 754 } 755 } 756 757 /** 758 * Overridable callback, called by \DBObject::DoComputeValues 759 * 760 * @api 761 */ 762 public function ComputeValues() 763 { 764 } 765 766 /** 767 * Compute scalar attributes that depend on any other type of attribute 768 * 769 * @throws \CoreException 770 * @throws \CoreUnexpectedValue 771 * 772 * @internal 773 */ 774 final public function DoComputeValues() 775 { 776 // TODO - use a flag rather than checking the call stack -> this will certainly accelerate things 777 778 // First check that we are not currently computing the fields 779 // (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...) 780 foreach (debug_backtrace() as $aCallInfo) 781 { 782 if (!array_key_exists("class", $aCallInfo)) continue; 783 if ($aCallInfo["class"] != get_class($this)) continue; 784 if ($aCallInfo["function"] != "ComputeValues") continue; 785 return; //skip! 786 } 787 788 // Set the "null-not-allowed" datetimes (and dates) whose value is not initialized 789 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 790 { 791 // AttributeDate is derived from AttributeDateTime 792 if (($oAttDef instanceof AttributeDateTime) && (!$oAttDef->IsNullAllowed()) && ($this->Get($sAttCode) == $oAttDef->GetNullValue())) 793 { 794 $this->Set($sAttCode, date($oAttDef->GetInternalFormat())); 795 } 796 } 797 798 $this->ComputeValues(); 799 } 800 801 public function GetAsHTML($sAttCode, $bLocalize = true) 802 { 803 $sClass = get_class($this); 804 $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); 805 806 if ($oAtt->IsExternalKey(EXTKEY_ABSOLUTE)) 807 { 808 //return $this->Get($sAttCode.'_friendlyname'); 809 /** @var \AttributeExternalKey $oAtt */ 810 $sTargetClass = $oAtt->GetTargetClass(EXTKEY_ABSOLUTE); 811 $iTargetKey = $this->Get($sAttCode); 812 if ($iTargetKey < 0) 813 { 814 // the key points to an object that exists only in memory... no hyperlink points to it yet 815 return ''; 816 } 817 else 818 { 819 $sHtmlLabel = htmlentities($this->Get($sAttCode.'_friendlyname'), ENT_QUOTES, 'UTF-8'); 820 $bArchived = $this->IsArchived($sAttCode); 821 $bObsolete = $this->IsObsolete($sAttCode); 822 return $this->MakeHyperLink($sTargetClass, $iTargetKey, $sHtmlLabel, null, true, $bArchived, $bObsolete); 823 } 824 } 825 826 // That's a standard attribute (might be an ext field or a direct field, etc.) 827 return $oAtt->GetAsHTML($this->Get($sAttCode), $this, $bLocalize); 828 } 829 830 public function GetEditValue($sAttCode) 831 { 832 $sClass = get_class($this); 833 $oAtt = MetaModel::GetAttributeDef($sClass, $sAttCode); 834 835 if ($oAtt->IsExternalKey()) 836 { 837 /** @var \AttributeExternalKey $oAtt */ 838 $sTargetClass = $oAtt->GetTargetClass(); 839 if ($this->IsNew()) 840 { 841 // The current object exists only in memory, don't try to query it in the DB ! 842 // instead let's query for the object pointed by the external key, and get its name 843 $targetObjId = $this->Get($sAttCode); 844 $oTargetObj = MetaModel::GetObject($sTargetClass, $targetObjId, false); // false => not sure it exists 845 if (is_object($oTargetObj)) 846 { 847 $sEditValue = $oTargetObj->GetName(); 848 } 849 else 850 { 851 $sEditValue = 0; 852 } 853 } 854 else 855 { 856 $sEditValue = $this->Get($sAttCode.'_friendlyname'); 857 } 858 } 859 else 860 { 861 $sEditValue = $oAtt->GetEditValue($this->Get($sAttCode), $this); 862 } 863 return $sEditValue; 864 } 865 866 public function GetAsXML($sAttCode, $bLocalize = true) 867 { 868 $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 869 return $oAtt->GetAsXML($this->Get($sAttCode), $this, $bLocalize); 870 } 871 872 public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) 873 { 874 $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 875 return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); 876 } 877 878 public function GetOriginalAsHTML($sAttCode, $bLocalize = true) 879 { 880 $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 881 return $oAtt->GetAsHTML($this->GetOriginal($sAttCode), $this, $bLocalize); 882 } 883 884 public function GetOriginalAsXML($sAttCode, $bLocalize = true) 885 { 886 $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 887 return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this, $bLocalize); 888 } 889 890 public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) 891 { 892 $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 893 return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); 894 } 895 896 /** 897 * @param string $sObjClass 898 * @param string $sObjKey 899 * @param string $sHtmlLabel Label with HTML entities escaped (< escaped as <) 900 * @param null $sUrlMakerClass 901 * @param bool|true $bWithNavigationContext 902 * @param bool|false $bArchived 903 * @param bool|false $bObsolete 904 * 905 * @return string 906 * @throws \ArchivedObjectException 907 * @throws \CoreException 908 * @throws \DictExceptionMissingString 909 */ 910 public static function MakeHyperLink($sObjClass, $sObjKey, $sHtmlLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true, $bArchived = false, $bObsolete = false) 911 { 912 if ($sObjKey <= 0) return '<em>'.Dict::S('UI:UndefinedObject').'</em>'; // Objects built in memory have negative IDs 913 914 // Safety net 915 // 916 if (empty($sHtmlLabel)) 917 { 918 // If the object if not issued from a query but constructed programmatically 919 // the label may be empty. In this case run a query to get the object's friendly name 920 $oTmpObj = MetaModel::GetObject($sObjClass, $sObjKey, false); 921 if (is_object($oTmpObj)) 922 { 923 $sHtmlLabel = $oTmpObj->GetName(); 924 } 925 else 926 { 927 // May happen in case the target object is not in the list of allowed values for this attribute 928 $sHtmlLabel = "<em>$sObjClass::$sObjKey</em>"; 929 } 930 } 931 $sHint = MetaModel::GetName($sObjClass)."::$sObjKey"; 932 $sUrl = ApplicationContext::MakeObjectUrl($sObjClass, $sObjKey, $sUrlMakerClass, $bWithNavigationContext); 933 934 $bClickable = !$bArchived || utils::IsArchiveMode(); 935 if ($bArchived) 936 { 937 $sSpanClass = 'archived'; 938 $sFA = 'fa-archive object-archived'; 939 $sHint = Dict::S('ObjectRef:Archived'); 940 } 941 elseif ($bObsolete) 942 { 943 $sSpanClass = 'obsolete'; 944 $sFA = 'fa-eye-slash object-obsolete'; 945 $sHint = Dict::S('ObjectRef:Obsolete'); 946 } 947 else 948 { 949 $sSpanClass = ''; 950 $sFA = ''; 951 } 952 if ($sFA == '') 953 { 954 $sIcon = ''; 955 } 956 else 957 { 958 if ($bClickable) 959 { 960 $sIcon = "<span class=\"object-ref-icon fa $sFA fa-1x fa-fw\"></span>"; 961 } 962 else 963 { 964 $sIcon = "<span class=\"object-ref-icon-disabled fa $sFA fa-1x fa-fw\"></span>"; 965 } 966 } 967 968 if ($bClickable && (strlen($sUrl) > 0)) 969 { 970 $sHLink = "<a class=\"object-ref-link\" href=\"$sUrl\">$sIcon$sHtmlLabel</a>"; 971 } 972 else 973 { 974 $sHLink = $sIcon.$sHtmlLabel; 975 } 976 $sRet = "<span class=\"object-ref $sSpanClass\" title=\"$sHint\">$sHLink</span>"; 977 return $sRet; 978 } 979 980 /** 981 * @param string $sUrlMakerClass 982 * @param bool $bWithNavigationContext 983 * @param string $sLabel 984 * 985 * @return string 986 * @throws \DictExceptionMissingString 987 */ 988 public function GetHyperlink($sUrlMakerClass = null, $bWithNavigationContext = true, $sLabel = null) 989 { 990 if($sLabel === null) 991 { 992 $sLabel = $this->GetName(); 993 } 994 $bArchived = $this->IsArchived(); 995 $bObsolete = $this->IsObsolete(); 996 return self::MakeHyperLink(get_class($this), $this->GetKey(), $sLabel, $sUrlMakerClass, $bWithNavigationContext, $bArchived, $bObsolete); 997 } 998 999 public static function ComputeStandardUIPage($sClass) 1000 { 1001 static $aUIPagesCache = array(); // Cache to store the php page used to display each class of object 1002 if (!isset($aUIPagesCache[$sClass])) 1003 { 1004 $UIPage = false; 1005 if (is_callable("$sClass::GetUIPage")) 1006 { 1007 $UIPage = eval("return $sClass::GetUIPage();"); // May return false in case of error 1008 } 1009 $aUIPagesCache[$sClass] = $UIPage === false ? './UI.php' : $UIPage; 1010 } 1011 $sPage = $aUIPagesCache[$sClass]; 1012 return $sPage; 1013 } 1014 1015 public static function GetUIPage() 1016 { 1017 return 'UI.php'; 1018 } 1019 1020 1021 // could be in the metamodel ? 1022 public static function IsValidPKey($value) 1023 { 1024 return ((string)$value === (string)(int)$value); 1025 } 1026 1027 public function GetKey() 1028 { 1029 return $this->m_iKey; 1030 } 1031 public function SetKey($iNewKey) 1032 { 1033 if (!self::IsValidPKey($iNewKey)) 1034 { 1035 throw new CoreException("An object id must be an integer value ($iNewKey)"); 1036 } 1037 1038 if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey)) 1039 { 1040 throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database"); 1041 } 1042 $this->m_iKey = $iNewKey; 1043 } 1044 /** 1045 * Get the icon representing this object 1046 * @param boolean $bImgTag If true the result is a full IMG tag (or an emtpy string if no icon is defined) 1047 * @return string Either the full IMG tag ($bImgTag == true) or just the URL to the icon file 1048 */ 1049 public function GetIcon($bImgTag = true) 1050 { 1051 $sCode = $this->ComputeHighlightCode(); 1052 if($sCode != '') 1053 { 1054 $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); 1055 if (array_key_exists($sCode, $aHighlightScale)) 1056 { 1057 $sIconUrl = $aHighlightScale[$sCode]['icon']; 1058 if($bImgTag) 1059 { 1060 return "<img src=\"$sIconUrl\" style=\"vertical-align:middle\"/>"; 1061 } 1062 else 1063 { 1064 return $sIconUrl; 1065 } 1066 } 1067 } 1068 return MetaModel::GetClassIcon(get_class($this), $bImgTag); 1069 } 1070 1071 /** 1072 * Get the name as defined in the dictionary 1073 * @return string (empty for default name scheme) 1074 */ 1075 public static function GetClassName($sClass) 1076 { 1077 $sStringCode = 'Class:'.$sClass; 1078 return Dict::S($sStringCode, str_replace('_', ' ', $sClass)); 1079 } 1080 1081 /** 1082 * Get the description as defined in the dictionary 1083 * @param string $sClass 1084 * 1085 * @return string 1086 */ 1087 final static public function GetClassDescription($sClass) 1088 { 1089 $sStringCode = 'Class:'.$sClass.'+'; 1090 return Dict::S($sStringCode, ''); 1091 } 1092 1093 /** 1094 * Gets the name of an object in a safe manner for displaying inside a web page 1095 * 1096 * @return string 1097 * @throws \CoreException 1098 */ 1099 public function GetName() 1100 { 1101 return htmlentities($this->GetRawName(), ENT_QUOTES, 'UTF-8'); 1102 } 1103 1104 /** 1105 * Gets the raw name of an object, this is not safe for displaying inside a web page 1106 * since the " < > characters are not escaped and the name may contain some XSS script 1107 * instructions. 1108 * Use this function only for internal computations or for an output to a non-HTML destination 1109 * 1110 * @return string 1111 * @throws \CoreException 1112 */ 1113 public function GetRawName() 1114 { 1115 return $this->Get('friendlyname'); 1116 } 1117 1118 /** 1119 * @return mixed|string '' if no state attribute, object representing its value otherwise 1120 * @throws \CoreException 1121 * @internal 1122 */ 1123 public function GetState() 1124 { 1125 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 1126 if (empty($sStateAttCode)) 1127 { 1128 return ''; 1129 } 1130 else 1131 { 1132 return $this->Get($sStateAttCode); 1133 } 1134 } 1135 1136 public function GetStateLabel() 1137 { 1138 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 1139 if (empty($sStateAttCode)) 1140 { 1141 return ''; 1142 } 1143 else 1144 { 1145 $sStateValue = $this->Get($sStateAttCode); 1146 return MetaModel::GetStateLabel(get_class($this), $sStateValue); 1147 } 1148 } 1149 1150 public function GetStateDescription() 1151 { 1152 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 1153 if (empty($sStateAttCode)) 1154 { 1155 return ''; 1156 } 1157 else 1158 { 1159 $sStateValue = $this->Get($sStateAttCode); 1160 return MetaModel::GetStateDescription(get_class($this), $sStateValue); 1161 } 1162 } 1163 1164 /** 1165 * Overridable - Define attributes read-only from the end-user perspective 1166 * 1167 * @return array List of attcodes 1168 */ 1169 public static function GetReadOnlyAttributes() 1170 { 1171 return null; 1172 } 1173 1174 1175 /** 1176 * Overridable - Get predefined objects (could be hardcoded) 1177 * The predefined objects will be synchronized with the DB at each install/upgrade 1178 * As soon as a class has predefined objects, then nobody can create nor delete objects 1179 * @return array An array of id => array of attcode => php value(so-called "real value": integer, string, ormDocument, DBObjectSet, etc.) 1180 */ 1181 public static function GetPredefinedObjects() 1182 { 1183 return null; 1184 } 1185 1186 /** 1187 * @param string $sAttCode $sAttCode The code of the attribute 1188 * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas) 1189 * @param string $sTargetState The target state in which to evalutate the flags, if empty the current state will be 1190 * used 1191 * 1192 * @return integer the binary combination of flags for the given attribute in the given state of the object<br> 1193 * Values can be one of the OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY, ... (see define in metamodel.class.php) 1194 * @throws \CoreException 1195 * 1196 * @api 1197 * 1198 * @see GetInitialStateAttributeFlags for creation 1199 */ 1200 public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '') 1201 { 1202 $iFlags = 0; // By default (if no life cycle) no flag at all 1203 1204 $aReadOnlyAtts = $this->GetReadOnlyAttributes(); 1205 if (($aReadOnlyAtts != null) && (in_array($sAttCode, $aReadOnlyAtts))) 1206 { 1207 return OPT_ATT_READONLY; 1208 } 1209 1210 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 1211 if (!empty($sStateAttCode)) 1212 { 1213 if ($sTargetState != '') 1214 { 1215 $iFlags = MetaModel::GetAttributeFlags(get_class($this), $sTargetState, $sAttCode); 1216 } 1217 else 1218 { 1219 $iFlags = MetaModel::GetAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); 1220 } 1221 } 1222 $aReasons = array(); 1223 $iSynchroFlags = 0; 1224 if ($this->InSyncScope()) 1225 { 1226 $iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); 1227 if ($iSynchroFlags & OPT_ATT_SLAVE) 1228 { 1229 $iSynchroFlags |= OPT_ATT_READONLY; 1230 } 1231 } 1232 return $iFlags | $iSynchroFlags; // Combine both sets of flags 1233 } 1234 1235 /** 1236 * @param string $sAttCode 1237 * @param array $aReasons To store the reasons why the attribute is read-only (info about the synchro replicas) 1238 * 1239 * @throws \CoreException 1240 */ 1241 public function IsAttributeReadOnlyForCurrentState($sAttCode, &$aReasons = array()) 1242 { 1243 $iAttFlags = $this->GetAttributeFlags($sAttCode, $aReasons); 1244 1245 return ($iAttFlags & OPT_ATT_READONLY); 1246 } 1247 1248 /** 1249 * Returns the set of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) 1250 * for the given attribute in a transition 1251 * @param $sAttCode string $sAttCode The code of the attribute 1252 * @param $sStimulus string The stimulus code to apply 1253 * @param $aReasons array To store the reasons why the attribute is read-only (info about the synchro replicas) 1254 * @param $sOriginState string The state from which to apply $sStimulus, if empty current state will be used 1255 * @return integer Flags: the binary combination of the flags applicable to this attribute 1256 */ 1257 public function GetTransitionFlags($sAttCode, $sStimulus, &$aReasons = array(), $sOriginState = '') 1258 { 1259 $iFlags = 0; // By default (if no lifecycle) no flag at all 1260 1261 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 1262 // If no state attribute, there is no lifecycle 1263 if (empty($sStateAttCode)) 1264 { 1265 return $iFlags; 1266 } 1267 1268 // Retrieving current state if necessary 1269 if ($sOriginState === '') 1270 { 1271 $sOriginState = $this->Get($sStateAttCode); 1272 } 1273 1274 // Retrieving attribute flags 1275 $iAttributeFlags = $this->GetAttributeFlags($sAttCode, $aReasons, $sOriginState); 1276 1277 // Retrieving transition flags 1278 $iTransitionFlags = MetaModel::GetTransitionFlags(get_class($this), $sOriginState, $sStimulus, $sAttCode); 1279 1280 // Merging transition flags with attribute flags 1281 $iFlags = $iTransitionFlags | $iAttributeFlags; 1282 1283 return $iFlags; 1284 } 1285 1286 /** 1287 * Returns an array of attribute codes (with their flags) when $sStimulus is applied on the object in the $sOriginState state. 1288 * Note: Attributes (and flags) from the target state and the transition are combined. 1289 * 1290 * @param $sStimulus string 1291 * @param $sOriginState string Default is current state 1292 * @return array 1293 */ 1294 public function GetTransitionAttributes($sStimulus, $sOriginState = null) 1295 { 1296 $sObjClass = get_class($this); 1297 1298 // Defining current state as origin state if not specified 1299 if($sOriginState === null) 1300 { 1301 $sOriginState = $this->GetState(); 1302 } 1303 1304 $aAttributes = MetaModel::GetTransitionAttributes($sObjClass, $sStimulus, $sOriginState); 1305 1306 return $aAttributes; 1307 } 1308 1309 /** 1310 * @param string $sAttCode The code of the attribute 1311 * @param array $aReasons 1312 * 1313 * @return integer The binary combination of the flags for the given attribute for the current state of the object 1314 * considered as an INITIAL state.<br> 1315 * Values can be one of the OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY, ... (see define in metamodel.class.php) 1316 * @throws \CoreException 1317 * 1318 * @api 1319 * 1320 * @see GetAttributeFlags when modifying the object 1321 */ 1322 public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array()) 1323 { 1324 $iFlags = 0; 1325 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 1326 if (!empty($sStateAttCode)) 1327 { 1328 $iFlags = MetaModel::GetInitialStateAttributeFlags(get_class($this), $this->Get($sStateAttCode), $sAttCode); 1329 } 1330 return $iFlags; // No need to care about the synchro flags since we'll be creating a new object anyway 1331 } 1332 1333 /** 1334 * Check if the given (or current) value is suitable for the attribute 1335 * 1336 * @param $sAttCode 1337 * @param boolean|string $value true if successfull, the error desciption otherwise 1338 * 1339 * @return bool|string 1340 * @throws \ArchivedObjectException 1341 * @throws \CoreException 1342 * @throws \OQLException 1343 * 1344 * @internal 1345 */ 1346 public function CheckValue($sAttCode, $value = null) 1347 { 1348 if (!is_null($value)) 1349 { 1350 $toCheck = $value; 1351 } 1352 else 1353 { 1354 $toCheck = $this->Get($sAttCode); 1355 } 1356 1357 $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 1358 if (!$oAtt->IsWritable()) 1359 { 1360 return true; 1361 } 1362 elseif ($oAtt->IsNull($toCheck)) 1363 { 1364 if ($oAtt->IsNullAllowed()) 1365 { 1366 return true; 1367 } 1368 else 1369 { 1370 return "Null not allowed"; 1371 } 1372 } 1373 elseif ($oAtt->IsExternalKey()) 1374 { 1375 if (!MetaModel::SkipCheckExtKeys()) 1376 { 1377 /** @var \AttributeExternalKey $oAtt */ 1378 $sTargetClass = $oAtt->GetTargetClass(); 1379 $oTargetObj = MetaModel::GetObject($sTargetClass, $toCheck, false /*must be found*/, true /*allow all data*/); 1380 if (is_null($oTargetObj)) 1381 { 1382 return "Target object not found ($sTargetClass::$toCheck)"; 1383 } 1384 } 1385 if ($oAtt->IsHierarchicalKey()) 1386 { 1387 // This check cannot be deactivated since otherwise the user may break things by a CSV import of a bulk modify 1388 $aValues = $oAtt->GetAllowedValues(array('this' => $this)); 1389 if (!array_key_exists($toCheck, $aValues)) 1390 { 1391 return "Value not allowed [$toCheck]"; 1392 } 1393 } 1394 } 1395 elseif ($oAtt instanceof AttributeTagSet) 1396 { 1397 if (is_string($toCheck)) 1398 { 1399 $oTag = new ormTagSet(get_class($this), $sAttCode, $oAtt->GetMaxItems()); 1400 try 1401 { 1402 $oTag->SetValues(explode(' ', $toCheck)); 1403 } catch (Exception $e) 1404 { 1405 return "Tag value '$toCheck' is not a valid tag list"; 1406 } 1407 1408 return true; 1409 } 1410 1411 if ($toCheck instanceof ormTagSet) 1412 { 1413 return true; 1414 } 1415 1416 return "Bad type"; 1417 } 1418 elseif ($oAtt instanceof AttributeClassAttCodeSet) 1419 { 1420 if (is_string($toCheck)) 1421 { 1422 $oTag = new ormSet(get_class($this), $sAttCode, $oAtt->GetMaxItems()); 1423 try 1424 { 1425 $aValues = array(); 1426 foreach(explode(',', $toCheck) as $sValue) 1427 { 1428 $aValues[] = trim($sValue); 1429 } 1430 $oTag->SetValues($aValues); 1431 } catch (Exception $e) 1432 { 1433 return "Set value '$toCheck' is not a valid set"; 1434 } 1435 1436 return true; 1437 } 1438 1439 if ($toCheck instanceof ormSet) 1440 { 1441 return true; 1442 } 1443 1444 return "Bad type"; 1445 } 1446 elseif ($oAtt->IsScalar()) 1447 { 1448 $aValues = $oAtt->GetAllowedValues($this->ToArgsForQuery()); 1449 if (is_array($aValues) && (count($aValues) > 0)) 1450 { 1451 if (!array_key_exists($toCheck, $aValues)) 1452 { 1453 return "Value not allowed [$toCheck]"; 1454 } 1455 } 1456 if (!is_null($iMaxSize = $oAtt->GetMaxSize())) 1457 { 1458 $iLen = strlen($toCheck); 1459 if ($iLen > $iMaxSize) 1460 { 1461 return "String too long (found $iLen, limited to $iMaxSize)"; 1462 } 1463 } 1464 if (!$oAtt->CheckFormat($toCheck)) 1465 { 1466 return "Wrong format [$toCheck]"; 1467 } 1468 } 1469 else 1470 { 1471 return $oAtt->CheckValue($this, $toCheck); 1472 } 1473 return true; 1474 } 1475 1476 /** 1477 * check attributes together 1478 * 1479 * @return bool 1480 * @api 1481 */ 1482 public function CheckConsistency() 1483 { 1484 return true; 1485 } 1486 1487 /** 1488 * @throws \CoreException 1489 * @throws \OQLException 1490 * @since 2.6 N°659 uniqueness constraint 1491 */ 1492 protected function DoCheckUniqueness() 1493 { 1494 $sCurrentClass = get_class($this); 1495 $aUniquenessRules = MetaModel::GetUniquenessRules($sCurrentClass); 1496 1497 foreach ($aUniquenessRules as $sUniquenessRuleId => $aUniquenessRuleProperties) 1498 { 1499 if ($aUniquenessRuleProperties['disabled'] === true) 1500 { 1501 continue; 1502 } 1503 1504 $bHasDuplicates = $this->HasObjectsInDbForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties); 1505 if ($bHasDuplicates) 1506 { 1507 $bIsBlockingRule = $aUniquenessRuleProperties['is_blocking']; 1508 if (is_null($bIsBlockingRule)) 1509 { 1510 $bIsBlockingRule = true; 1511 } 1512 1513 $sErrorMessage = $this->GetUniquenessRuleMessage($sUniquenessRuleId); 1514 1515 if ($bIsBlockingRule) 1516 { 1517 $this->m_aCheckIssues[] = $sErrorMessage; 1518 continue; 1519 } 1520 $this->m_aCheckWarnings[] = $sErrorMessage; 1521 continue; 1522 } 1523 } 1524 } 1525 1526 /** 1527 * @param string $sUniquenessRuleId 1528 * 1529 * @return string dict key : Class:$sClassName/UniquenessRule:$sUniquenessRuleId 1530 * if none then will use Core:UniquenessDefaultError 1531 * Dictionary keys can contain "$this" placeholders 1532 * 1533 * @since 2.6 N°659 uniqueness constraint 1534 */ 1535 protected function GetUniquenessRuleMessage($sUniquenessRuleId) 1536 { 1537 $sCurrentClass = get_class($this); 1538 $sClass = MetaModel::GetRootClassForUniquenessRule($sUniquenessRuleId, $sCurrentClass); 1539 $sMessageKey = "Class:$sClass/UniquenessRule:$sUniquenessRuleId"; 1540 $sTemplate = Dict::S($sMessageKey, ''); 1541 1542 if (empty($sTemplate)) 1543 { 1544 // we could add also a specific message if user is admin ("dict key is missing") 1545 return Dict::Format('Core:UniquenessDefaultError', $sUniquenessRuleId); 1546 } 1547 1548 $oString = new TemplateString($sTemplate); 1549 1550 return $oString->Render(array('this' => $this)); 1551 } 1552 1553 /** 1554 * @param string $sUniquenessRuleId uniqueness rule ID 1555 * @param array $aUniquenessRuleProperties uniqueness rule properties 1556 * 1557 * @return bool 1558 * @throws \CoreException 1559 * @throws \MissingQueryArgument 1560 * @throws \MySQLException 1561 * @throws \MySQLHasGoneAwayException 1562 * @throws \OQLException 1563 */ 1564 protected function HasObjectsInDbForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties) 1565 { 1566 $oUniquenessQuery = $this->GetSearchForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties); 1567 $oUniquenessDuplicates = new DBObjectSet($oUniquenessQuery); 1568 $bHasDuplicates = $oUniquenessDuplicates->CountExceeds(0); 1569 1570 return $bHasDuplicates; 1571 } 1572 1573 /** 1574 * @param string $sUniquenessRuleId uniqueness rule ID 1575 * @param array $aUniquenessRuleProperties uniqueness rule properties 1576 * 1577 * @return \DBSearch 1578 * @throws \CoreException 1579 * @throws \OQLException 1580 * @since 2.6 N°659 uniqueness constraint 1581 */ 1582 protected function GetSearchForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties) 1583 { 1584 $sRuleRootClass = $aUniquenessRuleProperties['root_class']; 1585 $sOqlUniquenessQuery = "SELECT $sRuleRootClass"; 1586 if (!(empty($sUniquenessFilter = $aUniquenessRuleProperties['filter']))) 1587 { 1588 $sOqlUniquenessQuery .= ' WHERE '.$sUniquenessFilter; 1589 } 1590 /** @var \DBObjectSearch $oUniquenessQuery */ 1591 $oUniquenessQuery = DBObjectSearch::FromOQL($sOqlUniquenessQuery); 1592 1593 if (!$this->IsNew()) 1594 { 1595 $oUniquenessQuery->AddCondition('id', $this->GetKey(), '<>'); 1596 } 1597 1598 foreach ($aUniquenessRuleProperties['attributes'] as $sAttributeCode) 1599 { 1600 $attributeValue = $this->Get($sAttributeCode); 1601 $oUniquenessQuery->AddCondition($sAttributeCode, $attributeValue, '='); 1602 } 1603 1604 $aChildClassesWithRuleDisabled = MetaModel::GetChildClassesWithDisabledUniquenessRule($sRuleRootClass, $sUniquenessRuleId); 1605 if (!empty($aChildClassesWithRuleDisabled)) 1606 { 1607 $oUniquenessQuery->AddConditionForInOperatorUsingParam('finalclass', $aChildClassesWithRuleDisabled, false); 1608 } 1609 1610 return $oUniquenessQuery; 1611 } 1612 1613 /** 1614 * Check integrity rules (before inserting or updating the object) 1615 * 1616 * Errors should be inserted in {@link $m_aCheckIssues} and {@link $m_aCheckWarnings} arrays 1617 * 1618 * @throws \ArchivedObjectException 1619 * @throws \CoreException 1620 * @throws \OQLException 1621 * 1622 * @api 1623 */ 1624 public function DoCheckToWrite() 1625 { 1626 $this->DoComputeValues(); 1627 1628 $this->DoCheckUniqueness(); 1629 1630 $aChanges = $this->ListChanges(); 1631 1632 foreach($aChanges as $sAttCode => $value) 1633 { 1634 $res = $this->CheckValue($sAttCode); 1635 if ($res !== true) 1636 { 1637 // $res contains the error description 1638 $this->m_aCheckIssues[] = "Unexpected value for attribute '$sAttCode': $res"; 1639 } 1640 } 1641 if (count($this->m_aCheckIssues) > 0) 1642 { 1643 // No need to check consistency between attributes if any of them has 1644 // an unexpected value 1645 return; 1646 } 1647 $res = $this->CheckConsistency(); 1648 if ($res !== true) 1649 { 1650 // $res contains the error description 1651 $this->m_aCheckIssues[] = "Consistency rules not followed: $res"; 1652 } 1653 1654 // Synchronization: are we attempting to modify an attribute for which an external source is master? 1655 // 1656 if ($this->m_bIsInDB && $this->InSyncScope() && (count($aChanges) > 0)) 1657 { 1658 foreach($aChanges as $sAttCode => $value) 1659 { 1660 $iFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons); 1661 if ($iFlags & OPT_ATT_SLAVE) 1662 { 1663 // Note: $aReasonInfo['name'] could be reported (the task owning the attribute) 1664 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 1665 $sAttLabel = $oAttDef->GetLabel(); 1666 if (!empty($aReasons)) 1667 { 1668 // Todo: associate the attribute code with the error 1669 $this->m_aCheckIssues[] = Dict::Format('UI:AttemptingToSetASlaveAttribute_Name', $sAttLabel); 1670 } 1671 } 1672 } 1673 } 1674 } 1675 1676 /** 1677 * @return array containing : 1678 * <ul> 1679 * <li>{@link $m_bCheckStatus} 1680 * <li>{@link $m_aCheckIssues} 1681 * <li>{@link $m_bSecurityIssue} 1682 * </ul> 1683 * 1684 * @throws \ArchivedObjectException 1685 * @throws \CoreException 1686 * @throws \OQLException 1687 * 1688 * @internal do not overwrite ! Use {@link DoCheckToWrite} instead 1689 */ 1690 final public function CheckToWrite() 1691 { 1692 if (MetaModel::SkipCheckToWrite()) 1693 { 1694 return array(true, array()); 1695 } 1696 if (is_null($this->m_bCheckStatus)) 1697 { 1698 $this->m_aCheckIssues = array(); 1699 1700 $oKPI = new ExecutionKPI(); 1701 $this->DoCheckToWrite(); 1702 $oKPI->ComputeStats('CheckToWrite', get_class($this)); 1703 if (count($this->m_aCheckIssues) == 0) 1704 { 1705 $this->m_bCheckStatus = true; 1706 } 1707 else 1708 { 1709 $this->m_bCheckStatus = false; 1710 } 1711 } 1712 return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue); 1713 } 1714 1715 // check if it is allowed to delete the existing object from the database 1716 // a displayable error is returned 1717 /** 1718 * check if it is allowed to delete the existing object from the database 1719 * 1720 * a displayable error is added in {@link $m_aDeleteIssues} 1721 * 1722 * @param \DeletionPlan $oDeletionPlan 1723 * 1724 * @throws \CoreException 1725 */ 1726 protected function DoCheckToDelete(&$oDeletionPlan) 1727 { 1728 $this->m_aDeleteIssues = array(); // Ok 1729 1730 if ($this->InSyncScope()) 1731 { 1732 1733 foreach ($this->GetSynchroData() as $iSourceId => $aSourceData) 1734 { 1735 foreach ($aSourceData['replica'] as $oReplica) 1736 { 1737 $oDeletionPlan->AddToDelete($oReplica, DEL_SILENT); 1738 } 1739 /** @var \SynchroDataSource $oDataSource */ 1740 $oDataSource = $aSourceData['source']; 1741 if ($oDataSource->GetKey() == SynchroExecution::GetCurrentTaskId()) 1742 { 1743 // The current task has the right to delete the object 1744 continue; 1745 } 1746 $oReplica = reset($aSourceData['replica']); // Take the first one 1747 if ($oReplica->Get('status_dest_creator') != 1) 1748 { 1749 // The object is not owned by the task 1750 continue; 1751 } 1752 1753 $sLink = $oDataSource->GetName(); 1754 $sUserDeletePolicy = $oDataSource->Get('user_delete_policy'); 1755 switch($sUserDeletePolicy) 1756 { 1757 case 'nobody': 1758 $this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink); 1759 break; 1760 1761 case 'administrators': 1762 if (!UserRights::IsAdministrator()) 1763 { 1764 $this->m_aDeleteIssues[] = Dict::Format('Core:Synchro:TheObjectCannotBeDeletedByUser_Source', $sLink); 1765 } 1766 break; 1767 1768 case 'everybody': 1769 default: 1770 // Ok 1771 break; 1772 } 1773 } 1774 } 1775 } 1776 1777 /** 1778 * @param \DeletionPlan $oDeletionPlan 1779 * 1780 * @return bool 1781 */ 1782 public function CheckToDelete(&$oDeletionPlan) 1783 { 1784 $this->MakeDeletionPlan($oDeletionPlan); 1785 $oDeletionPlan->ComputeResults(); 1786 return (!$oDeletionPlan->FoundStopper()); 1787 } 1788 1789 protected function ListChangedValues(array $aProposal) 1790 { 1791 $aDelta = array(); 1792 foreach ($aProposal as $sAtt => $proposedValue) 1793 { 1794 if (!array_key_exists($sAtt, $this->m_aOrigValues)) 1795 { 1796 // The value was not set 1797 $aDelta[$sAtt] = $proposedValue; 1798 } 1799 elseif(!array_key_exists($sAtt, $this->m_aTouchedAtt) || (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == false)) 1800 { 1801 // This attCode was never set, cannot be modified 1802 // or the same value - as the original value - was set, and has been verified as equivalent to the original value 1803 continue; 1804 } 1805 else if (array_key_exists($sAtt, $this->m_aModifiedAtt) && $this->m_aModifiedAtt[$sAtt] == true) 1806 { 1807 // We already know that the value is really modified 1808 $aDelta[$sAtt] = $proposedValue; 1809 } 1810 elseif(is_object($proposedValue)) 1811 { 1812 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt); 1813 // The value is an object, the comparison is not strict 1814 if (!$oAttDef->Equals($this->m_aOrigValues[$sAtt], $proposedValue)) 1815 { 1816 $aDelta[$sAtt] = $proposedValue; 1817 $this->m_aModifiedAtt[$sAtt] = true; // Really modified 1818 } 1819 else 1820 { 1821 $this->m_aModifiedAtt[$sAtt] = false; // Not really modified 1822 } 1823 } 1824 else 1825 { 1826 // The value is a scalar, the comparison must be 100% strict 1827 if($this->m_aOrigValues[$sAtt] !== $proposedValue) 1828 { 1829 //echo "$sAtt:<pre>\n"; 1830 //var_dump($this->m_aOrigValues[$sAtt]); 1831 //var_dump($proposedValue); 1832 //echo "</pre>\n"; 1833 $aDelta[$sAtt] = $proposedValue; 1834 $this->m_aModifiedAtt[$sAtt] = true; // Really modified 1835 } 1836 else 1837 { 1838 $this->m_aModifiedAtt[$sAtt] = false; // Not really modified 1839 } 1840 } 1841 } 1842 return $aDelta; 1843 } 1844 1845 /** 1846 * List the attributes that have been changed 1847 * 1848 * @return array attname => currentvalue 1849 * @internal 1850 */ 1851 public function ListChanges() 1852 { 1853 if ($this->m_bIsInDB) 1854 { 1855 return $this->ListChangedValues($this->m_aCurrValues); 1856 } 1857 else 1858 { 1859 return $this->m_aCurrValues; 1860 } 1861 } 1862 1863 // Tells whether or not an object was modified since last read (ie: does it differ from the DB ?) 1864 public function IsModified() 1865 { 1866 $aChanges = $this->ListChanges(); 1867 return (count($aChanges) != 0); 1868 } 1869 1870 /** 1871 * @param \DBObject $oSibling 1872 * 1873 * @return bool 1874 */ 1875 public function Equals($oSibling) 1876 { 1877 if (get_class($oSibling) != get_class($this)) 1878 { 1879 return false; 1880 } 1881 if ($this->GetKey() != $oSibling->GetKey()) 1882 { 1883 return false; 1884 } 1885 if ($this->m_bIsInDB) 1886 { 1887 // If one has changed, then consider them as being different 1888 if ($this->IsModified() || $oSibling->IsModified()) 1889 { 1890 return false; 1891 } 1892 } 1893 else 1894 { 1895 // Todo - implement this case (loop on every attribute) 1896 //foreach(MetaModel::ListAttributeDefs(get_class($this) as $sAttCode => $oAttDef) 1897 //{ 1898 //if (!isset($this->m_CurrentValues[$sAttCode])) continue; 1899 //if (!isset($this->m_CurrentValues[$sAttCode])) continue; 1900 //if (!$oAttDef->Equals($this->m_CurrentValues[$sAttCode], $oSibling->m_CurrentValues[$sAttCode])) 1901 //{ 1902 //return false; 1903 //} 1904 //} 1905 return false; 1906 } 1907 return true; 1908 } 1909 1910 /** 1911 * Used only by insert, Meant to be overloaded 1912 * 1913 * @api 1914 */ 1915 protected function OnObjectKeyReady() 1916 { 1917 } 1918 1919 /** 1920 * used both by insert/update 1921 * 1922 * @throws \CoreException 1923 * @internal 1924 */ 1925 private function DBWriteLinks() 1926 { 1927 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 1928 { 1929 if (!$oAttDef->IsLinkSet()) continue; 1930 if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; 1931 if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; 1932 1933 /** @var \ormLinkSet $oLinkSet */ 1934 $oLinkSet = $this->m_aCurrValues[$sAttCode]; 1935 $oLinkSet->DBWrite($this); 1936 } 1937 } 1938 1939 /** 1940 * Used both by insert/update 1941 * 1942 * @throws \CoreException 1943 * @internal 1944 */ 1945 private function WriteExternalAttributes() 1946 { 1947 foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 1948 { 1949 if (!$oAttDef->LoadInObject()) continue; 1950 if ($oAttDef->LoadFromDB()) continue; 1951 if (!array_key_exists($sAttCode, $this->m_aTouchedAtt)) continue; 1952 if (array_key_exists($sAttCode, $this->m_aModifiedAtt) && ($this->m_aModifiedAtt[$sAttCode] == false)) continue; 1953 /** @var \AttributeCustomFields $oAttDef */ 1954 $oAttDef->WriteValue($this, $this->m_aCurrValues[$sAttCode]); 1955 } 1956 } 1957 1958 // Note: this is experimental - it was designed to speed up the setup of iTop 1959 // Known limitations: 1960 // - does not work with multi-table classes (issue with the unique id to maintain in several tables) 1961 // - the id of the object is not updated 1962 static public final function BulkInsertStart() 1963 { 1964 self::$m_bBulkInsert = true; 1965 } 1966 1967 static public final function BulkInsertFlush() 1968 { 1969 if (!self::$m_bBulkInsert) return; 1970 1971 foreach(self::$m_aBulkInsertCols as $sClass => $aTables) 1972 { 1973 foreach ($aTables as $sTable => $sColumns) 1974 { 1975 $sValues = implode(', ', self::$m_aBulkInsertItems[$sClass][$sTable]); 1976 $sInsertSQL = "INSERT INTO `$sTable` ($sColumns) VALUES $sValues"; 1977 CMDBSource::InsertInto($sInsertSQL); 1978 } 1979 } 1980 1981 // Reset 1982 self::$m_aBulkInsertItems = array(); 1983 self::$m_aBulkInsertCols = array(); 1984 self::$m_bBulkInsert = false; 1985 } 1986 1987 /** 1988 * Persists new object in the DB 1989 * 1990 * @param $sTableClass 1991 * 1992 * @return bool|int false if nothing to persist (no change), new key value otherwise 1993 * @throws \CoreException 1994 * @throws \MySQLException 1995 * @internal 1996 */ 1997 private function DBInsertSingleTable($sTableClass) 1998 { 1999 $sTable = MetaModel::DBGetTable($sTableClass); 2000 // Abstract classes or classes having no specific attribute do not have an associated table 2001 if ($sTable == '') return false; 2002 2003 $sClass = get_class($this); 2004 2005 // fields in first array, values in the second 2006 $aFieldsToWrite = array(); 2007 $aValuesToWrite = array(); 2008 2009 if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) 2010 { 2011 // Add it to the list of fields to write 2012 $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; 2013 $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); 2014 } 2015 2016 $aHierarchicalKeys = array(); 2017 2018 foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) 2019 { 2020 // Skip this attribute if not defined in this table 2021 if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; 2022 $aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]); 2023 foreach($aAttColumns as $sColumn => $sValue) 2024 { 2025 $aFieldsToWrite[] = "`$sColumn`"; 2026 $aValuesToWrite[] = CMDBSource::Quote($sValue); 2027 } 2028 if ($oAttDef->IsHierarchicalKey()) 2029 { 2030 $aHierarchicalKeys[$sAttCode] = $oAttDef; 2031 } 2032 } 2033 2034 if (count($aValuesToWrite) == 0) return false; 2035 2036 if (MetaModel::DBIsReadOnly()) 2037 { 2038 $iNewKey = -1; 2039 } 2040 else 2041 { 2042 if (self::$m_bBulkInsert) 2043 { 2044 if (!isset(self::$m_aBulkInsertCols[$sClass][$sTable])) 2045 { 2046 self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite); 2047 } 2048 self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')'; 2049 2050 $iNewKey = 999999; // TODO - compute next id.... 2051 } 2052 else 2053 { 2054 if (count($aHierarchicalKeys) > 0) 2055 { 2056 foreach($aHierarchicalKeys as $sAttCode => $oAttDef) 2057 { 2058 $aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable); 2059 $aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`'; 2060 $aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()]; 2061 $aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`'; 2062 $aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()]; 2063 } 2064 } 2065 $sInsertSQL = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).")"; 2066 $iNewKey = CMDBSource::InsertInto($sInsertSQL); 2067 } 2068 } 2069 // Note that it is possible to have a key defined here, and the autoincrement expected, this is acceptable in a non root class 2070 if (empty($this->m_iKey)) 2071 { 2072 // Take the autonumber 2073 $this->m_iKey = $iNewKey; 2074 } 2075 return $this->m_iKey; 2076 } 2077 2078 /** 2079 * Persists object to new records in the DB 2080 * 2081 * @return int key of the newly created object 2082 * @throws \ArchivedObjectException 2083 * @throws \CoreCannotSaveObjectException if {@link CheckToWrite()} returns issues 2084 * @throws \CoreException 2085 * @throws \CoreUnexpectedValue 2086 * @throws \CoreWarning 2087 * @throws \MySQLException 2088 * @throws \OQLException 2089 * 2090 * @internal 2091 */ 2092 public function DBInsertNoReload() 2093 { 2094 if ($this->m_bIsInDB) 2095 { 2096 throw new CoreException("The object already exists into the Database, you may want to use the clone function"); 2097 } 2098 2099 $sClass = get_class($this); 2100 $sRootClass = MetaModel::GetRootClass($sClass); 2101 2102 // Ensure the update of the values (we are accessing the data directly) 2103 $this->DoComputeValues(); 2104 $this->OnInsert(); 2105 2106 if ($this->m_iKey < 0) 2107 { 2108 // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it! 2109 $this->m_iKey = null; 2110 } 2111 2112 // If not automatically computed, then check that the key is given by the caller 2113 if (!MetaModel::IsAutoIncrementKey($sRootClass)) 2114 { 2115 if (empty($this->m_iKey)) 2116 { 2117 throw new CoreWarning("Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber", array('class' => $sRootClass)); 2118 } 2119 } 2120 2121 // Ultimate check - ensure DB integrity 2122 list($bRes, $aIssues) = $this->CheckToWrite(); 2123 if (!$bRes) 2124 { 2125 throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey())); 2126 } 2127 2128 // Stop watches 2129 $sState = $this->GetState(); 2130 if ($sState != '') 2131 { 2132 foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 2133 { 2134 if ($oAttDef instanceof AttributeStopWatch) 2135 { 2136 if (in_array($sState, $oAttDef->GetStates())) 2137 { 2138 // Start the stop watch and compute the deadlines 2139 /** @var \ormStopWatch $oSW */ 2140 $oSW = $this->Get($sAttCode); 2141 $oSW->Start($this, $oAttDef); 2142 $oSW->ComputeDeadlines($this, $oAttDef); 2143 $this->Set($sAttCode, $oSW); 2144 } 2145 } 2146 } 2147 } 2148 2149 // First query built upon on the root class, because the ID must be created first 2150 $this->m_iKey = $this->DBInsertSingleTable($sRootClass); 2151 2152 // Then do the leaf class, if different from the root class 2153 if ($sClass != $sRootClass) 2154 { 2155 $this->DBInsertSingleTable($sClass); 2156 } 2157 2158 // Then do the other classes 2159 foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) 2160 { 2161 if ($sParentClass == $sRootClass) continue; 2162 $this->DBInsertSingleTable($sParentClass); 2163 } 2164 2165 $this->OnObjectKeyReady(); 2166 2167 $this->DBWriteLinks(); 2168 $this->WriteExternalAttributes(); 2169 2170 $this->m_bIsInDB = true; 2171 $this->m_bDirty = false; 2172 foreach ($this->m_aCurrValues as $sAttCode => $value) 2173 { 2174 if (is_object($value)) 2175 { 2176 $value = clone $value; 2177 } 2178 $this->m_aOrigValues[$sAttCode] = $value; 2179 } 2180 2181 $this->AfterInsert(); 2182 2183 // Activate any existing trigger 2184 $sClass = get_class($this); 2185 $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); 2186 $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectCreate AS t WHERE t.target_class IN ('$sClassList')")); 2187 while ($oTrigger = $oSet->Fetch()) 2188 { 2189 /** @var \Trigger $oTrigger */ 2190 $oTrigger->DoActivate($this->ToArgs('this')); 2191 } 2192 2193 // Callbacks registered with RegisterCallback 2194 if (isset($this->m_aCallbacks[self::CALLBACK_AFTERINSERT])) 2195 { 2196 foreach ($this->m_aCallbacks[self::CALLBACK_AFTERINSERT] as $aCallBackData) 2197 { 2198 call_user_func_array($aCallBackData['callback'], $aCallBackData['params']); 2199 } 2200 } 2201 2202 $this->RecordObjCreation(); 2203 2204 return $this->m_iKey; 2205 } 2206 2207 protected function MakeInsertStatementSingleTable($aAuthorizedExtKeys, &$aStatements, $sTableClass) 2208 { 2209 $sTable = MetaModel::DBGetTable($sTableClass); 2210 // Abstract classes or classes having no specific attribute do not have an associated table 2211 if ($sTable == '') return; 2212 2213 // fields in first array, values in the second 2214 $aFieldsToWrite = array(); 2215 $aValuesToWrite = array(); 2216 2217 if (!empty($this->m_iKey) && ($this->m_iKey >= 0)) 2218 { 2219 // Add it to the list of fields to write 2220 $aFieldsToWrite[] = '`'.MetaModel::DBGetKey($sTableClass).'`'; 2221 $aValuesToWrite[] = CMDBSource::Quote($this->m_iKey); 2222 } 2223 2224 $aHierarchicalKeys = array(); 2225 foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) 2226 { 2227 // Skip this attribute if not defined in this table 2228 if (!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode)) continue; 2229 // Skip link set that can still be undefined though the object is 100% loaded 2230 if ($oAttDef->IsLinkSet()) continue; 2231 2232 $value = $this->m_aCurrValues[$sAttCode]; 2233 if ($oAttDef->IsExternalKey()) 2234 { 2235 /** @var \AttributeExternalKey $oAttDef */ 2236 $sTargetClass = $oAttDef->GetTargetClass(); 2237 if (is_array($aAuthorizedExtKeys)) 2238 { 2239 if (!array_key_exists($sTargetClass, $aAuthorizedExtKeys) || !array_key_exists($value, $aAuthorizedExtKeys[$sTargetClass])) 2240 { 2241 $value = 0; 2242 } 2243 } 2244 } 2245 $aAttColumns = $oAttDef->GetSQLValues($value); 2246 foreach($aAttColumns as $sColumn => $sValue) 2247 { 2248 $aFieldsToWrite[] = "`$sColumn`"; 2249 $aValuesToWrite[] = CMDBSource::Quote($sValue); 2250 } 2251 if ($oAttDef->IsHierarchicalKey()) 2252 { 2253 $aHierarchicalKeys[$sAttCode] = $oAttDef; 2254 } 2255 } 2256 2257 if (count($aValuesToWrite) == 0) return; 2258 2259 if (count($aHierarchicalKeys) > 0) 2260 { 2261 foreach($aHierarchicalKeys as $sAttCode => $oAttDef) 2262 { 2263 $aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable); 2264 $aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`'; 2265 $aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()]; 2266 $aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`'; 2267 $aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()]; 2268 } 2269 } 2270 $aStatements[] = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).");"; 2271 } 2272 2273 public function MakeInsertStatements($aAuthorizedExtKeys, &$aStatements) 2274 { 2275 $sClass = get_class($this); 2276 $sRootClass = MetaModel::GetRootClass($sClass); 2277 2278 // First query built upon on the root class, because the ID must be created first 2279 $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sRootClass); 2280 2281 // Then do the leaf class, if different from the root class 2282 if ($sClass != $sRootClass) 2283 { 2284 $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sClass); 2285 } 2286 2287 // Then do the other classes 2288 foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) 2289 { 2290 if ($sParentClass == $sRootClass) continue; 2291 $this->MakeInsertStatementSingleTable($aAuthorizedExtKeys, $aStatements, $sParentClass); 2292 } 2293 } 2294 2295 /** 2296 * @return int|null inserted object key 2297 * @throws \ArchivedObjectException 2298 * @throws \CoreCannotSaveObjectException 2299 * @throws \CoreException 2300 * @throws \CoreUnexpectedValue 2301 * @throws \CoreWarning 2302 * @throws \MySQLException 2303 * @throws \OQLException 2304 * @internal 2305 */ 2306 public function DBInsert() 2307 { 2308 $this->DBInsertNoReload(); 2309 $this->Reload(); 2310 return $this->m_iKey; 2311 } 2312 2313 public function DBInsertTracked(CMDBChange $oChange) 2314 { 2315 CMDBObject::SetCurrentChange($oChange); 2316 return $this->DBInsert(); 2317 } 2318 2319 public function DBInsertTrackedNoReload(CMDBChange $oChange) 2320 { 2321 CMDBObject::SetCurrentChange($oChange); 2322 return $this->DBInsertNoReload(); 2323 } 2324 2325 // Creates a copy of the current object into the database 2326 // Returns the id of the newly created object 2327 public function DBClone($iNewKey = null) 2328 { 2329 $this->m_bIsInDB = false; 2330 $this->m_iKey = $iNewKey; 2331 $ret = $this->DBInsert(); 2332 $this->RecordObjCreation(); 2333 return $ret; 2334 } 2335 2336 /** 2337 * This function is automatically called after cloning an object with the "clone" PHP language construct 2338 * The purpose of this method is to reset the appropriate attributes of the object in 2339 * order to make sure that the newly cloned object is really distinct from its clone 2340 */ 2341 public function __clone() 2342 { 2343 $this->m_bIsInDB = false; 2344 $this->m_bDirty = true; 2345 $this->m_iKey = self::GetNextTempId(get_class($this)); 2346 } 2347 2348 /** 2349 * Update an object in DB 2350 * 2351 * @return int object key 2352 * @throws \CoreException 2353 * @throws \CoreCannotSaveObjectException if {@link CheckToWrite()} returns issues 2354 */ 2355 public function DBUpdate() 2356 { 2357 if (!$this->m_bIsInDB) 2358 { 2359 throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead"); 2360 } 2361 2362 // Protect against reentrance (e.g. cascading the update of ticket logs) 2363 static $aUpdateReentrance = array(); 2364 $sKey = get_class($this).'::'.$this->GetKey(); 2365 if (array_key_exists($sKey, $aUpdateReentrance)) 2366 { 2367 return false; 2368 } 2369 $aUpdateReentrance[$sKey] = true; 2370 2371 try 2372 { 2373 $this->DoComputeValues(); 2374 // Stop watches 2375 $sState = $this->GetState(); 2376 if ($sState != '') 2377 { 2378 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 2379 { 2380 if ($oAttDef instanceof AttributeStopWatch) 2381 { 2382 if (in_array($sState, $oAttDef->GetStates())) 2383 { 2384 // Compute or recompute the deadlines 2385 $oSW = $this->Get($sAttCode); 2386 $oSW->ComputeDeadlines($this, $oAttDef); 2387 $this->Set($sAttCode, $oSW); 2388 } 2389 } 2390 } 2391 } 2392 $this->OnUpdate(); 2393 2394 $aChanges = $this->ListChanges(); 2395 if (count($aChanges) == 0) 2396 { 2397 // Attempting to update an unchanged object 2398 unset($aUpdateReentrance[$sKey]); 2399 return $this->m_iKey; 2400 } 2401 2402 // Ultimate check - ensure DB integrity 2403 list($bRes, $aIssues) = $this->CheckToWrite(); 2404 if (!$bRes) 2405 { 2406 throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey())); 2407 } 2408 2409 // Save the original values (will be reset to the new values when the object get written to the DB) 2410 $aOriginalValues = $this->m_aOrigValues; 2411 2412 // Activate any existing trigger 2413 $sClass = get_class($this); 2414 $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); 2415 $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectUpdate AS t WHERE t.target_class IN ('$sClassList')")); 2416 while ($oTrigger = $oSet->Fetch()) 2417 { 2418 /** @var \Trigger $oTrigger */ 2419 $oTrigger->DoActivate($this->ToArgs('this')); 2420 } 2421 2422 $bHasANewExternalKeyValue = false; 2423 $aHierarchicalKeys = array(); 2424 $aDBChanges = array(); 2425 foreach($aChanges as $sAttCode => $valuecurr) 2426 { 2427 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 2428 if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true; 2429 if ($oAttDef->IsBasedOnDBColumns()) 2430 { 2431 $aDBChanges[$sAttCode] = $aChanges[$sAttCode]; 2432 } 2433 if ($oAttDef->IsHierarchicalKey()) 2434 { 2435 $aHierarchicalKeys[$sAttCode] = $oAttDef; 2436 } 2437 } 2438 2439 if (!MetaModel::DBIsReadOnly()) 2440 { 2441 // Update the left & right indexes for each hierarchical key 2442 foreach($aHierarchicalKeys as $sAttCode => $oAttDef) 2443 { 2444 $sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); 2445 $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey(); 2446 $aRes = CMDBSource::QueryToArray($sSQL); 2447 $iMyLeft = $aRes[0]['left']; 2448 $iMyRight = $aRes[0]['right']; 2449 $iDelta =$iMyRight - $iMyLeft + 1; 2450 MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); 2451 2452 if ($aDBChanges[$sAttCode] == 0) 2453 { 2454 // No new parent, insert completely at the right of the tree 2455 $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; 2456 $aRes = CMDBSource::QueryToArray($sSQL); 2457 if (count($aRes) == 0) 2458 { 2459 $iNewLeft = 1; 2460 } 2461 else 2462 { 2463 $iNewLeft = $aRes[0]['max']+1; 2464 } 2465 } 2466 else 2467 { 2468 // Insert at the right of the specified parent 2469 $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aDBChanges[$sAttCode]); 2470 $iNewLeft = CMDBSource::QueryToScalar($sSQL); 2471 } 2472 2473 MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); 2474 2475 $aHKChanges = array(); 2476 $aHKChanges[$sAttCode] = $aDBChanges[$sAttCode]; 2477 $aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft; 2478 $aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1; 2479 $aDBChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below 2480 } 2481 2482 // Update scalar attributes 2483 if (count($aDBChanges) != 0) 2484 { 2485 $oFilter = new DBObjectSearch(get_class($this)); 2486 $oFilter->AddCondition('id', $this->m_iKey, '='); 2487 $oFilter->AllowAllData(); 2488 2489 $sSQL = $oFilter->MakeUpdateQuery($aDBChanges); 2490 CMDBSource::Query($sSQL); 2491 } 2492 } 2493 2494 $this->DBWriteLinks(); 2495 $this->WriteExternalAttributes(); 2496 2497 $this->m_bDirty = false; 2498 $this->m_aTouchedAtt = array(); 2499 $this->m_aModifiedAtt = array(); 2500 2501 $this->AfterUpdate(); 2502 2503 // Reload to get the external attributes 2504 if ($bHasANewExternalKeyValue) 2505 { 2506 $this->Reload(true /* AllowAllData */); 2507 } 2508 else 2509 { 2510 // Reset original values although the object has not been reloaded 2511 foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded) 2512 { 2513 if ($bLoaded) 2514 { 2515 $value = $this->m_aCurrValues[$sAttCode]; 2516 $this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value; 2517 } 2518 } 2519 } 2520 2521 if (count($aChanges) != 0) 2522 { 2523 $this->RecordAttChanges($aChanges, $aOriginalValues); 2524 } 2525 } 2526 catch (CoreCannotSaveObjectException $e) 2527 { 2528 throw $e; 2529 } 2530 catch (Exception $e) 2531 { 2532 $aErrors = array($e->getMessage()); 2533 throw new CoreCannotSaveObjectException(array('id' => $this->GetKey(), 'class' => get_class($this), 'issues' => $aErrors)); 2534 } 2535 finally 2536 { 2537 unset($aUpdateReentrance[$sKey]); 2538 } 2539 2540 return $this->m_iKey; 2541 } 2542 2543 public function DBUpdateTracked(CMDBChange $oChange) 2544 { 2545 CMDBObject::SetCurrentChange($oChange); 2546 return $this->DBUpdate(); 2547 } 2548 2549 /** 2550 * Make the current changes persistent - clever wrapper for Insert or Update 2551 * 2552 * @return int 2553 * @throws \CoreCannotSaveObjectException 2554 * @throws \CoreException 2555 */ 2556 public function DBWrite() 2557 { 2558 if ($this->m_bIsInDB) 2559 { 2560 return $this->DBUpdate(); 2561 } 2562 else 2563 { 2564 return $this->DBInsert(); 2565 } 2566 } 2567 2568 private function DBDeleteSingleTable($sTableClass) 2569 { 2570 $sTable = MetaModel::DBGetTable($sTableClass); 2571 // Abstract classes or classes having no specific attribute do not have an associated table 2572 if ($sTable == '') return; 2573 2574 $sPKField = '`'.MetaModel::DBGetKey($sTableClass).'`'; 2575 $sKey = CMDBSource::Quote($this->m_iKey); 2576 2577 $sDeleteSQL = "DELETE FROM `$sTable` WHERE $sPKField = $sKey"; 2578 CMDBSource::DeleteFrom($sDeleteSQL); 2579 } 2580 2581 protected function DBDeleteSingleObject() 2582 { 2583 if (!MetaModel::DBIsReadOnly()) 2584 { 2585 $this->OnDelete(); 2586 2587 // Activate any existing trigger 2588 $sClass = get_class($this); 2589 $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); 2590 $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectDelete AS t WHERE t.target_class IN ('$sClassList')")); 2591 while ($oTrigger = $oSet->Fetch()) 2592 { 2593 /** @var \Trigger $oTrigger */ 2594 $oTrigger->DoActivate($this->ToArgs('this')); 2595 } 2596 2597 $this->RecordObjDeletion($this->m_iKey); // May cause a reload for storing history information 2598 2599 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 2600 { 2601 if ($oAttDef->IsHierarchicalKey()) 2602 { 2603 // Update the left & right indexes for each hierarchical key 2604 $sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); 2605 /** @var \AttributeHierarchicalKey $oAttDef */ 2606 $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".CMDBSource::Quote($this->m_iKey); 2607 $aRes = CMDBSource::QueryToArray($sSQL); 2608 $iMyLeft = $aRes[0]['left']; 2609 $iMyRight = $aRes[0]['right']; 2610 $iDelta =$iMyRight - $iMyLeft + 1; 2611 MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable); 2612 2613 // No new parent for now, insert completely at the right of the tree 2614 $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; 2615 $aRes = CMDBSource::QueryToArray($sSQL); 2616 if (count($aRes) == 0) 2617 { 2618 $iNewLeft = 1; 2619 } 2620 else 2621 { 2622 $iNewLeft = $aRes[0]['max']+1; 2623 } 2624 MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable); 2625 } 2626 elseif (!$oAttDef->LoadFromDB()) 2627 { 2628 /** @var \AttributeCustomFields $oAttDef */ 2629 $oAttDef->DeleteValue($this); 2630 } 2631 } 2632 2633 foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass) 2634 { 2635 $this->DBDeleteSingleTable($sParentClass); 2636 } 2637 2638 $this->AfterDelete(); 2639 2640 $this->m_bIsInDB = false; 2641 // Fix for N°926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete 2642 // objects, reported as bug N°926) 2643 // Thought the key is not reset, using DBInsert or DBWrite will create an object having the same characteristics and a new ID. DBUpdate is protected 2644 } 2645 } 2646 2647 // Delete an object... and guarantee data integrity 2648 // 2649 public function DBDelete(&$oDeletionPlan = null) 2650 { 2651 static $iLoopTimeLimit = null; 2652 if ($iLoopTimeLimit == null) 2653 { 2654 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 2655 } 2656 if (is_null($oDeletionPlan)) 2657 { 2658 $oDeletionPlan = new DeletionPlan(); 2659 } 2660 $this->MakeDeletionPlan($oDeletionPlan); 2661 $oDeletionPlan->ComputeResults(); 2662 2663 if ($oDeletionPlan->FoundStopper()) 2664 { 2665 $aIssues = $oDeletionPlan->GetIssues(); 2666 throw new DeleteException('Found issue(s)', array('target_class' => get_class($this), 'target_id' => $this->GetKey(), 'issues' => implode(', ', $aIssues))); 2667 } 2668 else 2669 { 2670 // Getting and setting time limit are not symetric: 2671 // www.php.net/manual/fr/function.set-time-limit.php#72305 2672 $iPreviousTimeLimit = ini_get('max_execution_time'); 2673 2674 foreach ($oDeletionPlan->ListDeletes() as $sClass => $aToDelete) 2675 { 2676 foreach ($aToDelete as $iId => $aData) 2677 { 2678 /** @var \DBObject $oToDelete */ 2679 $oToDelete = $aData['to_delete']; 2680 // The deletion based on a deletion plan should not be done for each oject if the deletion plan is common (Trac #457) 2681 // because for each object we would try to update all the preceding ones... that are already deleted 2682 // A better approach would be to change the API to apply the DBDelete on the deletion plan itself... just once 2683 // As a temporary fix: delete only the objects that are still to be deleted... 2684 if ($oToDelete->m_bIsInDB) 2685 { 2686 set_time_limit($iLoopTimeLimit); 2687 $oToDelete->DBDeleteSingleObject(); 2688 } 2689 } 2690 } 2691 2692 foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) 2693 { 2694 foreach ($aToUpdate as $iId => $aData) 2695 { 2696 $oToUpdate = $aData['to_reset']; 2697 /** @var \DBObject $oToUpdate */ 2698 foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) 2699 { 2700 $oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]); 2701 set_time_limit($iLoopTimeLimit); 2702 $oToUpdate->DBUpdate(); 2703 } 2704 } 2705 } 2706 2707 set_time_limit($iPreviousTimeLimit); 2708 } 2709 2710 return $oDeletionPlan; 2711 } 2712 2713 public function DBDeleteTracked(CMDBChange $oChange, $bSkipStrongSecurity = null, &$oDeletionPlan = null) 2714 { 2715 CMDBObject::SetCurrentChange($oChange); 2716 $this->DBDelete($oDeletionPlan); 2717 } 2718 2719 public function EnumTransitions() 2720 { 2721 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 2722 if (empty($sStateAttCode)) return array(); 2723 2724 $sState = $this->Get(MetaModel::GetStateAttributeCode(get_class($this))); 2725 return MetaModel::EnumTransitions(get_class($this), $sState); 2726 } 2727 2728 /** 2729 * Designed as an action to be called when a stop watch threshold times out 2730 */ 2731 public function ResetStopWatch($sAttCode) 2732 { 2733 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 2734 if (!$oAttDef instanceof AttributeStopWatch) 2735 { 2736 throw new CoreException("Invalid stop watch id: '$sAttCode'"); 2737 } 2738 $oSW = $this->Get($sAttCode); 2739 $oSW->Reset($this, $oAttDef); 2740 $this->Set($sAttCode, $oSW); 2741 return true; 2742 } 2743 2744 /** 2745 * Designed as an action to be called when a stop watch threshold times out 2746 * or from within the framework 2747 * @param $sStimulusCode 2748 * @param bool|false $bDoNotWrite 2749 * @return bool 2750 * @throws CoreException 2751 * @throws CoreUnexpectedValue 2752 */ 2753 public function ApplyStimulus($sStimulusCode, $bDoNotWrite = false) 2754 { 2755 $sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this)); 2756 if (empty($sStateAttCode)) 2757 { 2758 throw new CoreException('No lifecycle for the class '.get_class($this)); 2759 } 2760 2761 MyHelpers::CheckKeyInArray('object lifecycle stimulus', $sStimulusCode, MetaModel::EnumStimuli(get_class($this))); 2762 2763 $aStateTransitions = $this->EnumTransitions(); 2764 if (!array_key_exists($sStimulusCode, $aStateTransitions)) 2765 { 2766 // This simulus has no effect in the current state... do nothing 2767 return true; 2768 } 2769 $aTransitionDef = $aStateTransitions[$sStimulusCode]; 2770 2771 // Change the state before proceeding to the actions, this is necessary because an action might 2772 // trigger another stimuli (alternative: push the stimuli into a queue) 2773 $sPreviousState = $this->Get($sStateAttCode); 2774 $sNewState = $aTransitionDef['target_state']; 2775 $this->Set($sStateAttCode, $sNewState); 2776 2777 // $aTransitionDef is an 2778 // array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD 2779 2780 $bSuccess = true; 2781 foreach ($aTransitionDef['actions'] as $actionHandler) 2782 { 2783 if (is_string($actionHandler)) 2784 { 2785 // Old (pre-2.1.0 modules) action definition without any parameter 2786 $aActionCallSpec = array($this, $actionHandler); 2787 $sActionDesc = get_class($this).'::'.$actionHandler; 2788 2789 if (!is_callable($aActionCallSpec)) 2790 { 2791 throw new CoreException("Unable to call action: ".get_class($this)."::$actionHandler"); 2792 } 2793 $bRet = call_user_func($aActionCallSpec, $sStimulusCode); 2794 } 2795 else // if (is_array($actionHandler)) 2796 { 2797 // New syntax: 'verb' and typed parameters 2798 $sAction = $actionHandler['verb']; 2799 $sActionDesc = get_class($this).'::'.$sAction; 2800 $aParams = array(); 2801 foreach($actionHandler['params'] as $aDefinition) 2802 { 2803 $sParamType = array_key_exists('type', $aDefinition) ? $aDefinition['type'] : 'string'; 2804 switch($sParamType) 2805 { 2806 case 'int': 2807 $value = (int)$aDefinition['value']; 2808 break; 2809 2810 case 'float': 2811 $value = (float)$aDefinition['value']; 2812 break; 2813 2814 case 'bool': 2815 $value = (bool)$aDefinition['value']; 2816 break; 2817 2818 case 'reference': 2819 $value = ${$aDefinition['value']}; 2820 break; 2821 2822 case 'string': 2823 default: 2824 $value = (string)$aDefinition['value']; 2825 } 2826 $aParams[] = $value; 2827 } 2828 $aCallSpec = array($this, $sAction); 2829 $bRet = call_user_func_array($aCallSpec, $aParams); 2830 } 2831 // if one call fails, the whole is considered as failed 2832 // (in case there is no returned value, null is obtained and means "ok") 2833 if ($bRet === false) 2834 { 2835 IssueLog::Info("Lifecycle action $sActionDesc returned false on object #".$this->GetKey()); 2836 $bSuccess = false; 2837 } 2838 } 2839 if ($bSuccess) 2840 { 2841 $sClass = get_class($this); 2842 2843 // Stop watches 2844 foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 2845 { 2846 if ($oAttDef instanceof AttributeStopWatch) 2847 { 2848 $oSW = $this->Get($sAttCode); 2849 if (in_array($sNewState, $oAttDef->GetStates())) 2850 { 2851 $oSW->Start($this, $oAttDef); 2852 } 2853 else 2854 { 2855 $oSW->Stop($this, $oAttDef); 2856 } 2857 $this->Set($sAttCode, $oSW); 2858 } 2859 } 2860 2861 if (!$bDoNotWrite) 2862 { 2863 $this->DBWrite(); 2864 } 2865 2866 // Change state triggers... 2867 $sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL)); 2868 $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateLeave AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sPreviousState'")); 2869 while ($oTrigger = $oSet->Fetch()) 2870 { 2871 /** @var \Trigger $oTrigger */ 2872 $oTrigger->DoActivate($this->ToArgs('this')); 2873 } 2874 2875 $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnStateEnter AS t WHERE t.target_class IN ('$sClassList') AND t.state='$sNewState'")); 2876 while ($oTrigger = $oSet->Fetch()) 2877 { 2878 /** @var \Trigger $oTrigger */ 2879 $oTrigger->DoActivate($this->ToArgs('this')); 2880 } 2881 } 2882 2883 return $bSuccess; 2884 } 2885 2886 /** 2887 * Lifecycle action: Recover the default value (aka when an object is being created) 2888 */ 2889 public function Reset($sAttCode) 2890 { 2891 $this->Set($sAttCode, $this->GetDefaultValue($sAttCode)); 2892 return true; 2893 } 2894 2895 /** 2896 * Lifecycle action: Copy an attribute to another 2897 */ 2898 public function Copy($sDestAttCode, $sSourceAttCode) 2899 { 2900 $this->Set($sDestAttCode, $this->Get($sSourceAttCode)); 2901 return true; 2902 } 2903 2904 /** 2905 * Lifecycle action: Set the current date/time for the given attribute 2906 */ 2907 public function SetCurrentDate($sAttCode) 2908 { 2909 $this->Set($sAttCode, time()); 2910 return true; 2911 } 2912 2913 /** 2914 * Lifecycle action: Set the current logged in user for the given attribute 2915 */ 2916 public function SetCurrentUser($sAttCode) 2917 { 2918 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 2919 if ($oAttDef instanceof AttributeString) 2920 { 2921 // Note: the user friendly name is the contact friendly name if a contact is attached to the logged in user 2922 $this->Set($sAttCode, UserRights::GetUserFriendlyName()); 2923 } 2924 else 2925 { 2926 if ($oAttDef->IsExternalKey()) 2927 { 2928 /** @var \AttributeExternalKey $oAttDef */ 2929 if ($oAttDef->GetTargetClass() != 'User') 2930 { 2931 throw new Exception("SetCurrentUser: the attribute $sAttCode must be an external key to 'User', found '".$oAttDef->GetTargetClass()."'"); 2932 } 2933 } 2934 $this->Set($sAttCode, UserRights::GetUserId()); 2935 } 2936 return true; 2937 } 2938 2939 /** 2940 * Lifecycle action: Set the current logged in CONTACT for the given attribute 2941 */ 2942 public function SetCurrentPerson($sAttCode) 2943 { 2944 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 2945 if ($oAttDef instanceof AttributeString) 2946 { 2947 $iPerson = UserRights::GetContactId(); 2948 if ($iPerson == 0) 2949 { 2950 $this->Set($sAttCode, ''); 2951 } 2952 else 2953 { 2954 $oPerson = MetaModel::GetObject('Person', $iPerson); 2955 $this->Set($sAttCode, $oPerson->Get('friendlyname')); 2956 } 2957 } 2958 else 2959 { 2960 if ($oAttDef->IsExternalKey()) 2961 { 2962 /** @var \AttributeExternalKey $oAttDef */ 2963 if (!MetaModel::IsParentClass($oAttDef->GetTargetClass(), 'Person')) 2964 { 2965 throw new Exception("SetCurrentContact: the attribute $sAttCode must be an external key to 'Person' or any other class above 'Person', found '".$oAttDef->GetTargetClass()."'"); 2966 } 2967 } 2968 $this->Set($sAttCode, UserRights::GetContactId()); 2969 } 2970 return true; 2971 } 2972 2973 /** 2974 * Lifecycle action: Set the time elapsed since a reference point 2975 */ 2976 public function SetElapsedTime($sAttCode, $sRefAttCode, $sWorkingTimeComputer = null) 2977 { 2978 if (is_null($sWorkingTimeComputer)) 2979 { 2980 $sWorkingTimeComputer = class_exists('SLAComputation') ? 'SLAComputation' : 'DefaultWorkingTimeComputer'; 2981 } 2982 $oComputer = new $sWorkingTimeComputer(); 2983 $aCallSpec = array($oComputer, 'GetOpenDuration'); 2984 if (!is_callable($aCallSpec)) 2985 { 2986 throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'"); 2987 } 2988 2989 $iStartTime = AttributeDateTime::GetAsUnixSeconds($this->Get($sRefAttCode)); 2990 $oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2 2991 $oEndDate = new DateTime(); // now 2992 2993 if (class_exists('WorkingTimeRecorder')) 2994 { 2995 $sClass = get_class($this); 2996 WorkingTimeRecorder::Start($this, time(), "DBObject-SetElapsedTime-$sAttCode-$sRefAttCode", 'Core:ExplainWTC:ElapsedTime', array("Class:$sClass/Attribute:$sAttCode")); 2997 } 2998 $iElapsed = call_user_func($aCallSpec, $this, $oStartDate, $oEndDate); 2999 if (class_exists('WorkingTimeRecorder')) 3000 { 3001 WorkingTimeRecorder::End(); 3002 } 3003 3004 $this->Set($sAttCode, $iElapsed); 3005 return true; 3006 } 3007 3008 3009 3010 /** 3011 * Create query parameters (SELECT ... WHERE service = :this->service_id) 3012 * to be used with the APIs DBObjectSearch/DBObjectSet 3013 * 3014 * Starting 2.0.2 the parameters are computed on demand, at the lowest level, 3015 * in VariableExpression::Render() 3016 */ 3017 public function ToArgsForQuery($sArgName = 'this') 3018 { 3019 return array($sArgName.'->object()' => $this); 3020 } 3021 3022 /** 3023 * Create template placeholders: now equivalent to ToArgsForQuery since the actual 3024 * template placeholders are computed on demand. 3025 */ 3026 public function ToArgs($sArgName = 'this') 3027 { 3028 return $this->ToArgsForQuery($sArgName); 3029 } 3030 3031 public function GetForTemplate($sPlaceholderAttCode) 3032 { 3033 $ret = null; 3034 if (preg_match('/^([^-]+)-(>|>)(.+)$/', $sPlaceholderAttCode, $aMatches)) // Support both syntaxes: this->xxx or this->xxx for HTML compatibility 3035 { 3036 $sExtKeyAttCode = $aMatches[1]; 3037 $sRemoteAttCode = $aMatches[3]; 3038 if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) 3039 { 3040 throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this)); 3041 } 3042 3043 $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); 3044 if (!$oKeyAttDef instanceof AttributeExternalKey) 3045 { 3046 throw new CoreException("'$sExtKeyAttCode' is not an external key of the class ".get_class($this)); 3047 } 3048 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 3049 $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); 3050 if (is_null($oRemoteObj)) 3051 { 3052 $ret = Dict::S('UI:UndefinedObject'); 3053 } 3054 else 3055 { 3056 // Recurse 3057 $ret = $oRemoteObj->GetForTemplate($sRemoteAttCode); 3058 } 3059 } 3060 else 3061 { 3062 switch($sPlaceholderAttCode) 3063 { 3064 case 'id': 3065 $ret = $this->GetKey(); 3066 break; 3067 3068 case 'name()': 3069 $ret = $this->GetName(); 3070 break; 3071 3072 default: 3073 if (preg_match('/^([^(]+)\\((.*)\\)$/', $sPlaceholderAttCode, $aMatches)) 3074 { 3075 $sVerb = $aMatches[1]; 3076 $sAttCode = $aMatches[2]; 3077 } 3078 else 3079 { 3080 $sVerb = ''; 3081 $sAttCode = $sPlaceholderAttCode; 3082 } 3083 3084 if ($sVerb == 'hyperlink') 3085 { 3086 $sPortalId = ($sAttCode === '') ? 'console' : $sAttCode; 3087 if (!array_key_exists($sPortalId, self::$aPortalToURLMaker)) 3088 { 3089 throw new Exception("Unknown portal id '$sPortalId' in placeholder '$sPlaceholderAttCode''"); 3090 } 3091 $ret = $this->GetHyperlink(self::$aPortalToURLMaker[$sPortalId], false); 3092 } 3093 else 3094 { 3095 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 3096 $ret = $oAttDef->GetForTemplate($this->Get($sAttCode), $sVerb, $this); 3097 } 3098 } 3099 if ($ret === null) 3100 { 3101 $ret = ''; 3102 } 3103 } 3104 return $ret; 3105 } 3106 3107 static protected $aPortalToURLMaker = array('console' => 'iTopStandardURLMaker', 'portal' => 'PortalURLMaker'); 3108 3109 /** 3110 * Associate a portal to a class that implements iDBObjectURLMaker, 3111 * and which will be invoked with placeholders like $this->org_id->hyperlink(portal)$ 3112 * 3113 * @param string $sPortalId Identifies the portal. Conventions: the main portal is 'console', The user requests portal is 'portal'. 3114 * @param string $sUrlMakerClass 3115 */ 3116 static public function RegisterURLMakerClass($sPortalId, $sUrlMakerClass) 3117 { 3118 self::$aPortalToURLMaker[$sPortalId] = $sUrlMakerClass; 3119 } 3120 3121 /** 3122 * Can be overloaded 3123 * 3124 * @api 3125 */ 3126 protected function OnInsert() 3127 { 3128 } 3129 3130 /** 3131 * Can be overloaded 3132 * 3133 * @api 3134 */ 3135 protected function AfterInsert() 3136 { 3137 } 3138 3139 /** 3140 * Can be overloaded 3141 * 3142 * @api 3143 */ 3144 protected function OnUpdate() 3145 { 3146 } 3147 3148 /** 3149 * Can be overloaded 3150 * 3151 * @api 3152 */ 3153 protected function AfterUpdate() 3154 { 3155 } 3156 3157 /** 3158 * Can be overloaded 3159 * 3160 * @api 3161 */ 3162 protected function OnDelete() 3163 { 3164 } 3165 3166 /** 3167 * Can be overloaded 3168 * 3169 * @api 3170 */ 3171 protected function AfterDelete() 3172 { 3173 } 3174 3175 3176 /** 3177 * Common to the recording of link set changes (add/remove/modify) 3178 * 3179 * @param $iLinkSetOwnerId 3180 * @param \AttributeLinkedSet $oLinkSet 3181 * @param $sChangeOpClass 3182 * @param array $aOriginalValues 3183 * 3184 * @return \DBObject|null 3185 * @throws \ArchivedObjectException 3186 * @throws \CoreException 3187 * @throws \CoreUnexpectedValue 3188 */ 3189 private function PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, $sChangeOpClass, $aOriginalValues = null) 3190 { 3191 if ($iLinkSetOwnerId <= 0) 3192 { 3193 return null; 3194 } 3195 3196 if (!is_subclass_of($oLinkSet->GetHostClass(), 'CMDBObject')) 3197 { 3198 // The link set owner class does not keep track of its history 3199 return null; 3200 } 3201 3202 // Determine the linked item class and id 3203 // 3204 if ($oLinkSet->IsIndirect()) 3205 { 3206 // The "item" is on the other end (N-N links) 3207 /** @var \AttributeLinkedSetIndirect $oLinkSet */ 3208 $sExtKeyToRemote = $oLinkSet->GetExtKeyToRemote(); 3209 $oExtKeyToRemote = MetaModel::GetAttributeDef(get_class($this), $sExtKeyToRemote); 3210 /** @var \AttributeExternalKey $oExtKeyToRemote */ 3211 $sItemClass = $oExtKeyToRemote->GetTargetClass(); 3212 if ($aOriginalValues) 3213 { 3214 // Get the value from the original values 3215 $iItemId = $aOriginalValues[$sExtKeyToRemote]; 3216 } 3217 else 3218 { 3219 $iItemId = $this->Get($sExtKeyToRemote); 3220 } 3221 } 3222 else 3223 { 3224 // I am the "item" (1-N links) 3225 $sItemClass = get_class($this); 3226 $iItemId = $this->GetKey(); 3227 } 3228 3229 // Get the remote object, to determine its exact class 3230 // Possible optimization: implement a tool in MetaModel, to get the final class of an object (not always querying + query reduced to a select on the root table! 3231 $oOwner = MetaModel::GetObject($oLinkSet->GetHostClass(), $iLinkSetOwnerId, false); 3232 if ($oOwner) 3233 { 3234 $sLinkSetOwnerClass = get_class($oOwner); 3235 3236 $oMyChangeOp = MetaModel::NewObject($sChangeOpClass); 3237 $oMyChangeOp->Set("objclass", $sLinkSetOwnerClass); 3238 $oMyChangeOp->Set("objkey", $iLinkSetOwnerId); 3239 $oMyChangeOp->Set("attcode", $oLinkSet->GetCode()); 3240 $oMyChangeOp->Set("item_class", $sItemClass); 3241 $oMyChangeOp->Set("item_id", $iItemId); 3242 return $oMyChangeOp; 3243 } 3244 else 3245 { 3246 // Depending on the deletion order, it may happen that the id is already invalid... ignore 3247 return null; 3248 } 3249 } 3250 3251 /** 3252 * This object has been created/deleted, record that as a change in link sets pointing to this (if any) 3253 * 3254 * @internal 3255 */ 3256 private function RecordLinkSetListChange($bAdd = true) 3257 { 3258 foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) 3259 { 3260 /** @var \AttributeLinkedSet $oLinkSet */ 3261 if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; 3262 3263 $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); 3264 $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); 3265 if ($oMyChangeOp) 3266 { 3267 if ($bAdd) 3268 { 3269 $oMyChangeOp->Set("type", "added"); 3270 } 3271 else 3272 { 3273 $oMyChangeOp->Set("type", "removed"); 3274 } 3275 $oMyChangeOp->DBInsertNoReload(); 3276 } 3277 } 3278 } 3279 3280 /** 3281 * @internal 3282 */ 3283 protected function RecordObjCreation() 3284 { 3285 $this->RecordLinkSetListChange(true); 3286 } 3287 3288 protected function RecordObjDeletion($objkey) 3289 { 3290 $this->RecordLinkSetListChange(false); 3291 } 3292 3293 protected function RecordAttChanges(array $aValues, array $aOrigValues) 3294 { 3295 foreach(MetaModel::GetTrackForwardExternalKeys(get_class($this)) as $sExtKeyAttCode => $oLinkSet) 3296 { 3297 3298 if (array_key_exists($sExtKeyAttCode, $aValues)) 3299 { 3300 /** @var \AttributeLinkedSet $oLinkSet */ 3301 if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue; 3302 3303 // Keep track of link added/removed 3304 // 3305 $iLinkSetOwnerNext = $aValues[$sExtKeyAttCode]; 3306 $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerNext, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove'); 3307 if ($oMyChangeOp) 3308 { 3309 $oMyChangeOp->Set("type", "added"); 3310 $oMyChangeOp->DBInsertNoReload(); 3311 } 3312 3313 $iLinkSetOwnerPrevious = $aOrigValues[$sExtKeyAttCode]; 3314 $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerPrevious, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove', $aOrigValues); 3315 if ($oMyChangeOp) 3316 { 3317 $oMyChangeOp->Set("type", "removed"); 3318 $oMyChangeOp->DBInsertNoReload(); 3319 } 3320 } 3321 else 3322 { 3323 // Keep track of link changes 3324 // 3325 if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue; 3326 3327 $iLinkSetOwnerId = $this->Get($sExtKeyAttCode); 3328 $oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune'); 3329 if ($oMyChangeOp) 3330 { 3331 $oMyChangeOp->Set("link_id", $this->GetKey()); 3332 $oMyChangeOp->DBInsertNoReload(); 3333 } 3334 } 3335 } 3336 } 3337 3338 // Return an empty set for the parent of all 3339 // May be overloaded. 3340 // Anyhow, this way of implementing the relations suffers limitations (not handling the redundancy) 3341 // and you should consider defining those things in XML. 3342 public static function GetRelationQueries($sRelCode) 3343 { 3344 return array(); 3345 } 3346 3347 // Reserved: do not overload 3348 public static function GetRelationQueriesEx($sRelCode) 3349 { 3350 return array(); 3351 } 3352 3353 /** 3354 * Will be deprecated soon - use GetRelatedObjectsDown/Up instead to take redundancy into account 3355 */ 3356 public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array()) 3357 { 3358 // Temporary patch: until the impact analysis GUI gets rewritten, 3359 // let's consider that "depends on" is equivalent to "impacts/up" 3360 // The current patch has been implemented in DBObject and MetaModel 3361 $sHackedRelCode = $sRelCode; 3362 $bDown = true; 3363 if ($sRelCode == 'depends on') 3364 { 3365 $sHackedRelCode = 'impacts'; 3366 $bDown = false; 3367 } 3368 foreach (MetaModel::EnumRelationQueries(get_class($this), $sHackedRelCode, $bDown) as $sDummy => $aQueryInfo) 3369 { 3370 $sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp']; 3371 //$bPropagate = $aQueryInfo["bPropagate"]; 3372 //$iDepth = $bPropagate ? $iMaxDepth - 1 : 0; 3373 $iDepth = $iMaxDepth - 1; 3374 3375 // Note: the loop over the result set has been written in an unusual way for error reporting purposes 3376 // In the case of a wrong query parameter name, the error occurs on the first call to Fetch, 3377 // thus we need to have this first call into the try/catch, but 3378 // we do NOT want to nest the try/catch for the error message to be clear 3379 try 3380 { 3381 $oFlt = DBObjectSearch::FromOQL($sQuery); 3382 $oObjSet = new DBObjectSet($oFlt, array(), $this->ToArgsForQuery()); 3383 $oObj = $oObjSet->Fetch(); 3384 } 3385 catch (Exception $e) 3386 { 3387 $sClassOfDefinition = $aQueryInfo['_legacy_'] ? get_class($this).'(or a parent)::GetRelationQueries()' : $aQueryInfo['sDefinedInClass']; 3388 throw new Exception("Wrong query for the relation $sRelCode/$sClassOfDefinition/{$aQueryInfo['sNeighbour']}: ".$e->getMessage()); 3389 } 3390 if ($oObj) 3391 { 3392 do 3393 { 3394 $sRootClass = MetaModel::GetRootClass(get_class($oObj)); 3395 $sObjKey = $oObj->GetKey(); 3396 if (array_key_exists($sRootClass, $aResults)) 3397 { 3398 if (array_key_exists($sObjKey, $aResults[$sRootClass])) 3399 { 3400 continue; // already visited, skip 3401 } 3402 } 3403 3404 $aResults[$sRootClass][$sObjKey] = $oObj; 3405 if ($iDepth > 0) 3406 { 3407 $oObj->GetRelatedObjects($sRelCode, $iDepth, $aResults); 3408 } 3409 } 3410 while ($oObj = $oObjSet->Fetch()); 3411 } 3412 } 3413 return $aResults; 3414 } 3415 3416 /** 3417 * Compute the "RelatedObjects" (forward or "down" direction) for the object 3418 * for the specified relation 3419 * 3420 * @param string $sRelCode The code of the relation to use for the computation 3421 * @param int $iMaxDepth Maximum recursion depth 3422 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy 3423 * 3424 * @return RelationGraph The graph of all the related objects 3425 */ 3426 public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) 3427 { 3428 $oGraph = new RelationGraph(); 3429 $oGraph->AddSourceObject($this); 3430 $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy); 3431 return $oGraph; 3432 } 3433 3434 /** 3435 * Compute the "RelatedObjects" (reverse or "up" direction) for the object 3436 * for the specified relation 3437 * 3438 * @param string $sRelCode The code of the relation to use for the computation 3439 * @param int $iMaxDepth Maximum recursion depth 3440 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy 3441 * 3442 * @return RelationGraph The graph of all the related objects 3443 */ 3444 public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true) 3445 { 3446 $oGraph = new RelationGraph(); 3447 $oGraph->AddSourceObject($this); 3448 $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); 3449 return $oGraph; 3450 } 3451 3452 public function GetReferencingObjects($bAllowAllData = false) 3453 { 3454 $aDependentObjects = array(); 3455 $aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this)); 3456 foreach($aRererencingMe as $sRemoteClass => $aExtKeys) 3457 { 3458 foreach($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) 3459 { 3460 // skip if this external key is behind an external field 3461 /** @var \AttributeDefinition $oExtKeyAttDef */ 3462 if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) continue; 3463 3464 $oSearch = new DBObjectSearch($sRemoteClass); 3465 $oSearch->AddCondition($sExtKeyAttCode, $this->GetKey(), '='); 3466 if ($bAllowAllData) 3467 { 3468 $oSearch->AllowAllData(); 3469 } 3470 $oSet = new CMDBObjectSet($oSearch); 3471 if ($oSet->CountExceeds(0)) 3472 { 3473 $aDependentObjects[$sRemoteClass][$sExtKeyAttCode] = array( 3474 'attribute' => $oExtKeyAttDef, 3475 'objects' => $oSet, 3476 ); 3477 } 3478 } 3479 } 3480 return $aDependentObjects; 3481 } 3482 3483 /** 3484 * @param \DeletionPlan $oDeletionPlan 3485 * @param array $aVisited 3486 * @param int $iDeleteOption 3487 * 3488 * @throws \CoreException 3489 */ 3490 private function MakeDeletionPlan(&$oDeletionPlan, $aVisited = array(), $iDeleteOption = null) 3491 { 3492 static $iLoopTimeLimit = null; 3493 if ($iLoopTimeLimit == null) 3494 { 3495 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 3496 } 3497 $sClass = get_class($this); 3498 $iThisId = $this->GetKey(); 3499 3500 $oDeletionPlan->AddToDelete($this, $iDeleteOption); 3501 3502 if (array_key_exists($sClass, $aVisited)) 3503 { 3504 if (in_array($iThisId, $aVisited[$sClass])) 3505 { 3506 return; 3507 } 3508 } 3509 $aVisited[$sClass] = $iThisId; 3510 3511 if ($iDeleteOption == DEL_MANUAL) 3512 { 3513 // Stop the recursion here 3514 return; 3515 } 3516 // Check the node itself 3517 $this->DoCheckToDelete($oDeletionPlan); 3518 $oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue); 3519 3520 $aDependentObjects = $this->GetReferencingObjects(true /* allow all data */); 3521 3522 // Getting and setting time limit are not symetric: 3523 // www.php.net/manual/fr/function.set-time-limit.php#72305 3524 $iPreviousTimeLimit = ini_get('max_execution_time'); 3525 3526 foreach ($aDependentObjects as $sRemoteClass => $aPotentialDeletes) 3527 { 3528 foreach ($aPotentialDeletes as $sRemoteExtKey => $aData) 3529 { 3530 set_time_limit($iLoopTimeLimit); 3531 3532 /** @var \AttributeExternalKey $oAttDef */ 3533 $oAttDef = $aData['attribute']; 3534 $iDeletePropagationOption = $oAttDef->GetDeletionPropagationOption(); 3535 /** @var \DBObjectSet $oDepSet */ 3536 $oDepSet = $aData['objects']; 3537 $oDepSet->Rewind(); 3538 while ($oDependentObj = $oDepSet->fetch()) 3539 { 3540 if ($oAttDef->IsNullAllowed()) 3541 { 3542 // Optional external key, list to reset 3543 if (($iDeletePropagationOption == DEL_MOVEUP) && ($oAttDef->IsHierarchicalKey())) 3544 { 3545 // Move the child up one level i.e. set the same parent as the current object 3546 $iParentId = $this->Get($oAttDef->GetCode()); 3547 $oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef, $iParentId); 3548 } 3549 else 3550 { 3551 $oDeletionPlan->AddToUpdate($oDependentObj, $oAttDef); 3552 } 3553 } 3554 else 3555 { 3556 // Mandatory external key, list to delete 3557 $oDependentObj->MakeDeletionPlan($oDeletionPlan, $aVisited, $iDeletePropagationOption); 3558 } 3559 } 3560 } 3561 } 3562 set_time_limit($iPreviousTimeLimit); 3563 } 3564 3565 /** 3566 * WILL DEPRECATED SOON 3567 * Caching relying on an object set is not efficient since 2.0.3 3568 * Use GetSynchroData instead 3569 * 3570 * Get all the synchro replica related to this object 3571 * 3572 * @return DBObjectSet Set with two columns: R=SynchroReplica S=SynchroDataSource 3573 * @throws \OQLException 3574 */ 3575 public function GetMasterReplica() 3576 { 3577 $sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id"; 3578 $oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey())); 3579 return $oReplicaSet; 3580 } 3581 3582 /** 3583 * Get all the synchro data related to this object 3584 * 3585 * @return array of data_source_id => array 3586 * 'source' => $oSource, 3587 * 'attributes' => array of $oSynchroAttribute 3588 * 'replica' => array of $oReplica (though only one should exist, misuse of the data sync can have this consequence) 3589 * @throws \CoreException 3590 * @throws \CoreUnexpectedValue 3591 * @throws \MySQLException 3592 * @throws \OQLException 3593 */ 3594 public function GetSynchroData() 3595 { 3596 if (is_null($this->m_aSynchroData)) 3597 { 3598 $sOQL = "SELECT replica,datasource FROM SynchroReplica AS replica JOIN SynchroDataSource AS datasource ON replica.sync_source_id=datasource.id WHERE replica.dest_class = :dest_class AND replica.dest_id = :dest_id"; 3599 $oReplicaSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array('dest_class' => get_class($this), 'dest_id' => $this->GetKey())); 3600 $this->m_aSynchroData = array(); 3601 while($aData = $oReplicaSet->FetchAssoc()) 3602 { 3603 /** @var \DBObject[] $aData */ 3604 $iSourceId = $aData['datasource']->GetKey(); 3605 if (!array_key_exists($iSourceId, $this->m_aSynchroData)) 3606 { 3607 $aAttributes = array(); 3608 $oAttrSet = $aData['datasource']->Get('attribute_list'); 3609 while($oSyncAttr = $oAttrSet->Fetch()) 3610 { 3611 /** @var \DBObject $oSyncAttr */ 3612 $aAttributes[$oSyncAttr->Get('attcode')] = $oSyncAttr; 3613 } 3614 $this->m_aSynchroData[$iSourceId] = array( 3615 'source' => $aData['datasource'], 3616 'attributes' => $aAttributes, 3617 'replica' => array() 3618 ); 3619 } 3620 // Assumption: $aData['datasource'] will not be null because the data source id is always set... 3621 $this->m_aSynchroData[$iSourceId]['replica'][] = $aData['replica']; 3622 } 3623 } 3624 return $this->m_aSynchroData; 3625 } 3626 3627 public function GetSynchroReplicaFlags($sAttCode, &$aReason) 3628 { 3629 $iFlags = OPT_ATT_NORMAL; 3630 foreach ($this->GetSynchroData() as $iSourceId => $aSourceData) 3631 { 3632 if ($iSourceId == SynchroExecution::GetCurrentTaskId()) 3633 { 3634 // Ignore the current task (check to write => ok) 3635 continue; 3636 } 3637 // Assumption: one replica - take the first one! 3638 $oReplica = reset($aSourceData['replica']); 3639 $oSource = $aSourceData['source']; 3640 if (array_key_exists($sAttCode, $aSourceData['attributes'])) 3641 { 3642 /** @var \DBObject $oSyncAttr */ 3643 $oSyncAttr = $aSourceData['attributes'][$sAttCode]; 3644 if (($oSyncAttr->Get('update') == 1) && ($oSyncAttr->Get('update_policy') == 'master_locked')) 3645 { 3646 $iFlags |= OPT_ATT_SLAVE; 3647 /** @var \SynchroDataSource $oSource */ 3648 $sUrl = $oSource->GetApplicationUrl($this, $oReplica); 3649 $aReason[] = array('name' => $oSource->GetName(), 'description' => $oSource->Get('description'), 'url_application' => $sUrl); 3650 } 3651 } 3652 } 3653 return $iFlags; 3654 } 3655 3656 /** 3657 * @return bool true if this object is used in a data synchro 3658 * @throws \CoreException 3659 * @throws \CoreUnexpectedValue 3660 * @throws \MySQLException 3661 * @throws \OQLException 3662 * @internal 3663 * @see \SynchroDataSource 3664 */ 3665 public function InSyncScope() 3666 { 3667 // 3668 // Optimization: cache the list of Data Sources and classes candidates for synchro 3669 // 3670 static $aSynchroClasses = null; 3671 if (is_null($aSynchroClasses)) 3672 { 3673 $aSynchroClasses = array(); 3674 $sOQL = "SELECT SynchroDataSource AS datasource"; 3675 $oSourceSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array() /* order by*/, array()); 3676 while($oSource = $oSourceSet->Fetch()) 3677 { 3678 $sTarget = $oSource->Get('scope_class'); 3679 $aSynchroClasses[] = $sTarget; 3680 } 3681 } 3682 3683 foreach($aSynchroClasses as $sClass) 3684 { 3685 if ($this instanceof $sClass) 3686 { 3687 return true; 3688 } 3689 } 3690 return false; 3691 } 3692 ///////////////////////////////////////////////////////////////////////// 3693 // 3694 // Experimental iDisplay implementation 3695 // 3696 ///////////////////////////////////////////////////////////////////////// 3697 3698 public static function MapContextParam($sContextParam) 3699 { 3700 return null; 3701 } 3702 3703 public function GetHilightClass() 3704 { 3705 $sCode = $this->ComputeHighlightCode(); 3706 if($sCode != '') 3707 { 3708 $aHighlightScale = MetaModel::GetHighlightScale(get_class($this)); 3709 if (array_key_exists($sCode, $aHighlightScale)) 3710 { 3711 return $aHighlightScale[$sCode]['color']; 3712 } 3713 } 3714 return HILIGHT_CLASS_NONE; 3715 } 3716 3717 public function DisplayDetails(WebPage $oPage, $bEditMode = false) 3718 { 3719 $oPage->add('<h1>'.MetaModel::GetName(get_class($this)).': '.$this->GetName().'</h1>'); 3720 $aValues = array(); 3721 $aList = MetaModel::FlattenZList(MetaModel::GetZListItems(get_class($this), 'details')); 3722 if (empty($aList)) 3723 { 3724 $aList = array_keys(MetaModel::ListAttributeDefs(get_class($this))); 3725 } 3726 foreach($aList as $sAttCode) 3727 { 3728 $aValues[$sAttCode] = array('label' => MetaModel::GetLabel(get_class($this), $sAttCode), 'value' => $this->GetAsHTML($sAttCode)); 3729 } 3730 $oPage->details($aValues); 3731 } 3732 3733 3734 const CALLBACK_AFTERINSERT = 0; 3735 3736 /** 3737 * Register a call back that will be called when some internal event happens 3738 * 3739 * @param $iType string Any of the CALLBACK_x constants 3740 * @param $callback callable Call specification like a function name, or array('<class>', '<method>') or array($object, '<method>') 3741 * @param $aParameters array Values that will be passed to the callback, after $this 3742 * 3743 * @throws \Exception 3744 */ 3745 public function RegisterCallback($iType, $callback, $aParameters = array()) 3746 { 3747 $sCallBackName = ''; 3748 if (!is_callable($callback, false, $sCallBackName)) 3749 { 3750 throw new Exception('Registering an unknown/protected function or wrong syntax for the call spec: '.$sCallBackName); 3751 } 3752 $this->m_aCallbacks[$iType][] = array( 3753 'callback' => $callback, 3754 'params' => $aParameters 3755 ); 3756 } 3757 3758 /** 3759 * Computes a text-like fingerprint identifying the content of the object 3760 * but excluding the specified columns 3761 * @param $aExcludedColumns array The list of columns to exclude 3762 * @return string 3763 */ 3764 public function Fingerprint($aExcludedColumns = array()) 3765 { 3766 $sFingerprint = ''; 3767 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 3768 { 3769 if (!in_array($sAttCode, $aExcludedColumns)) 3770 { 3771 if ($oAttDef->IsPartOfFingerprint()) 3772 { 3773 $sFingerprint .= chr(0).$oAttDef->Fingerprint($this->Get($sAttCode)); 3774 } 3775 } 3776 } 3777 return $sFingerprint; 3778 } 3779 3780 /** 3781 * Execute a set of scripted actions onto the current object 3782 * See ExecAction for the syntax and features of the scripted actions 3783 * 3784 * @param $aActions array of statements (e.g. "set(name, Made after $source->name$)") 3785 * @param $aSourceObjects array of Alias => Context objects (Convention: some statements require the 'source' element 3786 * @throws Exception 3787 */ 3788 public function ExecActions($aActions, $aSourceObjects) 3789 { 3790 foreach($aActions as $sAction) 3791 { 3792 try 3793 { 3794 if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot 3795 { 3796 $sVerb = trim($aMatches[1]); 3797 $sParams = $aMatches[2]; 3798 3799 // the coma is the separator for the parameters 3800 // comas can be escaped: \, 3801 $sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams); 3802 $sParams = trim($sParams); 3803 3804 if (strlen($sParams) == 0) 3805 { 3806 $aParams = array(); 3807 } 3808 else 3809 { 3810 $aParams = explode(',', $sParams); 3811 foreach ($aParams as &$sParam) 3812 { 3813 $sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam); 3814 $sParam = trim($sParam); 3815 } 3816 } 3817 $this->ExecAction($sVerb, $aParams, $aSourceObjects); 3818 } 3819 else 3820 { 3821 throw new Exception("Invalid syntax"); 3822 } 3823 } 3824 catch(Exception $e) 3825 { 3826 throw new Exception('Action: '.$sAction.' - '.$e->getMessage()); 3827 } 3828 } 3829 } 3830 3831 /** 3832 * Helper to copy an attribute between two objects (in memory) 3833 * Originally designed for ExecAction() 3834 * 3835 * @param \DBObject $oSourceObject 3836 * @param $sSourceAttCode 3837 * @param $sDestAttCode 3838 * 3839 * @throws \CoreException 3840 * @throws \CoreUnexpectedValue 3841 * @throws \MySQLException 3842 */ 3843 public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode) 3844 { 3845 if ($sSourceAttCode == 'id') 3846 { 3847 $oSourceAttDef = null; 3848 } 3849 else 3850 { 3851 if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode)) 3852 { 3853 throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode); 3854 } 3855 if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode)) 3856 { 3857 throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode); 3858 } 3859 3860 $oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode); 3861 } 3862 if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet()) 3863 { 3864 // The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets) 3865 /** @var \AttributeLinkedSet $oSourceAttDef */ 3866 $oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass()); 3867 $oSourceSet = $oSourceObject->Get($sSourceAttCode); 3868 $oSourceSet->Rewind(); 3869 /** @var \DBObject $oSourceLink */ 3870 while ($oSourceLink = $oSourceSet->Fetch()) 3871 { 3872 // Clone the link 3873 $sLinkClass = get_class($oSourceLink); 3874 $oLinkClone = MetaModel::NewObject($sLinkClass); 3875 foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef) 3876 { 3877 // As of now, ignore other attribute (do not attempt to recurse!) 3878 if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) 3879 { 3880 $oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode)); 3881 } 3882 } 3883 3884 // Not necessary - this will be handled by DBObject 3885 // $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0); 3886 $oDestSet->AddObject($oLinkClone); 3887 } 3888 $this->Set($sDestAttCode, $oDestSet); 3889 } 3890 else 3891 { 3892 $this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode)); 3893 } 3894 } 3895 3896 /** 3897 * Execute a scripted action onto the current object 3898 * - clone (att1, att2, att3, ...) 3899 * - clone_scalars () 3900 * - copy (source_att, dest_att) 3901 * - reset (att) 3902 * - nullify (att) 3903 * - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) 3904 * - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...)) 3905 * - add_to_list (source_key_att, dest_att) 3906 * - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value) 3907 * - apply_stimulus (stimulus) 3908 * - call_method (method_name) 3909 * 3910 * @param $sVerb string Any of the verb listed above (e.g. "set") 3911 * @param $aParams array of strings (e.g. array('name', 'copied from $source->name$') 3912 * @param $aSourceObjects array of Alias => Context objects (Convention: some statements require the 'source' element 3913 * @throws CoreException 3914 * @throws CoreUnexpectedValue 3915 * @throws Exception 3916 */ 3917 public function ExecAction($sVerb, $aParams, $aSourceObjects) 3918 { 3919 switch($sVerb) 3920 { 3921 case 'clone': 3922 if (!array_key_exists('source', $aSourceObjects)) 3923 { 3924 throw new Exception('Missing conventional "source" object'); 3925 } 3926 $oObjectToRead = $aSourceObjects['source']; 3927 foreach($aParams as $sAttCode) 3928 { 3929 $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); 3930 } 3931 break; 3932 3933 case 'clone_scalars': 3934 if (!array_key_exists('source', $aSourceObjects)) 3935 { 3936 throw new Exception('Missing conventional "source" object'); 3937 } 3938 $oObjectToRead = $aSourceObjects['source']; 3939 foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) 3940 { 3941 if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) 3942 { 3943 $this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode); 3944 } 3945 } 3946 break; 3947 3948 case 'copy': 3949 if (!array_key_exists('source', $aSourceObjects)) 3950 { 3951 throw new Exception('Missing conventional "source" object'); 3952 } 3953 $oObjectToRead = $aSourceObjects['source']; 3954 if (!array_key_exists(0, $aParams)) 3955 { 3956 throw new Exception('Missing argument #1: source attribute'); 3957 } 3958 $sSourceAttCode = $aParams[0]; 3959 if (!array_key_exists(1, $aParams)) 3960 { 3961 throw new Exception('Missing argument #2: target attribute'); 3962 } 3963 $sDestAttCode = $aParams[1]; 3964 $this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode); 3965 break; 3966 3967 case 'reset': 3968 if (!array_key_exists(0, $aParams)) 3969 { 3970 throw new Exception('Missing argument #1: target attribute'); 3971 } 3972 $sAttCode = $aParams[0]; 3973 if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) 3974 { 3975 throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); 3976 } 3977 $this->Set($sAttCode, $this->GetDefaultValue($sAttCode)); 3978 break; 3979 3980 case 'nullify': 3981 if (!array_key_exists(0, $aParams)) 3982 { 3983 throw new Exception('Missing argument #1: target attribute'); 3984 } 3985 $sAttCode = $aParams[0]; 3986 if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) 3987 { 3988 throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); 3989 } 3990 $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); 3991 $this->Set($sAttCode, $oAttDef->GetNullValue()); 3992 break; 3993 3994 case 'set': 3995 if (!array_key_exists(0, $aParams)) 3996 { 3997 throw new Exception('Missing argument #1: target attribute'); 3998 } 3999 $sAttCode = $aParams[0]; 4000 if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) 4001 { 4002 throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); 4003 } 4004 if (!array_key_exists(1, $aParams)) 4005 { 4006 throw new Exception('Missing argument #2: value to set'); 4007 } 4008 $sRawValue = $aParams[1]; 4009 $aContext = array(); 4010 foreach ($aSourceObjects as $sAlias => $oObject) 4011 { 4012 $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); 4013 } 4014 $aContext['current_contact_id'] = UserRights::GetContactId(); 4015 $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); 4016 $aContext['current_date'] = date(AttributeDate::GetSQLFormat()); 4017 $aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat()); 4018 $sValue = MetaModel::ApplyParams($sRawValue, $aContext); 4019 $this->Set($sAttCode, $sValue); 4020 break; 4021 4022 case 'append': 4023 if (!array_key_exists(0, $aParams)) 4024 { 4025 throw new Exception('Missing argument #1: target attribute'); 4026 } 4027 $sAttCode = $aParams[0]; 4028 if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) 4029 { 4030 throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode); 4031 } 4032 if (!array_key_exists(1, $aParams)) 4033 { 4034 throw new Exception('Missing argument #2: value to append'); 4035 } 4036 $sRawAddendum = $aParams[1]; 4037 $aContext = array(); 4038 foreach ($aSourceObjects as $sAlias => $oObject) 4039 { 4040 $aContext = array_merge($aContext, $oObject->ToArgs($sAlias)); 4041 } 4042 $aContext['current_contact_id'] = UserRights::GetContactId(); 4043 $aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName(); 4044 $aContext['current_date'] = date(AttributeDate::GetSQLFormat()); 4045 $aContext['current_time'] = date(AttributeDateTime::GetSQLTimeFormat()); 4046 $sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext); 4047 $this->Set($sAttCode, $this->Get($sAttCode).$sAddendum); 4048 break; 4049 4050 case 'add_to_list': 4051 if (!array_key_exists('source', $aSourceObjects)) 4052 { 4053 throw new Exception('Missing conventional "source" object'); 4054 } 4055 $oObjectToRead = $aSourceObjects['source']; 4056 if (!array_key_exists(0, $aParams)) 4057 { 4058 throw new Exception('Missing argument #1: source attribute'); 4059 } 4060 $sSourceKeyAttCode = $aParams[0]; 4061 if (($sSourceKeyAttCode != 'id') && !MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode)) 4062 { 4063 throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode); 4064 } 4065 if (!array_key_exists(1, $aParams)) 4066 { 4067 throw new Exception('Missing argument #2: target attribute (link set)'); 4068 } 4069 $sTargetListAttCode = $aParams[1]; // indirect !!! 4070 if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode)) 4071 { 4072 throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode); 4073 } 4074 if (isset($aParams[2]) && isset($aParams[3])) 4075 { 4076 $sRoleAttCode = $aParams[2]; 4077 $sRoleValue = $aParams[3]; 4078 } 4079 4080 $iObjKey = $oObjectToRead->Get($sSourceKeyAttCode); 4081 if ($iObjKey > 0) 4082 { 4083 $oLinkSet = $this->Get($sTargetListAttCode); 4084 4085 /** @var \AttributeLinkedSetIndirect $oListAttDef */ 4086 $oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode); 4087 /** @var \AttributeLinkedSet $oListAttDef */ 4088 $oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass()); 4089 $oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey); 4090 if (isset($sRoleAttCode)) 4091 { 4092 if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode)) 4093 { 4094 throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode); 4095 } 4096 $oLnk->Set($sRoleAttCode, $sRoleValue); 4097 } 4098 $oLinkSet->AddObject($oLnk); 4099 $this->Set($sTargetListAttCode, $oLinkSet); 4100 } 4101 break; 4102 4103 case 'apply_stimulus': 4104 if (!array_key_exists(0, $aParams)) 4105 { 4106 throw new Exception('Missing argument #1: stimulus'); 4107 } 4108 $sStimulus = $aParams[0]; 4109 if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this)))) 4110 { 4111 throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus); 4112 } 4113 $this->ApplyStimulus($sStimulus); 4114 break; 4115 4116 case 'call_method': 4117 if (!array_key_exists('source', $aSourceObjects)) 4118 { 4119 throw new Exception('Missing conventional "source" object'); 4120 } 4121 $oObjectToRead = $aSourceObjects['source']; 4122 if (!array_key_exists(0, $aParams)) 4123 { 4124 throw new Exception('Missing argument #1: method name'); 4125 } 4126 $sMethod = $aParams[0]; 4127 $aCallSpec = array($this, $sMethod); 4128 if (!is_callable($aCallSpec)) 4129 { 4130 throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()'); 4131 } 4132 // Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToRead� 4133 call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects); 4134 break; 4135 4136 default: 4137 throw new Exception("Invalid verb"); 4138 } 4139 } 4140 4141 public function IsArchived($sKeyAttCode = null) 4142 { 4143 $bRet = false; 4144 $sFlagAttCode = is_null($sKeyAttCode) ? 'archive_flag' : $sKeyAttCode.'_archive_flag'; 4145 if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode)) 4146 { 4147 $bRet = true; 4148 } 4149 return $bRet; 4150 } 4151 4152 public function IsObsolete($sKeyAttCode = null) 4153 { 4154 $bRet = false; 4155 $sFlagAttCode = is_null($sKeyAttCode) ? 'obsolescence_flag' : $sKeyAttCode.'_obsolescence_flag'; 4156 if (MetaModel::IsValidAttCode(get_class($this), $sFlagAttCode) && $this->Get($sFlagAttCode)) 4157 { 4158 $bRet = true; 4159 } 4160 return $bRet; 4161 } 4162 4163 /** 4164 * @param $bArchive 4165 * @throws Exception 4166 */ 4167 protected function DBWriteArchiveFlag($bArchive) 4168 { 4169 if (!MetaModel::IsArchivable(get_class($this))) 4170 { 4171 throw new Exception(get_class($this).' is not an archivable class'); 4172 } 4173 4174 $iFlag = $bArchive ? 1 : 0; 4175 $sDate = $bArchive ? '"'.date(AttributeDate::GetSQLFormat()).'"' : 'null'; 4176 4177 $sClass = get_class($this); 4178 $sArchiveRoot = MetaModel::GetAttributeOrigin($sClass, 'archive_flag'); 4179 $sRootTable = MetaModel::DBGetTable($sArchiveRoot); 4180 $sRootKey = MetaModel::DBGetKey($sArchiveRoot); 4181 $aJoins = array("`$sRootTable`"); 4182 $aUpdates = array(); 4183 foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) as $sParentClass) 4184 { 4185 if (!MetaModel::IsValidAttCode($sParentClass, 'archive_flag')) continue; 4186 4187 $sTable = MetaModel::DBGetTable($sParentClass); 4188 $aUpdates[] = "`$sTable`.`archive_flag` = $iFlag"; 4189 if ($sParentClass == $sArchiveRoot) 4190 { 4191 if (!$bArchive || $this->Get('archive_date') == '') 4192 { 4193 // Erase or set the date (do not change it) 4194 $aUpdates[] = "`$sTable`.`archive_date` = $sDate"; 4195 } 4196 } 4197 else 4198 { 4199 $sKey = MetaModel::DBGetKey($sParentClass); 4200 $aJoins[] = "`$sTable` ON `$sTable`.`$sKey` = `$sRootTable`.`$sRootKey`"; 4201 } 4202 } 4203 $sJoins = implode(' INNER JOIN ', $aJoins); 4204 $sValues = implode(', ', $aUpdates); 4205 $sUpdateQuery = "UPDATE $sJoins SET $sValues WHERE `$sRootTable`.`$sRootKey` = ".$this->GetKey(); 4206 CMDBSource::Query($sUpdateQuery); 4207 } 4208 4209 /** 4210 * Can be called to repair the database (tables consistency) 4211 * The archive_date will be preserved 4212 * @throws Exception 4213 */ 4214 public function DBArchive() 4215 { 4216 $this->DBWriteArchiveFlag(true); 4217 $this->m_aCurrValues['archive_flag'] = true; 4218 $this->m_aOrigValues['archive_flag'] = true; 4219 } 4220 4221 public function DBUnarchive() 4222 { 4223 $this->DBWriteArchiveFlag(false); 4224 $this->m_aCurrValues['archive_flag'] = false; 4225 $this->m_aOrigValues['archive_flag'] = false; 4226 $this->m_aCurrValues['archive_date'] = null; 4227 $this->m_aOrigValues['archive_date'] = null; 4228 } 4229 4230 4231 4232 /** 4233 * @param string $sClass Needs to be an instanciable class 4234 * @returns $oObj 4235 **/ 4236 public static function MakeDefaultInstance($sClass) 4237 { 4238 $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); 4239 $oObj = MetaModel::NewObject($sClass); 4240 if (!empty($sStateAttCode)) 4241 { 4242 $sTargetState = MetaModel::GetDefaultState($sClass); 4243 $oObj->Set($sStateAttCode, $sTargetState); 4244 } 4245 return $oObj; 4246 } 4247 4248 /** 4249 * Complete a new object with data from context 4250 * @param array $aContextParam Context used for creation form prefilling 4251 * 4252 */ 4253 public function PrefillCreationForm(&$aContextParam) 4254 { 4255 } 4256 4257 /** 4258 * Complete an object after a state transition with data from context 4259 * @param array $aContextParam Context used for creation form prefilling 4260 * 4261 */ 4262 public function PrefillTransitionForm(&$aContextParam) 4263 { 4264 } 4265 4266 /** 4267 * Complete a filter ($aContextParam['filter']) data from context 4268 * (Called on source object) 4269 * @param array $aContextParam Context used for creation form prefilling 4270 * 4271 */ 4272 public function PrefillSearchForm(&$aContextParam) 4273 { 4274 } 4275 4276 /** 4277 * Prefill a creation / stimulus change / search form according to context, current state of an object, stimulus.. $sOperation 4278 * @param string $sOperation Operation identifier 4279 * @param array $aContextParam Context used for creation form prefilling 4280 * 4281 */ 4282 public function PrefillForm($sOperation, &$aContextParam) 4283 { 4284 switch($sOperation){ 4285 case 'creation_from_0': 4286 case 'creation_from_extkey': 4287 case 'creation_from_editinplace': 4288 $this->PrefillCreationForm($aContextParam); 4289 break; 4290 case 'state_change': 4291 $this->PrefillTransitionForm($aContextParam); 4292 break; 4293 case 'search': 4294 $this->PrefillSearchForm($aContextParam); 4295 break; 4296 default: 4297 break; 4298 } 4299 } 4300} 4301 4302