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 20require_once(APPROOT.'core/modulehandler.class.inc.php'); 21require_once(APPROOT.'core/querybuildercontext.class.inc.php'); 22require_once(APPROOT.'core/querymodifier.class.inc.php'); 23require_once(APPROOT.'core/metamodelmodifier.inc.php'); 24require_once(APPROOT.'core/computing.inc.php'); 25require_once(APPROOT.'core/relationgraph.class.inc.php'); 26require_once(APPROOT.'core/apc-compat.php'); 27require_once(APPROOT.'core/expressioncache.class.inc.php'); 28 29/** 30 * Metamodel 31 * 32 * @copyright Copyright (C) 2010-2018 Combodo SARL 33 * @license http://opensource.org/licenses/AGPL-3.0 34 */ 35 36/** 37 * @package iTopORM 38 */ 39define('ENUM_PARENT_CLASSES_EXCLUDELEAF', 1); 40/** 41 * @package iTopORM 42 */ 43define('ENUM_PARENT_CLASSES_ALL', 2); 44 45/** 46 * Specifies that this attribute is visible/editable.... normal (default config) 47 * 48 * @package iTopORM 49 */ 50define('OPT_ATT_NORMAL', 0); 51/** 52 * Specifies that this attribute is hidden in that state 53 * 54 * @package iTopORM 55 */ 56define('OPT_ATT_HIDDEN', 1); 57/** 58 * Specifies that this attribute is not editable in that state 59 * 60 * @package iTopORM 61 */ 62define('OPT_ATT_READONLY', 2); 63/** 64 * Specifieds that the attribute must be set (different than default value?) when arriving into that state 65 * 66 * @package iTopORM 67 */ 68define('OPT_ATT_MANDATORY', 4); 69/** 70 * Specifies that the attribute must change when arriving into that state 71 * 72 * @package iTopORM 73 */ 74define('OPT_ATT_MUSTCHANGE', 8); 75/** 76 * Specifies that the attribute must be proposed when arriving into that state 77 * 78 * @package iTopORM 79 */ 80define('OPT_ATT_MUSTPROMPT', 16); 81/** 82 * Specifies that the attribute is in 'slave' mode compared to one data exchange task: 83 * it should not be edited inside iTop anymore 84 * 85 * @package iTopORM 86 */ 87define('OPT_ATT_SLAVE', 32); 88 89/** 90 * DB Engine -should be moved into CMDBSource 91 * 92 * Used to be myisam, the switch was made with r798 93 * 94 * @package iTopORM 95 */ 96define('MYSQL_ENGINE', 'innodb'); 97 98 99/** 100 * (API) The objects definitions as well as their mapping to the database 101 * 102 * @package iTopORM 103 */ 104abstract class MetaModel 105{ 106 /////////////////////////////////////////////////////////////////////////// 107 // 108 // STATIC Members 109 // 110 /////////////////////////////////////////////////////////////////////////// 111 112 /** @var bool */ 113 private static $m_bTraceSourceFiles = false; 114 /** @var array */ 115 private static $m_aClassToFile = array(); 116 /** @var string */ 117 protected static $m_sEnvironment = 'production'; 118 119 /** 120 * @return array 121 */ 122 public static function GetClassFiles() 123 { 124 return self::$m_aClassToFile; 125 } 126 127 // 128 129 /** 130 * Purpose: workaround the following limitation = PHP5 does not allow to know the class (derived 131 * from the current one) from which a static function is called (__CLASS__ and self are 132 * interpreted during parsing) 133 * 134 * @param string $sExpectedFunctionName 135 * @param bool $bRecordSourceFile 136 * 137 * @return string 138 */ 139 private static function GetCallersPHPClass($sExpectedFunctionName = null, $bRecordSourceFile = false) 140 { 141 $aBacktrace = debug_backtrace(); 142 // $aBacktrace[0] is where we are 143 // $aBacktrace[1] is the caller of GetCallersPHPClass 144 // $aBacktrace[1] is the info we want 145 if (!empty($sExpectedFunctionName)) 146 { 147 assert($aBacktrace[2]['function'] == $sExpectedFunctionName); 148 } 149 if ($bRecordSourceFile) 150 { 151 self::$m_aClassToFile[$aBacktrace[2]["class"]] = $aBacktrace[1]["file"]; 152 } 153 return $aBacktrace[2]["class"]; 154 } 155 156 // Static init -why and how it works 157 // 158 // We found the following limitations: 159 //- it is not possible to define non scalar constants 160 //- it is not possible to declare a static variable as '= new myclass()' 161 // Then we had do propose this model, in which a derived (non abstract) 162 // class should implement Init(), to call InheritAttributes or AddAttribute. 163 164 /** 165 * @param string $sClass 166 * 167 * @throws \CoreException 168 */ 169 private static function _check_subclass($sClass) 170 { 171 // See also IsValidClass()... ???? #@# 172 // class is mandatory 173 // (it is not possible to guess it when called as myderived::...) 174 if (!array_key_exists($sClass, self::$m_aClassParams)) 175 { 176 throw new CoreException("Unknown class '$sClass'"); 177 } 178 } 179 180 public static function static_var_dump() 181 { 182 var_dump(get_class_vars(__CLASS__)); 183 } 184 185 /** @var Config m_oConfig */ 186 private static $m_oConfig = null; 187 /** @var array */ 188 protected static $m_aModulesParameters = array(); 189 190 /** @var bool */ 191 private static $m_bSkipCheckToWrite = false; 192 /** @var bool */ 193 private static $m_bSkipCheckExtKeys = false; 194 195 /** @var bool */ 196 private static $m_bUseAPCCache = false; 197 198 /** @var bool */ 199 private static $m_bLogIssue = false; 200 /** @var bool */ 201 private static $m_bLogNotification = false; 202 /** @var bool */ 203 private static $m_bLogWebService = false; 204 205 /** 206 * @return bool the current flag value 207 */ 208 public static function SkipCheckToWrite() 209 { 210 return self::$m_bSkipCheckToWrite; 211 } 212 213 /** 214 * @return bool the current flag value 215 */ 216 public static function SkipCheckExtKeys() 217 { 218 return self::$m_bSkipCheckExtKeys; 219 } 220 221 /** 222 * @return bool the current flag value 223 */ 224 public static function IsLogEnabledIssue() 225 { 226 return self::$m_bLogIssue; 227 } 228 229 /** 230 * @return bool the current flag value 231 */ 232 public static function IsLogEnabledNotification() 233 { 234 return self::$m_bLogNotification; 235 } 236 237 /** 238 * @return bool the current flag value 239 */ 240 public static function IsLogEnabledWebService() 241 { 242 return self::$m_bLogWebService; 243 } 244 245 /** @var string */ 246 private static $m_sDBName = ""; 247 /** 248 * table prefix for the current application instance (allow several applications on the same DB) 249 * 250 * @var string 251 */ 252 private static $m_sTablePrefix = ""; 253 /** @var array */ 254 private static $m_Category2Class = array(); 255 /** 256 * array of "classname" => "rootclass" 257 * 258 * @var array 259 */ 260 private static $m_aRootClasses = array(); 261 /** 262 * array of ("classname" => array of "parentclass") 263 * 264 * @var array 265 */ 266 private static $m_aParentClasses = array(); 267 /** 268 * array of ("classname" => array of "childclass") 269 * 270 * @var array 271 */ 272 private static $m_aChildClasses = array(); 273 274 /** 275 * array of ("classname" => array of class information) 276 * 277 * @var array 278 */ 279 private static $m_aClassParams = array(); 280 /** 281 * array of ("classname" => array of highlightscale information) 282 * 283 * @var array 284 */ 285 private static $m_aHighlightScales = array(); 286 287 /** 288 * @param string $sRefClass 289 * 290 * @return string 291 */ 292 static public function GetParentPersistentClass($sRefClass) 293 { 294 $sClass = get_parent_class($sRefClass); 295 if (!$sClass) 296 { 297 return ''; 298 } 299 300 if ($sClass == 'DBObject') 301 { 302 return ''; 303 } // Warning: __CLASS__ is lower case in my version of PHP 304 305 // Note: the UI/business model may implement pure PHP classes (intermediate layers) 306 if (array_key_exists($sClass, self::$m_aClassParams)) 307 { 308 return $sClass; 309 } 310 return self::GetParentPersistentClass($sClass); 311 } 312 313 /** 314 * @param string $sClass 315 * 316 * @return string 317 * @throws \CoreException 318 * @throws \DictExceptionMissingString 319 */ 320 final static public function GetName($sClass) 321 { 322 self::_check_subclass($sClass); 323 return $sClass::GetClassName($sClass); 324 } 325 326 /** 327 * @param string $sClass 328 * 329 * @return string 330 * @throws \CoreException 331 * @throws \DictExceptionMissingString 332 */ 333 final static public function GetName_Obsolete($sClass) 334 { 335 // Written for compatibility with a data model written prior to version 0.9.1 336 self::_check_subclass($sClass); 337 if (array_key_exists('name', self::$m_aClassParams[$sClass])) 338 { 339 return self::$m_aClassParams[$sClass]['name']; 340 } 341 else 342 { 343 return self::GetName($sClass); 344 } 345 } 346 347 /** 348 * @param string $sClassLabel 349 * @param bool $bCaseSensitive 350 * 351 * @return null 352 * @throws \CoreException 353 * @throws \DictExceptionMissingString 354 */ 355 final static public function GetClassFromLabel($sClassLabel, $bCaseSensitive = true) 356 { 357 foreach(self::GetClasses() as $sClass) 358 { 359 if ($bCaseSensitive) 360 { 361 if (self::GetName($sClass) == $sClassLabel) 362 { 363 return $sClass; 364 } 365 } 366 else 367 { 368 if (strcasecmp(self::GetName($sClass), $sClassLabel) == 0) 369 { 370 return $sClass; 371 } 372 } 373 } 374 375 return null; 376 } 377 378 /** 379 * @param string $sClass 380 * 381 * @return string 382 * @throws \CoreException 383 */ 384 final static public function GetCategory($sClass) 385 { 386 self::_check_subclass($sClass); 387 return self::$m_aClassParams[$sClass]["category"]; 388 } 389 390 /** 391 * @param string $sClass 392 * @param string $sCategory 393 * 394 * @return bool 395 * @throws \CoreException 396 */ 397 final static public function HasCategory($sClass, $sCategory) 398 { 399 self::_check_subclass($sClass); 400 return (strpos(self::$m_aClassParams[$sClass]["category"], $sCategory) !== false); 401 } 402 403 /** 404 * @param string $sClass 405 * 406 * @return string 407 * @throws \CoreException 408 * @throws \DictExceptionMissingString 409 */ 410 final static public function GetClassDescription($sClass) 411 { 412 self::_check_subclass($sClass); 413 return $sClass::GetClassDescription($sClass); 414 } 415 416 /** 417 * @param string $sClass 418 * 419 * @return string 420 * @throws \CoreException 421 * @throws \DictExceptionMissingString 422 */ 423 final static public function GetClassDescription_Obsolete($sClass) 424 { 425 // Written for compatibility with a data model written prior to version 0.9.1 426 self::_check_subclass($sClass); 427 if (array_key_exists('description', self::$m_aClassParams[$sClass])) 428 { 429 return self::$m_aClassParams[$sClass]['description']; 430 } 431 else 432 { 433 return self::GetClassDescription($sClass); 434 } 435 } 436 437 /** 438 * @param string $sClass 439 * @param bool $bImgTag 440 * @param string $sMoreStyles 441 * 442 * @return string 443 * @throws \CoreException 444 */ 445 final static public function GetClassIcon($sClass, $bImgTag = true, $sMoreStyles = '') 446 { 447 self::_check_subclass($sClass); 448 449 $sIcon = ''; 450 if (array_key_exists('icon', self::$m_aClassParams[$sClass])) 451 { 452 $sIcon = self::$m_aClassParams[$sClass]['icon']; 453 } 454 if (strlen($sIcon) == 0) 455 { 456 $sParentClass = self::GetParentPersistentClass($sClass); 457 if (strlen($sParentClass) > 0) 458 { 459 return self::GetClassIcon($sParentClass, $bImgTag, $sMoreStyles); 460 } 461 } 462 $sIcon = str_replace('/modules/', '/env-'.self::$m_sEnvironment.'/', $sIcon); // Support of pre-2.0 modules 463 if ($bImgTag && ($sIcon != '')) 464 { 465 $sIcon = "<img src=\"$sIcon\" style=\"vertical-align:middle;$sMoreStyles\"/>"; 466 } 467 468 return $sIcon; 469 } 470 471 /** 472 * @param string $sClass 473 * 474 * @return bool 475 * @throws \CoreException 476 */ 477 final static public function IsAutoIncrementKey($sClass) 478 { 479 self::_check_subclass($sClass); 480 return (self::$m_aClassParams[$sClass]["key_type"] == "autoincrement"); 481 } 482 483 /** 484 * @param string $sClass 485 * 486 * @return bool 487 * @throws \CoreException 488 */ 489 final static public function IsArchivable($sClass) 490 { 491 self::_check_subclass($sClass); 492 return self::$m_aClassParams[$sClass]["archive"]; 493 } 494 495 /** 496 * @param string $sClass 497 * 498 * @return bool 499 * @throws \CoreException 500 */ 501 final static public function IsObsoletable($sClass) 502 { 503 self::_check_subclass($sClass); 504 return (!is_null(self::$m_aClassParams[$sClass]['obsolescence_expression'])); 505 } 506 507 /** 508 * @param string $sClass 509 * 510 * @return \Expression 511 * @throws \CoreException 512 */ 513 final static public function GetObsolescenceExpression($sClass) 514 { 515 if (self::IsObsoletable($sClass)) 516 { 517 self::_check_subclass($sClass); 518 $sOql = self::$m_aClassParams[$sClass]['obsolescence_expression']; 519 $oRet = Expression::FromOQL("COALESCE($sOql, 0)"); 520 } 521 else 522 { 523 $oRet = Expression::FromOQL("0"); 524 } 525 526 return $oRet; 527 } 528 529 /** 530 * @param string $sClass 531 * @param bool $bClassDefinitionOnly if true then will only return properties defined in the specified class on not the properties 532 * from its parent classes 533 * 534 * @return array rule id as key, rule properties as value 535 * @throws \CoreException 536 * @since 2.6 N°659 uniqueness constraint 537 * @see #SetUniquenessRuleRootClass that fixes a specific 'root_class' property to know which class is root per rule 538 */ 539 final public static function GetUniquenessRules($sClass, $bClassDefinitionOnly = false) 540 { 541 if (!isset(self::$m_aClassParams[$sClass])) 542 { 543 return array(); 544 } 545 546 $aCurrentUniquenessRules = array(); 547 548 if (array_key_exists('uniqueness_rules', self::$m_aClassParams[$sClass])) 549 { 550 $aCurrentUniquenessRules = self::$m_aClassParams[$sClass]['uniqueness_rules']; 551 } 552 553 if ($bClassDefinitionOnly) 554 { 555 return $aCurrentUniquenessRules; 556 } 557 558 $sParentClass = self::GetParentClass($sClass); 559 if ($sParentClass) 560 { 561 $aParentUniquenessRules = self::GetUniquenessRules($sParentClass); 562 foreach ($aParentUniquenessRules as $sUniquenessRuleId => $aParentUniquenessRuleProperties) 563 { 564 $bCopyDisabledKey = true; 565 $bCurrentDisabledValue = null; 566 567 if (array_key_exists($sUniquenessRuleId, $aCurrentUniquenessRules)) 568 { 569 if (self::IsUniquenessRuleContainingOnlyDisabledKey($aCurrentUniquenessRules[$sUniquenessRuleId])) 570 { 571 $bCopyDisabledKey = false; 572 } 573 else 574 { 575 continue; 576 } 577 } 578 579 $aMergedUniquenessProperties = $aParentUniquenessRuleProperties; 580 if (!$bCopyDisabledKey) 581 { 582 $aMergedUniquenessProperties['disabled'] = $aCurrentUniquenessRules[$sUniquenessRuleId]['disabled']; 583 } 584 $aCurrentUniquenessRules[$sUniquenessRuleId] = $aMergedUniquenessProperties; 585 } 586 } 587 588 return $aCurrentUniquenessRules; 589 } 590 591 /** 592 * @param string $sRootClass 593 * @param string $sRuleId 594 * 595 * @throws \CoreException 596 * @since 2.6.1 N°1918 (sous les pavés, la plage) initialize in 'root_class' property the class that has the first 597 * definition of the rule in the hierarchy 598 */ 599 final private static function SetUniquenessRuleRootClass($sRootClass, $sRuleId) 600 { 601 foreach (self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL) as $sClass) 602 { 603 self::$m_aClassParams[$sClass]['uniqueness_rules'][$sRuleId]['root_class'] = $sClass; 604 } 605 } 606 607 /** 608 * @param string $sRuleId 609 * @param string $sLeafClassName 610 * 611 * @return string name of the class, null if not present 612 */ 613 final public static function GetRootClassForUniquenessRule($sRuleId, $sLeafClassName) 614 { 615 $sFirstClassWithRuleId = null; 616 if (isset(self::$m_aClassParams[$sLeafClassName]['uniqueness_rules'][$sRuleId])) 617 { 618 $sFirstClassWithRuleId = $sLeafClassName; 619 } 620 621 $sParentClass = self::GetParentClass($sLeafClassName); 622 if ($sParentClass) 623 { 624 $sParentClassWithRuleId = self::GetRootClassForUniquenessRule($sRuleId, $sParentClass); 625 if (!is_null($sParentClassWithRuleId)) 626 { 627 $sFirstClassWithRuleId = $sParentClassWithRuleId; 628 } 629 } 630 631 return $sFirstClassWithRuleId; 632 } 633 634 /** 635 * @param string $sRootClass 636 * @param string $sRuleId 637 * 638 * @return string[] child classes with the rule disabled, and that are concrete classes 639 * 640 * @throws \CoreException 641 * @since 2.6.1 N°1968 (soyez réalistes, demandez l'impossible) 642 */ 643 final public static function GetChildClassesWithDisabledUniquenessRule($sRootClass, $sRuleId) 644 { 645 $aClassesWithDisabledRule = array(); 646 foreach (self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP) as $sChildClass) 647 { 648 if (array_key_exists($sChildClass, $aClassesWithDisabledRule)) 649 { 650 continue; 651 } 652 if (!array_key_exists('uniqueness_rules', self::$m_aClassParams[$sChildClass])) 653 { 654 continue; 655 } 656 if (!array_key_exists($sRuleId, self::$m_aClassParams[$sChildClass]['uniqueness_rules'])) 657 { 658 continue; 659 } 660 661 if (self::$m_aClassParams[$sChildClass]['uniqueness_rules'][$sRuleId]['disabled'] === true) 662 { 663 $aDisabledClassChildren = self::EnumChildClasses($sChildClass, ENUM_CHILD_CLASSES_ALL); 664 foreach ($aDisabledClassChildren as $sDisabledClassChild) 665 { 666 if (!self::IsAbstract($sDisabledClassChild)) 667 { 668 $aClassesWithDisabledRule[] = $sDisabledClassChild; 669 } 670 } 671 } 672 } 673 674 return $aClassesWithDisabledRule; 675 } 676 677 /** 678 * @param array $aRuleProperties 679 * 680 * @return bool 681 * @since 2.6 N°659 uniqueness constraint 682 */ 683 private static function IsUniquenessRuleContainingOnlyDisabledKey($aRuleProperties) 684 { 685 $aNonNullRuleProperties = array_filter($aRuleProperties, function ($v) { 686 return (!is_null($v)); 687 }); 688 689 return ((count($aNonNullRuleProperties) == 1) && (array_key_exists('disabled', $aNonNullRuleProperties))); 690 } 691 692 693 /** 694 * @param string $sClass 695 * 696 * @return array 697 * @throws \CoreException 698 * @throws \DictExceptionMissingString 699 */ 700 final static public function GetNameSpec($sClass) 701 { 702 self::_check_subclass($sClass); 703 $nameRawSpec = self::$m_aClassParams[$sClass]["name_attcode"]; 704 if (is_array($nameRawSpec)) 705 { 706 $sFormat = Dict::S("Class:$sClass/Name", ''); 707 if (strlen($sFormat) == 0) 708 { 709 // Default to "%1$s %2$s..." 710 for($i = 1; $i <= count($nameRawSpec); $i++) 711 { 712 if (empty($sFormat)) 713 { 714 $sFormat .= '%'.$i.'$s'; 715 } 716 else 717 { 718 $sFormat .= ' %'.$i.'$s'; 719 } 720 } 721 } 722 return array($sFormat, $nameRawSpec); 723 } 724 elseif (empty($nameRawSpec)) 725 { 726 return array($sClass, array()); 727 } 728 else 729 { 730 // string -> attcode 731 return array('%1$s', array($nameRawSpec)); 732 } 733 } 734 735 /** 736 * Get the friendly name expression for a given class 737 * 738 * @param string $sClass 739 * 740 * @return Expression 741 * @throws \CoreException 742 * @throws \DictExceptionMissingString 743 */ 744 final static public function GetNameExpression($sClass) 745 { 746 $aNameSpec = self::GetNameSpec($sClass); 747 $sFormat = $aNameSpec[0]; 748 $aAttributes = $aNameSpec[1]; 749 750 $aPieces = preg_split('/%([0-9])\\$s/', $sFormat, -1, PREG_SPLIT_DELIM_CAPTURE); 751 $aExpressions = array(); 752 foreach($aPieces as $i => $sPiece) 753 { 754 if ($i & 1) 755 { 756 // $i is ODD - sPiece is a delimiter 757 // 758 $iReplacement = (int)$sPiece - 1; 759 760 if (isset($aAttributes[$iReplacement])) 761 { 762 $sAttCode = $aAttributes[$iReplacement]; 763 $aExpressions[] = new FieldExpression($sAttCode); 764 } 765 } 766 else 767 { 768 // $i is EVEN - sPiece is a literal 769 // 770 if (strlen($sPiece) > 0) 771 { 772 $aExpressions[] = new ScalarExpression($sPiece); 773 } 774 } 775 } 776 777 return new CharConcatExpression($aExpressions); 778 } 779 780 /** 781 * @param string $sClass 782 * 783 * @return string The friendly name IIF it is equivalent to a single attribute 784 * @throws \CoreException 785 * @throws \DictExceptionMissingString 786 */ 787 final static public function GetFriendlyNameAttributeCode($sClass) 788 { 789 $aNameSpec = self::GetNameSpec($sClass); 790 $sFormat = trim($aNameSpec[0]); 791 $aAttributes = $aNameSpec[1]; 792 if (($sFormat != '') && ($sFormat != '%1$s')) 793 { 794 return null; 795 } 796 if (count($aAttributes) > 1) 797 { 798 return null; 799 } 800 return reset($aAttributes); 801 } 802 803 /** 804 * Returns the list of attributes composing the friendlyname 805 * 806 * @param $sClass 807 * 808 * @return array 809 */ 810 final static public function GetFriendlyNameAttributeCodeList($sClass) 811 { 812 $aNameSpec = self::GetNameSpec($sClass); 813 $aAttributes = $aNameSpec[1]; 814 return $aAttributes; 815 } 816 817 /** 818 * @param string $sClass 819 * 820 * @return string 821 * @throws \CoreException 822 */ 823 final static public function GetStateAttributeCode($sClass) 824 { 825 self::_check_subclass($sClass); 826 return self::$m_aClassParams[$sClass]["state_attcode"]; 827 } 828 829 /** 830 * @param string $sClass 831 * 832 * @return string 833 * @throws \CoreException 834 * @throws \Exception 835 */ 836 final static public function GetDefaultState($sClass) 837 { 838 $sDefaultState = ''; 839 $sStateAttrCode = self::GetStateAttributeCode($sClass); 840 if (!empty($sStateAttrCode)) 841 { 842 $oStateAttrDef = self::GetAttributeDef($sClass, $sStateAttrCode); 843 $sDefaultState = $oStateAttrDef->GetDefaultValue(); 844 } 845 return $sDefaultState; 846 } 847 848 /** 849 * @param string $sClass 850 * 851 * @return array 852 * @throws \CoreException 853 */ 854 final static public function GetReconcKeys($sClass) 855 { 856 self::_check_subclass($sClass); 857 return self::$m_aClassParams[$sClass]["reconc_keys"]; 858 } 859 860 /** 861 * @param string $sClass 862 * 863 * @return string 864 * @throws \CoreException 865 */ 866 final static public function GetDisplayTemplate($sClass) 867 { 868 self::_check_subclass($sClass); 869 return array_key_exists("display_template", self::$m_aClassParams[$sClass]) ? self::$m_aClassParams[$sClass]["display_template"] : ''; 870 } 871 872 /** 873 * @param string $sClass 874 * @param bool $bOnlyDeclared 875 * 876 * @return array 877 * @throws \CoreException 878 */ 879 final static public function GetOrderByDefault($sClass, $bOnlyDeclared = false) 880 { 881 self::_check_subclass($sClass); 882 $aOrderBy = array_key_exists("order_by_default", self::$m_aClassParams[$sClass]) ? self::$m_aClassParams[$sClass]["order_by_default"] : array(); 883 if ($bOnlyDeclared) 884 { 885 // Used to reverse engineer the declaration of the data model 886 return $aOrderBy; 887 } 888 else 889 { 890 if (count($aOrderBy) == 0) 891 { 892 $aOrderBy['friendlyname'] = true; 893 } 894 return $aOrderBy; 895 } 896 } 897 898 /** 899 * @param string $sClass 900 * @param string $sAttCode 901 * 902 * @return mixed 903 * @throws \CoreException 904 */ 905 final static public function GetAttributeOrigin($sClass, $sAttCode) 906 { 907 self::_check_subclass($sClass); 908 return self::$m_aAttribOrigins[$sClass][$sAttCode]; 909 } 910 911 /** 912 * @param string $sClass 913 * @param string $sAttCode 914 * 915 * @return array 916 * @throws \CoreException 917 * @throws \Exception 918 */ 919 final static public function GetPrerequisiteAttributes($sClass, $sAttCode) 920 { 921 self::_check_subclass($sClass); 922 $oAtt = self::GetAttributeDef($sClass, $sAttCode); 923 // Temporary implementation: later, we might be able to compute 924 // the dependencies, based on the attributes definition 925 // (allowed values and default values) 926 927 // Even non-writable attributes (like ExternalFields) can now have Prerequisites 928 return $oAtt->GetPrerequisiteAttributes(); 929 } 930 931 /** 932 * Find all attributes that depend on the specified one (reverse of GetPrerequisiteAttributes) 933 * 934 * @param string $sClass Name of the class 935 * @param string $sAttCode Code of the attributes 936 * 937 * @return Array List of attribute codes that depend on the given attribute, empty array if none. 938 * @throws \CoreException 939 * @throws \Exception 940 */ 941 final static public function GetDependentAttributes($sClass, $sAttCode) 942 { 943 $aResults = array(); 944 self::_check_subclass($sClass); 945 foreach(self::ListAttributeDefs($sClass) as $sDependentAttCode => $void) 946 { 947 $aPrerequisites = self::GetPrerequisiteAttributes($sClass, $sDependentAttCode); 948 if (in_array($sAttCode, $aPrerequisites)) 949 { 950 $aResults[] = $sDependentAttCode; 951 } 952 } 953 return $aResults; 954 } 955 956 /** 957 * @param string $sClass 958 * @param string $sAttCode 959 * 960 * @return string 961 * @throws \CoreException 962 */ 963 final static public function DBGetTable($sClass, $sAttCode = null) 964 { 965 self::_check_subclass($sClass); 966 if (empty($sAttCode) || ($sAttCode == "id")) 967 { 968 $sTableRaw = self::$m_aClassParams[$sClass]["db_table"]; 969 if (empty($sTableRaw)) 970 { 971 // return an empty string whenever the table is undefined, meaning that there is no table associated to this 'abstract' class 972 return ''; 973 } 974 else 975 { 976 // If the format changes here, do not forget to update the setup index page (detection of installed modules) 977 return self::$m_sTablePrefix.$sTableRaw; 978 } 979 } 980 // This attribute has been inherited (compound objects) 981 return self::DBGetTable(self::$m_aAttribOrigins[$sClass][$sAttCode]); 982 } 983 984 /** 985 * @param string $sClass 986 * 987 * @return string 988 */ 989 final static public function DBGetView($sClass) 990 { 991 return self::$m_sTablePrefix."view_".$sClass; 992 } 993 994 /** 995 * @return array 996 * @throws \CoreException 997 */ 998 final static public function DBEnumTables() 999 { 1000 // This API does not rely on our capability to query the DB and retrieve 1001 // the list of existing tables 1002 // Rather, it uses the list of expected tables, corresponding to the data model 1003 $aTables = array(); 1004 foreach(self::GetClasses() as $sClass) 1005 { 1006 if (!self::HasTable($sClass)) 1007 { 1008 continue; 1009 } 1010 $sTable = self::DBGetTable($sClass); 1011 1012 // Could be completed later with all the classes that are using a given table 1013 if (!array_key_exists($sTable, $aTables)) 1014 { 1015 $aTables[$sTable] = array(); 1016 } 1017 $aTables[$sTable][] = $sClass; 1018 } 1019 1020 return $aTables; 1021 } 1022 1023 /** 1024 * @param string $sClass 1025 * 1026 * @return array 1027 * @throws \CoreException 1028 */ 1029 final static public function DBGetIndexes($sClass) 1030 { 1031 self::_check_subclass($sClass); 1032 if (isset(self::$m_aClassParams[$sClass]['indexes'])) 1033 { 1034 $aRet = self::$m_aClassParams[$sClass]['indexes']; 1035 } 1036 else 1037 { 1038 $aRet = array(); 1039 } 1040 1041 return $aRet; 1042 } 1043 1044 1045 /** 1046 * @param $sClass 1047 * @param $aColumns 1048 * @param $aTableInfo 1049 * 1050 * @return array 1051 * @throws \CoreException 1052 */ 1053 private static function DBGetIndexesLength($sClass, $aColumns, $aTableInfo) 1054 { 1055 $aLength = array(); 1056 $aAttDefs = self::ListAttributeDefs($sClass); 1057 foreach($aColumns as $sAttSqlCode) 1058 { 1059 $iLength = null; 1060 foreach($aAttDefs as $sAttCode => $oAttDef) 1061 { 1062 if (($sAttCode == $sAttSqlCode) || ($oAttDef->IsParam('sql') && ($oAttDef->Get('sql') == $sAttSqlCode))) 1063 { 1064 $iLength = $oAttDef->GetIndexLength(); 1065 break; 1066 } 1067 } 1068 $aLength[] = $iLength; 1069 } 1070 return $aLength; 1071 } 1072 1073 /** 1074 * @param string $sClass 1075 * 1076 * @return string 1077 * @throws \CoreException 1078 */ 1079 final static public function DBGetKey($sClass) 1080 { 1081 self::_check_subclass($sClass); 1082 return self::$m_aClassParams[$sClass]["db_key_field"]; 1083 } 1084 1085 /** 1086 * @param string $sClass 1087 * 1088 * @return mixed 1089 * @throws \CoreException 1090 */ 1091 final static public function DBGetClassField($sClass) 1092 { 1093 self::_check_subclass($sClass); 1094 return self::$m_aClassParams[$sClass]["db_finalclass_field"]; 1095 } 1096 1097 /** 1098 * @param string $sClass 1099 * 1100 * @return boolean true if the class has no parent and no children 1101 * @throws \CoreException 1102 */ 1103 final static public function IsStandaloneClass($sClass) 1104 { 1105 self::_check_subclass($sClass); 1106 1107 if (count(self::$m_aChildClasses[$sClass]) == 0) 1108 { 1109 if (count(self::$m_aParentClasses[$sClass]) == 0) 1110 { 1111 return true; 1112 } 1113 } 1114 1115 return false; 1116 } 1117 1118 /** 1119 * @param string $sParentClass 1120 * @param string $sChildClass 1121 * 1122 * @return bool 1123 * @throws \CoreException 1124 */ 1125 final static public function IsParentClass($sParentClass, $sChildClass) 1126 { 1127 self::_check_subclass($sChildClass); 1128 self::_check_subclass($sParentClass); 1129 if (in_array($sParentClass, self::$m_aParentClasses[$sChildClass])) 1130 { 1131 return true; 1132 } 1133 if ($sChildClass == $sParentClass) 1134 { 1135 return true; 1136 } 1137 1138 return false; 1139 } 1140 1141 /** 1142 * @param string $sClassA 1143 * @param string $sClassB 1144 * 1145 * @return bool 1146 * @throws \CoreException 1147 */ 1148 final static public function IsSameFamilyBranch($sClassA, $sClassB) 1149 { 1150 self::_check_subclass($sClassA); 1151 self::_check_subclass($sClassB); 1152 if (in_array($sClassA, self::$m_aParentClasses[$sClassB])) 1153 { 1154 return true; 1155 } 1156 if (in_array($sClassB, self::$m_aParentClasses[$sClassA])) 1157 { 1158 return true; 1159 } 1160 if ($sClassA == $sClassB) 1161 { 1162 return true; 1163 } 1164 1165 return false; 1166 } 1167 1168 /** 1169 * @param string $sClassA 1170 * @param string $sClassB 1171 * 1172 * @return bool 1173 * @throws \CoreException 1174 */ 1175 final static public function IsSameFamily($sClassA, $sClassB) 1176 { 1177 self::_check_subclass($sClassA); 1178 self::_check_subclass($sClassB); 1179 return (self::GetRootClass($sClassA) == self::GetRootClass($sClassB)); 1180 } 1181 1182 // Attributes of a given class may contain attributes defined in a parent class 1183 // - Some attributes are a copy of the definition 1184 // - Some attributes correspond to the upper class table definition (compound objects) 1185 // (see also filters definition) 1186 /** 1187 * array of ("classname" => array of attributes) 1188 * 1189 * @var \AttributeDefinition[] 1190 */ 1191 private static $m_aAttribDefs = array(); 1192 /** 1193 * array of ("classname" => array of ("attcode"=>"sourceclass")) 1194 * 1195 * @var array 1196 */ 1197 private static $m_aAttribOrigins = array(); 1198 /** 1199 * array of ("classname" => array of ("attcode")) 1200 * 1201 * @var array 1202 */ 1203 private static $m_aIgnoredAttributes = array(); 1204 /** 1205 * array of ("classname" => array of ("attcode" => array of ("metaattcode" => oMetaAttDef)) 1206 * 1207 * @var array 1208 */ 1209 private static $m_aEnumToMeta = array(); 1210 1211 /** 1212 * @param string $sClass 1213 * 1214 * @return AttributeDefinition[] 1215 * @throws \CoreException 1216 */ 1217 final static public function ListAttributeDefs($sClass) 1218 { 1219 self::_check_subclass($sClass); 1220 return self::$m_aAttribDefs[$sClass]; 1221 } 1222 1223 /** 1224 * @param string $sClass 1225 * 1226 * @return array 1227 * @throws \CoreException 1228 */ 1229 final public static function GetAttributesList($sClass) 1230 { 1231 self::_check_subclass($sClass); 1232 return array_keys(self::$m_aAttribDefs[$sClass]); 1233 } 1234 1235 /** 1236 * @param string $sClass 1237 * 1238 * @return array 1239 * @throws \CoreException 1240 */ 1241 final public static function GetFiltersList($sClass) 1242 { 1243 self::_check_subclass($sClass); 1244 return array_keys(self::$m_aFilterDefs[$sClass]); 1245 } 1246 1247 /** 1248 * @param string $sClass 1249 * 1250 * @return array 1251 * @throws \CoreException 1252 */ 1253 final public static function GetKeysList($sClass) 1254 { 1255 self::_check_subclass($sClass); 1256 $aExtKeys = array(); 1257 foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef) 1258 { 1259 if ($oAttDef->IsExternalKey()) 1260 { 1261 $aExtKeys[] = $sAttCode; 1262 } 1263 } 1264 1265 return $aExtKeys; 1266 } 1267 1268 /** 1269 * @param string $sClass 1270 * @param string $sAttCode 1271 * 1272 * @return bool 1273 */ 1274 final static public function IsValidKeyAttCode($sClass, $sAttCode) 1275 { 1276 if (!array_key_exists($sClass, self::$m_aAttribDefs)) 1277 { 1278 return false; 1279 } 1280 if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass])) 1281 { 1282 return false; 1283 } 1284 return (self::$m_aAttribDefs[$sClass][$sAttCode]->IsExternalKey()); 1285 } 1286 1287 /** 1288 * @param string $sClass 1289 * @param string $sAttCode 1290 * @param bool $bExtended 1291 * 1292 * @return bool 1293 * @throws \Exception 1294 */ 1295 final static public function IsValidAttCode($sClass, $sAttCode, $bExtended = false) 1296 { 1297 if (!array_key_exists($sClass, self::$m_aAttribDefs)) 1298 { 1299 return false; 1300 } 1301 1302 if ($bExtended) 1303 { 1304 if (($iPos = strpos($sAttCode, '->')) === false) 1305 { 1306 $bRes = array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]); 1307 } 1308 else 1309 { 1310 $sExtKeyAttCode = substr($sAttCode, 0, $iPos); 1311 $sRemoteAttCode = substr($sAttCode, $iPos + 2); 1312 if (MetaModel::IsValidAttCode($sClass, $sExtKeyAttCode)) 1313 { 1314 $oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode); 1315 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 1316 $bRes = MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode, true); 1317 } 1318 else 1319 { 1320 $bRes = false; 1321 } 1322 } 1323 } 1324 else 1325 { 1326 $bRes = array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]); 1327 } 1328 1329 return $bRes; 1330 } 1331 1332 /** 1333 * @param string $sClass 1334 * @param string $sAttCode 1335 * 1336 * @return bool 1337 */ 1338 final static public function IsAttributeOrigin($sClass, $sAttCode) 1339 { 1340 return (self::$m_aAttribOrigins[$sClass][$sAttCode] == $sClass); 1341 } 1342 1343 /** 1344 * @param string $sClass 1345 * @param string $sFilterCode 1346 * 1347 * @return bool 1348 */ 1349 final static public function IsValidFilterCode($sClass, $sFilterCode) 1350 { 1351 if (!array_key_exists($sClass, self::$m_aFilterDefs)) 1352 { 1353 return false; 1354 } 1355 return (array_key_exists($sFilterCode, self::$m_aFilterDefs[$sClass])); 1356 } 1357 1358 /** 1359 * @param string $sClass 1360 * 1361 * @return bool 1362 */ 1363 public static function IsValidClass($sClass) 1364 { 1365 return (array_key_exists($sClass, self::$m_aAttribDefs)); 1366 } 1367 1368 /** 1369 * @param $oObject 1370 * 1371 * @return bool 1372 */ 1373 public static function IsValidObject($oObject) 1374 { 1375 if (!is_object($oObject)) 1376 { 1377 return false; 1378 } 1379 return (self::IsValidClass(get_class($oObject))); 1380 } 1381 1382 /** 1383 * @param string $sClass 1384 * @param string $sAttCode 1385 * 1386 * @return bool 1387 * @throws \CoreException 1388 */ 1389 public static function IsReconcKey($sClass, $sAttCode) 1390 { 1391 return (in_array($sAttCode, self::GetReconcKeys($sClass))); 1392 } 1393 1394 /** 1395 * @param string $sClass Class name 1396 * @param string $sAttCode Attribute code 1397 * 1398 * @return AttributeDefinition the AttributeDefinition of the $sAttCode attribute of the $sClass class 1399 * @throws Exception 1400 */ 1401 final static public function GetAttributeDef($sClass, $sAttCode) 1402 { 1403 self::_check_subclass($sClass); 1404 if (isset(self::$m_aAttribDefs[$sClass][$sAttCode])) 1405 { 1406 return self::$m_aAttribDefs[$sClass][$sAttCode]; 1407 } 1408 elseif (($iPos = strpos($sAttCode, '->')) !== false) 1409 { 1410 $sExtKeyAttCode = substr($sAttCode, 0, $iPos); 1411 $sRemoteAttCode = substr($sAttCode, $iPos + 2); 1412 $oKeyAttDef = self::GetAttributeDef($sClass, $sExtKeyAttCode); 1413 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 1414 return self::GetAttributeDef($sRemoteClass, $sRemoteAttCode); 1415 } 1416 else 1417 { 1418 throw new Exception("Unknown attribute $sAttCode from class $sClass"); 1419 } 1420 } 1421 1422 /** 1423 * @param string $sClass 1424 * 1425 * @return array 1426 * @throws \CoreException 1427 */ 1428 final static public function GetExternalKeys($sClass) 1429 { 1430 $aExtKeys = array(); 1431 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) 1432 { 1433 if ($oAtt->IsExternalKey()) 1434 { 1435 $aExtKeys[$sAttCode] = $oAtt; 1436 } 1437 } 1438 1439 return $aExtKeys; 1440 } 1441 1442 /** 1443 * @param string $sClass 1444 * 1445 * @return array 1446 * @throws \CoreException 1447 */ 1448 final static public function GetLinkedSets($sClass) 1449 { 1450 $aLinkedSets = array(); 1451 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) 1452 { 1453 if (is_subclass_of($oAtt, 'AttributeLinkedSet')) 1454 { 1455 $aLinkedSets[$sAttCode] = $oAtt; 1456 } 1457 } 1458 1459 return $aLinkedSets; 1460 } 1461 1462 /** 1463 * @param string $sClass 1464 * @param string $sKeyAttCode 1465 * 1466 * @return mixed 1467 * @throws \CoreException 1468 */ 1469 final static public function GetExternalFields($sClass, $sKeyAttCode) 1470 { 1471 static $aExtFields = array(); 1472 if (!isset($aExtFields[$sClass][$sKeyAttCode])) 1473 { 1474 $aExtFields[$sClass][$sKeyAttCode] = array(); 1475 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) 1476 { 1477 if ($oAtt->IsExternalField() && ($oAtt->GetKeyAttCode() == $sKeyAttCode)) 1478 { 1479 $aExtFields[$sClass][$sKeyAttCode][$oAtt->GetExtAttCode()] = $oAtt; 1480 } 1481 } 1482 } 1483 return $aExtFields[$sClass][$sKeyAttCode]; 1484 } 1485 1486 /** 1487 * @param string $sClass 1488 * @param string $sKeyAttCode 1489 * @param string $sRemoteAttCode 1490 * 1491 * @return null|string 1492 * @throws \CoreException 1493 */ 1494 final static public function FindExternalField($sClass, $sKeyAttCode, $sRemoteAttCode) 1495 { 1496 $aExtFields = self::GetExternalFields($sClass, $sKeyAttCode); 1497 if (isset($aExtFields[$sRemoteAttCode])) 1498 { 1499 return $aExtFields[$sRemoteAttCode]; 1500 } 1501 else 1502 { 1503 return null; 1504 } 1505 } 1506 1507 /** @var array */ 1508 protected static $m_aTrackForwardCache = array(); 1509 1510 /** 1511 * List external keys for which there is a LinkSet (direct or indirect) on the other end 1512 * 1513 * For those external keys, a change will have a special meaning on the other end 1514 * in term of change tracking 1515 * 1516 * @param string $sClass 1517 * 1518 * @return mixed 1519 * @throws \CoreException 1520 */ 1521 final static public function GetTrackForwardExternalKeys($sClass) 1522 { 1523 if (!isset(self::$m_aTrackForwardCache[$sClass])) 1524 { 1525 $aRes = array(); 1526 foreach(MetaModel::GetExternalKeys($sClass) as $sAttCode => $oAttDef) 1527 { 1528 $sRemoteClass = $oAttDef->GetTargetClass(); 1529 foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) 1530 { 1531 if (!$oRemoteAttDef->IsLinkSet()) 1532 { 1533 continue; 1534 } 1535 if (!is_subclass_of($sClass, $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $sClass) 1536 { 1537 continue; 1538 } 1539 if ($oRemoteAttDef->GetExtKeyToMe() != $sAttCode) 1540 { 1541 continue; 1542 } 1543 $aRes[$sAttCode] = $oRemoteAttDef; 1544 } 1545 } 1546 self::$m_aTrackForwardCache[$sClass] = $aRes; 1547 } 1548 return self::$m_aTrackForwardCache[$sClass]; 1549 } 1550 1551 /** 1552 * @param string $sClass 1553 * @param string $sAttCode 1554 * 1555 * @return array 1556 */ 1557 final static public function ListMetaAttributes($sClass, $sAttCode) 1558 { 1559 if (isset(self::$m_aEnumToMeta[$sClass][$sAttCode])) 1560 { 1561 $aRet = self::$m_aEnumToMeta[$sClass][$sAttCode]; 1562 } 1563 else 1564 { 1565 $aRet = array(); 1566 } 1567 return $aRet; 1568 } 1569 1570 /** 1571 * Get the attribute label 1572 * 1573 * @param string sClass Persistent class 1574 * @param string sAttCodeEx Extended attribute code: attcode[->attcode] 1575 * @param bool $bShowMandatory If true, add a star character (at the end or before the ->) to show that the field 1576 * is mandatory 1577 * 1578 * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName 1579 * @throws \Exception 1580 */ 1581 public static function GetLabel($sClass, $sAttCodeEx, $bShowMandatory = false) 1582 { 1583 if (($iPos = strpos($sAttCodeEx, '->')) === false) 1584 { 1585 if ($sAttCodeEx == 'id') 1586 { 1587 $sLabel = Dict::S('UI:CSVImport:idField'); 1588 } 1589 else 1590 { 1591 $oAttDef = self::GetAttributeDef($sClass, $sAttCodeEx); 1592 $sMandatory = ($bShowMandatory && !$oAttDef->IsNullAllowed()) ? '*' : ''; 1593 $sLabel = $oAttDef->GetLabel().$sMandatory; 1594 } 1595 } 1596 else 1597 { 1598 $sExtKeyAttCode = substr($sAttCodeEx, 0, $iPos); 1599 $sRemoteAttCode = substr($sAttCodeEx, $iPos + 2); 1600 $oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode); 1601 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 1602 // Recurse 1603 $sLabel = self::GetLabel($sClass, $sExtKeyAttCode).'->'.self::GetLabel($sRemoteClass, $sRemoteAttCode); 1604 } 1605 1606 return $sLabel; 1607 } 1608 1609 /** 1610 * @param string $sClass 1611 * @param string $sAttCode 1612 * 1613 * @return string 1614 * @throws \Exception 1615 */ 1616 public static function GetDescription($sClass, $sAttCode) 1617 { 1618 $oAttDef = self::GetAttributeDef($sClass, $sAttCode); 1619 if ($oAttDef) 1620 { 1621 return $oAttDef->GetDescription(); 1622 } 1623 return ""; 1624 } 1625 1626 // Filters of a given class may contain filters defined in a parent class 1627 // - Some filters are a copy of the definition 1628 // - Some filters correspond to the upper class table definition (compound objects) 1629 // (see also attributes definition) 1630 /** 1631 * array of ("classname" => array filterdef) 1632 * 1633 * @var array 1634 */ 1635 private static $m_aFilterDefs = array(); 1636 /** 1637 * array of ("classname" => array of ("attcode"=>"sourceclass")) 1638 * 1639 * @var array 1640 */ 1641 private static $m_aFilterOrigins = array(); 1642 1643 /** 1644 * @param string $sClass 1645 * 1646 * @return mixed 1647 * @throws \CoreException 1648 */ 1649 public static function GetClassFilterDefs($sClass) 1650 { 1651 self::_check_subclass($sClass); 1652 return self::$m_aFilterDefs[$sClass]; 1653 } 1654 1655 /** 1656 * @param string $sClass 1657 * @param string $sFilterCode 1658 * 1659 * @return mixed 1660 * @throws \CoreException 1661 */ 1662 final static public function GetClassFilterDef($sClass, $sFilterCode) 1663 { 1664 self::_check_subclass($sClass); 1665 if (!array_key_exists($sFilterCode, self::$m_aFilterDefs[$sClass])) 1666 { 1667 throw new CoreException("Unknown filter code '$sFilterCode' for class '$sClass'"); 1668 } 1669 return self::$m_aFilterDefs[$sClass][$sFilterCode]; 1670 } 1671 1672 /** 1673 * @param string $sClass 1674 * @param string $sFilterCode 1675 * 1676 * @return string 1677 * @throws \CoreException 1678 */ 1679 public static function GetFilterLabel($sClass, $sFilterCode) 1680 { 1681 $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); 1682 if ($oFilter) 1683 { 1684 return $oFilter->GetLabel(); 1685 } 1686 1687 return ""; 1688 } 1689 1690 /** 1691 * @param string $sClass 1692 * @param string $sFilterCode 1693 * 1694 * @return string 1695 * @throws \CoreException 1696 */ 1697 public static function GetFilterDescription($sClass, $sFilterCode) 1698 { 1699 $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); 1700 if ($oFilter) 1701 { 1702 return $oFilter->GetDescription(); 1703 } 1704 return ""; 1705 } 1706 1707 /** 1708 * @param string $sClass 1709 * @param string $sFilterCode 1710 * 1711 * @return array 1712 * @throws \CoreException 1713 */ 1714 public static function GetFilterOperators($sClass, $sFilterCode) 1715 { 1716 $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); 1717 if ($oFilter) 1718 { 1719 return $oFilter->GetOperators(); 1720 } 1721 return array(); 1722 } 1723 1724 /** 1725 * @param string $sClass 1726 * @param string $sFilterCode 1727 * 1728 * @return array 1729 * @throws \CoreException 1730 */ 1731 public static function GetFilterLooseOperator($sClass, $sFilterCode) 1732 { 1733 $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); 1734 if ($oFilter) 1735 { 1736 return $oFilter->GetLooseOperator(); 1737 } 1738 1739 return array(); 1740 } 1741 1742 /** 1743 * @param string $sClass 1744 * @param string $sFilterCode 1745 * @param string $sOpCode 1746 * 1747 * @return string 1748 * @throws \CoreException 1749 */ 1750 public static function GetFilterOpDescription($sClass, $sFilterCode, $sOpCode) 1751 { 1752 $oFilter = self::GetClassFilterDef($sClass, $sFilterCode); 1753 if ($oFilter) 1754 { 1755 return $oFilter->GetOpDescription($sOpCode); 1756 } 1757 1758 return ""; 1759 } 1760 1761 /** 1762 * @param string $sFilterCode 1763 * 1764 * @return string 1765 */ 1766 public static function GetFilterHTMLInput($sFilterCode) 1767 { 1768 return "<INPUT name=\"$sFilterCode\">"; 1769 } 1770 1771 // Lists of attributes/search filters 1772 // 1773 /** 1774 * array of ("listcode" => various info on the list, common to every classes) 1775 * 1776 * @var array 1777 */ 1778 private static $m_aListInfos = array(); 1779 /** 1780 * array of ("classname" => array of "listcode" => list) 1781 * list may be an array of attcode / fltcode 1782 * list may be an array of "groupname" => (array of attcode / fltcode) 1783 * 1784 * @var array 1785 */ 1786 private static $m_aListData = array(); 1787 1788 /** 1789 * @return array 1790 */ 1791 public static function EnumZLists() 1792 { 1793 return array_keys(self::$m_aListInfos); 1794 } 1795 1796 /** 1797 * @param string $sListCode 1798 * 1799 * @return mixed 1800 */ 1801 final static public function GetZListInfo($sListCode) 1802 { 1803 return self::$m_aListInfos[$sListCode]; 1804 } 1805 1806 /** 1807 * @param string $sClass 1808 * @param string $sListCode 1809 * 1810 * @return array 1811 */ 1812 public static function GetZListItems($sClass, $sListCode) 1813 { 1814 if (array_key_exists($sClass, self::$m_aListData)) 1815 { 1816 if (array_key_exists($sListCode, self::$m_aListData[$sClass])) 1817 { 1818 return self::$m_aListData[$sClass][$sListCode]; 1819 } 1820 } 1821 $sParentClass = self::GetParentPersistentClass($sClass); 1822 if (empty($sParentClass)) 1823 { 1824 return array(); 1825 } // nothing for the mother of all classes 1826 // Dig recursively 1827 return self::GetZListItems($sParentClass, $sListCode); 1828 } 1829 1830 /** 1831 * @param string $sClass 1832 * @param string $sListCode 1833 * @param string $sAttCodeOrFltCode 1834 * @param string $sGroup 1835 * 1836 * @return bool 1837 */ 1838 public static function IsAttributeInZList($sClass, $sListCode, $sAttCodeOrFltCode, $sGroup = null) 1839 { 1840 $aZList = self::FlattenZlist(self::GetZListItems($sClass, $sListCode)); 1841 if (!$sGroup) 1842 { 1843 return (in_array($sAttCodeOrFltCode, $aZList)); 1844 } 1845 return (in_array($sAttCodeOrFltCode, $aZList[$sGroup])); 1846 } 1847 1848 // 1849 // Relations 1850 // 1851 /** 1852 * array of ("relcode" => various info on the list, common to every classes) 1853 * 1854 * @var array 1855 */ 1856 private static $m_aRelationInfos = array(); 1857 1858 /** 1859 * TO BE DEPRECATED: use EnumRelationsEx instead 1860 * 1861 * @param string $sClass 1862 * 1863 * @return array multitype:string unknown |Ambigous <string, multitype:> 1864 * @throws \CoreException 1865 * @throws \Exception 1866 * @throws \OQLException 1867 */ 1868 public static function EnumRelations($sClass = '') 1869 { 1870 $aResult = array_keys(self::$m_aRelationInfos); 1871 if (!empty($sClass)) 1872 { 1873 // Return only the relations that have a meaning (i.e. for which at least one query is defined) 1874 // for the specified class 1875 $aClassRelations = array(); 1876 foreach($aResult as $sRelCode) 1877 { 1878 $aQueriesDown = self::EnumRelationQueries($sClass, $sRelCode); 1879 if (count($aQueriesDown) > 0) 1880 { 1881 $aClassRelations[] = $sRelCode; 1882 } 1883 // Temporary patch: until the impact analysis GUI gets rewritten, 1884 // let's consider that "depends on" is equivalent to "impacts/up" 1885 // The current patch has been implemented in DBObject and MetaModel 1886 if ($sRelCode == 'impacts') 1887 { 1888 $aQueriesUp = self::EnumRelationQueries($sClass, 'impacts', false); 1889 if (count($aQueriesUp) > 0) 1890 { 1891 $aClassRelations[] = 'depends on'; 1892 } 1893 } 1894 } 1895 1896 return $aClassRelations; 1897 } 1898 1899 // Temporary patch: until the impact analysis GUI gets rewritten, 1900 // let's consider that "depends on" is equivalent to "impacts/up" 1901 // The current patch has been implemented in DBObject and MetaModel 1902 if (in_array('impacts', $aResult)) 1903 { 1904 $aResult[] = 'depends on'; 1905 } 1906 1907 return $aResult; 1908 } 1909 1910 /** 1911 * @param string $sClass 1912 * 1913 * @return array 1914 * @throws \CoreException 1915 * @throws \Exception 1916 * @throws \OQLException 1917 */ 1918 public static function EnumRelationsEx($sClass) 1919 { 1920 $aRelationInfo = array_keys(self::$m_aRelationInfos); 1921 // Return only the relations that have a meaning (i.e. for which at least one query is defined) 1922 // for the specified class 1923 $aClassRelations = array(); 1924 foreach($aRelationInfo as $sRelCode) 1925 { 1926 $aQueriesDown = self::EnumRelationQueries($sClass, $sRelCode, true /* Down */); 1927 if (count($aQueriesDown) > 0) 1928 { 1929 $aClassRelations[$sRelCode]['down'] = self::GetRelationLabel($sRelCode, true); 1930 } 1931 1932 $aQueriesUp = self::EnumRelationQueries($sClass, $sRelCode, false /* Up */); 1933 if (count($aQueriesUp) > 0) 1934 { 1935 $aClassRelations[$sRelCode]['up'] = self::GetRelationLabel($sRelCode, false); 1936 } 1937 } 1938 1939 return $aClassRelations; 1940 } 1941 1942 /** 1943 * @param string $sRelCode Relation code 1944 * @param bool $bDown Relation direction, is it downstream (true) or upstream (false). Default is true. 1945 * 1946 * @return string 1947 * @throws \DictExceptionMissingString 1948 */ 1949 final static public function GetRelationDescription($sRelCode, $bDown = true) 1950 { 1951 // Legacy convention had only one description describing the relation. 1952 // Now, as the relation is bidirectional, we have a description for each directions. 1953 $sLegacy = Dict::S("Relation:$sRelCode/Description"); 1954 1955 if($bDown) 1956 { 1957 $sKey = "Relation:$sRelCode/DownStream+"; 1958 } 1959 else 1960 { 1961 $sKey = "Relation:$sRelCode/UpStream+"; 1962 } 1963 $sRet = Dict::S($sKey, $sLegacy); 1964 1965 return $sRet; 1966 } 1967 1968 /** 1969 * @param string $sRelCode Relation code 1970 * @param bool $bDown Relation direction, is it downstream (true) or upstream (false). Default is true. 1971 * 1972 * @return string 1973 * @throws \DictExceptionMissingString 1974 */ 1975 final static public function GetRelationLabel($sRelCode, $bDown = true) 1976 { 1977 if ($bDown) 1978 { 1979 // The legacy convention is confusing with regard to the way we have conceptualized the relations: 1980 // In the former representation, the main stream was named after "up" 1981 // Now, the relation from A to B says that something is transmitted from A to B, thus going DOWNstream as described in a petri net. 1982 $sKey = "Relation:$sRelCode/DownStream"; 1983 $sLegacy = Dict::S("Relation:$sRelCode/VerbUp", $sKey); 1984 } 1985 else 1986 { 1987 $sKey = "Relation:$sRelCode/UpStream"; 1988 $sLegacy = Dict::S("Relation:$sRelCode/VerbDown", $sKey); 1989 } 1990 1991 return Dict::S($sKey, $sLegacy); 1992 } 1993 1994 /** 1995 * @param string $sRelCode 1996 * 1997 * @return array 1998 * @throws \CoreException 1999 * @throws \Exception 2000 * @throws \OQLException 2001 */ 2002 protected static function ComputeRelationQueries($sRelCode) 2003 { 2004 $bHasLegacy = false; 2005 $aQueries = array(); 2006 foreach(self::GetClasses() as $sClass) 2007 { 2008 $aQueries[$sClass]['down'] = array(); 2009 if (!array_key_exists('up', $aQueries[$sClass])) 2010 { 2011 $aQueries[$sClass]['up'] = array(); 2012 } 2013 2014 $aNeighboursDown = call_user_func_array(array($sClass, 'GetRelationQueriesEx'), array($sRelCode)); 2015 2016 // Translate attributes into queries (new style of spec only) 2017 foreach($aNeighboursDown as $sNeighbourId => $aNeighbourData) 2018 { 2019 $aNeighbourData['sFromClass'] = $aNeighbourData['sDefinedInClass']; 2020 try 2021 { 2022 if (strlen($aNeighbourData['sQueryDown']) == 0) 2023 { 2024 $oAttDef = self::GetAttributeDef($sClass, $aNeighbourData['sAttribute']); 2025 if ($oAttDef instanceof AttributeExternalKey) 2026 { 2027 $sTargetClass = $oAttDef->GetTargetClass(); 2028 $aNeighbourData['sToClass'] = $sTargetClass; 2029 $aNeighbourData['sQueryDown'] = 'SELECT '.$sTargetClass.' AS o WHERE o.id = :this->'.$aNeighbourData['sAttribute']; 2030 $aNeighbourData['sQueryUp'] = 'SELECT '.$aNeighbourData['sFromClass'].' AS o WHERE o.'.$aNeighbourData['sAttribute'].' = :this->id'; 2031 } 2032 elseif ($oAttDef instanceof AttributeLinkedSet) 2033 { 2034 $sLinkedClass = $oAttDef->GetLinkedClass(); 2035 $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); 2036 if ($oAttDef->IsIndirect()) 2037 { 2038 $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); 2039 $oRemoteAttDef = self::GetAttributeDef($sLinkedClass, $sExtKeyToRemote); 2040 $sRemoteClass = $oRemoteAttDef->GetTargetClass(); 2041 2042 $aNeighbourData['sToClass'] = $sRemoteClass; 2043 $aNeighbourData['sQueryDown'] = "SELECT $sRemoteClass AS o JOIN $sLinkedClass AS lnk ON lnk.$sExtKeyToRemote = o.id WHERE lnk.$sExtKeyToMe = :this->id"; 2044 $aNeighbourData['sQueryUp'] = "SELECT ".$aNeighbourData['sFromClass']." AS o JOIN $sLinkedClass AS lnk ON lnk.$sExtKeyToMe = o.id WHERE lnk.$sExtKeyToRemote = :this->id"; 2045 } 2046 else 2047 { 2048 $aNeighbourData['sToClass'] = $sLinkedClass; 2049 $aNeighbourData['sQueryDown'] = "SELECT $sLinkedClass AS o WHERE o.$sExtKeyToMe = :this->id"; 2050 $aNeighbourData['sQueryUp'] = "SELECT ".$aNeighbourData['sFromClass']." AS o WHERE o.id = :this->$sExtKeyToMe"; 2051 } 2052 } 2053 else 2054 { 2055 throw new Exception("Unexpected attribute type for '{$aNeighbourData['sAttribute']}'. Expecting a link set or external key."); 2056 } 2057 } 2058 else 2059 { 2060 $oSearch = DBObjectSearch::FromOQL($aNeighbourData['sQueryDown']); 2061 $aNeighbourData['sToClass'] = $oSearch->GetClass(); 2062 } 2063 } 2064 catch (Exception $e) 2065 { 2066 throw new Exception("Wrong definition for the relation $sRelCode/{$aNeighbourData['sDefinedInClass']}/{$aNeighbourData['sNeighbour']}: ".$e->getMessage()); 2067 } 2068 2069 if ($aNeighbourData['sDirection'] == 'down') 2070 { 2071 $aNeighbourData['sQueryUp'] = null; 2072 } 2073 2074 $sArrowId = $aNeighbourData['sDefinedInClass'].'_'.$sNeighbourId; 2075 $aQueries[$sClass]['down'][$sArrowId] = $aNeighbourData; 2076 2077 // Compute the reverse index 2078 if ($aNeighbourData['sDefinedInClass'] == $sClass) 2079 { 2080 if ($aNeighbourData['sDirection'] == 'both') 2081 { 2082 $sToClass = $aNeighbourData['sToClass']; 2083 foreach(self::EnumChildClasses($sToClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass) 2084 { 2085 $aQueries[$sSubClass]['up'][$sArrowId] = $aNeighbourData; 2086 } 2087 } 2088 } 2089 } 2090 2091 // Read legacy definitions 2092 // The up/down queries have to be reconcilied, which can only be done later when all the classes have been browsed 2093 // 2094 // The keys used to store a query (up or down) into the array are built differently between the modern and legacy made data: 2095 // Modern way: aQueries[sClass]['up'|'down'][sArrowId], where sArrowId is made of the source class + neighbour id (XML def) 2096 // Legacy way: aQueries[sClass]['up'|'down'][sRemoteClass] 2097 // The modern way does allow for several arrows between two classes 2098 // The legacy way aims at simplifying the transformation (reconciliation between up and down) 2099 if ($sRelCode == 'impacts') 2100 { 2101 $sRevertCode = 'depends on'; 2102 2103 $aLegacy = call_user_func_array(array($sClass, 'GetRelationQueries'), array($sRelCode)); 2104 foreach($aLegacy as $sId => $aLegacyEntry) 2105 { 2106 $bHasLegacy = true; 2107 2108 $oFilter = DBObjectSearch::FromOQL($aLegacyEntry['sQuery']); 2109 $sRemoteClass = $oFilter->GetClass(); 2110 2111 // Determine wether the query is inherited from a parent or not 2112 $bInherited = false; 2113 foreach(self::EnumParentClasses($sClass) as $sParent) 2114 { 2115 if (!isset($aQueries[$sParent]['down'][$sRemoteClass])) 2116 { 2117 continue; 2118 } 2119 if ($aLegacyEntry['sQuery'] == $aQueries[$sParent]['down'][$sRemoteClass]['sQueryDown']) 2120 { 2121 $bInherited = true; 2122 $aQueries[$sClass]['down'][$sRemoteClass] = $aQueries[$sParent]['down'][$sRemoteClass]; 2123 break; 2124 } 2125 } 2126 2127 if (!$bInherited) 2128 { 2129 $aQueries[$sClass]['down'][$sRemoteClass] = array( 2130 '_legacy_' => true, 2131 'sDefinedInClass' => $sClass, 2132 'sFromClass' => $sClass, 2133 'sToClass' => $sRemoteClass, 2134 'sDirection' => 'down', 2135 'sQueryDown' => $aLegacyEntry['sQuery'], 2136 'sQueryUp' => null, 2137 'sNeighbour' => $sRemoteClass // Normalize the neighbour id 2138 ); 2139 } 2140 } 2141 2142 $aLegacy = call_user_func_array(array($sClass, 'GetRelationQueries'), array($sRevertCode)); 2143 foreach($aLegacy as $sId => $aLegacyEntry) 2144 { 2145 $bHasLegacy = true; 2146 2147 $oFilter = DBObjectSearch::FromOQL($aLegacyEntry['sQuery']); 2148 $sRemoteClass = $oFilter->GetClass(); 2149 2150 // Determine wether the query is inherited from a parent or not 2151 $bInherited = false; 2152 foreach(self::EnumParentClasses($sClass) as $sParent) 2153 { 2154 if (!isset($aQueries[$sParent]['up'][$sRemoteClass])) 2155 { 2156 continue; 2157 } 2158 if ($aLegacyEntry['sQuery'] == $aQueries[$sParent]['up'][$sRemoteClass]['sQueryUp']) 2159 { 2160 $bInherited = true; 2161 $aQueries[$sClass]['up'][$sRemoteClass] = $aQueries[$sParent]['up'][$sRemoteClass]; 2162 break; 2163 } 2164 } 2165 2166 if (!$bInherited) 2167 { 2168 $aQueries[$sClass]['up'][$sRemoteClass] = array( 2169 '_legacy_' => true, 2170 'sDefinedInClass' => $sRemoteClass, 2171 'sFromClass' => $sRemoteClass, 2172 'sToClass' => $sClass, 2173 'sDirection' => 'both', 2174 'sQueryDown' => null, 2175 'sQueryUp' => $aLegacyEntry['sQuery'], 2176 'sNeighbour' => $sClass// Normalize the neighbour id 2177 ); 2178 } 2179 } 2180 } 2181 else 2182 { 2183 // Cannot take the legacy system into account... simply ignore it 2184 } 2185 } // foreach class 2186 2187 // Perform the up/down reconciliation for the legacy definitions 2188 if ($bHasLegacy) 2189 { 2190 foreach(self::GetClasses() as $sClass) 2191 { 2192 // Foreach "up" legacy query, update its "down" counterpart 2193 if (isset($aQueries[$sClass]['up'])) 2194 { 2195 foreach($aQueries[$sClass]['up'] as $sNeighbourId => $aNeighbourData) 2196 { 2197 if (!array_key_exists('_legacy_', $aNeighbourData)) 2198 { 2199 continue; 2200 } 2201 if (!$aNeighbourData['_legacy_']) 2202 { 2203 continue; 2204 } // Skip modern definitions 2205 2206 $sLocalClass = $aNeighbourData['sToClass']; 2207 foreach(self::EnumChildClasses($aNeighbourData['sFromClass'], ENUM_CHILD_CLASSES_ALL) as $sRemoteClass) 2208 { 2209 if (isset($aQueries[$sRemoteClass]['down'][$sLocalClass])) 2210 { 2211 $aQueries[$sRemoteClass]['down'][$sLocalClass]['sQueryUp'] = $aNeighbourData['sQueryUp']; 2212 $aQueries[$sRemoteClass]['down'][$sLocalClass]['sDirection'] = 'both'; 2213 } 2214 // Be silent in order to transparently support legacy data models where the counterpart query does not always exist 2215 //else 2216 //{ 2217 // throw new Exception("Legacy definition of the relation '$sRelCode/$sRevertCode', defined on $sLocalClass (relation: $sRevertCode, inherited to $sClass), missing the counterpart query on class $sRemoteClass ($sRelCode)"); 2218 //} 2219 } 2220 } 2221 } 2222 // Foreach "down" legacy query, update its "up" counterpart (if any) 2223 foreach($aQueries[$sClass]['down'] as $sNeighbourId => $aNeighbourData) 2224 { 2225 if (!$aNeighbourData['_legacy_']) 2226 { 2227 continue; 2228 } // Skip modern definitions 2229 2230 $sLocalClass = $aNeighbourData['sFromClass']; 2231 foreach(self::EnumChildClasses($aNeighbourData['sToClass'], ENUM_CHILD_CLASSES_ALL) as $sRemoteClass) 2232 { 2233 if (isset($aQueries[$sRemoteClass]['up'][$sLocalClass])) 2234 { 2235 $aQueries[$sRemoteClass]['up'][$sLocalClass]['sQueryDown'] = $aNeighbourData['sQueryDown']; 2236 } 2237 } 2238 } 2239 } 2240 } 2241 2242 return $aQueries; 2243 } 2244 2245 /** 2246 * @param string $sClass 2247 * @param string $sRelCode 2248 * @param bool $bDown 2249 * 2250 * @return array 2251 * @throws \CoreException 2252 * @throws \Exception 2253 * @throws \OQLException 2254 */ 2255 public static function EnumRelationQueries($sClass, $sRelCode, $bDown = true) 2256 { 2257 static $aQueries = array(); 2258 if (!isset($aQueries[$sRelCode])) 2259 { 2260 $aQueries[$sRelCode] = self::ComputeRelationQueries($sRelCode); 2261 } 2262 $sDirection = $bDown ? 'down' : 'up'; 2263 if (isset($aQueries[$sRelCode][$sClass][$sDirection])) 2264 { 2265 return $aQueries[$sRelCode][$sClass][$sDirection]; 2266 } 2267 else 2268 { 2269 return array(); 2270 } 2271 } 2272 2273 /** 2274 * Compute the "RelatedObjects" for a whole set of DBObjects 2275 * 2276 * @param string $sRelCode The code of the relation to use for the computation 2277 * @param array $aSourceObjects The objects to start with 2278 * @param int $iMaxDepth 2279 * @param boolean $bEnableRedundancy 2280 * @param array $aUnreachable Array of objects to be considered as 'unreachable' 2281 * @param array $aContexts 2282 * 2283 * @return RelationGraph The graph of all the related objects 2284 * @throws \Exception 2285 */ 2286 static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aUnreachable = array(), $aContexts = array()) 2287 { 2288 $oGraph = new RelationGraph(); 2289 foreach($aSourceObjects as $oObject) 2290 { 2291 $oGraph->AddSourceObject($oObject); 2292 } 2293 foreach($aContexts as $key => $sOQL) 2294 { 2295 $oGraph->AddContextQuery($key, $sOQL); 2296 } 2297 $oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachable); 2298 return $oGraph; 2299 } 2300 2301 /** 2302 * Compute the "RelatedObjects" in the reverse way 2303 * 2304 * @param string $sRelCode The code of the relation to use for the computation 2305 * @param array $aSourceObjects The objects to start with 2306 * @param int $iMaxDepth 2307 * @param boolean $bEnableRedundancy 2308 * @param array $aContexts 2309 * 2310 * @return RelationGraph The graph of all the related objects 2311 * @throws \Exception 2312 */ 2313 static public function GetRelatedObjectsUp($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true, $aContexts = array()) 2314 { 2315 $oGraph = new RelationGraph(); 2316 foreach($aSourceObjects as $oObject) 2317 { 2318 $oGraph->AddSinkObject($oObject); 2319 } 2320 foreach($aContexts as $key => $sOQL) 2321 { 2322 $oGraph->AddContextQuery($key, $sOQL); 2323 } 2324 $oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy); 2325 return $oGraph; 2326 } 2327 2328 // 2329 // Object lifecycle model 2330 // 2331 /** 2332 * array of ("classname" => array of "statecode"=>array('label'=>..., attribute_inherit=> attribute_list=>...)) 2333 * 2334 * @var array 2335 */ 2336 private static $m_aStates = array(); 2337 /** 2338 * array of ("classname" => array of ("stimuluscode"=>array('label'=>...))) 2339 * 2340 * @var array 2341 */ 2342 private static $m_aStimuli = array(); 2343 /** 2344 * array of ("classname" => array of ("statcode_from"=>array of ("stimuluscode" => array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD))) 2345 * 2346 * @var array 2347 */ 2348 private static $m_aTransitions = array(); 2349 2350 /** 2351 * @param string $sClass 2352 * 2353 * @return array 2354 */ 2355 public static function EnumStates($sClass) 2356 { 2357 if (array_key_exists($sClass, self::$m_aStates)) 2358 { 2359 return self::$m_aStates[$sClass]; 2360 } 2361 else 2362 { 2363 return array(); 2364 } 2365 } 2366 2367 /** 2368 * @param string $sClass 2369 * 2370 * @return array All possible initial states, including the default one 2371 * @throws \CoreException 2372 * @throws \Exception 2373 */ 2374 public static function EnumInitialStates($sClass) 2375 { 2376 if (array_key_exists($sClass, self::$m_aStates)) 2377 { 2378 $aRet = array(); 2379 // Add the states for which the flag 'is_initial_state' is set to <true> 2380 foreach(self::$m_aStates[$sClass] as $aStateCode => $aProps) 2381 { 2382 if (isset($aProps['initial_state_path'])) 2383 { 2384 $aRet[$aStateCode] = $aProps['initial_state_path']; 2385 } 2386 } 2387 // Add the default initial state 2388 $sMainInitialState = self::GetDefaultState($sClass); 2389 if (!isset($aRet[$sMainInitialState])) 2390 { 2391 $aRet[$sMainInitialState] = array(); 2392 } 2393 return $aRet; 2394 } 2395 else 2396 { 2397 return array(); 2398 } 2399 } 2400 2401 /** 2402 * @param string $sClass 2403 * 2404 * @return array 2405 */ 2406 public static function EnumStimuli($sClass) 2407 { 2408 if (array_key_exists($sClass, self::$m_aStimuli)) 2409 { 2410 return self::$m_aStimuli[$sClass]; 2411 } 2412 else 2413 { 2414 return array(); 2415 } 2416 } 2417 2418 /** 2419 * @param string $sClass 2420 * @param string $sStateValue 2421 * 2422 * @return mixed 2423 * @throws \CoreException 2424 * @throws \Exception 2425 */ 2426 public static function GetStateLabel($sClass, $sStateValue) 2427 { 2428 $sStateAttrCode = self::GetStateAttributeCode($sClass); 2429 $oAttDef = self::GetAttributeDef($sClass, $sStateAttrCode); 2430 return $oAttDef->GetValueLabel($sStateValue); 2431 } 2432 2433 /** 2434 * @param string $sClass 2435 * @param string $sStateValue 2436 * 2437 * @return mixed 2438 * @throws \CoreException 2439 * @throws \Exception 2440 */ 2441 public static function GetStateDescription($sClass, $sStateValue) 2442 { 2443 $sStateAttrCode = self::GetStateAttributeCode($sClass); 2444 $oAttDef = self::GetAttributeDef($sClass, $sStateAttrCode); 2445 return $oAttDef->GetValueDescription($sStateValue); 2446 } 2447 2448 /** 2449 * @param string $sClass 2450 * @param string $sStateCode 2451 * 2452 * @return array 2453 */ 2454 public static function EnumTransitions($sClass, $sStateCode) 2455 { 2456 if (array_key_exists($sClass, self::$m_aTransitions)) 2457 { 2458 if (array_key_exists($sStateCode, self::$m_aTransitions[$sClass])) 2459 { 2460 return self::$m_aTransitions[$sClass][$sStateCode]; 2461 } 2462 } 2463 return array(); 2464 } 2465 2466 /** 2467 * @param string $sClass 2468 * @param string $sState 2469 * @param string $sAttCode 2470 * 2471 * @return int the binary combination of flags (OPT_ATT_HIDDEN, OPT_ATT_READONLY, OPT_ATT_MANDATORY...) for the 2472 * given attribute in the given state of the object 2473 * @throws \CoreException 2474 * 2475 * @see \DBObject::GetAttributeFlags() 2476 */ 2477 public static function GetAttributeFlags($sClass, $sState, $sAttCode) 2478 { 2479 $iFlags = 0; // By default (if no life cycle) no flag at all 2480 $sStateAttCode = self::GetStateAttributeCode($sClass); 2481 if (!empty($sStateAttCode)) 2482 { 2483 $aStates = MetaModel::EnumStates($sClass); 2484 if (!array_key_exists($sState, $aStates)) 2485 { 2486 throw new CoreException("Invalid state '$sState' for class '$sClass', expecting a value in {".implode(', ', array_keys($aStates))."}"); 2487 } 2488 $aCurrentState = $aStates[$sState]; 2489 if ((array_key_exists('attribute_list', $aCurrentState)) && (array_key_exists($sAttCode, $aCurrentState['attribute_list']))) 2490 { 2491 $iFlags = $aCurrentState['attribute_list'][$sAttCode]; 2492 } 2493 } 2494 return $iFlags; 2495 } 2496 2497 /** 2498 * @param string $sClass string 2499 * @param string $sState string 2500 * @param string $sStimulus string 2501 * @param string $sAttCode string 2502 * 2503 * @return int The $sAttCode flags when $sStimulus is applied on an object of $sClass in the $sState state. 2504 * <strong>Note: This does NOT combine flags from the target state</strong> 2505 * @throws CoreException 2506 */ 2507 public static function GetTransitionFlags($sClass, $sState, $sStimulus, $sAttCode) 2508 { 2509 $iFlags = 0; // By default (if no lifecycle) no flag at all 2510 $sStateAttCode = self::GetStateAttributeCode($sClass); 2511 if (!empty($sStateAttCode)) 2512 { 2513 $aTransitions = MetaModel::EnumTransitions($sClass, $sState); 2514 if (!array_key_exists($sStimulus, $aTransitions)) 2515 { 2516 throw new CoreException("Invalid transition '$sStimulus' for class '$sClass', expecting a value in {".implode(', ', array_keys($aTransitions))."}"); 2517 } 2518 2519 $aCurrentTransition = $aTransitions[$sStimulus]; 2520 if ((array_key_exists('attribute_list', $aCurrentTransition)) && (array_key_exists($sAttCode, $aCurrentTransition['attribute_list']))) 2521 { 2522 $iFlags = $aCurrentTransition['attribute_list'][$sAttCode]; 2523 } 2524 } 2525 2526 return $iFlags; 2527 } 2528 2529 /** 2530 * @param string $sClass string Object class 2531 * @param string $sStimulus string Stimulus code applied 2532 * @param string $sOriginState string State the stimulus comes from 2533 * 2534 * @return array Attribute codes (with their flags) when $sStimulus is applied on an object of $sClass in the $sOriginState state. 2535 * <strong>Note: Attributes (and flags) from the target state and the transition are combined</strong> 2536 */ 2537 public static function GetTransitionAttributes($sClass, $sStimulus, $sOriginState) 2538 { 2539 $aAttributes = array(); 2540 2541 // Retrieving target state 2542 $aTransitions = MetaModel::EnumTransitions($sClass, $sOriginState); 2543 $aTransition = $aTransitions[$sStimulus]; 2544 $sTargetState = $aTransition['target_state']; 2545 2546 // Retrieving attributes from state 2547 $aStates = MetaModel::EnumStates($sClass); 2548 $aTargetState = $aStates[$sTargetState]; 2549 $aTargetStateAttributes = $aTargetState['attribute_list']; 2550 // - Merging with results (only MUST_XXX and MANDATORY) 2551 foreach($aTargetStateAttributes as $sTargetStateAttCode => $iTargetStateAttFlags) 2552 { 2553 $iTmpAttFlags = OPT_ATT_NORMAL; 2554 if ($iTargetStateAttFlags & OPT_ATT_MUSTPROMPT) 2555 { 2556 $iTmpAttFlags = $iTmpAttFlags | OPT_ATT_MUSTPROMPT; 2557 } 2558 if ($iTargetStateAttFlags & OPT_ATT_MUSTCHANGE) 2559 { 2560 $iTmpAttFlags = $iTmpAttFlags | OPT_ATT_MUSTCHANGE; 2561 } 2562 if ($iTargetStateAttFlags & OPT_ATT_MANDATORY) 2563 { 2564 $iTmpAttFlags = $iTmpAttFlags | OPT_ATT_MANDATORY; 2565 } 2566 2567 $aAttributes[$sTargetStateAttCode] = $iTmpAttFlags; 2568 } 2569 2570 // Retrieving attributes from transition 2571 $aTransitionAttributes = $aTransition['attribute_list']; 2572 // - Merging with results 2573 foreach($aTransitionAttributes as $sAttCode => $iAttributeFlags) 2574 { 2575 if (array_key_exists($sAttCode, $aAttributes)) 2576 { 2577 $aAttributes[$sAttCode] = $aAttributes[$sAttCode] | $iAttributeFlags; 2578 } 2579 else 2580 { 2581 $aAttributes[$sAttCode] = $iAttributeFlags; 2582 } 2583 } 2584 2585 return $aAttributes; 2586 } 2587 2588 /** 2589 * @param string $sClass 2590 * @param string $sState 2591 * @param string $sAttCode 2592 * 2593 * @return int Combines the flags from the all states that compose the initial_state_path 2594 * @throws \CoreException 2595 * @throws \Exception 2596 */ 2597 public static function GetInitialStateAttributeFlags($sClass, $sState, $sAttCode) 2598 { 2599 $iFlags = self::GetAttributeFlags($sClass, $sState, $sAttCode); // Be default set the same flags as the 'target' state 2600 $sStateAttCode = self::GetStateAttributeCode($sClass); 2601 if (!empty($sStateAttCode)) 2602 { 2603 $aStates = MetaModel::EnumInitialStates($sClass); 2604 if (array_key_exists($sState, $aStates)) 2605 { 2606 $bReadOnly = (($iFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY); 2607 $bHidden = (($iFlags & OPT_ATT_HIDDEN) == OPT_ATT_HIDDEN); 2608 foreach($aStates[$sState] as $sPrevState) 2609 { 2610 $iPrevFlags = self::GetAttributeFlags($sClass, $sPrevState, $sAttCode); 2611 if (($iPrevFlags & OPT_ATT_HIDDEN) != OPT_ATT_HIDDEN) 2612 { 2613 $bReadOnly = $bReadOnly && (($iPrevFlags & OPT_ATT_READONLY) == OPT_ATT_READONLY); // if it is/was not readonly => then it's not 2614 } 2615 $bHidden = $bHidden && (($iPrevFlags & OPT_ATT_HIDDEN) == OPT_ATT_HIDDEN); // if it is/was not hidden => then it's not 2616 } 2617 if ($bReadOnly) 2618 { 2619 $iFlags = $iFlags | OPT_ATT_READONLY; 2620 } 2621 else 2622 { 2623 $iFlags = $iFlags & ~OPT_ATT_READONLY; 2624 } 2625 if ($bHidden) 2626 { 2627 $iFlags = $iFlags | OPT_ATT_HIDDEN; 2628 } 2629 else 2630 { 2631 $iFlags = $iFlags & ~OPT_ATT_HIDDEN; 2632 } 2633 } 2634 } 2635 return $iFlags; 2636 } 2637 2638 /** 2639 * @param string $sClass 2640 * @param string $sAttCode 2641 * @param array $aArgs 2642 * @param string $sContains 2643 * 2644 * @return mixed 2645 * @throws \Exception 2646 */ 2647 public static function GetAllowedValues_att($sClass, $sAttCode, $aArgs = array(), $sContains = '') 2648 { 2649 $oAttDef = self::GetAttributeDef($sClass, $sAttCode); 2650 return $oAttDef->GetAllowedValues($aArgs, $sContains); 2651 } 2652 2653 /** 2654 * @param string $sClass 2655 * @param string $sFltCode 2656 * @param array $aArgs 2657 * @param string $sContains 2658 * 2659 * @return mixed 2660 * @throws \CoreException 2661 */ 2662 public static function GetAllowedValues_flt($sClass, $sFltCode, $aArgs = array(), $sContains = '') 2663 { 2664 $oFltDef = self::GetClassFilterDef($sClass, $sFltCode); 2665 return $oFltDef->GetAllowedValues($aArgs, $sContains); 2666 } 2667 2668 /** 2669 * @param string $sClass 2670 * @param string $sAttCode 2671 * @param array $aArgs 2672 * @param string $sContains 2673 * @param int $iAdditionalValue 2674 * 2675 * @return mixed 2676 * @throws \Exception 2677 */ 2678 public static function GetAllowedValuesAsObjectSet($sClass, $sAttCode, $aArgs = array(), $sContains = '', $iAdditionalValue = null) 2679 { 2680 $oAttDef = self::GetAttributeDef($sClass, $sAttCode); 2681 return $oAttDef->GetAllowedValuesAsObjectSet($aArgs, $sContains, $iAdditionalValue); 2682 } 2683 2684 2685 2686 // 2687 // Businezz model declaration verbs (should be static) 2688 // 2689 /** 2690 * @param string $sListCode 2691 * @param array $aListInfo 2692 * 2693 * @throws \CoreException 2694 */ 2695 public static function RegisterZList($sListCode, $aListInfo) 2696 { 2697 // Check mandatory params 2698 $aMandatParams = array( 2699 "description" => "detailed (though one line) description of the list", 2700 "type" => "attributes | filters", 2701 ); 2702 foreach($aMandatParams as $sParamName => $sParamDesc) 2703 { 2704 if (!array_key_exists($sParamName, $aListInfo)) 2705 { 2706 throw new CoreException("Declaration of list $sListCode - missing parameter $sParamName"); 2707 } 2708 } 2709 2710 self::$m_aListInfos[$sListCode] = $aListInfo; 2711 } 2712 2713 /** 2714 * @param string $sRelCode 2715 */ 2716 public static function RegisterRelation($sRelCode) 2717 { 2718 // Each item used to be an array of properties... 2719 self::$m_aRelationInfos[$sRelCode] = $sRelCode; 2720 } 2721 2722 /** 2723 * Helper to correctly add a magic attribute (called from InitClasses) 2724 * 2725 * @param \AttributeDefinition $oAttribute 2726 * @param string $sTargetClass 2727 * @param string $sOriginClass 2728 */ 2729 private static function AddMagicAttribute(AttributeDefinition $oAttribute, $sTargetClass, $sOriginClass = null) 2730 { 2731 $sCode = $oAttribute->GetCode(); 2732 if (is_null($sOriginClass)) 2733 { 2734 $sOriginClass = $sTargetClass; 2735 } 2736 $oAttribute->SetHostClass($sTargetClass); 2737 self::$m_aAttribDefs[$sTargetClass][$sCode] = $oAttribute; 2738 self::$m_aAttribOrigins[$sTargetClass][$sCode] = $sOriginClass; 2739 2740 $oFlt = new FilterFromAttribute($oAttribute); 2741 self::$m_aFilterDefs[$sTargetClass][$sCode] = $oFlt; 2742 self::$m_aFilterOrigins[$sTargetClass][$sCode] = $sOriginClass; 2743 } 2744 2745 /** 2746 * Must be called once and only once... 2747 * 2748 * @param string $sTablePrefix 2749 * 2750 * @throws \CoreException 2751 * @throws \Exception 2752 */ 2753 public static function InitClasses($sTablePrefix) 2754 { 2755 if (count(self::GetClasses()) > 0) 2756 { 2757 throw new CoreException("InitClasses should not be called more than once -skipped"); 2758 } 2759 2760 self::$m_sTablePrefix = $sTablePrefix; 2761 2762 // Build the list of available extensions 2763 // 2764 $aInterfaces = array('iApplicationUIExtension', 'iApplicationObjectExtension', 'iQueryModifier', 'iOnClassInitialization', 'iPopupMenuExtension', 'iPageUIExtension', 'iPortalUIExtension', 'ModuleHandlerApiInterface', 'iNewsroomProvider'); 2765 foreach($aInterfaces as $sInterface) 2766 { 2767 self::$m_aExtensionClasses[$sInterface] = array(); 2768 } 2769 2770 foreach(get_declared_classes() as $sPHPClass) 2771 { 2772 $oRefClass = new ReflectionClass($sPHPClass); 2773 $oExtensionInstance = null; 2774 foreach($aInterfaces as $sInterface) 2775 { 2776 if ($oRefClass->implementsInterface($sInterface) && $oRefClass->isInstantiable()) 2777 { 2778 if (is_null($oExtensionInstance)) 2779 { 2780 $oExtensionInstance = new $sPHPClass; 2781 } 2782 self::$m_aExtensionClasses[$sInterface][$sPHPClass] = $oExtensionInstance; 2783 } 2784 } 2785 } 2786 2787 // Initialize the classes (declared attributes, etc.) 2788 // 2789 $aObsoletableRootClasses = array(); 2790 foreach(get_declared_classes() as $sPHPClass) 2791 { 2792 if (is_subclass_of($sPHPClass, 'DBObject')) 2793 { 2794 $sParent = self::GetParentPersistentClass($sPHPClass); 2795 if (array_key_exists($sParent, self::$m_aIgnoredAttributes)) 2796 { 2797 // Inherit info about attributes to ignore 2798 self::$m_aIgnoredAttributes[$sPHPClass] = self::$m_aIgnoredAttributes[$sParent]; 2799 } 2800 try 2801 { 2802 $oMethod = new ReflectionMethod($sPHPClass, 'Init'); 2803 if ($oMethod->getDeclaringClass()->name == $sPHPClass) 2804 { 2805 call_user_func(array($sPHPClass, 'Init')); 2806 2807 // Inherit archive flag 2808 $bParentArchivable = isset(self::$m_aClassParams[$sParent]['archive']) ? self::$m_aClassParams[$sParent]['archive'] : false; 2809 $bArchivable = isset(self::$m_aClassParams[$sPHPClass]['archive']) ? self::$m_aClassParams[$sPHPClass]['archive'] : null; 2810 if (!$bParentArchivable && $bArchivable && !self::IsRootClass($sPHPClass)) 2811 { 2812 throw new Exception("Archivability must be declared on top of the class hierarchy above $sPHPClass (consistency throughout the whole class tree is a must)"); 2813 } 2814 if ($bParentArchivable && ($bArchivable === false)) 2815 { 2816 throw new Exception("$sPHPClass must be archivable (consistency throughout the whole class tree is a must)"); 2817 } 2818 $bReallyArchivable = $bParentArchivable || $bArchivable; 2819 self::$m_aClassParams[$sPHPClass]['archive'] = $bReallyArchivable; 2820 $bArchiveRoot = $bReallyArchivable && !$bParentArchivable; 2821 self::$m_aClassParams[$sPHPClass]['archive_root'] = $bArchiveRoot; 2822 if ($bReallyArchivable) 2823 { 2824 self::$m_aClassParams[$sPHPClass]['archive_root_class'] = $bArchiveRoot ? $sPHPClass : self::$m_aClassParams[$sParent]['archive_root_class']; 2825 } 2826 2827 // Inherit obsolescence expression 2828 $sObsolescence = null; 2829 if (isset(self::$m_aClassParams[$sPHPClass]['obsolescence_expression'])) 2830 { 2831 // Defined or overloaded 2832 $sObsolescence = self::$m_aClassParams[$sPHPClass]['obsolescence_expression']; 2833 $aObsoletableRootClasses[self::$m_aRootClasses[$sPHPClass]] = true; 2834 } 2835 elseif (isset(self::$m_aClassParams[$sParent]['obsolescence_expression'])) 2836 { 2837 // Inherited 2838 $sObsolescence = self::$m_aClassParams[$sParent]['obsolescence_expression']; 2839 } 2840 self::$m_aClassParams[$sPHPClass]['obsolescence_expression'] = $sObsolescence; 2841 2842 foreach(MetaModel::EnumPlugins('iOnClassInitialization') as $sPluginClass => $oClassInit) 2843 { 2844 $oClassInit->OnAfterClassInitialization($sPHPClass); 2845 } 2846 } 2847 2848 $aCurrentClassUniquenessRules = MetaModel::GetUniquenessRules($sPHPClass, true); 2849 if (!empty($aCurrentClassUniquenessRules)) 2850 { 2851 $aClassFields = self::GetAttributesList($sPHPClass); 2852 foreach ($aCurrentClassUniquenessRules as $sUniquenessRuleId => $aUniquenessRuleProperties) 2853 { 2854 $bIsRuleOverride = self::HasSameUniquenessRuleInParent($sPHPClass, $sUniquenessRuleId); 2855 try 2856 { 2857 self::CheckUniquenessRuleValidity($aUniquenessRuleProperties, $bIsRuleOverride, $aClassFields); 2858 } 2859 catch (CoreUnexpectedValue $e) 2860 { 2861 throw new Exception("Invalid uniqueness rule declaration : class={$sPHPClass}, rule=$sUniquenessRuleId, reason={$e->getMessage()}"); 2862 } 2863 2864 if (!$bIsRuleOverride) 2865 { 2866 self::SetUniquenessRuleRootClass($sPHPClass, $sUniquenessRuleId); 2867 } 2868 } 2869 } 2870 2871 } 2872 catch (ReflectionException $e) 2873 { 2874 // This class is only implementing methods, ignore it from the MetaModel perspective 2875 } 2876 } 2877 } 2878 2879 // Add a 'class' attribute/filter to the root classes and their children 2880 // 2881 foreach(self::EnumRootClasses() as $sRootClass) 2882 { 2883 if (self::IsStandaloneClass($sRootClass)) 2884 { 2885 continue; 2886 } 2887 2888 $sDbFinalClassField = self::DBGetClassField($sRootClass); 2889 if (strlen($sDbFinalClassField) == 0) 2890 { 2891 $sDbFinalClassField = 'finalclass'; 2892 self::$m_aClassParams[$sRootClass]["db_finalclass_field"] = 'finalclass'; 2893 } 2894 $oClassAtt = new AttributeFinalClass('finalclass', array( 2895 "sql" => $sDbFinalClassField, 2896 "default_value" => $sRootClass, 2897 "is_null_allowed" => false, 2898 "depends_on" => array() 2899 )); 2900 self::AddMagicAttribute($oClassAtt, $sRootClass); 2901 2902 $bObsoletable = array_key_exists($sRootClass, $aObsoletableRootClasses); 2903 if ($bObsoletable && is_null(self::$m_aClassParams[$sRootClass]['obsolescence_expression'])) 2904 { 2905 self::$m_aClassParams[$sRootClass]['obsolescence_expression'] = '0'; 2906 } 2907 2908 2909 foreach(self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP) as $sChildClass) 2910 { 2911 if (array_key_exists('finalclass', self::$m_aAttribDefs[$sChildClass])) 2912 { 2913 throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as an attribute code"); 2914 } 2915 if (array_key_exists('finalclass', self::$m_aFilterDefs[$sChildClass])) 2916 { 2917 throw new CoreException("Class $sChildClass, 'finalclass' is a reserved keyword, it cannot be used as a filter code"); 2918 } 2919 $oCloned = clone $oClassAtt; 2920 $oCloned->SetFixedValue($sChildClass); 2921 self::AddMagicAttribute($oCloned, $sChildClass, $sRootClass); 2922 2923 if ($bObsoletable && is_null(self::$m_aClassParams[$sChildClass]['obsolescence_expression'])) 2924 { 2925 self::$m_aClassParams[$sChildClass]['obsolescence_expression'] = '0'; 2926 } 2927 } 2928 } 2929 2930 // Add magic attributes to the classes 2931 foreach(self::GetClasses() as $sClass) 2932 { 2933 $sRootClass = self::$m_aRootClasses[$sClass]; 2934 2935 // Create the friendly name attribute 2936 $sFriendlyNameAttCode = 'friendlyname'; 2937 $oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode); 2938 self::AddMagicAttribute($oFriendlyName, $sClass); 2939 2940 if (self::$m_aClassParams[$sClass]["archive_root"]) 2941 { 2942 // Create archive attributes on top the archivable hierarchy 2943 $oArchiveFlag = new AttributeArchiveFlag('archive_flag'); 2944 self::AddMagicAttribute($oArchiveFlag, $sClass); 2945 2946 $oArchiveDate = new AttributeArchiveDate('archive_date', array('magic' => true, "allowed_values" => null, "sql" => 'archive_date', "default_value" => '', "is_null_allowed" => true, "depends_on" => array())); 2947 self::AddMagicAttribute($oArchiveDate, $sClass); 2948 } 2949 elseif (self::$m_aClassParams[$sClass]["archive"]) 2950 { 2951 $sArchiveRoot = self::$m_aClassParams[$sClass]['archive_root_class']; 2952 // Inherit archive attributes 2953 $oArchiveFlag = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_flag']; 2954 $oArchiveFlag->SetHostClass($sClass); 2955 self::$m_aAttribDefs[$sClass]['archive_flag'] = $oArchiveFlag; 2956 self::$m_aAttribOrigins[$sClass]['archive_flag'] = $sArchiveRoot; 2957 $oArchiveDate = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_date']; 2958 $oArchiveDate->SetHostClass($sClass); 2959 self::$m_aAttribDefs[$sClass]['archive_date'] = $oArchiveDate; 2960 self::$m_aAttribOrigins[$sClass]['archive_date'] = $sArchiveRoot; 2961 } 2962 if (!is_null(self::$m_aClassParams[$sClass]['obsolescence_expression'])) 2963 { 2964 $oObsolescenceFlag = new AttributeObsolescenceFlag('obsolescence_flag'); 2965 self::AddMagicAttribute($oObsolescenceFlag, $sClass); 2966 2967 if (self::$m_aRootClasses[$sClass] == $sClass) 2968 { 2969 $oObsolescenceDate = new AttributeObsolescenceDate('obsolescence_date', array('magic' => true, "allowed_values" => null, "sql" => 'obsolescence_date', "default_value" => '', "is_null_allowed" => true, "depends_on" => array())); 2970 self::AddMagicAttribute($oObsolescenceDate, $sClass); 2971 } 2972 else 2973 { 2974 $oObsolescenceDate = clone self::$m_aAttribDefs[$sRootClass]['obsolescence_date']; 2975 $oObsolescenceDate->SetHostClass($sClass); 2976 self::$m_aAttribDefs[$sClass]['obsolescence_date'] = $oObsolescenceDate; 2977 self::$m_aAttribOrigins[$sClass]['obsolescence_date'] = $sRootClass; 2978 } 2979 } 2980 } 2981 2982 // Prepare external fields and filters 2983 // Add final class to external keys 2984 // Add magic attributes to external keys (finalclass, friendlyname, archive_flag, obsolescence_flag) 2985 foreach(self::GetClasses() as $sClass) 2986 { 2987 foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef) 2988 { 2989 // Compute the filter codes 2990 // 2991 foreach($oAttDef->GetFilterDefinitions() as $sFilterCode => $oFilterDef) 2992 { 2993 self::$m_aFilterDefs[$sClass][$sFilterCode] = $oFilterDef; 2994 2995 if ($oAttDef->IsExternalField()) 2996 { 2997 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 2998 $oKeyDef = self::GetAttributeDef($sClass, $sKeyAttCode); 2999 self::$m_aFilterOrigins[$sClass][$sFilterCode] = $oKeyDef->GetTargetClass(); 3000 } 3001 else 3002 { 3003 self::$m_aFilterOrigins[$sClass][$sFilterCode] = self::$m_aAttribOrigins[$sClass][$sAttCode]; 3004 } 3005 } 3006 3007 // Compute the fields that will be used to display a pointer to another object 3008 // 3009 if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) 3010 { 3011 // oAttDef is either 3012 // - an external KEY / FIELD (direct), 3013 // - an external field pointing to an external KEY / FIELD 3014 // - an external field pointing to an external field pointing to.... 3015 $sRemoteClass = $oAttDef->GetTargetClass(EXTKEY_ABSOLUTE); 3016 3017 if ($oAttDef->IsExternalField()) 3018 { 3019 // This is a key, but the value comes from elsewhere 3020 // Create an external field pointing to the remote friendly name attribute 3021 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 3022 $sRemoteAttCode = $oAttDef->GetExtAttCode()."_friendlyname"; 3023 $sFriendlyNameAttCode = $sAttCode.'_friendlyname'; 3024 $oFriendlyName = new AttributeExternalField($sFriendlyNameAttCode, array("allowed_values" => null, "extkey_attcode" => $sKeyAttCode, "target_attcode" => $sRemoteAttCode, "depends_on" => array())); 3025 self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]); 3026 } 3027 else 3028 { 3029 // Create the friendly name attribute 3030 $sFriendlyNameAttCode = $sAttCode.'_friendlyname'; 3031 $oFriendlyName = new AttributeExternalField($sFriendlyNameAttCode, array('allowed_values' => null, 'extkey_attcode' => $sAttCode, "target_attcode" => 'friendlyname', 'depends_on' => array())); 3032 self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]); 3033 3034 if (self::HasChildrenClasses($sRemoteClass)) 3035 { 3036 // First, create an external field attribute, that gets the final class 3037 $sClassRecallAttCode = $sAttCode.'_finalclass_recall'; 3038 $oClassRecall = new AttributeExternalField($sClassRecallAttCode, array( 3039 "allowed_values" => null, 3040 "extkey_attcode" => $sAttCode, 3041 "target_attcode" => "finalclass", 3042 "is_null_allowed" => true, 3043 "depends_on" => array() 3044 )); 3045 self::AddMagicAttribute($oClassRecall, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]); 3046 3047 // Add it to the ZLists where the external key is present 3048 //foreach(self::$m_aListData[$sClass] as $sListCode => $aAttributes) 3049 $sListCode = 'list'; 3050 if (isset(self::$m_aListData[$sClass][$sListCode])) 3051 { 3052 $aAttributes = self::$m_aListData[$sClass][$sListCode]; 3053 // temporary.... no loop 3054 { 3055 if (in_array($sAttCode, $aAttributes)) 3056 { 3057 $aNewList = array(); 3058 foreach($aAttributes as $iPos => $sAttToDisplay) 3059 { 3060 if (is_string($sAttToDisplay) && ($sAttToDisplay == $sAttCode)) 3061 { 3062 // Insert the final class right before 3063 $aNewList[] = $sClassRecallAttCode; 3064 } 3065 $aNewList[] = $sAttToDisplay; 3066 } 3067 self::$m_aListData[$sClass][$sListCode] = $aNewList; 3068 } 3069 } 3070 } 3071 } 3072 } 3073 3074 if (self::IsArchivable($sRemoteClass)) 3075 { 3076 $sCode = $sAttCode.'_archive_flag'; 3077 if ($oAttDef->IsExternalField()) 3078 { 3079 // This is a key, but the value comes from elsewhere 3080 // Create an external field pointing to the remote attribute 3081 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 3082 $sRemoteAttCode = $oAttDef->GetExtAttCode().'_archive_flag'; 3083 } 3084 else 3085 { 3086 $sKeyAttCode = $sAttCode; 3087 $sRemoteAttCode = 'archive_flag'; 3088 } 3089 $oMagic = new AttributeExternalField($sCode, array("allowed_values" => null, "extkey_attcode" => $sKeyAttCode, "target_attcode" => $sRemoteAttCode, "depends_on" => array())); 3090 self::AddMagicAttribute($oMagic, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]); 3091 3092 } 3093 if (self::IsObsoletable($sRemoteClass)) 3094 { 3095 $sCode = $sAttCode.'_obsolescence_flag'; 3096 if ($oAttDef->IsExternalField()) 3097 { 3098 // This is a key, but the value comes from elsewhere 3099 // Create an external field pointing to the remote attribute 3100 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 3101 $sRemoteAttCode = $oAttDef->GetExtAttCode().'_obsolescence_flag'; 3102 } 3103 else 3104 { 3105 $sKeyAttCode = $sAttCode; 3106 $sRemoteAttCode = 'obsolescence_flag'; 3107 } 3108 $oMagic = new AttributeExternalField($sCode, array("allowed_values" => null, "extkey_attcode" => $sKeyAttCode, "target_attcode" => $sRemoteAttCode, "depends_on" => array())); 3109 self::AddMagicAttribute($oMagic, $sClass, self::$m_aAttribOrigins[$sClass][$sKeyAttCode]); 3110 } 3111 } 3112 if ($oAttDef instanceof AttributeMetaEnum) 3113 { 3114 $aMappingData = $oAttDef->GetMapRule($sClass); 3115 if ($aMappingData != null) 3116 { 3117 $sEnumAttCode = $aMappingData['attcode']; 3118 self::$m_aEnumToMeta[$sClass][$sEnumAttCode][$sAttCode] = $oAttDef; 3119 } 3120 } 3121 } 3122 3123 // Add a 'id' filter 3124 // 3125 if (array_key_exists('id', self::$m_aAttribDefs[$sClass])) 3126 { 3127 throw new CoreException("Class $sClass, 'id' is a reserved keyword, it cannot be used as an attribute code"); 3128 } 3129 if (array_key_exists('id', self::$m_aFilterDefs[$sClass])) 3130 { 3131 throw new CoreException("Class $sClass, 'id' is a reserved keyword, it cannot be used as a filter code"); 3132 } 3133 $oFilter = new FilterPrivateKey('id', array('id_field' => self::DBGetKey($sClass))); 3134 self::$m_aFilterDefs[$sClass]['id'] = $oFilter; 3135 self::$m_aFilterOrigins[$sClass]['id'] = $sClass; 3136 } 3137 } 3138 3139 /** 3140 * @param string $sClassName 3141 * @param string $sUniquenessRuleId 3142 * 3143 * @return bool true if one of the parent class (recursive) has the same rule defined 3144 * @throws \CoreException 3145 */ 3146 private static function HasSameUniquenessRuleInParent($sClassName, $sUniquenessRuleId) 3147 { 3148 $sParentClass = self::GetParentClass($sClassName); 3149 if (empty($sParentClass)) 3150 { 3151 return false; 3152 } 3153 3154 $aParentClassUniquenessRules = self::GetUniquenessRules($sParentClass); 3155 if (array_key_exists($sUniquenessRuleId, $aParentClassUniquenessRules)) 3156 { 3157 return true; 3158 } 3159 3160 return self::HasSameUniquenessRuleInParent($sParentClass, $sUniquenessRuleId); 3161 } 3162 3163 /** 3164 * @param array $aUniquenessRuleProperties 3165 * @param bool $bRuleOverride if false then control an original declaration validity, 3166 * otherwise an override validity (can only have the 'disabled' key) 3167 * @param string[] $aExistingClassFields if non empty, will check that all fields declared in the rules exists in the class 3168 * 3169 * @throws \CoreUnexpectedValue if the rule is invalid 3170 * 3171 * @since 2.6 N°659 uniqueness constraint 3172 * @since 2.6.1 N°1968 (joli mois de mai...) disallow overrides of 'attributes' properties 3173 */ 3174 public static function CheckUniquenessRuleValidity($aUniquenessRuleProperties, $bRuleOverride = true, $aExistingClassFields = array()) 3175 { 3176 $MANDATORY_ATTRIBUTES = array('attributes'); 3177 $UNIQUENESS_MANDATORY_KEYS_NB = count($MANDATORY_ATTRIBUTES); 3178 3179 $bHasMissingMandatoryKey = true; 3180 $iMissingMandatoryKeysNb = $UNIQUENESS_MANDATORY_KEYS_NB; 3181 /** @var boolean $bHasNonDisabledKeys true if rule contains at least one key that is not 'disabled' */ 3182 $bHasNonDisabledKeys = false; 3183 $bDisabledKeyValue = null; 3184 3185 foreach ($aUniquenessRuleProperties as $sUniquenessRuleKey => $aUniquenessRuleProperty) 3186 { 3187 if ($sUniquenessRuleKey === 'disabled') 3188 { 3189 $bDisabledKeyValue = $aUniquenessRuleProperty; 3190 if (!is_null($aUniquenessRuleProperty)) 3191 { 3192 continue; 3193 } 3194 } 3195 if (is_null($aUniquenessRuleProperty)) 3196 { 3197 continue; 3198 } 3199 3200 $bHasNonDisabledKeys = true; 3201 3202 if (in_array($sUniquenessRuleKey, $MANDATORY_ATTRIBUTES, true)) { 3203 $iMissingMandatoryKeysNb--; 3204 } 3205 3206 if ($sUniquenessRuleKey === 'attributes') 3207 { 3208 if (!empty($aExistingClassFields)) 3209 { 3210 foreach ($aUniquenessRuleProperties[$sUniquenessRuleKey] as $sRuleAttribute) 3211 { 3212 if (!in_array($sRuleAttribute, $aExistingClassFields, true)) 3213 { 3214 throw new CoreUnexpectedValue("Uniqueness rule : non existing field '$sRuleAttribute'"); 3215 } 3216 } 3217 } 3218 } 3219 } 3220 3221 if ($iMissingMandatoryKeysNb === 0) 3222 { 3223 $bHasMissingMandatoryKey = false; 3224 } 3225 3226 if ($bRuleOverride && $bHasNonDisabledKeys) 3227 { 3228 throw new CoreUnexpectedValue('Uniqueness rule : only the \'disabled\' key can be overridden'); 3229 } 3230 if ($bRuleOverride && is_null($bDisabledKeyValue)) 3231 { 3232 throw new CoreUnexpectedValue('Uniqueness rule : when overriding a rule, value must be set for the \'disabled\' key'); 3233 } 3234 if (!$bRuleOverride && $bHasMissingMandatoryKey) 3235 { 3236 throw new CoreUnexpectedValue('Uniqueness rule : missing mandatory property'); 3237 } 3238 } 3239 3240 /** 3241 * To be overriden, must be called for any object class (optimization) 3242 */ 3243 public static function Init() 3244 { 3245 // In fact it is an ABSTRACT function, but this is not compatible with the fact that it is STATIC (error in E_STRICT interpretation) 3246 } 3247 3248 /** 3249 * To be overloaded by biz model declarations 3250 * 3251 * @param string $sRelCode 3252 * 3253 * @return array 3254 */ 3255 public static function GetRelationQueries($sRelCode) 3256 { 3257 // In fact it is an ABSTRACT function, but this is not compatible with the fact that it is STATIC (error in E_STRICT interpretation) 3258 return array(); 3259 } 3260 3261 /** 3262 * @param array $aParams 3263 * 3264 * @throws \CoreException 3265 */ 3266 public static function Init_Params($aParams) 3267 { 3268 // Check mandatory params 3269 $aMandatParams = array( 3270 "category" => "group classes by modules defining their visibility in the UI", 3271 "key_type" => "autoincrement | string", 3272 "name_attcode" => "define wich attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'", 3273 "state_attcode" => "define wich attribute is representing the state (object lifecycle)", 3274 "reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes", 3275 "db_table" => "database table", 3276 "db_key_field" => "database field which is the key", 3277 "db_finalclass_field" => "database field wich is the reference to the actual class of the object, considering that this will be a compound class", 3278 ); 3279 3280 $sClass = self::GetCallersPHPClass("Init", self::$m_bTraceSourceFiles); 3281 3282 foreach($aMandatParams as $sParamName => $sParamDesc) 3283 { 3284 if (!array_key_exists($sParamName, $aParams)) 3285 { 3286 throw new CoreException("Declaration of class $sClass - missing parameter $sParamName"); 3287 } 3288 } 3289 3290 $aCategories = explode(',', $aParams['category']); 3291 foreach($aCategories as $sCategory) 3292 { 3293 self::$m_Category2Class[$sCategory][] = $sClass; 3294 } 3295 self::$m_Category2Class[''][] = $sClass; // all categories, include this one 3296 3297 3298 self::$m_aRootClasses[$sClass] = $sClass; // first, let consider that I am the root... updated on inheritance 3299 self::$m_aParentClasses[$sClass] = array(); 3300 self::$m_aChildClasses[$sClass] = array(); 3301 3302 self::$m_aClassParams[$sClass] = $aParams; 3303 3304 self::$m_aAttribDefs[$sClass] = array(); 3305 self::$m_aAttribOrigins[$sClass] = array(); 3306 self::$m_aFilterDefs[$sClass] = array(); 3307 self::$m_aFilterOrigins[$sClass] = array(); 3308 } 3309 3310 /** 3311 * @param array $aSource1 3312 * @param array $aSource2 3313 * 3314 * @return array 3315 */ 3316 protected static function object_array_mergeclone($aSource1, $aSource2) 3317 { 3318 $aRes = array(); 3319 foreach($aSource1 as $key => $object) 3320 { 3321 $aRes[$key] = clone $object; 3322 } 3323 foreach($aSource2 as $key => $object) 3324 { 3325 $aRes[$key] = clone $object; 3326 } 3327 3328 return $aRes; 3329 } 3330 3331 /** 3332 * @param string $sSourceClass 3333 */ 3334 public static function Init_InheritAttributes($sSourceClass = null) 3335 { 3336 $sTargetClass = self::GetCallersPHPClass("Init"); 3337 if (empty($sSourceClass)) 3338 { 3339 // Default: inherit from parent class 3340 $sSourceClass = self::GetParentPersistentClass($sTargetClass); 3341 if (empty($sSourceClass)) 3342 { 3343 return; 3344 } // no attributes for the mother of all classes 3345 } 3346 if (isset(self::$m_aAttribDefs[$sSourceClass])) 3347 { 3348 if (!isset(self::$m_aAttribDefs[$sTargetClass])) 3349 { 3350 self::$m_aAttribDefs[$sTargetClass] = array(); 3351 self::$m_aAttribOrigins[$sTargetClass] = array(); 3352 } 3353 self::$m_aAttribDefs[$sTargetClass] = self::object_array_mergeclone(self::$m_aAttribDefs[$sTargetClass], self::$m_aAttribDefs[$sSourceClass]); 3354 foreach(self::$m_aAttribDefs[$sTargetClass] as $sAttCode => $oAttDef) 3355 { 3356 $oAttDef->SetHostClass($sTargetClass); 3357 } 3358 self::$m_aAttribOrigins[$sTargetClass] = array_merge(self::$m_aAttribOrigins[$sTargetClass], self::$m_aAttribOrigins[$sSourceClass]); 3359 } 3360 // Build root class information 3361 if (array_key_exists($sSourceClass, self::$m_aRootClasses)) 3362 { 3363 // Inherit... 3364 self::$m_aRootClasses[$sTargetClass] = self::$m_aRootClasses[$sSourceClass]; 3365 } 3366 else 3367 { 3368 // This class will be the root class 3369 self::$m_aRootClasses[$sSourceClass] = $sSourceClass; 3370 self::$m_aRootClasses[$sTargetClass] = $sSourceClass; 3371 } 3372 self::$m_aParentClasses[$sTargetClass] += self::$m_aParentClasses[$sSourceClass]; 3373 self::$m_aParentClasses[$sTargetClass][] = $sSourceClass; 3374 // I am the child of each and every parent... 3375 foreach(self::$m_aParentClasses[$sTargetClass] as $sAncestorClass) 3376 { 3377 self::$m_aChildClasses[$sAncestorClass][] = $sTargetClass; 3378 } 3379 } 3380 3381 /** 3382 * @param string $sClass 3383 * 3384 * @return bool 3385 */ 3386 protected static function Init_IsKnownClass($sClass) 3387 { 3388 // Differs from self::IsValidClass() 3389 // because it is being called before all the classes have been initialized 3390 if (!class_exists($sClass)) 3391 { 3392 return false; 3393 } 3394 if (!is_subclass_of($sClass, 'DBObject')) 3395 { 3396 return false; 3397 } 3398 3399 return true; 3400 } 3401 3402 /** 3403 * @param \AttributeDefinition $oAtt 3404 * @param string $sTargetClass 3405 * 3406 * @throws \Exception 3407 */ 3408 public static function Init_AddAttribute(AttributeDefinition $oAtt, $sTargetClass = null) 3409 { 3410 if (!$sTargetClass) 3411 { 3412 $sTargetClass = self::GetCallersPHPClass("Init"); 3413 } 3414 3415 $sAttCode = $oAtt->GetCode(); 3416 if ($sAttCode == 'finalclass') 3417 { 3418 throw new Exception("Declaration of $sTargetClass: using the reserved keyword '$sAttCode' in attribute declaration"); 3419 } 3420 if ($sAttCode == 'friendlyname') 3421 { 3422 throw new Exception("Declaration of $sTargetClass: using the reserved keyword '$sAttCode' in attribute declaration"); 3423 } 3424 if (array_key_exists($sAttCode, self::$m_aAttribDefs[$sTargetClass])) 3425 { 3426 throw new Exception("Declaration of $sTargetClass: attempting to redeclare the inherited attribute '$sAttCode', originaly declared in ".self::$m_aAttribOrigins[$sTargetClass][$sAttCode]); 3427 } 3428 3429 // Set the "host class" as soon as possible, since HierarchicalKeys use it for their 'target class' as well 3430 // and this needs to be know early (for Init_IsKnowClass 19 lines below) 3431 $oAtt->SetHostClass($sTargetClass); 3432 3433 // Some attributes could refer to a class 3434 // declared in a module which is currently not installed/active 3435 // We simply discard those attributes 3436 // 3437 if ($oAtt->IsLinkSet()) 3438 { 3439 $sRemoteClass = $oAtt->GetLinkedClass(); 3440 if (!self::Init_IsKnownClass($sRemoteClass)) 3441 { 3442 self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = $sRemoteClass; 3443 return; 3444 } 3445 } 3446 elseif ($oAtt->IsExternalKey()) 3447 { 3448 $sRemoteClass = $oAtt->GetTargetClass(); 3449 if (!self::Init_IsKnownClass($sRemoteClass)) 3450 { 3451 self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = $sRemoteClass; 3452 return; 3453 } 3454 } 3455 elseif ($oAtt->IsExternalField()) 3456 { 3457 $sExtKeyAttCode = $oAtt->GetKeyAttCode(); 3458 if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$sExtKeyAttCode])) 3459 { 3460 // The corresponding external key has already been ignored 3461 self::$m_aIgnoredAttributes[$sTargetClass][$oAtt->GetCode()] = self::$m_aIgnoredAttributes[$sTargetClass][$sExtKeyAttCode]; 3462 return; 3463 } 3464 //TODO Check if the target attribute is still there 3465 // this is not simple to implement because is involves 3466 // several passes (the load order has a significant influence on that) 3467 } 3468 3469 self::$m_aAttribDefs[$sTargetClass][$oAtt->GetCode()] = $oAtt; 3470 self::$m_aAttribOrigins[$sTargetClass][$oAtt->GetCode()] = $sTargetClass; 3471 // Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used 3472 } 3473 3474 /** 3475 * @param string $sListCode 3476 * @param array $aItems 3477 * @param string $sTargetClass 3478 */ 3479 public static function Init_SetZListItems($sListCode, $aItems, $sTargetClass = null) 3480 { 3481 MyHelpers::CheckKeyInArray('list code', $sListCode, self::$m_aListInfos); 3482 3483 if (!$sTargetClass) 3484 { 3485 $sTargetClass = self::GetCallersPHPClass("Init"); 3486 } 3487 3488 // Discard attributes that do not make sense 3489 // (missing classes in the current module combination, resulting in irrelevant ext key or link set) 3490 // 3491 self::Init_CheckZListItems($aItems, $sTargetClass); 3492 self::$m_aListData[$sTargetClass][$sListCode] = $aItems; 3493 } 3494 3495 /** 3496 * @param array $aItems 3497 * @param string $sTargetClass 3498 */ 3499 protected static function Init_CheckZListItems(&$aItems, $sTargetClass) 3500 { 3501 foreach($aItems as $iFoo => $attCode) 3502 { 3503 if (is_array($attCode)) 3504 { 3505 // Note: to make sure that the values will be updated recursively, 3506 // do not pass $attCode, but $aItems[$iFoo] instead 3507 self::Init_CheckZListItems($aItems[$iFoo], $sTargetClass); 3508 if (count($aItems[$iFoo]) == 0) 3509 { 3510 unset($aItems[$iFoo]); 3511 } 3512 } 3513 else 3514 { 3515 if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$attCode])) 3516 { 3517 unset($aItems[$iFoo]); 3518 } 3519 } 3520 } 3521 } 3522 3523 /** 3524 * @param array $aList 3525 * 3526 * @return array 3527 */ 3528 public static function FlattenZList($aList) 3529 { 3530 $aResult = array(); 3531 foreach($aList as $value) 3532 { 3533 if (!is_array($value)) 3534 { 3535 $aResult[] = $value; 3536 } 3537 else 3538 { 3539 $aResult = array_merge($aResult, self::FlattenZList($value)); 3540 } 3541 } 3542 3543 return $aResult; 3544 } 3545 3546 /** 3547 * @param string $sStateCode 3548 * @param array $aStateDef 3549 */ 3550 public static function Init_DefineState($sStateCode, $aStateDef) 3551 { 3552 $sTargetClass = self::GetCallersPHPClass("Init"); 3553 if (is_null($aStateDef['attribute_list'])) 3554 { 3555 $aStateDef['attribute_list'] = array(); 3556 } 3557 3558 $sParentState = $aStateDef['attribute_inherit']; 3559 if (!empty($sParentState)) 3560 { 3561 // Inherit from the given state (must be defined !) 3562 // 3563 $aToInherit = self::$m_aStates[$sTargetClass][$sParentState]; 3564 3565 // Reset the constraint when it was mandatory to set the value at the previous state 3566 // 3567 foreach($aToInherit['attribute_list'] as $sState => $iFlags) 3568 { 3569 $iFlags = $iFlags & ~OPT_ATT_MUSTPROMPT; 3570 $iFlags = $iFlags & ~OPT_ATT_MUSTCHANGE; 3571 $aToInherit['attribute_list'][$sState] = $iFlags; 3572 } 3573 3574 // The inherited configuration could be overriden 3575 $aStateDef['attribute_list'] = array_merge($aToInherit['attribute_list'], $aStateDef['attribute_list']); 3576 } 3577 3578 foreach($aStateDef['attribute_list'] as $sAttCode => $iFlags) 3579 { 3580 if (isset(self::$m_aIgnoredAttributes[$sTargetClass][$sAttCode])) 3581 { 3582 unset($aStateDef['attribute_list'][$sAttCode]); 3583 } 3584 } 3585 3586 self::$m_aStates[$sTargetClass][$sStateCode] = $aStateDef; 3587 3588 // by default, create an empty set of transitions associated to that state 3589 self::$m_aTransitions[$sTargetClass][$sStateCode] = array(); 3590 } 3591 3592 /** 3593 * @param array $aHighlightScale 3594 */ 3595 public static function Init_DefineHighlightScale($aHighlightScale) 3596 { 3597 $sTargetClass = self::GetCallersPHPClass("Init"); 3598 self::$m_aHighlightScales[$sTargetClass] = $aHighlightScale; 3599 } 3600 3601 /** 3602 * @param string $sTargetClass 3603 * 3604 * @return array 3605 */ 3606 public static function GetHighlightScale($sTargetClass) 3607 { 3608 $aScale = array(); 3609 $aParentScale = array(); 3610 $sParentClass = self::GetParentPersistentClass($sTargetClass); 3611 if (!empty($sParentClass)) 3612 { 3613 // inherit the scale from the parent class 3614 $aParentScale = self::GetHighlightScale($sParentClass); 3615 } 3616 if (array_key_exists($sTargetClass, self::$m_aHighlightScales)) 3617 { 3618 $aScale = self::$m_aHighlightScales[$sTargetClass]; 3619 } 3620 return array_merge($aParentScale, $aScale); // Merge both arrays, the values from the last one have precedence 3621 } 3622 3623 /** 3624 * @param string $sTargetClass 3625 * @param string $sStateCode 3626 * 3627 * @return string 3628 */ 3629 public static function GetHighlightCode($sTargetClass, $sStateCode) 3630 { 3631 $sCode = ''; 3632 if (array_key_exists($sTargetClass, self::$m_aStates) 3633 && array_key_exists($sStateCode, self::$m_aStates[$sTargetClass]) 3634 && array_key_exists('highlight', self::$m_aStates[$sTargetClass][$sStateCode])) 3635 { 3636 $sCode = self::$m_aStates[$sTargetClass][$sStateCode]['highlight']['code']; 3637 } 3638 else 3639 { 3640 // Check the parent's definition 3641 $sParentClass = self::GetParentPersistentClass($sTargetClass); 3642 if (!empty($sParentClass)) 3643 { 3644 $sCode = self::GetHighlightCode($sParentClass, $sStateCode); 3645 } 3646 } 3647 3648 return $sCode; 3649 } 3650 3651 /** 3652 * @param string $sStateCode 3653 * @param string $sAttCode 3654 * @param int $iFlags 3655 */ 3656 public static function Init_OverloadStateAttribute($sStateCode, $sAttCode, $iFlags) 3657 { 3658 // Warning: this is not sufficient: the flags have to be copied to the states that are inheriting from this state 3659 $sTargetClass = self::GetCallersPHPClass("Init"); 3660 self::$m_aStates[$sTargetClass][$sStateCode]['attribute_list'][$sAttCode] = $iFlags; 3661 } 3662 3663 /** 3664 * @param ObjectStimulus $oStimulus 3665 */ 3666 public static function Init_DefineStimulus($oStimulus) 3667 { 3668 $sTargetClass = self::GetCallersPHPClass("Init"); 3669 self::$m_aStimuli[$sTargetClass][$oStimulus->GetCode()] = $oStimulus; 3670 3671 // I wanted to simplify the syntax of the declaration of objects in the biz model 3672 // Therefore, the reference to the host class is set there 3673 $oStimulus->SetHostClass($sTargetClass); 3674 } 3675 3676 /** 3677 * @param string $sStateCode 3678 * @param string $sStimulusCode 3679 * @param array $aTransitionDef 3680 */ 3681 public static function Init_DefineTransition($sStateCode, $sStimulusCode, $aTransitionDef) 3682 { 3683 $sTargetClass = self::GetCallersPHPClass("Init"); 3684 if (is_null($aTransitionDef['actions'])) 3685 { 3686 $aTransitionDef['actions'] = array(); 3687 } 3688 self::$m_aTransitions[$sTargetClass][$sStateCode][$sStimulusCode] = $aTransitionDef; 3689 } 3690 3691 /** 3692 * @param string $sSourceClass 3693 */ 3694 public static function Init_InheritLifecycle($sSourceClass = '') 3695 { 3696 $sTargetClass = self::GetCallersPHPClass("Init"); 3697 if (empty($sSourceClass)) 3698 { 3699 // Default: inherit from parent class 3700 $sSourceClass = self::GetParentPersistentClass($sTargetClass); 3701 if (empty($sSourceClass)) 3702 { 3703 return; 3704 } // no attributes for the mother of all classes 3705 } 3706 3707 self::$m_aClassParams[$sTargetClass]["state_attcode"] = self::$m_aClassParams[$sSourceClass]["state_attcode"]; 3708 self::$m_aStates[$sTargetClass] = self::$m_aStates[$sSourceClass]; 3709 // #@# Note: the aim is to clone the data, could be an issue if the simuli objects are changed 3710 self::$m_aStimuli[$sTargetClass] = self::$m_aStimuli[$sSourceClass]; 3711 self::$m_aTransitions[$sTargetClass] = self::$m_aTransitions[$sSourceClass]; 3712 } 3713 3714 // 3715 // Static API 3716 // 3717 3718 /** 3719 * @param string $sClass 3720 * 3721 * @return string 3722 * @throws \CoreException 3723 */ 3724 public static function GetRootClass($sClass = null) 3725 { 3726 self::_check_subclass($sClass); 3727 return self::$m_aRootClasses[$sClass]; 3728 } 3729 3730 /** 3731 * @param string $sClass 3732 * 3733 * @return bool 3734 * @throws \CoreException 3735 */ 3736 public static function IsRootClass($sClass) 3737 { 3738 self::_check_subclass($sClass); 3739 return (self::GetRootClass($sClass) == $sClass); 3740 } 3741 3742 /** 3743 * @param string $sClass 3744 * 3745 * @return string 3746 */ 3747 public static function GetParentClass($sClass) 3748 { 3749 if (count(self::$m_aParentClasses[$sClass]) == 0) 3750 { 3751 return null; 3752 } 3753 else 3754 { 3755 return end(self::$m_aParentClasses[$sClass]); 3756 } 3757 } 3758 3759 /** 3760 * @param string[] $aClasses 3761 * 3762 * @return string 3763 * @throws \CoreException 3764 */ 3765 public static function GetLowestCommonAncestor($aClasses) 3766 { 3767 $sAncestor = null; 3768 foreach($aClasses as $sClass) 3769 { 3770 if (is_null($sAncestor)) 3771 { 3772 // first loop 3773 $sAncestor = $sClass; 3774 } 3775 elseif ($sClass == $sAncestor) 3776 { 3777 // remains the same 3778 } 3779 elseif (self::GetRootClass($sClass) != self::GetRootClass($sAncestor)) 3780 { 3781 $sAncestor = null; 3782 break; 3783 } 3784 else 3785 { 3786 $sAncestor = self::LowestCommonAncestor($sAncestor, $sClass); 3787 } 3788 } 3789 return $sAncestor; 3790 } 3791 3792 /** 3793 * Note: assumes that class A and B have a common ancestor 3794 * 3795 * @param string $sClassA 3796 * @param string $sClassB 3797 * 3798 * @return string 3799 */ 3800 protected static function LowestCommonAncestor($sClassA, $sClassB) 3801 { 3802 if ($sClassA == $sClassB) 3803 { 3804 $sRet = $sClassA; 3805 } 3806 elseif (is_subclass_of($sClassA, $sClassB)) 3807 { 3808 $sRet = $sClassB; 3809 } 3810 elseif (is_subclass_of($sClassB, $sClassA)) 3811 { 3812 $sRet = $sClassA; 3813 } 3814 else 3815 { 3816 // Recurse 3817 $sRet = self::LowestCommonAncestor($sClassA, self::GetParentClass($sClassB)); 3818 } 3819 return $sRet; 3820 } 3821 3822 /** 3823 * Tells if a class contains a hierarchical key, and if so what is its AttCode 3824 * 3825 * @param string $sClass 3826 * 3827 * @return mixed String = sAttCode or false if the class is not part of a hierarchy 3828 * @throws \CoreException 3829 */ 3830 public static function IsHierarchicalClass($sClass) 3831 { 3832 $sHierarchicalKeyCode = false; 3833 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAtt) 3834 { 3835 if ($oAtt->IsHierarchicalKey()) 3836 { 3837 $sHierarchicalKeyCode = $sAttCode; // Found the hierarchical key, no need to continue 3838 break; 3839 } 3840 } 3841 return $sHierarchicalKeyCode; 3842 } 3843 3844 /** 3845 * @return array 3846 */ 3847 public static function EnumRootClasses() 3848 { 3849 return array_unique(self::$m_aRootClasses); 3850 } 3851 3852 /** 3853 * @param string $sClass 3854 * @param int $iOption 3855 * @param bool $bRootFirst 3856 * 3857 * @return array 3858 * @throws \CoreException 3859 */ 3860 public static function EnumParentClasses($sClass, $iOption = ENUM_PARENT_CLASSES_EXCLUDELEAF, $bRootFirst = true) 3861 { 3862 self::_check_subclass($sClass); 3863 if ($bRootFirst) 3864 { 3865 $aRes = self::$m_aParentClasses[$sClass]; 3866 } 3867 else 3868 { 3869 $aRes = array_reverse(self::$m_aParentClasses[$sClass], true); 3870 } 3871 if ($iOption != ENUM_PARENT_CLASSES_EXCLUDELEAF) 3872 { 3873 if ($bRootFirst) 3874 { 3875 // Leaf class at the end 3876 $aRes[] = $sClass; 3877 } 3878 else 3879 { 3880 // Leaf class on top 3881 array_unshift($aRes, $sClass); 3882 } 3883 } 3884 3885 return $aRes; 3886 } 3887 3888 /** 3889 * @param string $sClass 3890 * @param int $iOption one of ENUM_CHILD_CLASSES_EXCLUDETOP, ENUM_CHILD_CLASSES_ALL 3891 * 3892 * @return array 3893 * @throws \CoreException 3894 */ 3895 public static function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP) 3896 { 3897 self::_check_subclass($sClass); 3898 3899 $aRes = self::$m_aChildClasses[$sClass]; 3900 if ($iOption != ENUM_CHILD_CLASSES_EXCLUDETOP) 3901 { 3902 // Add it to the list 3903 $aRes[] = $sClass; 3904 } 3905 3906 return $aRes; 3907 } 3908 3909 /** 3910 * @return array 3911 * @throws \CoreException 3912 */ 3913 public static function EnumArchivableClasses() 3914 { 3915 $aRes = array(); 3916 foreach(self::GetClasses() as $sClass) 3917 { 3918 if (self::IsArchivable($sClass)) 3919 { 3920 $aRes[] = $sClass; 3921 } 3922 } 3923 3924 return $aRes; 3925 } 3926 3927 /** 3928 * @param bool $bRootClassesOnly 3929 * 3930 * @return array 3931 * @throws \CoreException 3932 */ 3933 public static function EnumObsoletableClasses($bRootClassesOnly = true) 3934 { 3935 $aRes = array(); 3936 foreach(self::GetClasses() as $sClass) 3937 { 3938 if (self::IsObsoletable($sClass)) 3939 { 3940 if ($bRootClassesOnly && !static::IsRootClass($sClass)) 3941 { 3942 continue; 3943 } 3944 $aRes[] = $sClass; 3945 } 3946 } 3947 return $aRes; 3948 } 3949 3950 /** 3951 * @param string $sClass 3952 * 3953 * @return bool 3954 */ 3955 public static function HasChildrenClasses($sClass) 3956 { 3957 return (count(self::$m_aChildClasses[$sClass]) > 0); 3958 } 3959 3960 /** 3961 * @return array 3962 */ 3963 public static function EnumCategories() 3964 { 3965 return array_keys(self::$m_Category2Class); 3966 } 3967 3968 // Note: use EnumChildClasses to take the compound objects into account 3969 3970 /** 3971 * @param string $sClass 3972 * 3973 * @return array 3974 * @throws \CoreException 3975 */ 3976 public static function GetSubclasses($sClass) 3977 { 3978 self::_check_subclass($sClass); 3979 $aSubClasses = array(); 3980 foreach(self::$m_aClassParams as $sSubClass => $foo) 3981 { 3982 if (is_subclass_of($sSubClass, $sClass)) 3983 { 3984 $aSubClasses[] = $sSubClass; 3985 } 3986 } 3987 3988 return $aSubClasses; 3989 } 3990 3991 /** 3992 * @param string $sCategories 3993 * @param bool $bStrict 3994 * 3995 * @return array 3996 * @throws \CoreException 3997 */ 3998 public static function GetClasses($sCategories = '', $bStrict = false) 3999 { 4000 $aCategories = explode(',', $sCategories); 4001 $aClasses = array(); 4002 foreach($aCategories as $sCategory) 4003 { 4004 $sCategory = trim($sCategory); 4005 if (strlen($sCategory) == 0) 4006 { 4007 return array_keys(self::$m_aClassParams); 4008 } 4009 4010 if (array_key_exists($sCategory, self::$m_Category2Class)) 4011 { 4012 $aClasses = array_merge($aClasses, self::$m_Category2Class[$sCategory]); 4013 } 4014 elseif ($bStrict) 4015 { 4016 throw new CoreException("unkown class category '$sCategory', expecting a value in {".implode(', ', array_keys(self::$m_Category2Class))."}"); 4017 } 4018 } 4019 4020 return array_unique($aClasses); 4021 } 4022 4023 /** 4024 * @param string $sClass 4025 * 4026 * @return bool 4027 * @throws \CoreException 4028 */ 4029 public static function HasTable($sClass) 4030 { 4031 if (strlen(self::DBGetTable($sClass)) == 0) 4032 { 4033 return false; 4034 } 4035 return true; 4036 } 4037 4038 /** 4039 * @param string $sClass 4040 * 4041 * @return bool 4042 */ 4043 public static function IsAbstract($sClass) 4044 { 4045 $oReflection = new ReflectionClass($sClass); 4046 return $oReflection->isAbstract(); 4047 } 4048 4049 /** 4050 * Normalizes query arguments and adds magic parameters: 4051 * - current_contact_id 4052 * - current_contact (DBObject) 4053 * - current_user (DBObject) 4054 * 4055 * @param array $aArgs Context arguments (some can be persistent objects) 4056 * @param array $aMoreArgs Other query parameters 4057 * @return array 4058 */ 4059 public static function PrepareQueryArguments($aArgs, $aMoreArgs = array()) 4060 { 4061 $aScalarArgs = array(); 4062 foreach(array_merge($aArgs, $aMoreArgs) as $sArgName => $value) 4063 { 4064 if (self::IsValidObject($value)) 4065 { 4066 if (strpos($sArgName, '->object()') === false) 4067 { 4068 // Normalize object arguments 4069 $aScalarArgs[$sArgName.'->object()'] = $value; 4070 } 4071 else 4072 { 4073 // Leave as is 4074 $aScalarArgs[$sArgName] = $value; 4075 } 4076 } 4077 else 4078 { 4079 if (is_scalar($value)) 4080 { 4081 $aScalarArgs[$sArgName] = (string)$value; 4082 } 4083 elseif (is_null($value)) 4084 { 4085 $aScalarArgs[$sArgName] = null; 4086 } 4087 elseif (is_array($value)) 4088 { 4089 $aScalarArgs[$sArgName] = $value; 4090 } 4091 } 4092 } 4093 4094 return static::AddMagicPlaceholders($aScalarArgs); 4095 } 4096 4097 /** 4098 * @param array $aPlaceholders The array into which standard placeholders should be added 4099 * 4100 * @return array of placeholder (or name->object()) => value (or object) 4101 */ 4102 public static function AddMagicPlaceholders($aPlaceholders) 4103 { 4104 // Add standard magic arguments 4105 // 4106 $aPlaceholders['current_contact_id'] = UserRights::GetContactId(); // legacy 4107 4108 $oUser = UserRights::GetUserObject(); 4109 if (!is_null($oUser)) 4110 { 4111 $aPlaceholders['current_user->object()'] = $oUser; 4112 4113 $oContact = UserRights::GetContactObject(); 4114 if (!is_null($oContact)) 4115 { 4116 $aPlaceholders['current_contact->object()'] = $oContact; 4117 } 4118 } 4119 4120 return $aPlaceholders; 4121 } 4122 4123 /** 4124 * @param \DBSearch $oFilter 4125 * 4126 * @return array 4127 */ 4128 public static function MakeModifierProperties($oFilter) 4129 { 4130 // Compute query modifiers properties (can be set in the search itself, by the context, etc.) 4131 // 4132 $aModifierProperties = array(); 4133 foreach(MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier) 4134 { 4135 // Lowest precedence: the application context 4136 $aPluginProps = ApplicationContext::GetPluginProperties($sPluginClass); 4137 // Highest precedence: programmatically specified (or OQL) 4138 foreach($oFilter->GetModifierProperties($sPluginClass) as $sProp => $value) 4139 { 4140 $aPluginProps[$sProp] = $value; 4141 } 4142 if (count($aPluginProps) > 0) 4143 { 4144 $aModifierProperties[$sPluginClass] = $aPluginProps; 4145 } 4146 } 4147 return $aModifierProperties; 4148 } 4149 4150 4151 /** 4152 * Special processing for the hierarchical keys stored as nested sets 4153 * 4154 * @param int $iId integer The identifier of the parent 4155 * @param \AttributeDefinition $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key 4156 * @param string The name of the database table containing the hierarchical key 4157 * 4158 * @throws \MySQLException 4159 */ 4160 public static function HKInsertChildUnder($iId, $oAttDef, $sTable) 4161 { 4162 // Get the parent id.right value 4163 if ($iId == 0) 4164 { 4165 // No parent, insert completely at the right of the tree 4166 $sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`"; 4167 $aRes = CMDBSource::QueryToArray($sSQL); 4168 if (count($aRes) == 0) 4169 { 4170 $iMyRight = 1; 4171 } 4172 else 4173 { 4174 $iMyRight = $aRes[0]['max'] + 1; 4175 } 4176 } 4177 else 4178 { 4179 $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".$iId; 4180 $iMyRight = CMDBSource::QueryToScalar($sSQL); 4181 $sSQLUpdateRight = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` + 2 WHERE `".$oAttDef->GetSQLRight()."` >= $iMyRight"; 4182 CMDBSource::Query($sSQLUpdateRight); 4183 $sSQLUpdateLeft = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` + 2 WHERE `".$oAttDef->GetSQLLeft()."` > $iMyRight"; 4184 CMDBSource::Query($sSQLUpdateLeft); 4185 } 4186 return array($oAttDef->GetSQLRight() => $iMyRight + 1, $oAttDef->GetSQLLeft() => $iMyRight); 4187 } 4188 4189 /** 4190 * Special processing for the hierarchical keys stored as nested sets: temporary remove the branch 4191 * 4192 * @param int $iMyLeft 4193 * @param int $iMyRight 4194 * @param \AttributeDefinition $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key 4195 * @param string The name of the database table containing the hierarchical key 4196 * 4197 * @throws \MySQLException 4198 * @throws \MySQLHasGoneAwayException 4199 */ 4200 public static function HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable) 4201 { 4202 $iDelta = $iMyRight - $iMyLeft + 1; 4203 $sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = $iMyLeft - `".$oAttDef->GetSQLRight()."`, `".$oAttDef->GetSQLLeft()."` = $iMyLeft - `".$oAttDef->GetSQLLeft(); 4204 $sSQL .= "` WHERE `".$oAttDef->GetSQLLeft()."`> $iMyLeft AND `".$oAttDef->GetSQLRight()."`< $iMyRight"; 4205 CMDBSource::Query($sSQL); 4206 $sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` - $iDelta WHERE `".$oAttDef->GetSQLLeft()."` > $iMyRight"; 4207 CMDBSource::Query($sSQL); 4208 $sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` - $iDelta WHERE `".$oAttDef->GetSQLRight()."` > $iMyRight"; 4209 CMDBSource::Query($sSQL); 4210 } 4211 4212 /** 4213 * Special processing for the hierarchical keys stored as nested sets: replug the temporary removed branch 4214 * 4215 * @param integer $iNewLeft 4216 * @param integer $iNewRight 4217 * @param \AttributeDefinition $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key 4218 * @param string $sTable string The name of the database table containing the hierarchical key 4219 * 4220 * @throws \MySQLException 4221 * @throws \MySQLHasGoneAwayException 4222 */ 4223 public static function HKReplugBranch($iNewLeft, $iNewRight, $oAttDef, $sTable) 4224 { 4225 $iDelta = $iNewRight - $iNewLeft + 1; 4226 $sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` + $iDelta WHERE `".$oAttDef->GetSQLLeft()."` > $iNewLeft"; 4227 CMDBSource::Query($sSQL); 4228 $sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` + $iDelta WHERE `".$oAttDef->GetSQLRight()."` >= $iNewLeft"; 4229 CMDBSource::Query($sSQL); 4230 $sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = $iNewLeft - `".$oAttDef->GetSQLRight()."`, `".$oAttDef->GetSQLLeft()."` = $iNewLeft - `".$oAttDef->GetSQLLeft()."` WHERE `".$oAttDef->GetSQLRight()."`< 0"; 4231 CMDBSource::Query($sSQL); 4232 } 4233 4234 /** 4235 * Check (and updates if needed) the hierarchical keys 4236 * 4237 * @param boolean $bDiagnosticsOnly If true only a diagnostic pass will be run, returning true or false 4238 * @param boolean $bVerbose Displays some information about what is done/what needs to be done 4239 * @param boolean $bForceComputation If true, the _left and _right parameters will be recomputed even if some 4240 * values already exist in the DB 4241 * 4242 * @throws \CoreException 4243 * @throws \Exception 4244 */ 4245 public static function CheckHKeys($bDiagnosticsOnly = false, $bVerbose = false, $bForceComputation = false) 4246 { 4247 $bChangeNeeded = false; 4248 foreach(self::GetClasses() as $sClass) 4249 { 4250 if (!self::HasTable($sClass)) 4251 { 4252 continue; 4253 } 4254 4255 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 4256 { 4257 // Check (once) all the attributes that are hierarchical keys 4258 if ((self::GetAttributeOrigin($sClass, $sAttCode) == $sClass) && $oAttDef->IsHierarchicalKey()) 4259 { 4260 if ($bVerbose) 4261 { 4262 echo "The attribute $sAttCode from $sClass is a hierarchical key.\n"; 4263 } 4264 $bResult = self::HKInit($sClass, $sAttCode, $bDiagnosticsOnly, $bVerbose, $bForceComputation); 4265 $bChangeNeeded |= $bResult; 4266 if ($bVerbose && !$bResult) 4267 { 4268 echo "Ok, the attribute $sAttCode from class $sClass seems up to date.\n"; 4269 } 4270 } 4271 } 4272 } 4273 return $bChangeNeeded; 4274 } 4275 4276 /** 4277 * Initializes (i.e converts) a hierarchy stored using a 'parent_id' external key 4278 * into a hierarchy stored with a HierarchicalKey, by initializing the _left and _right values 4279 * to correspond to the existing hierarchy in the database 4280 * 4281 * @param string $sClass Name of the class to process 4282 * @param string $sAttCode Code of the attribute to process 4283 * @param boolean $bDiagnosticsOnly If true only a diagnostic pass will be run, returning true or false 4284 * @param boolean $bVerbose Displays some information about what is done/what needs to be done 4285 * @param boolean $bForceComputation If true, the _left and _right parameters will be recomputed even if some 4286 * values already exist in the DB 4287 * 4288 * @return boolean true if an update is needed (diagnostics only) / was performed 4289 * @throws \Exception 4290 * @throws \CoreException 4291 */ 4292 public static function HKInit($sClass, $sAttCode, $bDiagnosticsOnly = false, $bVerbose = false, $bForceComputation = false) 4293 { 4294 $idx = 1; 4295 $bUpdateNeeded = $bForceComputation; 4296 $oAttDef = self::GetAttributeDef($sClass, $sAttCode); 4297 $sTable = self::DBGetTable($sClass, $sAttCode); 4298 if ($oAttDef->IsHierarchicalKey()) 4299 { 4300 // Check if some values already exist in the table for the _right value, if so, do nothing 4301 $sRight = $oAttDef->GetSQLRight(); 4302 $sSQL = "SELECT MAX(`$sRight`) AS MaxRight FROM `$sTable`"; 4303 $iMaxRight = CMDBSource::QueryToScalar($sSQL); 4304 $sSQL = "SELECT COUNT(*) AS Count FROM `$sTable`"; // Note: COUNT(field) returns zero if the given field contains only NULLs 4305 $iCount = CMDBSource::QueryToScalar($sSQL); 4306 if (!$bForceComputation && ($iCount != 0) && ($iMaxRight == 0)) 4307 { 4308 $bUpdateNeeded = true; 4309 if ($bVerbose) 4310 { 4311 echo "The table '$sTable' must be updated to compute the fields $sRight and ".$oAttDef->GetSQLLeft()."\n"; 4312 } 4313 } 4314 if ($bForceComputation && !$bDiagnosticsOnly) 4315 { 4316 echo "Rebuilding the fields $sRight and ".$oAttDef->GetSQLLeft()." from table '$sTable'...\n"; 4317 } 4318 if ($bUpdateNeeded && !$bDiagnosticsOnly) 4319 { 4320 try 4321 { 4322 CMDBSource::Query('START TRANSACTION'); 4323 self::HKInitChildren($sTable, $sAttCode, $oAttDef, 0, $idx); 4324 CMDBSource::Query('COMMIT'); 4325 if ($bVerbose) 4326 { 4327 echo "Ok, table '$sTable' successfully updated.\n"; 4328 } 4329 } 4330 catch (Exception $e) 4331 { 4332 CMDBSource::Query('ROLLBACK'); 4333 throw new Exception("An error occured (".$e->getMessage().") while initializing the hierarchy for ($sClass, $sAttCode). The database was not modified."); 4334 } 4335 } 4336 } 4337 return $bUpdateNeeded; 4338 } 4339 4340 /** 4341 * Recursive helper function called by HKInit 4342 * 4343 * @param string $sTable 4344 * @param string $sAttCode 4345 * @param \AttributeDefinition $oAttDef 4346 * @param int $iId 4347 * @param int $iCurrIndex 4348 * 4349 * @throws \MySQLException 4350 * @throws \MySQLHasGoneAwayException 4351 */ 4352 protected static function HKInitChildren($sTable, $sAttCode, $oAttDef, $iId, &$iCurrIndex) 4353 { 4354 $sSQL = "SELECT id FROM `$sTable` WHERE `$sAttCode` = $iId"; 4355 $aRes = CMDBSource::QueryToArray($sSQL); 4356 $sLeft = $oAttDef->GetSQLLeft(); 4357 $sRight = $oAttDef->GetSQLRight(); 4358 foreach($aRes as $aValues) 4359 { 4360 $iChildId = $aValues['id']; 4361 $iLeft = $iCurrIndex++; 4362 //FIXME calling ourselves but no return statement in this method ?!!??? 4363 $aChildren = self::HKInitChildren($sTable, $sAttCode, $oAttDef, $iChildId, $iCurrIndex); 4364 $iRight = $iCurrIndex++; 4365 $sSQL = "UPDATE `$sTable` SET `$sLeft` = $iLeft, `$sRight` = $iRight WHERE id= $iChildId"; 4366 CMDBSource::Query($sSQL); 4367 } 4368 } 4369 4370 /** 4371 * Update the meta enums 4372 * 4373 * @param boolean $bVerbose Displays some information about what is done/what needs to be done 4374 * 4375 * @throws \CoreException 4376 * @throws \Exception 4377 * 4378 * @see AttributeMetaEnum::MapValue that must be aligned with the above implementation 4379 */ 4380 public static function RebuildMetaEnums($bVerbose = false) 4381 { 4382 foreach(self::GetClasses() as $sClass) 4383 { 4384 if (!self::HasTable($sClass)) 4385 { 4386 continue; 4387 } 4388 4389 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 4390 { 4391 // Check (once) all the attributes that are hierarchical keys 4392 if ((self::GetAttributeOrigin($sClass, $sAttCode) == $sClass) && $oAttDef instanceof AttributeEnum) 4393 { 4394 if (isset(self::$m_aEnumToMeta[$sClass][$sAttCode])) 4395 { 4396 foreach(self::$m_aEnumToMeta[$sClass][$sAttCode] as $sMetaAttCode => $oMetaAttDef) 4397 { 4398 $aMetaValues = array(); // array of (metavalue => array of values) 4399 foreach($oAttDef->GetAllowedValues() as $sCode => $sLabel) 4400 { 4401 $aMappingData = $oMetaAttDef->GetMapRule($sClass); 4402 if ($aMappingData == null) 4403 { 4404 $sMetaValue = $oMetaAttDef->GetDefaultValue(); 4405 } 4406 else 4407 { 4408 if (array_key_exists($sCode, $aMappingData['values'])) 4409 { 4410 $sMetaValue = $aMappingData['values'][$sCode]; 4411 } 4412 elseif ($oMetaAttDef->GetDefaultValue() != '') 4413 { 4414 $sMetaValue = $oMetaAttDef->GetDefaultValue(); 4415 } 4416 else 4417 { 4418 throw new Exception('MetaModel::RebuildMetaEnums(): mapping not found for value "'.$sCode.'"" in '.$sClass.', on attribute '.self::GetAttributeOrigin($sClass, $oMetaAttDef->GetCode()).'::'.$oMetaAttDef->GetCode()); 4419 } 4420 } 4421 $aMetaValues[$sMetaValue][] = $sCode; 4422 } 4423 foreach($aMetaValues as $sMetaValue => $aEnumValues) 4424 { 4425 $sMetaTable = self::DBGetTable($sClass, $sMetaAttCode); 4426 $sEnumTable = self::DBGetTable($sClass); 4427 $aColumns = array_keys($oMetaAttDef->GetSQLColumns()); 4428 $sMetaColumn = reset($aColumns); 4429 $aColumns = array_keys($oAttDef->GetSQLColumns()); 4430 $sEnumColumn = reset($aColumns); 4431 $sValueList = implode(', ', CMDBSource::Quote($aEnumValues)); 4432 $sSql = "UPDATE `$sMetaTable` JOIN `$sEnumTable` ON `$sEnumTable`.id = `$sMetaTable`.id SET `$sMetaTable`.`$sMetaColumn` = '$sMetaValue' WHERE `$sEnumTable`.`$sEnumColumn` IN ($sValueList) AND `$sMetaTable`.`$sMetaColumn` != '$sMetaValue'"; 4433 if ($bVerbose) 4434 { 4435 echo "Executing query: $sSql\n"; 4436 } 4437 CMDBSource::Query($sSql); 4438 } 4439 } 4440 } 4441 } 4442 } 4443 } 4444 } 4445 4446 4447 /** 4448 * @param boolean $bDiagnostics 4449 * @param boolean $bVerbose 4450 * 4451 * @return bool 4452 * @throws \OQLException 4453 */ 4454 public static function CheckDataSources($bDiagnostics, $bVerbose) 4455 { 4456 $sOQL = 'SELECT SynchroDataSource'; 4457 $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL)); 4458 $bFixNeeded = false; 4459 if ($bVerbose && $oSet->Count() == 0) 4460 { 4461 echo "There are no Data Sources in the database.\n"; 4462 } 4463 while ($oSource = $oSet->Fetch()) 4464 { 4465 if ($bVerbose) 4466 { 4467 echo "Checking Data Source '".$oSource->GetName()."'...\n"; 4468 $bFixNeeded = $bFixNeeded | $oSource->CheckDBConsistency($bDiagnostics, $bVerbose); 4469 } 4470 } 4471 if (!$bFixNeeded && $bVerbose) 4472 { 4473 echo "Ok.\n"; 4474 } 4475 4476 return $bFixNeeded; 4477 } 4478 4479 /** 4480 * @param array $aAliases 4481 * @param string $sNewName 4482 * @param string $sRealName 4483 * 4484 * @return string 4485 * @throws \CoreException 4486 */ 4487 public static function GenerateUniqueAlias(&$aAliases, $sNewName, $sRealName) 4488 { 4489 if (!array_key_exists($sNewName, $aAliases)) 4490 { 4491 $aAliases[$sNewName] = $sRealName; 4492 return $sNewName; 4493 } 4494 4495 for($i = 1; $i < 100; $i++) 4496 { 4497 $sAnAlias = $sNewName.$i; 4498 if (!array_key_exists($sAnAlias, $aAliases)) 4499 { 4500 // Create that new alias 4501 $aAliases[$sAnAlias] = $sRealName; 4502 return $sAnAlias; 4503 } 4504 } 4505 throw new CoreException('Failed to create an alias', array('aliases' => $aAliases, 'new' => $sNewName)); 4506 } 4507 4508 /** 4509 * @param bool $bExitOnError 4510 * 4511 * @throws \CoreException 4512 * @throws \DictExceptionMissingString 4513 * @throws \Exception 4514 */ 4515 public static function CheckDefinitions($bExitOnError = true) 4516 { 4517 if (count(self::GetClasses()) == 0) 4518 { 4519 throw new CoreException("MetaModel::InitClasses() has not been called, or no class has been declared ?!?!"); 4520 } 4521 4522 $aErrors = array(); 4523 $aSugFix = array(); 4524 foreach(self::GetClasses() as $sClass) 4525 { 4526 $sTable = self::DBGetTable($sClass); 4527 $sTableLowercase = strtolower($sTable); 4528 if ($sTableLowercase != $sTable) 4529 { 4530 $aErrors[$sClass][] = "Table name '".$sTable."' has upper case characters. You might encounter issues when moving your installation between Linux and Windows."; 4531 $aSugFix[$sClass][] = "Use '$sTableLowercase' instead. Step 1: If already installed, then rename manually in the DB: RENAME TABLE `$sTable` TO `{$sTableLowercase}_tempname`, `{$sTableLowercase}_tempname` TO `$sTableLowercase`; Step 2: Rename the table in the datamodel and compile the application. Note: the MySQL statement provided in step 1 has been designed to be compatible with Windows or Linux."; 4532 } 4533 4534 $aNameSpec = self::GetNameSpec($sClass); 4535 foreach($aNameSpec[1] as $i => $sAttCode) 4536 { 4537 if (!self::IsValidAttCode($sClass, $sAttCode)) 4538 { 4539 $aErrors[$sClass][] = "Unknown attribute code '".$sAttCode."' for the name definition"; 4540 $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); 4541 } 4542 } 4543 4544 foreach(self::GetReconcKeys($sClass) as $sReconcKeyAttCode) 4545 { 4546 if (!empty($sReconcKeyAttCode) && !self::IsValidAttCode($sClass, $sReconcKeyAttCode)) 4547 { 4548 $aErrors[$sClass][] = "Unknown attribute code '".$sReconcKeyAttCode."' in the list of reconciliation keys"; 4549 $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); 4550 } 4551 } 4552 4553 $bHasWritableAttribute = false; 4554 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 4555 { 4556 // It makes no sense to check the attributes again and again in the subclasses 4557 if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) 4558 { 4559 continue; 4560 } 4561 4562 if ($oAttDef->IsExternalKey()) 4563 { 4564 if (!self::IsValidClass($oAttDef->GetTargetClass())) 4565 { 4566 $aErrors[$sClass][] = "Unknown class '".$oAttDef->GetTargetClass()."' for the external key '$sAttCode'"; 4567 $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetClasses())."}"; 4568 } 4569 } 4570 elseif ($oAttDef->IsExternalField()) 4571 { 4572 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 4573 if (!self::IsValidAttCode($sClass, $sKeyAttCode) || !self::IsValidKeyAttCode($sClass, $sKeyAttCode)) 4574 { 4575 $aErrors[$sClass][] = "Unknown key attribute code '".$sKeyAttCode."' for the external field $sAttCode"; 4576 $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sClass))."}"; 4577 } 4578 else 4579 { 4580 $oKeyAttDef = self::GetAttributeDef($sClass, $sKeyAttCode); 4581 $sTargetClass = $oKeyAttDef->GetTargetClass(); 4582 $sExtAttCode = $oAttDef->GetExtAttCode(); 4583 if (!self::IsValidAttCode($sTargetClass, $sExtAttCode)) 4584 { 4585 $aErrors[$sClass][] = "Unknown key attribute code '".$sExtAttCode."' for the external field $sAttCode"; 4586 $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetKeysList($sTargetClass))."}"; 4587 } 4588 } 4589 } 4590 else 4591 { 4592 if ($oAttDef->IsLinkSet()) 4593 { 4594 // Do nothing... 4595 } 4596 else 4597 { 4598 if ($oAttDef instanceof AttributeStopWatch) 4599 { 4600 $aThresholds = $oAttDef->ListThresholds(); 4601 if (is_array($aThresholds)) 4602 { 4603 foreach($aThresholds as $iPercent => $aDef) 4604 { 4605 if (array_key_exists('highlight', $aDef)) 4606 { 4607 if (!array_key_exists('code', $aDef['highlight'])) 4608 { 4609 $aErrors[$sClass][] = "The 'code' element is missing for the 'highlight' property of the $iPercent% threshold in the attribute: '$sAttCode'."; 4610 $aSugFix[$sClass][] = "Add a 'code' entry specifying the value of the highlight code for this threshold."; 4611 } 4612 else 4613 { 4614 $aScale = self::GetHighlightScale($sClass); 4615 if (!array_key_exists($aDef['highlight']['code'], $aScale)) 4616 { 4617 $aErrors[$sClass][] = "'{$aDef['highlight']['code']}' is not a valid value for the 'code' element of the $iPercent% threshold in the attribute: '$sAttCode'."; 4618 $aSugFix[$sClass][] = "The possible highlight codes for this class are: ".implode(', ', array_keys($aScale))."."; 4619 } 4620 } 4621 } 4622 } 4623 } 4624 } 4625 else // standard attributes 4626 { 4627 // Check that the default values definition is a valid object! 4628 $oValSetDef = $oAttDef->GetValuesDef(); 4629 if (!is_null($oValSetDef) && !$oValSetDef instanceof ValueSetDefinition) 4630 { 4631 $aErrors[$sClass][] = "Allowed values for attribute $sAttCode is not of the relevant type"; 4632 $aSugFix[$sClass][] = "Please set it as an instance of a ValueSetDefinition object."; 4633 } 4634 else 4635 { 4636 // Default value must be listed in the allowed values (if defined) 4637 $aAllowedValues = self::GetAllowedValues_att($sClass, $sAttCode); 4638 if (!is_null($aAllowedValues)) 4639 { 4640 $sDefaultValue = $oAttDef->GetDefaultValue(); 4641 if (is_string($sDefaultValue) && !array_key_exists($sDefaultValue, $aAllowedValues)) 4642 { 4643 $aErrors[$sClass][] = "Default value '".$sDefaultValue."' for attribute $sAttCode is not an allowed value"; 4644 $aSugFix[$sClass][] = "Please pickup the default value out of {'".implode(", ", array_keys($aAllowedValues))."'}"; 4645 } 4646 } 4647 } 4648 } 4649 } 4650 } 4651 // Check dependencies 4652 if ($oAttDef->IsWritable()) 4653 { 4654 $bHasWritableAttribute = true; 4655 foreach($oAttDef->GetPrerequisiteAttributes() as $sDependOnAttCode) 4656 { 4657 if (!self::IsValidAttCode($sClass, $sDependOnAttCode)) 4658 { 4659 $aErrors[$sClass][] = "Unknown attribute code '".$sDependOnAttCode."' in the list of prerequisite attributes"; 4660 $aSugFix[$sClass][] = "Expecting a value in ".implode(", ", self::GetAttributesList($sClass)); 4661 } 4662 } 4663 } 4664 } 4665 foreach(self::GetClassFilterDefs($sClass) as $sFltCode => $oFilterDef) 4666 { 4667 if (method_exists($oFilterDef, '__GetRefAttribute')) 4668 { 4669 $oAttDef = $oFilterDef->__GetRefAttribute(); 4670 if (!self::IsValidAttCode($sClass, $oAttDef->GetCode())) 4671 { 4672 $aErrors[$sClass][] = "Wrong attribute code '".$oAttDef->GetCode()."' (wrong class) for the \"basic\" filter $sFltCode"; 4673 $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}"; 4674 } 4675 } 4676 } 4677 4678 // Lifecycle 4679 // 4680 $sStateAttCode = self::GetStateAttributeCode($sClass); 4681 if (strlen($sStateAttCode) > 0) 4682 { 4683 // Lifecycle - check that the state attribute does exist as an attribute 4684 if (!self::IsValidAttCode($sClass, $sStateAttCode)) 4685 { 4686 $aErrors[$sClass][] = "Unknown attribute code '".$sStateAttCode."' for the state definition"; 4687 $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}"; 4688 } 4689 else 4690 { 4691 // Lifecycle - check that there is a value set constraint on the state attribute 4692 $aAllowedValuesRaw = self::GetAllowedValues_att($sClass, $sStateAttCode); 4693 $aStates = array_keys(self::EnumStates($sClass)); 4694 if (is_null($aAllowedValuesRaw)) 4695 { 4696 $aErrors[$sClass][] = "Attribute '".$sStateAttCode."' will reflect the state of the object. It must be restricted to a set of values"; 4697 $aSugFix[$sClass][] = "Please define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')]"; 4698 } 4699 else 4700 { 4701 $aAllowedValues = array_keys($aAllowedValuesRaw); 4702 4703 // Lifecycle - check the the state attribute allowed values are defined states 4704 foreach($aAllowedValues as $sValue) 4705 { 4706 if (!in_array($sValue, $aStates)) 4707 { 4708 $aErrors[$sClass][] = "Attribute '".$sStateAttCode."' (object state) has an allowed value ($sValue) which is not a known state"; 4709 $aSugFix[$sClass][] = "You may define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')], or reconsider the list of states"; 4710 } 4711 } 4712 4713 // Lifecycle - check that defined states are allowed values 4714 foreach($aStates as $sStateValue) 4715 { 4716 if (!in_array($sStateValue, $aAllowedValues)) 4717 { 4718 $aErrors[$sClass][] = "Attribute '".$sStateAttCode."' (object state) has a state ($sStateValue) which is not an allowed value"; 4719 $aSugFix[$sClass][] = "You may define its allowed_values property as [new ValueSetEnum('".implode(", ", $aStates)."')], or reconsider the list of states"; 4720 } 4721 } 4722 } 4723 4724 // Lifecycle - check that the action handlers are defined 4725 foreach(self::EnumStates($sClass) as $sStateCode => $aStateDef) 4726 { 4727 foreach(self::EnumTransitions($sClass, $sStateCode) as $sStimulusCode => $aTransitionDef) 4728 { 4729 foreach($aTransitionDef['actions'] as $actionHandler) 4730 { 4731 if (is_string($actionHandler)) 4732 { 4733 if (!method_exists($sClass, $actionHandler)) 4734 { 4735 $aErrors[$sClass][] = "Unknown function '$actionHandler' in transition [$sStateCode/$sStimulusCode] for state attribute '$sStateAttCode'"; 4736 $aSugFix[$sClass][] = "Specify a function which prototype is in the form [public function $actionHandler(\$sStimulusCode){return true;}]"; 4737 } 4738 } 4739 else // if(is_array($actionHandler)) 4740 { 4741 $sActionHandler = $actionHandler['verb']; 4742 if (!method_exists($sClass, $sActionHandler)) 4743 { 4744 $aErrors[$sClass][] = "Unknown function '$sActionHandler' in transition [$sStateCode/$sStimulusCode] for state attribute '$sStateAttCode'"; 4745 $aSugFix[$sClass][] = "Specify a function which prototype is in the form [public function $sActionHandler(...){return true;}]"; 4746 } 4747 } 4748 } 4749 } 4750 if (array_key_exists('highlight', $aStateDef)) 4751 { 4752 if (!array_key_exists('code', $aStateDef['highlight'])) 4753 { 4754 $aErrors[$sClass][] = "The 'code' element is missing for the 'highlight' property of state: '$sStateCode'."; 4755 $aSugFix[$sClass][] = "Add a 'code' entry specifying the value of the highlight code for this state."; 4756 } 4757 else 4758 { 4759 $aScale = self::GetHighlightScale($sClass); 4760 if (!array_key_exists($aStateDef['highlight']['code'], $aScale)) 4761 { 4762 $aErrors[$sClass][] = "'{$aStateDef['highlight']['code']}' is not a valid value for the 'code' element in the 'highlight' property of state: '$sStateCode'."; 4763 $aSugFix[$sClass][] = "The possible highlight codes for this class are: ".implode(', ', array_keys($aScale))."."; 4764 } 4765 } 4766 } 4767 } 4768 } 4769 } 4770 4771 if ($bHasWritableAttribute) 4772 { 4773 if (!self::HasTable($sClass)) 4774 { 4775 $aErrors[$sClass][] = "No table has been defined for this class"; 4776 $aSugFix[$sClass][] = "Either define a table name or move the attributes elsewhere"; 4777 } 4778 } 4779 4780 4781 // ZList 4782 // 4783 foreach(self::EnumZLists() as $sListCode) 4784 { 4785 foreach(self::FlattenZList(self::GetZListItems($sClass, $sListCode)) as $sMyAttCode) 4786 { 4787 if (!self::IsValidAttCode($sClass, $sMyAttCode)) 4788 { 4789 $aErrors[$sClass][] = "Unknown attribute code '".$sMyAttCode."' from ZList '$sListCode'"; 4790 $aSugFix[$sClass][] = "Expecting a value in {".implode(", ", self::GetAttributesList($sClass))."}"; 4791 } 4792 } 4793 } 4794 4795 // Check SQL columns uniqueness 4796 // 4797 if (self::HasTable($sClass)) 4798 { 4799 $aTableColumns = array(); // array of column => attcode (the column is used by this attribute) 4800 $aTableColumns[self::DBGetKey($sClass)] = 'id'; 4801 4802 // Check that SQL columns are declared only once 4803 // 4804 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 4805 { 4806 // Skip this attribute if not originaly defined in this class 4807 if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) 4808 { 4809 continue; 4810 } 4811 4812 foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType) 4813 { 4814 if (array_key_exists($sField, $aTableColumns)) 4815 { 4816 $aErrors[$sClass][] = "Column '$sField' declared for attribute $sAttCode, but already used for attribute ".$aTableColumns[$sField]; 4817 $aSugFix[$sClass][] = "Please find another name for the SQL column"; 4818 } 4819 else 4820 { 4821 $aTableColumns[$sField] = $sAttCode; 4822 } 4823 } 4824 } 4825 } 4826 } // foreach class 4827 4828 if (count($aErrors) > 0) 4829 { 4830 echo "<div style=\"width:100%;padding:10px;background:#FFAAAA;display:;\">"; 4831 echo "<h3>Business model inconsistencies have been found</h3>\n"; 4832 // #@# later -> this is the responsibility of the caller to format the output 4833 foreach($aErrors as $sClass => $aMessages) 4834 { 4835 echo "<p>Wrong declaration for class <b>$sClass</b></p>\n"; 4836 echo "<ul class=\"treeview\">\n"; 4837 $i = 0; 4838 foreach($aMessages as $sMsg) 4839 { 4840 echo "<li>$sMsg ({$aSugFix[$sClass][$i]})</li>\n"; 4841 $i++; 4842 } 4843 echo "</ul>\n"; 4844 } 4845 if ($bExitOnError) 4846 { 4847 echo "<p>Aborting...</p>\n"; 4848 } 4849 echo "</div>\n"; 4850 if ($bExitOnError) 4851 { 4852 exit; 4853 } 4854 } 4855 } 4856 4857 /** 4858 * @param string $sRepairUrl 4859 * @param string $sSQLStatementArgName 4860 * @param string[] $aSQLFixes 4861 */ 4862 public static function DBShowApplyForm($sRepairUrl, $sSQLStatementArgName, $aSQLFixes) 4863 { 4864 if (empty($sRepairUrl)) 4865 { 4866 return; 4867 } 4868 4869 // By design, some queries might be blank, we have to ignore them 4870 $aCleanFixes = array(); 4871 foreach($aSQLFixes as $sSQLFix) 4872 { 4873 if (!empty($sSQLFix)) 4874 { 4875 $aCleanFixes[] = $sSQLFix; 4876 } 4877 } 4878 if (count($aCleanFixes) == 0) 4879 { 4880 return; 4881 } 4882 4883 echo "<form action=\"$sRepairUrl\" method=\"POST\">\n"; 4884 echo " <input type=\"hidden\" name=\"$sSQLStatementArgName\" value=\"".htmlentities(implode("##SEP##", $aCleanFixes), ENT_QUOTES, 'UTF-8')."\">\n"; 4885 echo " <input type=\"submit\" value=\" Apply changes (".count($aCleanFixes)." queries) \">\n"; 4886 echo "</form>\n"; 4887 } 4888 4889 /** 4890 * @param bool $bMustBeComplete 4891 * 4892 * @return bool returns true if at least one table exists 4893 * @throws \CoreException 4894 * @throws \MySQLException 4895 */ 4896 public static function DBExists($bMustBeComplete = true) 4897 { 4898 if (!CMDBSource::IsDB(self::$m_sDBName)) 4899 { 4900 return false; 4901 } 4902 CMDBSource::SelectDB(self::$m_sDBName); 4903 4904 $aFound = array(); 4905 $aMissing = array(); 4906 foreach(self::DBEnumTables() as $sTable => $aClasses) 4907 { 4908 if (CMDBSource::IsTable($sTable)) 4909 { 4910 $aFound[] = $sTable; 4911 } 4912 else 4913 { 4914 $aMissing[] = $sTable; 4915 } 4916 } 4917 4918 if (count($aFound) == 0) 4919 { 4920 // no expected table has been found 4921 return false; 4922 } 4923 else 4924 { 4925 if (count($aMissing) == 0) 4926 { 4927 // the database is complete (still, could be some fields missing!) 4928 return true; 4929 } 4930 else 4931 { 4932 // not all the tables, could be an older version 4933 return !$bMustBeComplete; 4934 } 4935 } 4936 } 4937 4938 /** 4939 * Do drop only tables corresponding to the sub-database (table prefix) 4940 * then possibly drop the DB itself (if no table remain) 4941 */ 4942 public static function DBDrop() 4943 { 4944 $bDropEntireDB = true; 4945 4946 if (!empty(self::$m_sTablePrefix)) 4947 { 4948 foreach(CMDBSource::EnumTables() as $sTable) 4949 { 4950 // perform a case insensitive test because on Windows the table names become lowercase :-( 4951 if (strtolower(substr($sTable, 0, strlen(self::$m_sTablePrefix))) == strtolower(self::$m_sTablePrefix)) 4952 { 4953 CMDBSource::DropTable($sTable); 4954 } 4955 else 4956 { 4957 // There is at least one table which is out of the scope of the current application 4958 $bDropEntireDB = false; 4959 } 4960 } 4961 } 4962 4963 if ($bDropEntireDB) 4964 { 4965 CMDBSource::DropDB(self::$m_sDBName); 4966 } 4967 } 4968 4969 4970 /** 4971 * @param callable $aCallback 4972 * 4973 * @throws \MySQLException 4974 * @throws \MySQLHasGoneAwayException 4975 * @throws \CoreException 4976 * @throws \Exception 4977 */ 4978 public static function DBCreate($aCallback = null) 4979 { 4980 // Note: we have to check if the DB does exist, because we may share the DB 4981 // with other applications (in which case the DB does exist, not the tables with the given prefix) 4982 if (!CMDBSource::IsDB(self::$m_sDBName)) 4983 { 4984 CMDBSource::CreateDB(self::$m_sDBName); 4985 } 4986 self::DBCreateTables($aCallback); 4987 self::DBCreateViews(); 4988 } 4989 4990 /** 4991 * @param callable $aCallback 4992 * 4993 * @throws \CoreException 4994 */ 4995 protected static function DBCreateTables($aCallback = null) 4996 { 4997 list($aErrors, $aSugFix, $aCondensedQueries) = self::DBCheckFormat(); 4998 4999 //$sSQL = implode('; ', $aCondensedQueries); Does not work - multiple queries not allowed 5000 foreach($aCondensedQueries as $sQuery) 5001 { 5002 $fStart = microtime(true); 5003 CMDBSource::CreateTable($sQuery); 5004 $fDuration = microtime(true) - $fStart; 5005 if ($aCallback != null) 5006 { 5007 call_user_func($aCallback, $sQuery, $fDuration); 5008 } 5009 } 5010 } 5011 5012 /** 5013 * @throws \CoreException 5014 * @throws \Exception 5015 * @throws \MissingQueryArgument 5016 */ 5017 protected static function DBCreateViews() 5018 { 5019 list($aErrors, $aSugFix) = self::DBCheckViews(); 5020 5021 foreach($aSugFix as $sClass => $aTarget) 5022 { 5023 foreach($aTarget as $aQueries) 5024 { 5025 foreach($aQueries as $sQuery) 5026 { 5027 if (!empty($sQuery)) 5028 { 5029 // forces a refresh of cached information 5030 CMDBSource::CreateTable($sQuery); 5031 } 5032 } 5033 } 5034 } 5035 } 5036 5037 /** 5038 * @return array 5039 * @throws \CoreException 5040 * @throws \MySQLException 5041 */ 5042 public static function DBDump() 5043 { 5044 $aDataDump = array(); 5045 foreach(self::DBEnumTables() as $sTable => $aClasses) 5046 { 5047 $aRows = CMDBSource::DumpTable($sTable); 5048 $aDataDump[$sTable] = $aRows; 5049 } 5050 return $aDataDump; 5051 } 5052 5053 /** 5054 * Determines wether the target DB is frozen or not 5055 * 5056 * @return bool 5057 */ 5058 public static function DBIsReadOnly() 5059 { 5060 // Improvement: check the mySQL variable -> Read-only 5061 5062 if (utils::IsArchiveMode()) 5063 { 5064 return true; 5065 } 5066 if (UserRights::IsAdministrator()) 5067 { 5068 return (!self::DBHasAccess(ACCESS_ADMIN_WRITE)); 5069 } 5070 else 5071 { 5072 return (!self::DBHasAccess(ACCESS_USER_WRITE)); 5073 } 5074 } 5075 5076 /** 5077 * @param int $iRequested 5078 * 5079 * @return bool 5080 */ 5081 public static function DBHasAccess($iRequested = ACCESS_FULL) 5082 { 5083 $iMode = self::$m_oConfig->Get('access_mode'); 5084 if (($iMode & $iRequested) == 0) 5085 { 5086 return false; 5087 } 5088 5089 return true; 5090 } 5091 5092 /** 5093 * @param string $sKey 5094 * @param string $sValueFromOldSystem 5095 * @param string $sDefaultValue 5096 * @param boolean $bNotInDico 5097 * 5098 * @return string 5099 * @throws \DictExceptionMissingString 5100 */ 5101 protected static function MakeDictEntry($sKey, $sValueFromOldSystem, $sDefaultValue, &$bNotInDico) 5102 { 5103 $sValue = Dict::S($sKey, 'x-no-nothing'); 5104 if ($sValue == 'x-no-nothing') 5105 { 5106 $bNotInDico = true; 5107 $sValue = $sValueFromOldSystem; 5108 if (strlen($sValue) == 0) 5109 { 5110 $sValue = $sDefaultValue; 5111 } 5112 } 5113 return " '$sKey' => '".str_replace("'", "\\'", $sValue)."',\n"; 5114 } 5115 5116 /** 5117 * @param string $sModules 5118 * @param string $sOutputFilter 5119 * 5120 * @return string 5121 * @throws \CoreException 5122 * @throws \DictExceptionMissingString 5123 * @throws \Exception 5124 */ 5125 public static function MakeDictionaryTemplate($sModules = '', $sOutputFilter = 'NotInDictionary') 5126 { 5127 $sRes = ''; 5128 5129 $sRes .= "// Dictionnay conventions\n"; 5130 $sRes .= htmlentities("// Class:<class_name>\n", ENT_QUOTES, 'UTF-8'); 5131 $sRes .= htmlentities("// Class:<class_name>+\n", ENT_QUOTES, 'UTF-8'); 5132 $sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>\n", ENT_QUOTES, 'UTF-8'); 5133 $sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>+\n", ENT_QUOTES, 'UTF-8'); 5134 $sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>/Value:<value>\n", ENT_QUOTES, 'UTF-8'); 5135 $sRes .= htmlentities("// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+\n", ENT_QUOTES, 'UTF-8'); 5136 $sRes .= htmlentities("// Class:<class_name>/Stimulus:<stimulus_code>\n", ENT_QUOTES, 'UTF-8'); 5137 $sRes .= htmlentities("// Class:<class_name>/Stimulus:<stimulus_code>+\n", ENT_QUOTES, 'UTF-8'); 5138 $sRes .= "\n"; 5139 5140 // Note: I did not use EnumCategories(), because a given class maybe found in several categories 5141 // Need to invent the "module", to characterize the origins of a class 5142 if (strlen($sModules) == 0) 5143 { 5144 $aModules = array('bizmodel', 'core/cmdb', 'gui', 'application', 'addon/userrights'); 5145 } 5146 else 5147 { 5148 $aModules = explode(', ', $sModules); 5149 } 5150 5151 $sRes .= "//////////////////////////////////////////////////////////////////////\n"; 5152 $sRes .= "// Note: The classes have been grouped by categories: ".implode(', ', $aModules)."\n"; 5153 $sRes .= "//////////////////////////////////////////////////////////////////////\n"; 5154 5155 foreach($aModules as $sCategory) 5156 { 5157 $sRes .= "//////////////////////////////////////////////////////////////////////\n"; 5158 $sRes .= "// Classes in '<em>$sCategory</em>'\n"; 5159 $sRes .= "//////////////////////////////////////////////////////////////////////\n"; 5160 $sRes .= "//\n"; 5161 $sRes .= "\n"; 5162 foreach(self::GetClasses($sCategory) as $sClass) 5163 { 5164 if (!self::HasTable($sClass)) 5165 { 5166 continue; 5167 } 5168 5169 $bNotInDico = false; 5170 5171 $sClassRes = "//\n"; 5172 $sClassRes .= "// Class: $sClass\n"; 5173 $sClassRes .= "//\n"; 5174 $sClassRes .= "\n"; 5175 $sClassRes .= "Dict::Add('EN US', 'English', 'English', array(\n"; 5176 $sClassRes .= self::MakeDictEntry("Class:$sClass", self::GetName_Obsolete($sClass), $sClass, $bNotInDico); 5177 $sClassRes .= self::MakeDictEntry("Class:$sClass+", self::GetClassDescription_Obsolete($sClass), '', $bNotInDico); 5178 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 5179 { 5180 // Skip this attribute if not originaly defined in this class 5181 if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) 5182 { 5183 continue; 5184 } 5185 5186 $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode", $oAttDef->GetLabel_Obsolete(), $sAttCode, $bNotInDico); 5187 $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode+", $oAttDef->GetDescription_Obsolete(), '', $bNotInDico); 5188 if ($oAttDef instanceof AttributeEnum) 5189 { 5190 if (self::GetStateAttributeCode($sClass) == $sAttCode) 5191 { 5192 foreach(self::EnumStates($sClass) as $sStateCode => $aStateData) 5193 { 5194 if (array_key_exists('label', $aStateData)) 5195 { 5196 $sValue = $aStateData['label']; 5197 } 5198 else 5199 { 5200 $sValue = MetaModel::GetStateLabel($sClass, $sStateCode); 5201 } 5202 if (array_key_exists('description', $aStateData)) 5203 { 5204 $sValuePlus = $aStateData['description']; 5205 } 5206 else 5207 { 5208 $sValuePlus = MetaModel::GetStateDescription($sClass, $sStateCode); 5209 } 5210 $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sStateCode", $sValue, '', $bNotInDico); 5211 $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sStateCode+", $sValuePlus, '', $bNotInDico); 5212 } 5213 } 5214 else 5215 { 5216 foreach($oAttDef->GetAllowedValues() as $sKey => $value) 5217 { 5218 $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sKey", $value, '', $bNotInDico); 5219 $sClassRes .= self::MakeDictEntry("Class:$sClass/Attribute:$sAttCode/Value:$sKey+", $value, '', $bNotInDico); 5220 } 5221 } 5222 } 5223 } 5224 foreach(self::EnumStimuli($sClass) as $sStimulusCode => $oStimulus) 5225 { 5226 $sClassRes .= self::MakeDictEntry("Class:$sClass/Stimulus:$sStimulusCode", $oStimulus->GetLabel_Obsolete(), '', $bNotInDico); 5227 $sClassRes .= self::MakeDictEntry("Class:$sClass/Stimulus:$sStimulusCode+", $oStimulus->GetDescription_Obsolete(), '', $bNotInDico); 5228 } 5229 5230 $sClassRes .= "));\n"; 5231 $sClassRes .= "\n"; 5232 5233 if ($bNotInDico || ($sOutputFilter != 'NotInDictionary')) 5234 { 5235 $sRes .= $sClassRes; 5236 } 5237 } 5238 } 5239 5240 return $sRes; 5241 } 5242 5243 5244 /** 5245 * @return array 5246 * @throws \CoreException 5247 * @throws \Exception 5248 */ 5249 public static function DBCheckFormat() 5250 { 5251 $aErrors = array(); 5252 $aSugFix = array(); 5253 5254 $sAlterDBMetaData = CMDBSource::DBCheckCharsetAndCollation(); 5255 5256 // A new way of representing things to be done - quicker to execute ! 5257 $aCreateTable = array(); // array of <table> => <table options> 5258 $aCreateTableItems = array(); // array of <table> => array of <create definition> 5259 $aAlterTableMetaData = array(); 5260 $aAlterTableItems = array(); // array of <table> => <alter specification> 5261 $aPostTableAlteration = array(); // array of <table> => post alteration queries 5262 5263 foreach(self::GetClasses() as $sClass) 5264 { 5265 if (!self::HasTable($sClass)) 5266 { 5267 continue; 5268 } 5269 5270 // Check that the table exists 5271 // 5272 $sTable = self::DBGetTable($sClass); 5273 $aSugFix[$sClass]['*First'] = array(); 5274 5275 $aTableInfo = CMDBSource::GetTableInfo($sTable); 5276 5277 $bTableToCreate = false; 5278 $sKeyField = self::DBGetKey($sClass); 5279 $sDbCharset = DEFAULT_CHARACTER_SET; 5280 $sDbCollation = DEFAULT_COLLATION; 5281 $sAutoIncrement = (self::IsAutoIncrementKey($sClass) ? "AUTO_INCREMENT" : ""); 5282 $sKeyFieldDefinition = "`$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY"; 5283 $aTableInfo['Indexes']['PRIMARY']['used'] = true; 5284 if (!CMDBSource::IsTable($sTable)) 5285 { 5286 $bTableToCreate = true; 5287 $aErrors[$sClass]['*'][] = "table '$sTable' could not be found in the DB"; 5288 $aSugFix[$sClass]['*'][] = "CREATE TABLE `$sTable` ($sKeyFieldDefinition) ENGINE = ".MYSQL_ENGINE." CHARACTER SET $sDbCharset COLLATE $sDbCollation"; 5289 $aCreateTable[$sTable] = "ENGINE = ".MYSQL_ENGINE." CHARACTER SET $sDbCharset COLLATE $sDbCollation"; 5290 $aCreateTableItems[$sTable][$sKeyField] = $sKeyFieldDefinition; 5291 } 5292 // Check that the key field exists 5293 // 5294 elseif (!CMDBSource::IsField($sTable, $sKeyField)) 5295 { 5296 $aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) could not be found"; 5297 $aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` ADD $sKeyFieldDefinition"; 5298 if (!$bTableToCreate) 5299 { 5300 $aAlterTableItems[$sTable][$sKeyField] = "ADD $sKeyFieldDefinition"; 5301 } 5302 } 5303 else 5304 { 5305 // Check the key field properties 5306 // 5307 if (!CMDBSource::IsKey($sTable, $sKeyField)) 5308 { 5309 $aErrors[$sClass]['id'][] = "key '$sKeyField' is not a key for table '$sTable'"; 5310 $aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable`, DROP PRIMARY KEY, ADD PRIMARY key(`$sKeyField`)"; 5311 if (!$bTableToCreate) 5312 { 5313 $aAlterTableItems[$sTable][$sKeyField] = "CHANGE `$sKeyField` $sKeyFieldDefinition"; 5314 } 5315 } 5316 if (self::IsAutoIncrementKey($sClass) && !CMDBSource::IsAutoIncrement($sTable, $sKeyField)) 5317 { 5318 $aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) is not automatically incremented"; 5319 $aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` CHANGE `$sKeyField` $sKeyFieldDefinition"; 5320 if (!$bTableToCreate) 5321 { 5322 $aAlterTableItems[$sTable][$sKeyField] = "CHANGE `$sKeyField` $sKeyFieldDefinition"; 5323 } 5324 } 5325 } 5326 5327 if (!$bTableToCreate) 5328 { 5329 $sAlterTableMetaDataQuery = CMDBSource::DBCheckTableCharsetAndCollation($sTable); 5330 if (!empty($sAlterTableMetaDataQuery)) 5331 { 5332 $aAlterTableMetaData[$sTable] = $sAlterTableMetaDataQuery; 5333 } 5334 } 5335 5336 // Check that any defined field exists 5337 // 5338 $aTableInfo['Fields'][$sKeyField]['used'] = true; 5339 $aFriendlynameAttcodes = self::GetFriendlyNameAttributeCodeList($sClass); 5340 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 5341 { 5342 if (!$oAttDef->CopyOnAllTables()) 5343 { 5344 // Skip this attribute if not originaly defined in this class 5345 if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) 5346 { 5347 continue; 5348 } 5349 } 5350 foreach($oAttDef->GetSQLColumns(true) as $sField => $sDBFieldSpec) 5351 { 5352 // Keep track of columns used by iTop 5353 $aTableInfo['Fields'][$sField]['used'] = true; 5354 5355 $bIndexNeeded = $oAttDef->RequiresIndex(); 5356 $bFullTextIndexNeeded = false; 5357 if (!$bIndexNeeded) 5358 { 5359 // Add an index on the columns of the friendlyname 5360 if (in_array($sField, $aFriendlynameAttcodes)) 5361 { 5362 $bIndexNeeded = true; 5363 } 5364 } 5365 else 5366 { 5367 if ($oAttDef->RequiresFullTextIndex()) 5368 { 5369 $bFullTextIndexNeeded = true; 5370 } 5371 } 5372 5373 $sFieldDefinition = "`$sField` $sDBFieldSpec"; 5374 if (!CMDBSource::IsField($sTable, $sField)) 5375 { 5376 $aErrors[$sClass][$sAttCode][] = "field '$sField' could not be found in table '$sTable'"; 5377 $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD $sFieldDefinition"; 5378 5379 if ($bTableToCreate) 5380 { 5381 $aCreateTableItems[$sTable][$sField] = $sFieldDefinition; 5382 } 5383 else 5384 { 5385 $aAlterTableItems[$sTable][$sField] = "ADD $sFieldDefinition"; 5386 } 5387 5388 if ($bIndexNeeded) 5389 { 5390 $aTableInfo['Indexes'][$sField]['used'] = true; 5391 $sIndexName = $sField; 5392 $sColumns = '`'.$sField.'`'; 5393 5394 if ($bFullTextIndexNeeded) 5395 { 5396 $sIndexType = 'FULLTEXT INDEX'; 5397 } 5398 else 5399 { 5400 $sIndexType = 'INDEX'; 5401 $aColumns = array($sField); 5402 $aLength = self::DBGetIndexesLength($sClass, $aColumns, $aTableInfo); 5403 if (!is_null($aLength[0])) 5404 { 5405 $sColumns .= ' ('.$aLength[0].')'; 5406 } 5407 } 5408 $sSugFix = "ALTER TABLE `$sTable` ADD $sIndexType `$sIndexName` ($sColumns)"; 5409 $aSugFix[$sClass][$sAttCode][] = $sSugFix; 5410 if ($bFullTextIndexNeeded) 5411 { 5412 // MySQL does not support multi fulltext index creation in a single query (mysql_errno = 1795) 5413 $aPostTableAlteration[$sTable][] = $sSugFix; 5414 } 5415 elseif ($bTableToCreate) 5416 { 5417 $aCreateTableItems[$sTable][] = "$sIndexType `$sIndexName` ($sColumns)"; 5418 } 5419 else 5420 { 5421 $aAlterTableItems[$sTable][] = "ADD $sIndexType `$sIndexName` ($sColumns)"; 5422 } 5423 } 5424 5425 } 5426 else 5427 { 5428 // Create indexes (external keys only... so far) 5429 // (drop before change, add after change) 5430 $sSugFixAfterChange = ''; 5431 $sAlterTableItemsAfterChange = ''; 5432 if ($bIndexNeeded) 5433 { 5434 $aTableInfo['Indexes'][$sField]['used'] = true; 5435 5436 if ($bFullTextIndexNeeded) 5437 { 5438 $sIndexType = 'FULLTEXT INDEX'; 5439 $aColumns = null; 5440 $aLength = null; 5441 } 5442 else 5443 { 5444 $sIndexType = 'INDEX'; 5445 $aColumns = array($sField); 5446 $aLength = self::DBGetIndexesLength($sClass, $aColumns, $aTableInfo); 5447 } 5448 5449 if (!CMDBSource::HasIndex($sTable, $sField, $aColumns, $aLength)) 5450 { 5451 $sIndexName = $sField; 5452 $sColumns = '`'.$sField.'`'; 5453 if (!is_null($aLength[0])) 5454 { 5455 $sColumns .= ' ('.$aLength[0].')'; 5456 } 5457 5458 $aErrors[$sClass][$sAttCode][] = "Foreign key '$sField' in table '$sTable' should have an index"; 5459 if (CMDBSource::HasIndex($sTable, $sField)) 5460 { 5461 $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexName`"; 5462 $aAlterTableItems[$sTable][] = "DROP INDEX `$sIndexName`"; 5463 } 5464 $sSugFixAfterChange = "ALTER TABLE `$sTable` ADD $sIndexType `$sIndexName` ($sColumns)"; 5465 $sAlterTableItemsAfterChange = "ADD $sIndexType `$sIndexName` ($sColumns)"; 5466 } 5467 } 5468 5469 // The field already exists, does it have the relevant properties? 5470 // 5471 $bToBeChanged = false; 5472 $sActualFieldSpec = CMDBSource::GetFieldSpec($sTable, $sField); 5473 if (strcasecmp($sDBFieldSpec, $sActualFieldSpec) != 0) 5474 { 5475 $bToBeChanged = true; 5476 $aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' has a wrong type: found '$sActualFieldSpec' while expecting '$sDBFieldSpec'"; 5477 } 5478 if ($bToBeChanged) 5479 { 5480 $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` $sFieldDefinition"; 5481 $aAlterTableItems[$sTable][$sField] = "CHANGE `$sField` $sFieldDefinition"; 5482 } 5483 5484 // Create indexes (external keys only... so far) 5485 // 5486 if (!empty($sSugFixAfterChange)) 5487 { 5488 $aSugFix[$sClass][$sAttCode][] = $sSugFixAfterChange; 5489 if ($bFullTextIndexNeeded) 5490 { 5491 // MySQL does not support multi fulltext index creation in a single query (mysql_errno = 1795) 5492 $aPostTableAlteration[$sTable][] = $sSugFixAfterChange; 5493 } 5494 else 5495 { 5496 $aAlterTableItems[$sTable][] = $sAlterTableItemsAfterChange; 5497 } 5498 } 5499 } 5500 } 5501 } 5502 5503 // Check indexes 5504 foreach(self::DBGetIndexes($sClass) as $aColumns) 5505 { 5506 $sIndexId = implode('_', $aColumns); 5507 5508 if (isset($aTableInfo['Indexes'][$sIndexId]['used']) && $aTableInfo['Indexes'][$sIndexId]['used']) 5509 { 5510 continue; 5511 } 5512 5513 $aLength = self::DBGetIndexesLength($sClass, $aColumns, $aTableInfo); 5514 $aTableInfo['Indexes'][$sIndexId]['used'] = true; 5515 5516 if (!CMDBSource::HasIndex($sTable, $sIndexId, $aColumns, $aLength)) 5517 { 5518 $sColumns = ''; 5519 5520 for ($i = 0; $i < count($aColumns); $i++) 5521 { 5522 if (!empty($sColumns)) 5523 { 5524 $sColumns .= ', '; 5525 } 5526 $sColumns .= '`'.$aColumns[$i].'`'; 5527 if (!is_null($aLength[$i])) 5528 { 5529 $sColumns .= ' ('.$aLength[$i].')'; 5530 } 5531 } 5532 if (CMDBSource::HasIndex($sTable, $sIndexId)) 5533 { 5534 $aErrors[$sClass]['*'][] = "Wrong index '$sIndexId' ($sColumns) in table '$sTable'"; 5535 $aSugFix[$sClass]['*First'][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexId`"; 5536 $aSugFix[$sClass]['*'][] = "ALTER TABLE `$sTable` ADD INDEX `$sIndexId` ($sColumns)"; 5537 } 5538 else 5539 { 5540 $aErrors[$sClass]['*'][] = "Missing index '$sIndexId' ($sColumns) in table '$sTable'"; 5541 $aSugFix[$sClass]['*'][] = "ALTER TABLE `$sTable` ADD INDEX `$sIndexId` ($sColumns)"; 5542 } 5543 if ($bTableToCreate) 5544 { 5545 $aCreateTableItems[$sTable][] = "INDEX `$sIndexId` ($sColumns)"; 5546 } 5547 else 5548 { 5549 if (CMDBSource::HasIndex($sTable, $sIndexId)) 5550 { 5551 // Add the drop before CHARSET alteration 5552 if (!isset($aAlterTableItems[$sTable])) 5553 { 5554 $aAlterTableItems[$sTable] = array(); 5555 } 5556 array_unshift($aAlterTableItems[$sTable], "DROP INDEX `$sIndexId`"); 5557 } 5558 $aAlterTableItems[$sTable][] = "ADD INDEX `$sIndexId` ($sColumns)"; 5559 } 5560 } 5561 } 5562 5563 // Find out unused columns 5564 // 5565 foreach($aTableInfo['Fields'] as $sField => $aFieldData) 5566 { 5567 if (!isset($aFieldData['used']) || !$aFieldData['used']) 5568 { 5569 $aErrors[$sClass]['*'][] = "Column '$sField' in table '$sTable' is not used"; 5570 if (!CMDBSource::IsNullAllowed($sTable, $sField)) 5571 { 5572 // Allow null values so that new record can be inserted 5573 // without specifying the value of this unknown column 5574 $sFieldDefinition = "`$sField` ".CMDBSource::GetFieldType($sTable, $sField).' NULL'; 5575 $aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` $sFieldDefinition"; 5576 $aAlterTableItems[$sTable][$sField] = "CHANGE `$sField` $sFieldDefinition"; 5577 } 5578 $aSugFix[$sClass][$sAttCode][] = "-- Recommended action: ALTER TABLE `$sTable` DROP `$sField`"; 5579 } 5580 } 5581 5582 // Find out unused indexes 5583 // 5584 foreach($aTableInfo['Indexes'] as $sIndexId => $aIndexData) 5585 { 5586 if (!isset($aIndexData['used']) || !$aIndexData['used']) 5587 { 5588 $aErrors[$sClass]['*'][] = "Index '$sIndexId' in table '$sTable' is not used and will be removed"; 5589 $aSugFix[$sClass]['*First'][] = "ALTER TABLE `$sTable` DROP INDEX `$sIndexId`"; 5590 // Add the drop before CHARSET alteration 5591 if (!isset($aAlterTableItems[$sTable])) 5592 { 5593 $aAlterTableItems[$sTable] = array(); 5594 } 5595 array_unshift($aAlterTableItems[$sTable], "DROP INDEX `$sIndexId`"); 5596 } 5597 } 5598 5599 if (empty($aSugFix[$sClass]['*First'])) unset($aSugFix[$sClass]['*First']); 5600 } 5601 5602 $aCondensedQueries = array(); 5603 if (!empty($sAlterDBMetaData)) 5604 { 5605 $aCondensedQueries[] = $sAlterDBMetaData; 5606 } 5607 foreach($aCreateTable as $sTable => $sTableOptions) 5608 { 5609 $sTableItems = implode(', ', $aCreateTableItems[$sTable]); 5610 $aCondensedQueries[] = "CREATE TABLE `$sTable` ($sTableItems) $sTableOptions"; 5611 } 5612 foreach ($aAlterTableMetaData as $sTableAlterQuery) 5613 { 5614 $aCondensedQueries[] = $sTableAlterQuery; 5615 } 5616 foreach ($aAlterTableItems as $sTable => $aChangeList) 5617 { 5618 $sChangeList = implode(', ', $aChangeList); 5619 $aCondensedQueries[] = "ALTER TABLE `$sTable` $sChangeList"; 5620 } 5621 foreach($aPostTableAlteration as $sTable => $aChangeList) 5622 { 5623 $aCondensedQueries = array_merge($aCondensedQueries, $aChangeList); 5624 } 5625 5626 return array($aErrors, $aSugFix, $aCondensedQueries); 5627 } 5628 5629 5630 /** 5631 * @return array 5632 * @throws \CoreException 5633 * @throws \Exception 5634 * @throws \MissingQueryArgument 5635 */ 5636 public static function DBCheckViews() 5637 { 5638 $aErrors = array(); 5639 $aSugFix = array(); 5640 5641 // Reporting views (must be created after any other table) 5642 // 5643 foreach(self::GetClasses('bizmodel') as $sClass) 5644 { 5645 $sView = self::DBGetView($sClass); 5646 if (CMDBSource::IsTable($sView)) 5647 { 5648 // Check that the view is complete 5649 // 5650 // Note: checking the list of attributes is not enough because the columns can be stable while the SELECT is not stable 5651 // Example: new way to compute the friendly name 5652 // The correct comparison algorithm is to compare the queries, 5653 // by using "SHOW CREATE VIEW" (MySQL 5.0.1 required) or to look into INFORMATION_SCHEMA/views 5654 // both requiring some privileges 5655 // Decision: to simplify, let's consider the views as being wrong anytime 5656 // Rework the view 5657 // 5658 $oFilter = new DBObjectSearch($sClass, ''); 5659 $oFilter->AllowAllData(); 5660 $sSQL = $oFilter->MakeSelectQuery(); 5661 $aErrors[$sClass]['*'][] = "Redeclare view '$sView' (systematic - to support an eventual change in the friendly name computation)"; 5662 $aSugFix[$sClass]['*'][] = "ALTER VIEW `$sView` AS $sSQL"; 5663 } 5664 else 5665 { 5666 // Create the view 5667 // 5668 $oFilter = new DBObjectSearch($sClass, ''); 5669 $oFilter->AllowAllData(); 5670 $sSQL = $oFilter->MakeSelectQuery(); 5671 $aErrors[$sClass]['*'][] = "Missing view for class: $sClass"; 5672 $aSugFix[$sClass]['*'][] = "DROP VIEW IF EXISTS `$sView`"; 5673 $aSugFix[$sClass]['*'][] = "CREATE VIEW `$sView` AS $sSQL"; 5674 } 5675 } 5676 return array($aErrors, $aSugFix); 5677 } 5678 5679 /** 5680 * @param string $sSelWrongRecs 5681 * @param string $sErrorDesc 5682 * @param string $sClass 5683 * @param array $aErrorsAndFixes 5684 * @param int $iNewDelCount 5685 * @param array $aPlannedDel 5686 * @param bool $bProcessingFriends 5687 * 5688 * @throws \CoreException 5689 */ 5690 private static function DBCheckIntegrity_Check2Delete($sSelWrongRecs, $sErrorDesc, $sClass, &$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel, $bProcessingFriends = false) 5691 { 5692 $sRootClass = self::GetRootClass($sClass); 5693 $sTable = self::DBGetTable($sClass); 5694 $sKeyField = self::DBGetKey($sClass); 5695 5696 if (array_key_exists($sTable, $aPlannedDel) && count($aPlannedDel[$sTable]) > 0) 5697 { 5698 $sSelWrongRecs .= " AND maintable.`$sKeyField` NOT IN ('".implode("', '", $aPlannedDel[$sTable])."')"; 5699 } 5700 $aWrongRecords = CMDBSource::QueryToCol($sSelWrongRecs, "id"); 5701 if (count($aWrongRecords) == 0) 5702 { 5703 return; 5704 } 5705 5706 if (!array_key_exists($sRootClass, $aErrorsAndFixes)) 5707 { 5708 $aErrorsAndFixes[$sRootClass] = array(); 5709 } 5710 if (!array_key_exists($sTable, $aErrorsAndFixes[$sRootClass])) 5711 { 5712 $aErrorsAndFixes[$sRootClass][$sTable] = array(); 5713 } 5714 5715 foreach($aWrongRecords as $iRecordId) 5716 { 5717 if (array_key_exists($iRecordId, $aErrorsAndFixes[$sRootClass][$sTable])) 5718 { 5719 switch ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action']) 5720 { 5721 case 'Delete': 5722 // Already planned for a deletion 5723 // Let's concatenate the errors description together 5724 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] .= ', '.$sErrorDesc; 5725 break; 5726 5727 case 'Update': 5728 // Let's plan a deletion 5729 break; 5730 } 5731 } 5732 else 5733 { 5734 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] = $sErrorDesc; 5735 } 5736 5737 if (!$bProcessingFriends) 5738 { 5739 if (!array_key_exists($sTable, $aPlannedDel) || !in_array($iRecordId, $aPlannedDel[$sTable])) 5740 { 5741 // Something new to be deleted... 5742 $iNewDelCount++; 5743 } 5744 } 5745 5746 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'] = 'Delete'; 5747 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] = array(); 5748 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Pass'] = 123; 5749 $aPlannedDel[$sTable][] = $iRecordId; 5750 } 5751 5752 // Now make sure that we would delete the records of the other tables for that class 5753 // 5754 if (!$bProcessingFriends) 5755 { 5756 $sDeleteKeys = "'".implode("', '", $aWrongRecords)."'"; 5757 foreach(self::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL) as $sFriendClass) 5758 { 5759 $sFriendTable = self::DBGetTable($sFriendClass); 5760 $sFriendKey = self::DBGetKey($sFriendClass); 5761 5762 // skip the current table 5763 if ($sFriendTable == $sTable) 5764 { 5765 continue; 5766 } 5767 5768 $sFindRelatedRec = "SELECT DISTINCT maintable.`$sFriendKey` AS id FROM `$sFriendTable` AS maintable WHERE maintable.`$sFriendKey` IN ($sDeleteKeys)"; 5769 self::DBCheckIntegrity_Check2Delete($sFindRelatedRec, 5770 "Cascading deletion of record in friend table `<em>$sTable</em>`", $sFriendClass, $aErrorsAndFixes, 5771 $iNewDelCount, $aPlannedDel, 5772 true); 5773 } 5774 } 5775 } 5776 5777 /** 5778 * @param string $sSelWrongRecs 5779 * @param string $sErrorDesc 5780 * @param string $sColumn 5781 * @param string $sNewValue 5782 * @param string $sClass 5783 * @param array $aErrorsAndFixes 5784 * @param int $iNewDelCount 5785 * @param array $aPlannedDel 5786 * 5787 * @throws \CoreException 5788 */ 5789 private static function DBCheckIntegrity_Check2Update($sSelWrongRecs, $sErrorDesc, $sColumn, $sNewValue, $sClass, &$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel) 5790 { 5791 $sRootClass = self::GetRootClass($sClass); 5792 $sTable = self::DBGetTable($sClass); 5793 $sKeyField = self::DBGetKey($sClass); 5794 5795 if (array_key_exists($sTable, $aPlannedDel) && count($aPlannedDel[$sTable]) > 0) 5796 { 5797 $sSelWrongRecs .= " AND maintable.`$sKeyField` NOT IN ('".implode("', '", $aPlannedDel[$sTable])."')"; 5798 } 5799 $aWrongRecords = CMDBSource::QueryToCol($sSelWrongRecs, "id"); 5800 if (count($aWrongRecords) == 0) 5801 { 5802 return; 5803 } 5804 5805 if (!array_key_exists($sRootClass, $aErrorsAndFixes)) 5806 { 5807 $aErrorsAndFixes[$sRootClass] = array(); 5808 } 5809 if (!array_key_exists($sTable, $aErrorsAndFixes[$sRootClass])) 5810 { 5811 $aErrorsAndFixes[$sRootClass][$sTable] = array(); 5812 } 5813 5814 foreach($aWrongRecords as $iRecordId) 5815 { 5816 if (array_key_exists($iRecordId, $aErrorsAndFixes[$sRootClass][$sTable])) 5817 { 5818 $sAction = $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action']; 5819 5820 if ($sAction == 'Delete') 5821 { 5822 // No need to update, the record will be deleted! 5823 } 5824 5825 if ($sAction == 'Update') 5826 { 5827 // Already planned for an update 5828 // Add this new update spec to the list 5829 $bFoundSameSpec = false; 5830 foreach ($aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] as $aUpdateSpec) 5831 { 5832 if (($sColumn == $aUpdateSpec['column']) && ($sNewValue == $aUpdateSpec['newvalue'])) 5833 { 5834 $bFoundSameSpec = true; 5835 } 5836 } 5837 if (!$bFoundSameSpec) 5838 { 5839 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'][] = (array( 5840 'column' => $sColumn, 5841 'newvalue' => $sNewValue 5842 )); 5843 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] .= ', '.$sErrorDesc; 5844 } 5845 } 5846 } 5847 else 5848 { 5849 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Reason'] = $sErrorDesc; 5850 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action'] = 'Update'; 5851 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Action_Details'] = array(array('column' => $sColumn, 'newvalue' => $sNewValue)); 5852 $aErrorsAndFixes[$sRootClass][$sTable][$iRecordId]['Pass'] = 123; 5853 } 5854 5855 } 5856 } 5857 5858 /** 5859 * @param array $aErrorsAndFixes 5860 * @param int $iNewDelCount 5861 * @param array $aPlannedDel 5862 * 5863 * @throws \CoreException 5864 * @throws \Exception 5865 */ 5866 public static function DBCheckIntegrity_SinglePass(&$aErrorsAndFixes, &$iNewDelCount, &$aPlannedDel) 5867 { 5868 foreach(self::GetClasses() as $sClass) 5869 { 5870 if (!self::HasTable($sClass)) 5871 { 5872 continue; 5873 } 5874 $sRootClass = self::GetRootClass($sClass); 5875 $sTable = self::DBGetTable($sClass); 5876 $sKeyField = self::DBGetKey($sClass); 5877 5878 if (!self::IsStandaloneClass($sClass)) 5879 { 5880 if (self::IsRootClass($sClass)) 5881 { 5882 // Check that the final class field contains the name of a class which inherited from the current class 5883 // 5884 $sFinalClassField = self::DBGetClassField($sClass); 5885 5886 $aAllowedValues = self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); 5887 $sAllowedValues = implode(",", CMDBSource::Quote($aAllowedValues, true)); 5888 5889 $sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE `$sFinalClassField` NOT IN ($sAllowedValues)"; 5890 self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "final class (field `<em>$sFinalClassField</em>`) is wrong (expected a value in {".$sAllowedValues."})", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5891 } 5892 else 5893 { 5894 $sRootTable = self::DBGetTable($sRootClass); 5895 $sRootKey = self::DBGetKey($sRootClass); 5896 $sFinalClassField = self::DBGetClassField($sRootClass); 5897 5898 $aExpectedClasses = self::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); 5899 $sExpectedClasses = implode(",", CMDBSource::Quote($aExpectedClasses, true)); 5900 5901 // Check that any record found here has its counterpart in the root table 5902 // and which refers to a child class 5903 // 5904 $sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` as maintable LEFT JOIN `$sRootTable` ON maintable.`$sKeyField` = `$sRootTable`.`$sRootKey` AND `$sRootTable`.`$sFinalClassField` IN ($sExpectedClasses) WHERE `$sRootTable`.`$sRootKey` IS NULL"; 5905 self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Found a record in `<em>$sTable</em>`, but no counterpart in root table `<em>$sRootTable</em>` (inc. records pointing to a class in {".$sExpectedClasses."})", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5906 5907 // Check that any record found in the root table and referring to a child class 5908 // has its counterpart here (detect orphan nodes -root or in the middle of the hierarchy) 5909 // 5910 $sSelWrongRecs = "SELECT DISTINCT maintable.`$sRootKey` AS id FROM `$sRootTable` AS maintable LEFT JOIN `$sTable` ON maintable.`$sRootKey` = `$sTable`.`$sKeyField` WHERE `$sTable`.`$sKeyField` IS NULL AND maintable.`$sFinalClassField` IN ($sExpectedClasses)"; 5911 self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Found a record in root table `<em>$sRootTable</em>`, but no counterpart in table `<em>$sTable</em>` (root records pointing to a class in {".$sExpectedClasses."})", $sRootClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5912 } 5913 } 5914 5915 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 5916 { 5917 // Skip this attribute if not defined in this table 5918 if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) 5919 { 5920 continue; 5921 } 5922 5923 if ($oAttDef->IsExternalKey()) 5924 { 5925 // Check that any external field is pointing to an existing object 5926 // 5927 $sRemoteClass = $oAttDef->GetTargetClass(); 5928 $sRemoteTable = self::DBGetTable($sRemoteClass); 5929 $sRemoteKey = self::DBGetKey($sRemoteClass); 5930 5931 $aCols = $oAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) 5932 $sExtKeyField = current($aCols); // get the first column for an external key 5933 5934 // Note: a class/table may have an external key on itself 5935 $sSelBase = "SELECT DISTINCT maintable.`$sKeyField` AS id, maintable.`$sExtKeyField` AS extkey FROM `$sTable` AS maintable LEFT JOIN `$sRemoteTable` ON maintable.`$sExtKeyField` = `$sRemoteTable`.`$sRemoteKey`"; 5936 5937 $sSelWrongRecs = $sSelBase." WHERE `$sRemoteTable`.`$sRemoteKey` IS NULL"; 5938 if ($oAttDef->IsNullAllowed()) 5939 { 5940 // Exclude the records pointing to 0/null from the errors 5941 $sSelWrongRecs .= " AND maintable.`$sExtKeyField` IS NOT NULL"; 5942 $sSelWrongRecs .= " AND maintable.`$sExtKeyField` != 0"; 5943 self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') non existing objects", $sExtKeyField, 'null', $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5944 } 5945 else 5946 { 5947 self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') non existing objects", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5948 } 5949 5950 // Do almost the same, taking into account the records planned for deletion 5951 if (array_key_exists($sRemoteTable, $aPlannedDel) && count($aPlannedDel[$sRemoteTable]) > 0) 5952 { 5953 // This could be done by the mean of a 'OR ... IN (aIgnoreRecords) 5954 // but in that case you won't be able to track the root cause (cascading) 5955 $sSelWrongRecs = $sSelBase." WHERE maintable.`$sExtKeyField` IN ('".implode("', '", $aPlannedDel[$sRemoteTable])."')"; 5956 if ($oAttDef->IsNullAllowed()) 5957 { 5958 // Exclude the records pointing to 0/null from the errors 5959 $sSelWrongRecs .= " AND maintable.`$sExtKeyField` IS NOT NULL"; 5960 $sSelWrongRecs .= " AND maintable.`$sExtKeyField` != 0"; 5961 self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') a record planned for deletion", $sExtKeyField, 'null', $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5962 } 5963 else 5964 { 5965 self::DBCheckIntegrity_Check2Delete($sSelWrongRecs, "Record pointing to (external key '<em>$sAttCode</em>') a record planned for deletion", $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5966 } 5967 } 5968 } 5969 else 5970 { 5971 if ($oAttDef->IsBasedOnDBColumns()) 5972 { 5973 // Check that the values fit the allowed values 5974 // 5975 $aAllowedValues = self::GetAllowedValues_att($sClass, $sAttCode); 5976 if (!is_null($aAllowedValues) && count($aAllowedValues) > 0) 5977 { 5978 $sExpectedValues = implode(",", CMDBSource::Quote(array_keys($aAllowedValues), true)); 5979 5980 $aCols = $oAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) 5981 $sMyAttributeField = current($aCols); // get the first column for the moment 5982 $sDefaultValue = $oAttDef->GetDefaultValue(); 5983 $sSelWrongRecs = "SELECT DISTINCT maintable.`$sKeyField` AS id FROM `$sTable` AS maintable WHERE maintable.`$sMyAttributeField` NOT IN ($sExpectedValues)"; 5984 self::DBCheckIntegrity_Check2Update($sSelWrongRecs, "Record having a column ('<em>$sAttCode</em>') with an unexpected value", $sMyAttributeField, CMDBSource::Quote($sDefaultValue), $sClass, $aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 5985 } 5986 } 5987 } 5988 } 5989 } 5990 } 5991 5992 /** 5993 * @param string $sRepairUrl 5994 * @param string $sSQLStatementArgName 5995 * 5996 * @throws \CoreException 5997 * @throws \CoreWarning 5998 * @throws \Exception 5999 */ 6000 public static function DBCheckIntegrity($sRepairUrl = "", $sSQLStatementArgName = "") 6001 { 6002 // Records in error, and action to be taken: delete or update 6003 // by RootClass/Table/Record 6004 $aErrorsAndFixes = array(); 6005 6006 // Records to be ignored in the current/next pass 6007 // by Table = array of RecordId 6008 $aPlannedDel = array(); 6009 6010 // Count of errors in the next pass: no error means that we can leave... 6011 $iErrorCount = 0; 6012 // Limit in case of a bug in the algorythm 6013 $iLoopCount = 0; 6014 6015 $iNewDelCount = 1; // startup... 6016 while ($iNewDelCount > 0) 6017 { 6018 $iNewDelCount = 0; 6019 self::DBCheckIntegrity_SinglePass($aErrorsAndFixes, $iNewDelCount, $aPlannedDel); 6020 $iErrorCount += $iNewDelCount; 6021 6022 // Safety net #1 - limit the planned deletions 6023 // 6024 $iMaxDel = 1000; 6025 $iPlannedDel = 0; 6026 foreach($aPlannedDel as $sTable => $aPlannedDelOnTable) 6027 { 6028 $iPlannedDel += count($aPlannedDelOnTable); 6029 } 6030 if ($iPlannedDel > $iMaxDel) 6031 { 6032 throw new CoreWarning("DB Integrity Check safety net - Exceeding the limit of $iMaxDel planned record deletion"); 6033 } 6034 // Safety net #2 - limit the iterations 6035 // 6036 $iLoopCount++; 6037 $iMaxLoops = 10; 6038 if ($iLoopCount > $iMaxLoops) 6039 { 6040 throw new CoreWarning("DB Integrity Check safety net - Reached the limit of $iMaxLoops loops"); 6041 } 6042 } 6043 6044 // Display the results 6045 // 6046 $iIssueCount = 0; 6047 $aFixesDelete = array(); 6048 $aFixesUpdate = array(); 6049 6050 foreach($aErrorsAndFixes as $sRootClass => $aTables) 6051 { 6052 foreach($aTables as $sTable => $aRecords) 6053 { 6054 foreach($aRecords as $iRecord => $aError) 6055 { 6056 $sAction = $aError['Action']; 6057 $sReason = $aError['Reason']; 6058 6059 switch ($sAction) 6060 { 6061 case 'Delete': 6062 $sActionDetails = ""; 6063 $aFixesDelete[$sTable][] = $iRecord; 6064 break; 6065 6066 case 'Update': 6067 $aUpdateDesc = array(); 6068 foreach($aError['Action_Details'] as $aUpdateSpec) 6069 { 6070 $aUpdateDesc[] = $aUpdateSpec['column']." -> ".$aUpdateSpec['newvalue']; 6071 $aFixesUpdate[$sTable][$aUpdateSpec['column']][$aUpdateSpec['newvalue']][] = $iRecord; 6072 } 6073 $sActionDetails = "Set ".implode(", ", $aUpdateDesc); 6074 6075 break; 6076 6077 default: 6078 $sActionDetails = "bug: unknown action '$sAction'"; 6079 } 6080 $aIssues[] = "$sRootClass / $sTable / $iRecord / $sReason / $sAction / $sActionDetails"; 6081 $iIssueCount++; 6082 } 6083 } 6084 } 6085 6086 if ($iIssueCount > 0) 6087 { 6088 // Build the queries to fix in the database 6089 // 6090 // First step, be able to get class data out of the table name 6091 // Could be optimized, because we've made the job earlier... but few benefits, so... 6092 $aTable2ClassProp = array(); 6093 foreach(self::GetClasses() as $sClass) 6094 { 6095 if (!self::HasTable($sClass)) 6096 { 6097 continue; 6098 } 6099 6100 $sRootClass = self::GetRootClass($sClass); 6101 $sTable = self::DBGetTable($sClass); 6102 $sKeyField = self::DBGetKey($sClass); 6103 6104 $aErrorsAndFixes[$sRootClass][$sTable] = array(); 6105 $aTable2ClassProp[$sTable] = array('rootclass' => $sRootClass, 'class' => $sClass, 'keyfield' => $sKeyField); 6106 } 6107 // Second step, build a flat list of SQL queries 6108 $aSQLFixes = array(); 6109 $iPlannedUpdate = 0; 6110 foreach($aFixesUpdate as $sTable => $aColumns) 6111 { 6112 foreach($aColumns as $sColumn => $aNewValues) 6113 { 6114 foreach($aNewValues as $sNewValue => $aRecords) 6115 { 6116 $iPlannedUpdate += count($aRecords); 6117 $sWrongRecords = "'".implode("', '", $aRecords)."'"; 6118 $sKeyField = $aTable2ClassProp[$sTable]['keyfield']; 6119 6120 $aSQLFixes[] = "UPDATE `$sTable` SET `$sColumn` = $sNewValue WHERE `$sKeyField` IN ($sWrongRecords)"; 6121 } 6122 } 6123 } 6124 $iPlannedDel = 0; 6125 foreach($aFixesDelete as $sTable => $aRecords) 6126 { 6127 $iPlannedDel += count($aRecords); 6128 $sWrongRecords = "'".implode("', '", $aRecords)."'"; 6129 $sKeyField = $aTable2ClassProp[$sTable]['keyfield']; 6130 6131 $aSQLFixes[] = "DELETE FROM `$sTable` WHERE `$sKeyField` IN ($sWrongRecords)"; 6132 } 6133 6134 // Report the results 6135 // 6136 echo "<div style=\"width:100%;padding:10px;background:#FFAAAA;display:;\">"; 6137 echo "<h3>Database corruption error(s): $iErrorCount issues have been encountered. $iPlannedDel records will be deleted, $iPlannedUpdate records will be updated:</h3>\n"; 6138 // #@# later -> this is the responsibility of the caller to format the output 6139 echo "<ul class=\"treeview\">\n"; 6140 foreach($aIssues as $sIssueDesc) 6141 { 6142 echo "<li>$sIssueDesc</li>\n"; 6143 } 6144 echo "</ul>\n"; 6145 self::DBShowApplyForm($sRepairUrl, $sSQLStatementArgName, $aSQLFixes); 6146 echo "<p>Aborting...</p>\n"; 6147 echo "</div>\n"; 6148 exit; 6149 } 6150 } 6151 6152 /** 6153 * @param string|Config $config config file content or {@link Config} object 6154 * @param bool $bModelOnly 6155 * @param bool $bAllowCache 6156 * @param bool $bTraceSourceFiles 6157 * @param string $sEnvironment 6158 * 6159 * @throws \MySQLException 6160 * @throws \CoreException 6161 * @throws \DictExceptionUnknownLanguage 6162 * @throws \Exception 6163 */ 6164 public static function Startup($config, $bModelOnly = false, $bAllowCache = true, $bTraceSourceFiles = false, $sEnvironment = 'production') 6165 { 6166 self::$m_sEnvironment = $sEnvironment; 6167 6168 if (!defined('MODULESROOT')) 6169 { 6170 define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/'); 6171 6172 self::$m_bTraceSourceFiles = $bTraceSourceFiles; 6173 6174 // $config can be either a filename, or a Configuration object (volatile!) 6175 if ($config instanceof Config) 6176 { 6177 self::LoadConfig($config, $bAllowCache); 6178 } 6179 else 6180 { 6181 self::LoadConfig(new Config($config), $bAllowCache); 6182 } 6183 6184 if ($bModelOnly) 6185 { 6186 return; 6187 } 6188 } 6189 6190 CMDBSource::SelectDB(self::$m_sDBName); 6191 6192 foreach(MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) 6193 { 6194 $oPHPClass::OnMetaModelStarted(); 6195 } 6196 6197 ExpressionCache::Warmup(); 6198 } 6199 6200 /** 6201 * @param Config $oConfiguration 6202 * @param bool $bAllowCache 6203 * 6204 * @throws \CoreException 6205 * @throws \DictExceptionUnknownLanguage 6206 * @throws \Exception 6207 * @throws \MySQLException 6208 */ 6209 public static function LoadConfig($oConfiguration, $bAllowCache = false) 6210 { 6211 self::$m_oConfig = $oConfiguration; 6212 6213 // Set log ASAP 6214 if (self::$m_oConfig->GetLogGlobal()) 6215 { 6216 if (self::$m_oConfig->GetLogIssue()) 6217 { 6218 self::$m_bLogIssue = true; 6219 IssueLog::Enable(APPROOT.'log/error.log'); 6220 } 6221 self::$m_bLogNotification = self::$m_oConfig->GetLogNotification(); 6222 self::$m_bLogWebService = self::$m_oConfig->GetLogWebService(); 6223 6224 ToolsLog::Enable(APPROOT.'log/tools.log'); 6225 } 6226 else 6227 { 6228 self::$m_bLogIssue = false; 6229 self::$m_bLogNotification = false; 6230 self::$m_bLogWebService = false; 6231 } 6232 6233 ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration')); 6234 ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory')); 6235 ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id')); 6236 6237 self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write'); 6238 self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys'); 6239 6240 self::$m_bUseAPCCache = $bAllowCache 6241 && self::$m_oConfig->Get('apc_cache.enabled') 6242 && function_exists('apc_fetch') 6243 && function_exists('apc_store'); 6244 6245 DBSearch::EnableQueryCache(self::$m_oConfig->GetQueryCacheEnabled(), self::$m_bUseAPCCache, self::$m_oConfig->Get('apc_cache.query_ttl')); 6246 DBSearch::EnableQueryTrace(self::$m_oConfig->GetLogQueries()); 6247 DBSearch::EnableQueryIndentation(self::$m_oConfig->Get('query_indentation_enabled')); 6248 DBSearch::EnableOptimizeQuery(self::$m_oConfig->Get('query_optimization_enabled')); 6249 6250 // PHP timezone first... 6251 // 6252 $sPHPTimezone = self::$m_oConfig->Get('timezone'); 6253 if ($sPHPTimezone == '') 6254 { 6255 // Leave as is... up to the admin to set a value somewhere... 6256 //$sPHPTimezone = date_default_timezone_get(); 6257 } 6258 else 6259 { 6260 date_default_timezone_set($sPHPTimezone); 6261 } 6262 6263 // Note: load the dictionary as soon as possible, because it might be 6264 // needed when some error occur 6265 $sAppIdentity = 'itop-'.MetaModel::GetEnvironmentId(); 6266 if (self::$m_bUseAPCCache) 6267 { 6268 Dict::EnableCache($sAppIdentity); 6269 } 6270 require_once(APPROOT.'env-'.self::$m_sEnvironment.'/dictionaries/languages.php'); 6271 6272 // Set the default language... 6273 Dict::SetDefaultLanguage(self::$m_oConfig->GetDefaultLanguage()); 6274 6275 // Romain: this is the only way I've found to cope with the fact that 6276 // classes have to be derived from cmdbabstract (to be editable in the UI) 6277 require_once(APPROOT.'/application/cmdbabstract.class.inc.php'); 6278 6279 require_once(APPROOT.'core/autoload.php'); 6280 require_once(APPROOT.'env-'.self::$m_sEnvironment.'/autoload.php'); 6281 6282 foreach(self::$m_oConfig->GetAddons() as $sModule => $sToInclude) 6283 { 6284 self::IncludeModule($sToInclude, 'addons'); 6285 } 6286 6287 $sSource = self::$m_oConfig->Get('db_name'); 6288 $sTablePrefix = self::$m_oConfig->Get('db_subname'); 6289 6290 if (self::$m_bUseAPCCache) 6291 { 6292 $oKPI = new ExecutionKPI(); 6293 // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter 6294 // 6295 $sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-metamodel'; 6296 $result = apc_fetch($sOqlAPCCacheId); 6297 6298 if (is_array($result)) 6299 { 6300 // todo - verifier que toutes les classes mentionnees ici sont chargees dans InitClasses() 6301 self::$m_aExtensionClasses = $result['m_aExtensionClasses']; 6302 self::$m_Category2Class = $result['m_Category2Class']; 6303 self::$m_aRootClasses = $result['m_aRootClasses']; 6304 self::$m_aParentClasses = $result['m_aParentClasses']; 6305 self::$m_aChildClasses = $result['m_aChildClasses']; 6306 self::$m_aClassParams = $result['m_aClassParams']; 6307 self::$m_aAttribDefs = $result['m_aAttribDefs']; 6308 self::$m_aAttribOrigins = $result['m_aAttribOrigins']; 6309 self::$m_aIgnoredAttributes = $result['m_aIgnoredAttributes']; 6310 self::$m_aFilterDefs = $result['m_aFilterDefs']; 6311 self::$m_aFilterOrigins = $result['m_aFilterOrigins']; 6312 self::$m_aListInfos = $result['m_aListInfos']; 6313 self::$m_aListData = $result['m_aListData']; 6314 self::$m_aRelationInfos = $result['m_aRelationInfos']; 6315 self::$m_aStates = $result['m_aStates']; 6316 self::$m_aStimuli = $result['m_aStimuli']; 6317 self::$m_aTransitions = $result['m_aTransitions']; 6318 self::$m_aHighlightScales = $result['m_aHighlightScales']; 6319 self::$m_aEnumToMeta = $result['m_aEnumToMeta']; 6320 } 6321 $oKPI->ComputeAndReport('Metamodel APC (fetch + read)'); 6322 } 6323 6324 if (count(self::$m_aAttribDefs) == 0) 6325 { 6326 // The includes have been included, let's browse the existing classes and 6327 // develop some data based on the proposed model 6328 $oKPI = new ExecutionKPI(); 6329 6330 self::InitClasses($sTablePrefix); 6331 6332 $oKPI->ComputeAndReport('Initialization of Data model structures'); 6333 if (self::$m_bUseAPCCache) 6334 { 6335 $oKPI = new ExecutionKPI(); 6336 6337 $aCache = array(); 6338 $aCache['m_aExtensionClasses'] = self::$m_aExtensionClasses; 6339 $aCache['m_Category2Class'] = self::$m_Category2Class; 6340 $aCache['m_aRootClasses'] = self::$m_aRootClasses; // array of "classname" => "rootclass" 6341 $aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass") 6342 $aCache['m_aChildClasses'] = self::$m_aChildClasses; // array of ("classname" => array of "childclass") 6343 $aCache['m_aClassParams'] = self::$m_aClassParams; // array of ("classname" => array of class information) 6344 $aCache['m_aAttribDefs'] = self::$m_aAttribDefs; // array of ("classname" => array of attributes) 6345 $aCache['m_aAttribOrigins'] = self::$m_aAttribOrigins; // array of ("classname" => array of ("attcode"=>"sourceclass")) 6346 $aCache['m_aIgnoredAttributes'] = self::$m_aIgnoredAttributes; //array of ("classname" => array of ("attcode") 6347 $aCache['m_aFilterDefs'] = self::$m_aFilterDefs; // array of ("classname" => array filterdef) 6348 $aCache['m_aFilterOrigins'] = self::$m_aFilterOrigins; // array of ("classname" => array of ("attcode"=>"sourceclass")) 6349 $aCache['m_aListInfos'] = self::$m_aListInfos; // array of ("listcode" => various info on the list, common to every classes) 6350 $aCache['m_aListData'] = self::$m_aListData; // array of ("classname" => array of "listcode" => list) 6351 $aCache['m_aRelationInfos'] = self::$m_aRelationInfos; // array of ("relcode" => various info on the list, common to every classes) 6352 $aCache['m_aStates'] = self::$m_aStates; // array of ("classname" => array of "statecode"=>array('label'=>..., attribute_inherit=> attribute_list=>...)) 6353 $aCache['m_aStimuli'] = self::$m_aStimuli; // array of ("classname" => array of ("stimuluscode"=>array('label'=>...))) 6354 $aCache['m_aTransitions'] = self::$m_aTransitions; // array of ("classname" => array of ("statcode_from"=>array of ("stimuluscode" => array('target_state'=>..., 'actions'=>array of handlers procs, 'user_restriction'=>TBD))) 6355 $aCache['m_aHighlightScales'] = self::$m_aHighlightScales; // array of ("classname" => array of higlightcodes))) 6356 $aCache['m_aEnumToMeta'] = self::$m_aEnumToMeta; 6357 apc_store($sOqlAPCCacheId, $aCache); 6358 $oKPI->ComputeAndReport('Metamodel APC (store)'); 6359 } 6360 } 6361 6362 self::$m_sDBName = $sSource; 6363 self::$m_sTablePrefix = $sTablePrefix; 6364 6365 CMDBSource::InitFromConfig(self::$m_oConfig); 6366 // Later when timezone implementation is correctly done: CMDBSource::SetTimezone($sDBTimezone); 6367 } 6368 6369 /** 6370 * @param string $sModule 6371 * @param string $sProperty 6372 * @param $defaultvalue 6373 * 6374 * @return mixed 6375 */ 6376 public static function GetModuleSetting($sModule, $sProperty, $defaultvalue = null) 6377 { 6378 return self::$m_oConfig->GetModuleSetting($sModule, $sProperty, $defaultvalue); 6379 } 6380 6381 /** 6382 * @param string $sModule 6383 * @param string $sProperty 6384 * @param $defaultvalue 6385 * 6386 * @return ?? 6387 */ 6388 public static function GetModuleParameter($sModule, $sProperty, $defaultvalue = null) 6389 { 6390 $value = $defaultvalue; 6391 if (!self::$m_aModulesParameters[$sModule] == null) 6392 { 6393 $value = self::$m_aModulesParameters[$sModule]->Get($sProperty, $defaultvalue); 6394 } 6395 return $value; 6396 } 6397 6398 /** 6399 * @return Config 6400 */ 6401 public static function GetConfig() 6402 { 6403 return self::$m_oConfig; 6404 } 6405 6406 /** 6407 * @return string The environment in which the model has been loaded (e.g. 'production') 6408 */ 6409 public static function GetEnvironment() 6410 { 6411 return self::$m_sEnvironment; 6412 } 6413 6414 /** 6415 * @return string 6416 */ 6417 public static function GetEnvironmentId() 6418 { 6419 return md5(APPROOT).'-'.self::$m_sEnvironment; 6420 } 6421 6422 /** @var array */ 6423 protected static $m_aExtensionClasses = array(); 6424 6425 /** 6426 * @param string $sToInclude 6427 * @param string $sModuleType 6428 * 6429 * @throws \CoreException 6430 */ 6431 public static function IncludeModule($sToInclude, $sModuleType = null) 6432 { 6433 $sFirstChar = substr($sToInclude, 0, 1); 6434 $sSecondChar = substr($sToInclude, 1, 1); 6435 if (($sFirstChar != '/') && ($sFirstChar != '\\') && ($sSecondChar != ':')) 6436 { 6437 // It is a relative path, prepend APPROOT 6438 if (substr($sToInclude, 0, 3) == '../') 6439 { 6440 // Preserve compatibility with config files written before 1.0.1 6441 // Replace '../' by '<root>/' 6442 $sFile = APPROOT.'/'.substr($sToInclude, 3); 6443 } 6444 else 6445 { 6446 $sFile = APPROOT.'/'.$sToInclude; 6447 } 6448 } 6449 else 6450 { 6451 // Leave as is - should be an absolute path 6452 $sFile = $sToInclude; 6453 } 6454 if (!file_exists($sFile)) 6455 { 6456 $sConfigFile = self::$m_oConfig->GetLoadedFile(); 6457 if ($sModuleType == null) 6458 { 6459 throw new CoreException("Include: unable to load the file '$sFile'"); 6460 } 6461 else 6462 { 6463 if (strlen($sConfigFile) > 0) 6464 { 6465 throw new CoreException('Include: wrong file name in configuration file', array('config file' => $sConfigFile, 'section' => $sModuleType, 'filename' => $sFile)); 6466 } 6467 else 6468 { 6469 // The configuration is in memory only 6470 throw new CoreException('Include: wrong file name in configuration file (in memory)', array('section' => $sModuleType, 'filename' => $sFile)); 6471 } 6472 } 6473 } 6474 6475 // Note: We do not expect the modules to output characters while loading them. 6476 // Therefore, and because unexpected characters can corrupt the output, 6477 // they must be trashed here. 6478 // Additionnaly, pages aiming at delivering data in their output can call WebPage::TrashUnexpectedOutput() 6479 // to get rid of chars that could be generated during the execution of the code 6480 ob_start(); 6481 require_once($sFile); 6482 $sPreviousContent = ob_get_clean(); 6483 if (self::$m_oConfig->Get('debug_report_spurious_chars')) 6484 { 6485 if ($sPreviousContent != '') 6486 { 6487 IssueLog::Error("Spurious characters injected by '$sFile'"); 6488 } 6489 } 6490 } 6491 6492 // Building an object 6493 // 6494 // 6495 /** @var array */ 6496 private static $aQueryCacheGetObject = array(); 6497 /** @var array */ 6498 private static $aQueryCacheGetObjectHits = array(); 6499 6500 /** 6501 * @return string 6502 */ 6503 public static function GetQueryCacheStatus() 6504 { 6505 $aRes = array(); 6506 $iTotalHits = 0; 6507 foreach(self::$aQueryCacheGetObjectHits as $sClassSign => $iHits) 6508 { 6509 $aRes[] = "$sClassSign: $iHits"; 6510 $iTotalHits += $iHits; 6511 } 6512 return $iTotalHits.' ('.implode(', ', $aRes).')'; 6513 } 6514 6515 /** 6516 * @param string $sClass 6517 * @param int $iKey 6518 * @param bool $bMustBeFound 6519 * @param bool $bAllowAllData if true then no rights filtering 6520 * @param array $aModifierProperties 6521 * 6522 * @return string[] column name / value array 6523 * @throws CoreException if no result found and $bMustBeFound=true 6524 * @throws \Exception 6525 * 6526 * @see utils::PushArchiveMode() to enable search on archived objects 6527 */ 6528 public static function MakeSingleRow($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null) 6529 { 6530 // Build the query cache signature 6531 // 6532 $sQuerySign = $sClass; 6533 if ($bAllowAllData) 6534 { 6535 $sQuerySign .= '_all_'; 6536 } 6537 if (is_array($aModifierProperties) && (count($aModifierProperties) > 0)) 6538 { 6539 array_multisort($aModifierProperties); 6540 $sModifierProperties = json_encode($aModifierProperties); 6541 $sQuerySign .= '_all_'.md5($sModifierProperties); 6542 } 6543 $sQuerySign .= utils::IsArchiveMode() ? '_arch_' : ''; 6544 6545 if (!array_key_exists($sQuerySign, self::$aQueryCacheGetObject)) 6546 { 6547 // NOTE: Quick and VERY dirty caching mechanism which relies on 6548 // the fact that the string '987654321' will never appear in the 6549 // standard query 6550 // This could be simplified a little, relying solely on the query cache, 6551 // but this would slow down -by how much time?- the application 6552 $oFilter = new DBObjectSearch($sClass); 6553 $oFilter->AddCondition('id', 987654321, '='); 6554 if ($aModifierProperties) 6555 { 6556 foreach($aModifierProperties as $sPluginClass => $aProperties) 6557 { 6558 foreach($aProperties as $sProperty => $value) 6559 { 6560 $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value); 6561 } 6562 } 6563 } 6564 if ($bAllowAllData) 6565 { 6566 $oFilter->AllowAllData(); 6567 } 6568 $oFilter->NoContextParameters(); 6569 $sSQL = $oFilter->MakeSelectQuery(); 6570 self::$aQueryCacheGetObject[$sQuerySign] = $sSQL; 6571 self::$aQueryCacheGetObjectHits[$sQuerySign] = 0; 6572 } 6573 else 6574 { 6575 $sSQL = self::$aQueryCacheGetObject[$sQuerySign]; 6576 self::$aQueryCacheGetObjectHits[$sQuerySign] += 1; 6577 } 6578 $sSQL = str_replace(CMDBSource::Quote(987654321), CMDBSource::Quote($iKey), $sSQL); 6579 $res = CMDBSource::Query($sSQL); 6580 6581 $aRow = CMDBSource::FetchArray($res); 6582 CMDBSource::FreeResult($res); 6583 6584 if ($bMustBeFound && empty($aRow)) 6585 { 6586 throw new CoreException("No result for the single row query: '$sSQL'"); 6587 } 6588 6589 return $aRow; 6590 } 6591 6592 /** 6593 * Converts a column name / value array to a {@link DBObject} 6594 * 6595 * @param string $sClass 6596 * @param string[] $aRow column name / value array 6597 * @param string $sClassAlias 6598 * @param string[] $aAttToLoad 6599 * @param array $aExtendedDataSpec 6600 * 6601 * @return DBObject 6602 * @throws CoreUnexpectedValue if finalClass attribute wasn't specified but is needed 6603 * @throws CoreException if finalClass cannot be found 6604 */ 6605 public static function GetObjectByRow($sClass, $aRow, $sClassAlias = '', $aAttToLoad = null, $aExtendedDataSpec = null) 6606 { 6607 self::_check_subclass($sClass); 6608 6609 if (strlen($sClassAlias) == 0) 6610 { 6611 $sClassAlias = $sClass; 6612 } 6613 6614 // Compound objects: if available, get the final object class 6615 // 6616 if (!array_key_exists($sClassAlias."finalclass", $aRow)) 6617 { 6618 // Either this is a bug (forgot to specify a root class with a finalclass field 6619 // Or this is the expected behavior, because the object is not made of several tables 6620 if (self::IsAbstract($sClass)) 6621 { 6622 throw new CoreUnexpectedValue("Querying the abstract '$sClass' class without finalClass attribute"); 6623 } 6624 if (self::HasChildrenClasses($sClass)) 6625 { 6626 throw new CoreUnexpectedValue("Querying the '$sClass' class without the finalClass attribute, whereas this class has children"); 6627 } 6628 } 6629 elseif (empty($aRow[$sClassAlias."finalclass"])) 6630 { 6631 // The data is missing in the DB 6632 // @#@ possible improvement: check that the class is valid ! 6633 $sRootClass = self::GetRootClass($sClass); 6634 $sFinalClassField = self::DBGetClassField($sRootClass); 6635 throw new CoreException("Empty class name for object $sClass::{$aRow["id"]} (root class '$sRootClass', field '{$sFinalClassField}' is empty)"); 6636 } 6637 else 6638 { 6639 // do the job for the real target class 6640 $sClass = $aRow[$sClassAlias."finalclass"]; 6641 } 6642 return new $sClass($aRow, $sClassAlias, $aAttToLoad, $aExtendedDataSpec); 6643 } 6644 6645 /** 6646 * Search for the specified class and id. 6647 * 6648 * @param string $sClass 6649 * @param int $iKey id value of the object to retrieve 6650 * @param bool $bMustBeFound see throws ArchivedObjectException 6651 * @param bool $bAllowAllData if true then no rights filtering 6652 * @param null $aModifierProperties 6653 * 6654 * @return DBObject|null null if : (the object is not found) or (archive mode disabled and object is archived and 6655 * $bMustBeFound=false) 6656 * @throws CoreException if no result found and $bMustBeFound=true 6657 * @throws ArchivedObjectException if archive mode disabled and result is archived and $bMustBeFound=true 6658 * @throws \Exception 6659 * 6660 * @see MetaModel::GetObjectWithArchive to get object even if it's archived 6661 * @see utils::PushArchiveMode() to enable search on archived objects 6662 */ 6663 public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null) 6664 { 6665 $oObject = self::GetObjectWithArchive($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties); 6666 6667 if (empty($oObject)) 6668 { 6669 return null; 6670 } 6671 6672 if (!utils::IsArchiveMode() && $oObject->IsArchived()) 6673 { 6674 if ($bMustBeFound) 6675 { 6676 throw new ArchivedObjectException("The object $sClass::$iKey is archived"); 6677 } 6678 else 6679 { 6680 return null; 6681 } 6682 } 6683 6684 return $oObject; 6685 } 6686 6687 /** 6688 * Search for the specified class and id. If the object is archived it will be returned anyway (this is for pre-2.4 6689 * module compatibility, see N.1108) 6690 * 6691 * @param string $sClass 6692 * @param int $iKey 6693 * @param bool $bMustBeFound 6694 * @param bool $bAllowAllData 6695 * @param array $aModifierProperties 6696 * 6697 * @return DBObject|null 6698 * @throws CoreException if no result found and $bMustBeFound=true 6699 * @throws \Exception 6700 * 6701 * @since 2.4 introduction of the archive functionalities 6702 * 6703 * @see MetaModel::GetObject() same but returns null or ArchivedObjectFoundException if object exists but is 6704 * archived 6705 */ 6706 public static function GetObjectWithArchive($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null) 6707 { 6708 self::_check_subclass($sClass); 6709 6710 utils::PushArchiveMode(true); 6711 try 6712 { 6713 $aRow = self::MakeSingleRow($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties); 6714 } 6715 catch(Exception $e) 6716 { 6717 // In the finally block we will pop the pushed archived mode 6718 // otherwise the application stays in ArchiveMode true which has caused hazardious behavior! 6719 throw $e; 6720 } 6721 finally 6722 { 6723 utils::PopArchiveMode(); 6724 } 6725 6726 if (empty($aRow)) 6727 { 6728 return null; 6729 } 6730 6731 return self::GetObjectByRow($sClass, $aRow); // null should not be returned, this is handled in the callee 6732 } 6733 6734 /** 6735 * @param string $sClass 6736 * @param string $sName 6737 * @param bool $bMustBeFound 6738 * 6739 * @return \DBObject|null 6740 * @throws \CoreException 6741 */ 6742 public static function GetObjectByName($sClass, $sName, $bMustBeFound = true) 6743 { 6744 self::_check_subclass($sClass); 6745 6746 $oObjSearch = new DBObjectSearch($sClass); 6747 $oObjSearch->AddNameCondition($sName); 6748 $oSet = new DBObjectSet($oObjSearch); 6749 if ($oSet->Count() != 1) 6750 { 6751 if ($bMustBeFound) 6752 { 6753 throw new CoreException('Failed to get an object by its name', array('class' => $sClass, 'name' => $sName)); 6754 } 6755 return null; 6756 } 6757 6758 return $oSet->fetch(); 6759 } 6760 6761 /** @var array */ 6762 static protected $m_aCacheObjectByColumn = array(); 6763 6764 /** 6765 * @param string $sClass 6766 * @param string $sAttCode 6767 * @param $value 6768 * @param bool $bMustBeFoundUnique 6769 * 6770 * @return \DBObject 6771 * @throws \CoreException 6772 * @throws \Exception 6773 */ 6774 public static function GetObjectByColumn($sClass, $sAttCode, $value, $bMustBeFoundUnique = true) 6775 { 6776 if (!isset(self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value])) 6777 { 6778 self::_check_subclass($sClass); 6779 6780 $oObjSearch = new DBObjectSearch($sClass); 6781 $oObjSearch->AddCondition($sAttCode, $value, '='); 6782 $oSet = new DBObjectSet($oObjSearch); 6783 if ($oSet->Count() == 1) 6784 { 6785 self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value] = $oSet->fetch(); 6786 } 6787 else 6788 { 6789 if ($bMustBeFoundUnique) 6790 { 6791 throw new CoreException('Failed to get an object by column', array('class' => $sClass, 'attcode' => $sAttCode, 'value' => $value, 'matches' => $oSet->Count())); 6792 } 6793 self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value] = null; 6794 } 6795 } 6796 6797 return self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value]; 6798 } 6799 6800 /** 6801 * @param string $sQuery 6802 * @param array $aParams 6803 * @param bool $bAllowAllData 6804 * 6805 * @return \DBObject 6806 * @throws \OQLException 6807 */ 6808 public static function GetObjectFromOQL($sQuery, $aParams = null, $bAllowAllData = false) 6809 { 6810 $oFilter = DBObjectSearch::FromOQL($sQuery, $aParams); 6811 if ($bAllowAllData) 6812 { 6813 $oFilter->AllowAllData(); 6814 } 6815 $oSet = new DBObjectSet($oFilter); 6816 6817 return $oSet->Fetch(); 6818 } 6819 6820 /** 6821 * @param string $sTargetClass 6822 * @param int $iKey 6823 * 6824 * @return string 6825 * @throws \ArchivedObjectException 6826 * @throws \CoreException 6827 * @throws \DictExceptionMissingString 6828 * @throws \OQLException 6829 * @throws \Exception 6830 */ 6831 public static function GetHyperLink($sTargetClass, $iKey) 6832 { 6833 if ($iKey < 0) 6834 { 6835 return "$sTargetClass: $iKey (invalid value)"; 6836 } 6837 $oObj = self::GetObject($sTargetClass, $iKey, false); 6838 if (is_null($oObj)) 6839 { 6840 // Whatever we are looking for, the root class is the key to search for 6841 $sRootClass = self::GetRootClass($sTargetClass); 6842 $oSearch = DBObjectSearch::FromOQL('SELECT CMDBChangeOpDelete WHERE objclass = :objclass AND objkey = :objkey', array('objclass' => $sRootClass, 'objkey' => $iKey)); 6843 $oSet = new DBObjectSet($oSearch); 6844 $oRecord = $oSet->Fetch(); 6845 // An empty fname is obtained with iTop < 2.0 6846 if (is_null($oRecord) || (strlen(trim($oRecord->Get('fname'))) == 0)) 6847 { 6848 $sName = Dict::Format('Core:UnknownObjectLabel', $sTargetClass, $iKey); 6849 $sTitle = Dict::S('Core:UnknownObjectTip'); 6850 } 6851 else 6852 { 6853 $sName = $oRecord->Get('fname'); 6854 $sTitle = Dict::Format('Core:DeletedObjectTip', $oRecord->Get('date'), $oRecord->Get('userinfo')); 6855 } 6856 return '<span class="itop-deleted-object" title="'.htmlentities($sTitle, ENT_QUOTES, 'UTF-8').'">'.htmlentities($sName, ENT_QUOTES, 'UTF-8').'</span>'; 6857 } 6858 return $oObj->GetHyperLink(); 6859 } 6860 6861 /** 6862 * @param string $sClass 6863 * @param array|null $aValues array of attcode => value 6864 * 6865 * @return DBObject 6866 * @throws \CoreException 6867 */ 6868 public static function NewObject($sClass, $aValues = null) 6869 { 6870 self::_check_subclass($sClass); 6871 $oRet = new $sClass(); 6872 if (is_array($aValues)) 6873 { 6874 foreach($aValues as $sAttCode => $value) 6875 { 6876 $oRet->Set($sAttCode, $value); 6877 } 6878 } 6879 6880 return $oRet; 6881 } 6882 6883 /** 6884 * @param string $sClass 6885 * 6886 * @return int 6887 * @throws \CoreException 6888 */ 6889 public static function GetNextKey($sClass) 6890 { 6891 $sRootClass = MetaModel::GetRootClass($sClass); 6892 $sRootTable = MetaModel::DBGetTable($sRootClass); 6893 6894 return CMDBSource::GetNextInsertId($sRootTable); 6895 } 6896 6897 /** 6898 * Deletion of records, bypassing {@link DBObject::DBDelete} !!! 6899 * It is NOT recommended to use this shortcut 6900 * In particular, it will not work 6901 * - if the class is not a final class 6902 * - if the class has a hierarchical key (need to rebuild the indexes) 6903 * - if the class overload DBDelete ! 6904 * 6905 * @todo: protect it against forbidden usages (in such a case, delete objects one by one) 6906 * 6907 * @param \DBObjectSearch $oFilter 6908 * 6909 * @throws \MySQLException 6910 * @throws \MySQLHasGoneAwayException 6911 */ 6912 public static function BulkDelete(DBObjectSearch $oFilter) 6913 { 6914 $sSQL = $oFilter->MakeDeleteQuery(); 6915 if (!self::DBIsReadOnly()) 6916 { 6917 CMDBSource::Query($sSQL); 6918 } 6919 } 6920 6921 /** 6922 * @param DBObjectSearch $oFilter 6923 * @param array $aValues array of attcode => value 6924 * 6925 * @return int Modified objects 6926 * @throws \MySQLException 6927 * @throws \MySQLHasGoneAwayException 6928 */ 6929 public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues) 6930 { 6931 // $aValues is an array of $sAttCode => $value 6932 $sSQL = $oFilter->MakeUpdateQuery($aValues); 6933 if (!self::DBIsReadOnly()) 6934 { 6935 CMDBSource::Query($sSQL); 6936 } 6937 return CMDBSource::AffectedRows(); 6938 } 6939 6940 /** 6941 * Helper to remove selected objects without calling any handler 6942 * Surpasses BulkDelete as it can handle abstract classes, but has the other limitation as it bypasses standard 6943 * objects handlers 6944 * 6945 * @param string $oFilter Scope of objects to wipe out 6946 * 6947 * @return int The count of deleted objects 6948 * @throws \CoreException 6949 */ 6950 public static function PurgeData($oFilter) 6951 { 6952 $sTargetClass = $oFilter->GetClass(); 6953 $oSet = new DBObjectSet($oFilter); 6954 $oSet->OptimizeColumnLoad(array($sTargetClass => array('finalclass'))); 6955 $aIdToClass = $oSet->GetColumnAsArray('finalclass', true); 6956 6957 $aIds = array_keys($aIdToClass); 6958 if (count($aIds) > 0) 6959 { 6960 $aQuotedIds = CMDBSource::Quote($aIds); 6961 $sIdList = implode(',', $aQuotedIds); 6962 $aTargetClasses = array_merge( 6963 self::EnumChildClasses($sTargetClass, ENUM_CHILD_CLASSES_ALL), 6964 self::EnumParentClasses($sTargetClass, ENUM_PARENT_CLASSES_EXCLUDELEAF) 6965 ); 6966 foreach($aTargetClasses as $sSomeClass) 6967 { 6968 $sTable = MetaModel::DBGetTable($sSomeClass); 6969 $sPKField = MetaModel::DBGetKey($sSomeClass); 6970 6971 $sDeleteSQL = "DELETE FROM `$sTable` WHERE `$sPKField` IN ($sIdList)"; 6972 CMDBSource::DeleteFrom($sDeleteSQL); 6973 } 6974 } 6975 return count($aIds); 6976 } 6977 6978 // Links 6979 // 6980 // 6981 /** 6982 * @param string $sClass 6983 * 6984 * @return array 6985 * @throws \CoreException 6986 */ 6987 public static function EnumReferencedClasses($sClass) 6988 { 6989 self::_check_subclass($sClass); 6990 6991 // 1-N links (referenced by my class), returns an array of sAttCode=>sClass 6992 $aResult = array(); 6993 foreach(self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef) 6994 { 6995 if ($oAttDef->IsExternalKey()) 6996 { 6997 $aResult[$sAttCode] = $oAttDef->GetTargetClass(); 6998 } 6999 } 7000 7001 return $aResult; 7002 } 7003 7004 /** 7005 * @param string $sClass 7006 * @param bool $bSkipLinkingClasses 7007 * @param bool $bInnerJoinsOnly 7008 * 7009 * @return array 7010 * @throws \CoreException 7011 */ 7012 public static function EnumReferencingClasses($sClass, $bSkipLinkingClasses = false, $bInnerJoinsOnly = false) 7013 { 7014 self::_check_subclass($sClass); 7015 7016 if ($bSkipLinkingClasses) 7017 { 7018 $aLinksClasses = self::EnumLinksClasses(); 7019 } 7020 7021 // 1-N links (referencing my class), array of sClass => array of sAttcode 7022 $aResult = array(); 7023 foreach(self::$m_aAttribDefs as $sSomeClass => $aClassAttributes) 7024 { 7025 if ($bSkipLinkingClasses && in_array($sSomeClass, $aLinksClasses)) 7026 { 7027 continue; 7028 } 7029 7030 $aExtKeys = array(); 7031 foreach($aClassAttributes as $sAttCode => $oAttDef) 7032 { 7033 if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass) 7034 { 7035 continue; 7036 } 7037 if ($oAttDef->IsExternalKey() && (self::IsParentClass($oAttDef->GetTargetClass(), $sClass))) 7038 { 7039 if ($bInnerJoinsOnly && $oAttDef->IsNullAllowed()) 7040 { 7041 continue; 7042 } 7043 // Ok, I want this one 7044 $aExtKeys[$sAttCode] = $oAttDef; 7045 } 7046 } 7047 if (count($aExtKeys) != 0) 7048 { 7049 $aResult[$sSomeClass] = $aExtKeys; 7050 } 7051 } 7052 return $aResult; 7053 } 7054 7055 /** 7056 * @deprecated It is not recommended to use this function: call {@link MetaModel::GetLinkClasses} instead ! 7057 * The only difference with EnumLinkingClasses is the output format 7058 * 7059 * @return string[] classes having at least two external keys (thus too many classes as compared to GetLinkClasses) 7060 * 7061 * @see MetaModel::GetLinkClasses 7062 */ 7063 public static function EnumLinksClasses() 7064 { 7065 // Returns a flat array of classes having at least two external keys 7066 $aResult = array(); 7067 foreach(self::$m_aAttribDefs as $sSomeClass => $aClassAttributes) 7068 { 7069 $iExtKeyCount = 0; 7070 foreach($aClassAttributes as $sAttCode => $oAttDef) 7071 { 7072 if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass) 7073 { 7074 continue; 7075 } 7076 if ($oAttDef->IsExternalKey()) 7077 { 7078 $iExtKeyCount++; 7079 } 7080 } 7081 if ($iExtKeyCount >= 2) 7082 { 7083 $aResult[] = $sSomeClass; 7084 } 7085 } 7086 return $aResult; 7087 } 7088 7089 /** 7090 * @deprecated It is not recommended to use this function: call {@link MetaModel::GetLinkClasses} instead ! 7091 * The only difference with EnumLinksClasses is the output format 7092 * 7093 * @param string $sClass 7094 * 7095 * @return string[] classes having at least two external keys (thus too many classes as compared to GetLinkClasses) 7096 * @throws \CoreException 7097 * 7098 * @see MetaModel::GetLinkClasses 7099 */ 7100 public static function EnumLinkingClasses($sClass = "") 7101 { 7102 // N-N links, array of sLinkClass => (array of sAttCode=>sClass) 7103 $aResult = array(); 7104 foreach(self::EnumLinksClasses() as $sSomeClass) 7105 { 7106 $aTargets = array(); 7107 $bFoundClass = false; 7108 foreach(self::ListAttributeDefs($sSomeClass) as $sAttCode => $oAttDef) 7109 { 7110 if (self::$m_aAttribOrigins[$sSomeClass][$sAttCode] != $sSomeClass) 7111 { 7112 continue; 7113 } 7114 if ($oAttDef->IsExternalKey()) 7115 { 7116 $sRemoteClass = $oAttDef->GetTargetClass(); 7117 if (empty($sClass)) 7118 { 7119 $aTargets[$sAttCode] = $sRemoteClass; 7120 } 7121 elseif ($sClass == $sRemoteClass) 7122 { 7123 $bFoundClass = true; 7124 } 7125 else 7126 { 7127 $aTargets[$sAttCode] = $sRemoteClass; 7128 } 7129 } 7130 } 7131 if (empty($sClass) || $bFoundClass) 7132 { 7133 $aResult[$sSomeClass] = $aTargets; 7134 } 7135 } 7136 return $aResult; 7137 } 7138 7139 /** 7140 * This function has two siblings that will be soon deprecated: 7141 * {@link MetaModel::EnumLinkingClasses} and {@link MetaModel::EnumLinkClasses} 7142 * 7143 * Using GetLinkClasses is the recommended way to determine if a class is 7144 * actually an N-N relation because it is based on the decision made by the 7145 * designer the data model 7146 * 7147 * @return array external key code => target class 7148 * @throws \CoreException 7149 */ 7150 public static function GetLinkClasses() 7151 { 7152 $aRet = array(); 7153 foreach(self::GetClasses() as $sClass) 7154 { 7155 if (isset(self::$m_aClassParams[$sClass]["is_link"])) 7156 { 7157 if (self::$m_aClassParams[$sClass]["is_link"]) 7158 { 7159 $aExtKeys = array(); 7160 foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 7161 { 7162 if ($oAttDef->IsExternalKey()) 7163 { 7164 $aExtKeys[$sAttCode] = $oAttDef->GetTargetClass(); 7165 } 7166 } 7167 $aRet[$sClass] = $aExtKeys; 7168 } 7169 } 7170 } 7171 7172 return $aRet; 7173 } 7174 7175 /** 7176 * @param string $sLinkClass 7177 * @param string $sAttCode 7178 * 7179 * @return string 7180 * @throws \CoreException 7181 * @throws \Exception 7182 */ 7183 public static function GetLinkLabel($sLinkClass, $sAttCode) 7184 { 7185 self::_check_subclass($sLinkClass); 7186 7187 // e.g. "supported by" (later: $this->GetLinkLabel(), computed on link data!) 7188 return self::GetLabel($sLinkClass, $sAttCode); 7189 } 7190 7191 /** 7192 * Replaces all the parameters by the values passed in the hash array 7193 * 7194 * @param string $sInput 7195 * @param array $aParams 7196 * 7197 * @return mixed 7198 */ 7199 static public function ApplyParams($sInput, $aParams) 7200 { 7201 $aParams = static::AddMagicPlaceholders($aParams); 7202 7203 // Declare magic parameters 7204 $aParams['APP_URL'] = utils::GetAbsoluteUrlAppRoot(); 7205 $aParams['MODULES_URL'] = utils::GetAbsoluteUrlModulesRoot(); 7206 7207 $aSearches = array(); 7208 $aReplacements = array(); 7209 foreach($aParams as $sSearch => $replace) 7210 { 7211 // Some environment parameters are objects, we just need scalars 7212 if (is_object($replace)) 7213 { 7214 $iPos = strpos($sSearch, '->object()'); 7215 if ($iPos !== false) 7216 { 7217 // Expand the parameters for the object 7218 $sName = substr($sSearch, 0, $iPos); 7219 $aRegExps = array( 7220 '/(\\$)'.$sName.'-(>|>)([^\\$]+)\\$/', // Support both syntaxes: $this->xxx$ or $this->xxx$ for HTML compatibility 7221 '/(%24)'.$sName.'-(>|>)([^%24]+)%24/', // Support for urlencoded in HTML attributes (%20this->xxx%20) 7222 ); 7223 foreach($aRegExps as $sRegExp) 7224 { 7225 if(preg_match_all($sRegExp, $sInput, $aMatches)) 7226 { 7227 foreach($aMatches[3] as $idx => $sPlaceholderAttCode) 7228 { 7229 try 7230 { 7231 $sReplacement = $replace->GetForTemplate($sPlaceholderAttCode); 7232 if($sReplacement !== null) 7233 { 7234 $aReplacements[] = $sReplacement; 7235 $aSearches[] = $aMatches[1][$idx] . $sName . '-' . $aMatches[2][$idx] . $sPlaceholderAttCode . $aMatches[1][$idx]; 7236 } 7237 } 7238 catch(Exception $e) 7239 { 7240 // No replacement will occur 7241 } 7242 } 7243 } 7244 } 7245 } 7246 else 7247 { 7248 continue; // Ignore this non-scalar value 7249 } 7250 } 7251 else 7252 { 7253 $aSearches[] = '$'.$sSearch.'$'; 7254 $aReplacements[] = (string)$replace; 7255 } 7256 } 7257 return str_replace($aSearches, $aReplacements, $sInput); 7258 } 7259 7260 /** 7261 * @param string $sInterface 7262 * 7263 * @return array classes=>instance implementing the given interface 7264 */ 7265 public static function EnumPlugins($sInterface) 7266 { 7267 if (array_key_exists($sInterface, self::$m_aExtensionClasses)) 7268 { 7269 return self::$m_aExtensionClasses[$sInterface]; 7270 } 7271 else 7272 { 7273 return array(); 7274 } 7275 } 7276 7277 /** 7278 * @param string $sInterface 7279 * @param string $sClassName 7280 * 7281 * @return mixed the instance of the specified plug-ins for the given interface 7282 */ 7283 public static function GetPlugins($sInterface, $sClassName) 7284 { 7285 $oInstance = null; 7286 if (array_key_exists($sInterface, self::$m_aExtensionClasses)) 7287 { 7288 if (array_key_exists($sClassName, self::$m_aExtensionClasses[$sInterface])) 7289 { 7290 return self::$m_aExtensionClasses[$sInterface][$sClassName]; 7291 } 7292 } 7293 7294 return $oInstance; 7295 } 7296 7297 /** 7298 * @param string $sEnvironment 7299 * 7300 * @return array 7301 */ 7302 public static function GetCacheEntries($sEnvironment = null) 7303 { 7304 if (is_null($sEnvironment)) 7305 { 7306 $sEnvironment = MetaModel::GetEnvironmentId(); 7307 } 7308 $aEntries = array(); 7309 $aCacheUserData = apc_cache_info_compat(); 7310 if (is_array($aCacheUserData) && isset($aCacheUserData['cache_list'])) 7311 { 7312 $sPrefix = 'itop-'.$sEnvironment.'-'; 7313 7314 foreach($aCacheUserData['cache_list'] as $i => $aEntry) 7315 { 7316 $sEntryKey = array_key_exists('info', $aEntry) ? $aEntry['info'] : $aEntry['key']; 7317 if (strpos($sEntryKey, $sPrefix) === 0) 7318 { 7319 $sCleanKey = substr($sEntryKey, strlen($sPrefix)); 7320 $aEntries[$sCleanKey] = $aEntry; 7321 $aEntries[$sCleanKey]['info'] = $sEntryKey; 7322 } 7323 } 7324 } 7325 7326 return $aEntries; 7327 } 7328 7329 /** 7330 * @param string $sEnvironmentId 7331 */ 7332 public static function ResetCache($sEnvironmentId = null) 7333 { 7334 if (is_null($sEnvironmentId)) 7335 { 7336 $sEnvironmentId = MetaModel::GetEnvironmentId(); 7337 } 7338 7339 $sAppIdentity = 'itop-'.$sEnvironmentId; 7340 require_once(APPROOT.'/core/dict.class.inc.php'); 7341 Dict::ResetCache($sAppIdentity); 7342 7343 if (function_exists('apc_delete')) 7344 { 7345 foreach(self::GetCacheEntries($sEnvironmentId) as $sKey => $aAPCInfo) 7346 { 7347 $sAPCKey = $aAPCInfo['info']; 7348 apc_delete($sAPCKey); 7349 } 7350 } 7351 7352 require_once(APPROOT.'core/userrights.class.inc.php'); 7353 UserRights::FlushPrivileges(); 7354 } 7355 7356 /** 7357 * Given a field spec, get the most relevant (unique) representation 7358 * Examples for a user request: 7359 * - friendlyname => ref 7360 * - org_name => org_id->name 7361 * - org_id_friendlyname => org_id=>name 7362 * - caller_name => caller_id->name 7363 * - caller_id_friendlyname => caller_id->friendlyname 7364 * 7365 * @param string $sClass 7366 * @param string $sField 7367 * 7368 * @return string 7369 * @throws \CoreException 7370 * @throws \DictExceptionMissingString 7371 * @throws \Exception 7372 */ 7373 public static function NormalizeFieldSpec($sClass, $sField) 7374 { 7375 $sRet = $sField; 7376 7377 if ($sField == 'id') 7378 { 7379 $sRet = 'id'; 7380 } 7381 elseif ($sField == 'friendlyname') 7382 { 7383 $sFriendlyNameAttCode = static::GetFriendlyNameAttributeCode($sClass); 7384 if (!is_null($sFriendlyNameAttCode)) 7385 { 7386 // The friendly name is made of a single attribute 7387 $sRet = $sFriendlyNameAttCode; 7388 } 7389 } 7390 else 7391 { 7392 $oAttDef = static::GetAttributeDef($sClass, $sField); 7393 if ($oAttDef->IsExternalField()) 7394 { 7395 if ($oAttDef->IsFriendlyName()) 7396 { 7397 $oKeyAttDef = MetaModel::GetAttributeDef($sClass, $oAttDef->GetKeyAttCode()); 7398 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 7399 $sFriendlyNameAttCode = static::GetFriendlyNameAttributeCode($sRemoteClass); 7400 if (is_null($sFriendlyNameAttCode)) 7401 { 7402 // The friendly name is made of several attributes 7403 $sRet = $oAttDef->GetKeyAttCode().'->friendlyname'; 7404 } 7405 else 7406 { 7407 // The friendly name is made of a single attribute 7408 $sRet = $oAttDef->GetKeyAttCode().'->'.$sFriendlyNameAttCode; 7409 } 7410 } 7411 else 7412 { 7413 $sRet = $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode(); 7414 } 7415 } 7416 } 7417 return $sRet; 7418 } 7419 7420} 7421 7422 7423// Standard attribute lists 7424MetaModel::RegisterZList("noneditable", array("description" => "non editable fields", "type" => "attributes")); 7425 7426MetaModel::RegisterZList("details", array("description" => "All attributes to be displayed for the 'details' of an object", "type" => "attributes")); 7427MetaModel::RegisterZList("list", array("description" => "All attributes to be displayed for a list of objects", "type" => "attributes")); 7428MetaModel::RegisterZList("preview", array("description" => "All attributes visible in preview mode", "type" => "attributes")); 7429 7430MetaModel::RegisterZList("standard_search", array("description" => "List of criteria for the standard search", "type" => "filters")); 7431MetaModel::RegisterZList("advanced_search", array("description" => "List of criteria for the advanced search", "type" => "filters")); 7432MetaModel::RegisterZList("default_search", array("description" => "List of criteria displayed by default during search", "type" => "filters")); 7433