1<?php 2// Copyright (C) 2010-2018 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19 20/** 21 * Typology for the attributes 22 * 23 * @copyright Copyright (C) 2010-2018 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27 28require_once('MyHelpers.class.inc.php'); 29require_once('ormdocument.class.inc.php'); 30require_once('ormstopwatch.class.inc.php'); 31require_once('ormpassword.class.inc.php'); 32require_once('ormcaselog.class.inc.php'); 33require_once('ormlinkset.class.inc.php'); 34require_once('ormset.class.inc.php'); 35require_once('ormtagset.class.inc.php'); 36require_once('htmlsanitizer.class.inc.php'); 37require_once(APPROOT.'sources/autoload.php'); 38require_once('customfieldshandler.class.inc.php'); 39require_once('ormcustomfieldsvalue.class.inc.php'); 40require_once('datetimeformat.class.inc.php'); 41// This should be changed to a use when we go full-namespace 42require_once(APPROOT.'sources/form/validator/validator.class.inc.php'); 43require_once(APPROOT.'sources/form/validator/notemptyextkeyvalidator.class.inc.php'); 44 45/** 46 * MissingColumnException - sent if an attribute is being created but the column is missing in the row 47 * 48 * @package iTopORM 49 */ 50class MissingColumnException extends Exception 51{ 52} 53 54/** 55 * add some description here... 56 * 57 * @package iTopORM 58 */ 59define('EXTKEY_RELATIVE', 1); 60 61/** 62 * add some description here... 63 * 64 * @package iTopORM 65 */ 66define('EXTKEY_ABSOLUTE', 2); 67 68/** 69 * Propagation of the deletion through an external key - ask the user to delete the referencing object 70 * 71 * @package iTopORM 72 */ 73define('DEL_MANUAL', 1); 74 75/** 76 * Propagation of the deletion through an external key - ask the user to delete the referencing object 77 * 78 * @package iTopORM 79 */ 80define('DEL_AUTO', 2); 81/** 82 * Fully silent delete... not yet implemented 83 */ 84define('DEL_SILENT', 2); 85/** 86 * For HierarchicalKeys only: move all the children up one level automatically 87 */ 88define('DEL_MOVEUP', 3); 89 90 91/** 92 * For Link sets: tracking_level 93 * 94 * @package iTopORM 95 */ 96define('ATTRIBUTE_TRACKING_NONE', 0); // Do not track changes of the attribute 97define('ATTRIBUTE_TRACKING_ALL', 3); // Do track all changes of the attribute 98define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set 99define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items 100define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items 101define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items 102 103define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from inside this object 104define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object 105define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu 106define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place 107define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place 108 109 110/** 111 * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) 112 * 113 * @package iTopORM 114 */ 115abstract class AttributeDefinition 116{ 117 const SEARCH_WIDGET_TYPE_RAW = 'raw'; 118 const SEARCH_WIDGET_TYPE_STRING = 'string'; 119 const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric'; 120 const SEARCH_WIDGET_TYPE_ENUM = 'enum'; 121 const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key'; 122 const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key'; 123 const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field'; 124 const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time'; 125 const SEARCH_WIDGET_TYPE_DATE = 'date'; 126 const SEARCH_WIDGET_TYPE_SET = 'set'; 127 const SEARCH_WIDGET_TYPE_TAG_SET = 'tag_set'; 128 129 130 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 131 132 const INDEX_LENGTH = 95; 133 134 protected $aCSSClasses; 135 136 public function GetType() 137 { 138 return Dict::S('Core:'.get_class($this)); 139 } 140 141 public function GetTypeDesc() 142 { 143 return Dict::S('Core:'.get_class($this).'+'); 144 } 145 146 abstract public function GetEditClass(); 147 148 /** 149 * Return the search widget type corresponding to this attribute 150 * 151 * @return string 152 */ 153 public function GetSearchType() 154 { 155 return static::SEARCH_WIDGET_TYPE; 156 } 157 158 /** 159 * @return bool 160 */ 161 public function IsSearchable() 162 { 163 return static::SEARCH_WIDGET_TYPE != static::SEARCH_WIDGET_TYPE_RAW; 164 } 165 166 protected $m_sCode; 167 private $m_aParams = array(); 168 protected $m_sHostClass = '!undefined!'; 169 170 public function Get($sParamName) 171 { 172 return $this->m_aParams[$sParamName]; 173 } 174 175 public function GetIndexLength() 176 { 177 $iMaxLength = $this->GetMaxSize(); 178 if (is_null($iMaxLength)) 179 { 180 return null; 181 } 182 if ($iMaxLength > static::INDEX_LENGTH) 183 { 184 return static::INDEX_LENGTH; 185 } 186 187 return $iMaxLength; 188 } 189 190 public function IsParam($sParamName) 191 { 192 return (array_key_exists($sParamName, $this->m_aParams)); 193 } 194 195 protected function GetOptional($sParamName, $default) 196 { 197 if (array_key_exists($sParamName, $this->m_aParams)) 198 { 199 return $this->m_aParams[$sParamName]; 200 } 201 else 202 { 203 return $default; 204 } 205 } 206 207 /** 208 * AttributeDefinition constructor. 209 * 210 * @param string $sCode 211 * @param array $aParams 212 * 213 * @throws \Exception 214 */ 215 public function __construct($sCode, $aParams) 216 { 217 $this->m_sCode = $sCode; 218 $this->m_aParams = $aParams; 219 $this->ConsistencyCheck(); 220 $this->aCSSClasses = array('attribute'); 221 } 222 223 public function GetParams() 224 { 225 return $this->m_aParams; 226 } 227 228 public function HasParam($sParam) 229 { 230 return array_key_exists($sParam, $this->m_aParams); 231 } 232 233 public function SetHostClass($sHostClass) 234 { 235 $this->m_sHostClass = $sHostClass; 236 } 237 238 public function GetHostClass() 239 { 240 return $this->m_sHostClass; 241 } 242 243 /** 244 * @return array 245 * 246 * @throws \CoreException 247 */ 248 public function ListSubItems() 249 { 250 $aSubItems = array(); 251 foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef) 252 { 253 if ($oAttDef instanceof AttributeSubItem) 254 { 255 if ($oAttDef->Get('target_attcode') == $this->m_sCode) 256 { 257 $aSubItems[$sAttCode] = $oAttDef; 258 } 259 } 260 } 261 262 return $aSubItems; 263 } 264 265 // Note: I could factorize this code with the parameter management made for the AttributeDef class 266 // to be overloaded 267 static public function ListExpectedParams() 268 { 269 return array(); 270 } 271 272 /** 273 * @throws \Exception 274 */ 275 private function ConsistencyCheck() 276 { 277 // Check that any mandatory param has been specified 278 // 279 $aExpectedParams = $this->ListExpectedParams(); 280 foreach($aExpectedParams as $sParamName) 281 { 282 if (!array_key_exists($sParamName, $this->m_aParams)) 283 { 284 $aBacktrace = debug_backtrace(); 285 $sTargetClass = $aBacktrace[2]["class"]; 286 $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; 287 throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); 288 } 289 } 290 } 291 292 /** 293 * Check the validity of the given value 294 * 295 * @param \DBObject $oHostObject 296 * @param $value Object error if any, null otherwise 297 * 298 * @return bool 299 */ 300 public function CheckValue(DBObject $oHostObject, $value) 301 { 302 // later: factorize here the cases implemented into DBObject 303 return true; 304 } 305 306 // table, key field, name field 307 308 /** 309 * @return string 310 * @deprecated never used 311 */ 312 public function ListDBJoins() 313 { 314 return ""; 315 // e.g: return array("Site", "infrid", "name"); 316 } 317 318 public function GetFinalAttDef() 319 { 320 return $this; 321 } 322 323 /** 324 * Deprecated - use IsBasedOnDBColumns instead 325 * 326 * @return bool 327 */ 328 public function IsDirectField() 329 { 330 return static::IsBasedOnDBColumns(); 331 } 332 333 /** 334 * Returns true if the attribute value is built after DB columns 335 * 336 * @return bool 337 */ 338 static public function IsBasedOnDBColumns() 339 { 340 return false; 341 } 342 343 /** 344 * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via 345 * GetOQLExpression) 346 * 347 * @return bool 348 */ 349 static public function IsBasedOnOQLExpression() 350 { 351 return false; 352 } 353 354 /** 355 * Returns true if the attribute value can be shown as a string 356 * 357 * @return bool 358 */ 359 static public function IsScalar() 360 { 361 return false; 362 } 363 364 /** 365 * Returns true if the attribute value is a set of related objects (1-N or N-N) 366 * 367 * @return bool 368 */ 369 static public function IsLinkSet() 370 { 371 return false; 372 } 373 374 /** 375 * @param int $iType 376 * 377 * @return bool true if the attribute is an external key, either directly (RELATIVE to the host class), or 378 * indirectly (ABSOLUTELY) 379 */ 380 public function IsExternalKey($iType = EXTKEY_RELATIVE) 381 { 382 return false; 383 } 384 385 /** 386 * @return bool true if the attribute value is an external key, pointing to the host class 387 */ 388 static public function IsHierarchicalKey() 389 { 390 return false; 391 } 392 393 /** 394 * @return bool true if the attribute value is stored on an object pointed to be an external key 395 */ 396 static public function IsExternalField() 397 { 398 return false; 399 } 400 401 /** 402 * @return bool true if the attribute can be written (by essence : metamodel field option) 403 * @see \DBObject::IsAttributeReadOnlyForCurrentState() for a specific object instance (depending on its workflow) 404 */ 405 public function IsWritable() 406 { 407 return false; 408 } 409 410 /** 411 * @return bool true if the attribute has been added automatically by the framework 412 */ 413 public function IsMagic() 414 { 415 return $this->GetOptional('magic', false); 416 } 417 418 /** 419 * @return bool true if the attribute value is kept in the loaded object (in memory) 420 */ 421 static public function LoadInObject() 422 { 423 return true; 424 } 425 426 /** 427 * @return bool true if the attribute value comes from the database in one way or another 428 */ 429 static public function LoadFromDB() 430 { 431 return true; 432 } 433 434 /** 435 * @return bool true if the attribute should be loaded anytime (in addition to the column selected by the user) 436 */ 437 public function AlwaysLoadInTables() 438 { 439 return $this->GetOptional('always_load_in_tables', false); 440 } 441 442 /** 443 * @param \DBObject $oHostObject 444 * 445 * @return mixed Must return the value if LoadInObject returns false 446 */ 447 public function GetValue($oHostObject) 448 { 449 return null; 450 } 451 452 /** 453 * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull()) 454 * 455 * @return bool 456 */ 457 public function IsNullAllowed() 458 { 459 return true; 460 } 461 462 /** 463 * Returns the attribute code (identifies the attribute in the host class) 464 * 465 * @return string 466 */ 467 public function GetCode() 468 { 469 return $this->m_sCode; 470 } 471 472 /** 473 * Find the corresponding "link" attribute on the target class, if any 474 * 475 * @return null | AttributeDefinition 476 */ 477 public function GetMirrorLinkAttribute() 478 { 479 return null; 480 } 481 482 /** 483 * Helper to browse the hierarchy of classes, searching for a label 484 * 485 * @param string $sDictEntrySuffix 486 * @param string $sDefault 487 * @param bool $bUserLanguageOnly 488 * 489 * @return string 490 * @throws \Exception 491 */ 492 protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly) 493 { 494 $sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly); 495 if (strlen($sLabel) == 0) 496 { 497 // Nothing found: go higher in the hierarchy (if possible) 498 // 499 $sLabel = $sDefault; 500 $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); 501 if ($sParentClass) 502 { 503 if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) 504 { 505 $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); 506 $sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly); 507 } 508 } 509 } 510 511 return $sLabel; 512 } 513 514 /** 515 * @param string|null $sDefault 516 * 517 * @return string 518 * 519 * @throws \Exception 520 */ 521 public function GetLabel($sDefault = null) 522 { 523 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, null, true /*user lang*/); 524 if (is_null($sLabel)) 525 { 526 // If no default value is specified, let's define the most relevant one for developping purposes 527 if (is_null($sDefault)) 528 { 529 $sDefault = str_replace('_', ' ', $this->m_sCode); 530 } 531 // Browse the hierarchy again, accepting default (english) translations 532 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false); 533 } 534 535 return $sLabel; 536 } 537 538 /** 539 * To be overloaded for localized enums 540 * 541 * @param string $sValue 542 * 543 * @return string label corresponding to the given value (in plain text) 544 */ 545 public function GetValueLabel($sValue) 546 { 547 return $sValue; 548 } 549 550 /** 551 * Get the value from a given string (plain text, CSV import) 552 * 553 * @param string $sProposedValue 554 * @param bool $bLocalizedValue 555 * @param string $sSepItem 556 * @param string $sSepAttribute 557 * @param string $sSepValue 558 * @param string $sAttributeQualifier 559 * 560 * @return mixed null if no match could be found 561 */ 562 public function MakeValueFromString( 563 $sProposedValue, 564 $bLocalizedValue = false, 565 $sSepItem = null, 566 $sSepAttribute = null, 567 $sSepValue = null, 568 $sAttributeQualifier = null 569 ) { 570 return $this->MakeRealValue($sProposedValue, null); 571 } 572 573 /** 574 * Parses a search string coming from user input 575 * 576 * @param string $sSearchString 577 * 578 * @return string 579 */ 580 public function ParseSearchString($sSearchString) 581 { 582 return $sSearchString; 583 } 584 585 /** 586 * @return string 587 * 588 * @throws \Exception 589 */ 590 public function GetLabel_Obsolete() 591 { 592 // Written for compatibility with a data model written prior to version 0.9.1 593 if (array_key_exists('label', $this->m_aParams)) 594 { 595 return $this->m_aParams['label']; 596 } 597 else 598 { 599 return $this->GetLabel(); 600 } 601 } 602 603 /** 604 * @param string|null $sDefault 605 * 606 * @return string 607 * 608 * @throws \Exception 609 */ 610 public function GetDescription($sDefault = null) 611 { 612 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', null, true /*user lang*/); 613 if (is_null($sLabel)) 614 { 615 // If no default value is specified, let's define the most relevant one for developping purposes 616 if (is_null($sDefault)) 617 { 618 $sDefault = ''; 619 } 620 // Browse the hierarchy again, accepting default (english) translations 621 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false); 622 } 623 624 return $sLabel; 625 } 626 627 /** 628 * @param string|null $sDefault 629 * 630 * @return string 631 * 632 * @throws \Exception 633 */ 634 public function GetHelpOnEdition($sDefault = null) 635 { 636 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', null, true /*user lang*/); 637 if (is_null($sLabel)) 638 { 639 // If no default value is specified, let's define the most relevant one for developping purposes 640 if (is_null($sDefault)) 641 { 642 $sDefault = ''; 643 } 644 // Browse the hierarchy again, accepting default (english) translations 645 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false); 646 } 647 648 return $sLabel; 649 } 650 651 public function GetHelpOnSmartSearch() 652 { 653 $aParents = array_merge(array(get_class($this) => get_class($this)), class_parents($this)); 654 foreach($aParents as $sClass) 655 { 656 $sHelp = Dict::S("Core:$sClass?SmartSearch", '-missing-'); 657 if ($sHelp != '-missing-') 658 { 659 return $sHelp; 660 } 661 } 662 663 return ''; 664 } 665 666 /** 667 * @return string 668 * 669 * @throws \Exception 670 */ 671 public function GetDescription_Obsolete() 672 { 673 // Written for compatibility with a data model written prior to version 0.9.1 674 if (array_key_exists('description', $this->m_aParams)) 675 { 676 return $this->m_aParams['description']; 677 } 678 else 679 { 680 return $this->GetDescription(); 681 } 682 } 683 684 public function GetTrackingLevel() 685 { 686 return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL); 687 } 688 689 /** 690 * @return \ValueSetObjects 691 */ 692 public function GetValuesDef() 693 { 694 return null; 695 } 696 697 public function GetPrerequisiteAttributes($sClass = null) 698 { 699 return array(); 700 } 701 702 public function GetNullValue() 703 { 704 return null; 705 } 706 707 public function IsNull($proposedValue) 708 { 709 return is_null($proposedValue); 710 } 711 712 /** 713 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! 714 * 715 * @param $proposedValue 716 * @param $oHostObj 717 * 718 * @return mixed 719 */ 720 public function MakeRealValue($proposedValue, $oHostObj) 721 { 722 return $proposedValue; 723 } 724 725 public function Equals($val1, $val2) 726 { 727 return ($val1 == $val2); 728 } 729 730 /** 731 * @param string $sPrefix 732 * 733 * @return array suffix/expression pairs (1 in most of the cases), for READING (Select) 734 */ 735 public function GetSQLExpressions($sPrefix = '') 736 { 737 return array(); 738 } 739 740 /** 741 * @param array $aCols 742 * @param string $sPrefix 743 * 744 * @return mixed a value out of suffix/value pairs, for SELECT result interpretation 745 */ 746 public function FromSQLToValue($aCols, $sPrefix = '') 747 { 748 return null; 749 } 750 751 /** 752 * @param bool $bFullSpec 753 * 754 * @return array column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) 755 * @see \CMDBSource::GetFieldSpec() 756 */ 757 public function GetSQLColumns($bFullSpec = false) 758 { 759 return array(); 760 } 761 762 /** 763 * @param $value 764 * 765 * @return array column/value pairs (1 in most of the cases), for WRITING (Insert, Update) 766 */ 767 public function GetSQLValues($value) 768 { 769 return array(); 770 } 771 772 public function RequiresIndex() 773 { 774 return false; 775 } 776 777 public function RequiresFullTextIndex() 778 { 779 return false; 780 } 781 782 public function CopyOnAllTables() 783 { 784 return false; 785 } 786 787 public function GetOrderBySQLExpressions($sClassAlias) 788 { 789 // Note: This is the responsibility of this function to place backticks around column aliases 790 return array('`'.$sClassAlias.$this->GetCode().'`'); 791 } 792 793 public function GetOrderByHint() 794 { 795 return ''; 796 } 797 798 // Import - differs slightly from SQL input, but identical in most cases 799 // 800 public function GetImportColumns() 801 { 802 return $this->GetSQLColumns(); 803 } 804 805 public function FromImportToValue($aCols, $sPrefix = '') 806 { 807 $aValues = array(); 808 foreach($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr) 809 { 810 // This is working, based on the assumption that importable fields 811 // are not computed fields => the expression is the name of a column 812 $aValues[$sPrefix.$sAlias] = $aCols[$sExpr]; 813 } 814 815 return $this->FromSQLToValue($aValues, $sPrefix); 816 } 817 818 public function GetValidationPattern() 819 { 820 return ''; 821 } 822 823 public function CheckFormat($value) 824 { 825 return true; 826 } 827 828 public function GetMaxSize() 829 { 830 return null; 831 } 832 833 /** 834 * @return mixed|null 835 * @deprecated never used 836 */ 837 public function MakeValue() 838 { 839 $sComputeFunc = $this->Get("compute_func"); 840 if (empty($sComputeFunc)) 841 { 842 return null; 843 } 844 845 return call_user_func($sComputeFunc); 846 } 847 848 abstract public function GetDefaultValue(DBObject $oHostObject = null); 849 850 // 851 // To be overloaded in subclasses 852 // 853 854 abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description" 855 856 abstract public function GetBasicFilterLooseOperator(); // returns an "opCode" 857 858 //abstract protected GetBasicFilterHTMLInput(); 859 abstract public function GetBasicFilterSQLExpr($sOpCode, $value); 860 861 public function GetFilterDefinitions() 862 { 863 return array(); 864 } 865 866 public function GetEditValue($sValue, $oHostObj = null) 867 { 868 return (string)$sValue; 869 } 870 871 /** 872 * For fields containing a potential markup, return the value without this markup 873 * 874 * @param string $sValue 875 * @param \DBObject $oHostObj 876 * 877 * @return string 878 */ 879 public function GetAsPlainText($sValue, $oHostObj = null) 880 { 881 return (string)$this->GetEditValue($sValue, $oHostObj); 882 } 883 884 /** 885 * Helper to get a value that will be JSON encoded 886 * The operation is the opposite to FromJSONToValue 887 * 888 * @param $value 889 * 890 * @return string 891 */ 892 public function GetForJSON($value) 893 { 894 // In most of the cases, that will be the expected behavior... 895 return $this->GetEditValue($value); 896 } 897 898 /** 899 * Helper to form a value, given JSON decoded data 900 * The operation is the opposite to GetForJSON 901 * 902 * @param $json 903 * 904 * @return mixed 905 */ 906 public function FromJSONToValue($json) 907 { 908 // Passthrough in most of the cases 909 return $json; 910 } 911 912 /** 913 * Override to display the value in the GUI 914 * 915 * @param string $sValue 916 * @param \DBObject $oHostObject 917 * @param bool $bLocalize 918 * 919 * @return string 920 */ 921 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 922 { 923 return Str::pure2html((string)$sValue); 924 } 925 926 /** 927 * Override to export the value in XML 928 * 929 * @param string $sValue 930 * @param \DBObject $oHostObject 931 * @param bool $bLocalize 932 * 933 * @return mixed 934 */ 935 public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) 936 { 937 return Str::pure2xml((string)$sValue); 938 } 939 940 /** 941 * Override to escape the value when read by DBObject::GetAsCSV() 942 * 943 * @param string $sValue 944 * @param string $sSeparator 945 * @param string $sTextQualifier 946 * @param \DBObject $oHostObject 947 * @param bool $bLocalize 948 * @param bool $bConvertToPlainText 949 * 950 * @return string 951 */ 952 public function GetAsCSV( 953 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 954 $bConvertToPlainText = false 955 ) { 956 return (string)$sValue; 957 } 958 959 /** 960 * Override to differentiate a value displayed in the UI or in the history 961 * 962 * @param string $sValue 963 * @param \DBObject $oHostObject 964 * @param bool $bLocalize 965 * 966 * @return string 967 */ 968 public function GetAsHTMLForHistory($sValue, $oHostObject = null, $bLocalize = true) 969 { 970 return $this->GetAsHTML($sValue, $oHostObject, $bLocalize); 971 } 972 973 static public function GetFormFieldClass() 974 { 975 return '\\Combodo\\iTop\\Form\\Field\\StringField'; 976 } 977 978 /** 979 * Override to specify Field class 980 * 981 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the 982 * $oFormField is passed, MakeFormField behave more like a Prepare. 983 * 984 * @param \DBObject $oObject 985 * @param \Combodo\iTop\Form\Field\Field $oFormField 986 * 987 * @return null 988 * @throws \CoreException 989 * @throws \Exception 990 */ 991 public function MakeFormField(DBObject $oObject, $oFormField = null) 992 { 993 // This is a fallback in case the AttributeDefinition subclass has no overloading of this function. 994 if ($oFormField === null) 995 { 996 $sFormFieldClass = static::GetFormFieldClass(); 997 $oFormField = new $sFormFieldClass($this->GetCode()); 998 //$oFormField->SetReadOnly(true); 999 } 1000 1001 $oFormField->SetLabel($this->GetLabel()); 1002 1003 // Attributes flags 1004 // - Retrieving flags for the current object 1005 if ($oObject->IsNew()) 1006 { 1007 $iFlags = $oObject->GetInitialStateAttributeFlags($this->GetCode()); 1008 } 1009 else 1010 { 1011 $iFlags = $oObject->GetAttributeFlags($this->GetCode()); 1012 } 1013 1014 // - Comparing flags 1015 if ($this->IsWritable() && (!$this->IsNullAllowed() || (($iFlags & OPT_ATT_MANDATORY) === OPT_ATT_MANDATORY))) 1016 { 1017 $oFormField->SetMandatory(true); 1018 } 1019 if ((!$oObject->IsNew() || !$oFormField->GetMandatory()) && (($iFlags & OPT_ATT_READONLY) === OPT_ATT_READONLY)) 1020 { 1021 $oFormField->SetReadOnly(true); 1022 } 1023 1024 // CurrentValue 1025 $oFormField->SetCurrentValue($oObject->Get($this->GetCode())); 1026 1027 // Validation pattern 1028 if ($this->GetValidationPattern() !== '') 1029 { 1030 $oFormField->AddValidator(new \Combodo\iTop\Form\Validator\Validator($this->GetValidationPattern())); 1031 } 1032 1033 return $oFormField; 1034 } 1035 1036 /** 1037 * List the available verbs for 'GetForTemplate' 1038 */ 1039 public function EnumTemplateVerbs() 1040 { 1041 return array( 1042 '' => 'Plain text (unlocalized) representation', 1043 'html' => 'HTML representation', 1044 'label' => 'Localized representation', 1045 'text' => 'Plain text representation (without any markup)', 1046 ); 1047 } 1048 1049 /** 1050 * Get various representations of the value, for insertion into a template (e.g. in Notifications) 1051 * 1052 * @param mixed $value The current value of the field 1053 * @param string $sVerb The verb specifying the representation of the value 1054 * @param \DBObject $oHostObject 1055 * @param bool $bLocalize Whether or not to localize the value 1056 * 1057 * @return mixed|null|string 1058 * 1059 * @throws \Exception 1060 */ 1061 public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) 1062 { 1063 if ($this->IsScalar()) 1064 { 1065 switch ($sVerb) 1066 { 1067 case '': 1068 return $value; 1069 1070 case 'html': 1071 return $this->GetAsHtml($value, $oHostObject, $bLocalize); 1072 1073 case 'label': 1074 return $this->GetEditValue($value); 1075 1076 case 'text': 1077 return $this->GetAsPlainText($value); 1078 break; 1079 1080 default: 1081 throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); 1082 } 1083 } 1084 1085 return null; 1086 } 1087 1088 /** 1089 * @param array $aArgs 1090 * @param string $sContains 1091 * 1092 * @return array|null 1093 * @throws \CoreException 1094 * @throws \OQLException 1095 */ 1096 public function GetAllowedValues($aArgs = array(), $sContains = '') 1097 { 1098 $oValSetDef = $this->GetValuesDef(); 1099 if (!$oValSetDef) 1100 { 1101 return null; 1102 } 1103 1104 return $oValSetDef->GetValues($aArgs, $sContains); 1105 } 1106 1107 /** 1108 * Explain the change of the attribute (history) 1109 * 1110 * @param string $sOldValue 1111 * @param string $sNewValue 1112 * @param string $sLabel 1113 * 1114 * @return string 1115 * @throws \ArchivedObjectException 1116 * @throws \CoreException 1117 * @throws \DictExceptionMissingString 1118 * @throws \OQLException 1119 * @throws \Exception 1120 */ 1121 public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) 1122 { 1123 if (is_null($sLabel)) 1124 { 1125 $sLabel = $this->GetLabel(); 1126 } 1127 1128 $sNewValueHtml = $this->GetAsHTMLForHistory($sNewValue); 1129 $sOldValueHtml = $this->GetAsHTMLForHistory($sOldValue); 1130 1131 if ($this->IsExternalKey()) 1132 { 1133 /** @var \AttributeExternalKey $this */ 1134 $sTargetClass = $this->GetTargetClass(); 1135 $sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null; 1136 $sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null; 1137 } 1138 if ((($this->GetType() == 'String') || ($this->GetType() == 'Text')) && 1139 (strlen($sNewValue) > strlen($sOldValue))) 1140 { 1141 // Check if some text was not appended to the field 1142 if (substr($sNewValue, 0, strlen($sOldValue)) == $sOldValue) // Text added at the end 1143 { 1144 $sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue))); 1145 $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); 1146 } 1147 else 1148 { 1149 if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning 1150 { 1151 $sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue))); 1152 $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); 1153 } 1154 else 1155 { 1156 if (strlen($sOldValue) == 0) 1157 { 1158 $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); 1159 } 1160 else 1161 { 1162 if (is_null($sNewValue)) 1163 { 1164 $sNewValueHtml = Dict::S('UI:UndefinedObject'); 1165 } 1166 $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, 1167 $sNewValueHtml, $sOldValueHtml); 1168 } 1169 } 1170 } 1171 } 1172 else 1173 { 1174 if (strlen($sOldValue) == 0) 1175 { 1176 $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); 1177 } 1178 else 1179 { 1180 if (is_null($sNewValue)) 1181 { 1182 $sNewValueHtml = Dict::S('UI:UndefinedObject'); 1183 } 1184 $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, 1185 $sOldValueHtml); 1186 } 1187 } 1188 1189 return $sResult; 1190 } 1191 1192 1193 /** 1194 * Parses a string to find some smart search patterns and build the corresponding search/OQL condition 1195 * Each derived class is reponsible for defining and processing their own smart patterns, the base class 1196 * does nothing special, and just calls the default (loose) operator 1197 * 1198 * @param string $sSearchText The search string to analyze for smart patterns 1199 * @param \FieldExpression $oField 1200 * @param array $aParams Values of the query parameters 1201 * 1202 * @return \Expression The search condition to be added (AND) to the current search 1203 * 1204 * @throws \CoreException 1205 */ 1206 public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams) 1207 { 1208 $sParamName = $oField->GetParent().'_'.$oField->GetName(); 1209 $oRightExpr = new VariableExpression($sParamName); 1210 $sOperator = $this->GetBasicFilterLooseOperator(); 1211 switch ($sOperator) 1212 { 1213 case 'Contains': 1214 $aParams[$sParamName] = "%$sSearchText%"; 1215 $sSQLOperator = 'LIKE'; 1216 break; 1217 1218 default: 1219 $sSQLOperator = $sOperator; 1220 $aParams[$sParamName] = $sSearchText; 1221 } 1222 $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); 1223 1224 return $oNewCondition; 1225 } 1226 1227 /** 1228 * Tells if an attribute is part of the unique fingerprint of the object (used for comparing two objects) 1229 * All attributes which value is not based on a value from the object itself (like ExternalFields or LinkedSet) 1230 * must be excluded from the object's signature 1231 * 1232 * @return boolean 1233 */ 1234 public function IsPartOfFingerprint() 1235 { 1236 return true; 1237 } 1238 1239 /** 1240 * The part of the current attribute in the object's signature, for the supplied value 1241 * 1242 * @param mixed $value The value of this attribute for the object 1243 * 1244 * @return string The "signature" for this field/attribute 1245 */ 1246 public function Fingerprint($value) 1247 { 1248 return (string)$value; 1249 } 1250} 1251 1252class AttributeDashboard extends AttributeDefinition 1253{ 1254 static public function ListExpectedParams() 1255 { 1256 return array_merge(parent::ListExpectedParams(), 1257 array("definition_file", "is_user_editable")); 1258 } 1259 1260 public function GetDashboard() 1261 { 1262 $sAttCode = $this->GetCode(); 1263 $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode); 1264 $sFilePath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$this->Get('definition_file'); 1265 return RuntimeDashboard::GetDashboard($sFilePath, $sClass.'__'.$sAttCode); 1266 } 1267 1268 public function IsUserEditable() 1269 { 1270 return $this->Get('is_user_editable'); 1271 } 1272 1273 public function IsWritable() 1274 { 1275 return false; 1276 } 1277 1278 public function GetEditClass() 1279 { 1280 return ""; 1281 } 1282 1283 public function GetDefaultValue(DBObject $oHostObject = null) 1284 { 1285 return null; 1286 } 1287 1288 public function GetBasicFilterOperators() 1289 { 1290 return array(); 1291 } 1292 1293 public function GetBasicFilterLooseOperator() 1294 { 1295 return '='; 1296 } 1297 1298 public function GetBasicFilterSQLExpr($sOpCode, $value) 1299 { 1300 return ''; 1301 } 1302 1303 /** 1304 * @inheritdoc 1305 */ 1306 public function MakeFormField(DBObject $oObject, $oFormField = null) 1307 { 1308 return null; 1309 } 1310 1311 // if this verb returns false, then GetValue must be implemented 1312 static public function LoadInObject() 1313 { 1314 return false; 1315 } 1316 1317 public function GetValue($oHostObject) 1318 { 1319 return ''; 1320 } 1321} 1322 1323/** 1324 * Set of objects directly linked to an object, and being part of its definition 1325 * 1326 * @package iTopORM 1327 */ 1328class AttributeLinkedSet extends AttributeDefinition 1329{ 1330 static public function ListExpectedParams() 1331 { 1332 return array_merge(parent::ListExpectedParams(), 1333 array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max")); 1334 } 1335 1336 public function GetEditClass() 1337 { 1338 return "LinkedSet"; 1339 } 1340 1341 public function IsWritable() 1342 { 1343 return true; 1344 } 1345 1346 static public function IsLinkSet() 1347 { 1348 return true; 1349 } 1350 1351 public function IsIndirect() 1352 { 1353 return false; 1354 } 1355 1356 public function GetValuesDef() 1357 { 1358 return $this->Get("allowed_values"); 1359 } 1360 1361 public function GetPrerequisiteAttributes($sClass = null) 1362 { 1363 return $this->Get("depends_on"); 1364 } 1365 1366 /** 1367 * @param \DBObject|null $oHostObject 1368 * 1369 * @return \ormLinkSet 1370 * 1371 * @throws \Exception 1372 * @throws \CoreException 1373 * @throws \CoreWarning 1374 */ 1375 public function GetDefaultValue(DBObject $oHostObject = null) 1376 { 1377 if ($oHostObject === null) 1378 { 1379 return null; 1380 } 1381 1382 $sLinkClass = $this->GetLinkedClass(); 1383 $sExtKeyToMe = $this->GetExtKeyToMe(); 1384 1385 // The class to target is not the current class, because if this is a derived class, 1386 // it may differ from the target class, then things start to become confusing 1387 /** @var \AttributeExternalKey $oRemoteExtKeyAtt */ 1388 $oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe); 1389 $sMyClass = $oRemoteExtKeyAtt->GetTargetClass(); 1390 1391 $oMyselfSearch = new DBObjectSearch($sMyClass); 1392 if ($oHostObject !== null) 1393 { 1394 $oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '='); 1395 } 1396 1397 $oLinkSearch = new DBObjectSearch($sLinkClass); 1398 $oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe); 1399 if ($this->IsIndirect()) 1400 { 1401 // Join the remote class so that the archive flag will be taken into account 1402 /** @var \AttributeLinkedSetIndirect $this */ 1403 $sExtKeyToRemote = $this->GetExtKeyToRemote(); 1404 /** @var \AttributeExternalKey $oExtKeyToRemote */ 1405 $oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote); 1406 $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); 1407 if (MetaModel::IsArchivable($sRemoteClass)) 1408 { 1409 $oRemoteSearch = new DBObjectSearch($sRemoteClass); 1410 /** @var \AttributeLinkedSetIndirect $this */ 1411 $oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote()); 1412 } 1413 } 1414 $oLinks = new DBObjectSet($oLinkSearch); 1415 $oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks); 1416 1417 return $oLinkSet; 1418 } 1419 1420 public function GetTrackingLevel() 1421 { 1422 return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default')); 1423 } 1424 1425 public function GetEditMode() 1426 { 1427 return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS); 1428 } 1429 1430 public function GetLinkedClass() 1431 { 1432 return $this->Get('linked_class'); 1433 } 1434 1435 public function GetExtKeyToMe() 1436 { 1437 return $this->Get('ext_key_to_me'); 1438 } 1439 1440 public function GetBasicFilterOperators() 1441 { 1442 return array(); 1443 } 1444 1445 public function GetBasicFilterLooseOperator() 1446 { 1447 return ''; 1448 } 1449 1450 public function GetBasicFilterSQLExpr($sOpCode, $value) 1451 { 1452 return ''; 1453 } 1454 1455 /** 1456 * @param string $sValue 1457 * @param \DBObject $oHostObject 1458 * @param bool $bLocalize 1459 * 1460 * @return string|null 1461 * 1462 * @throws \CoreException 1463 */ 1464 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 1465 { 1466 if (is_object($sValue) && ($sValue instanceof ormLinkSet)) 1467 { 1468 $sValue->Rewind(); 1469 $aItems = array(); 1470 while ($oObj = $sValue->Fetch()) 1471 { 1472 // Show only relevant information (hide the external key to the current object) 1473 $aAttributes = array(); 1474 foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) 1475 { 1476 if ($sAttCode == $this->GetExtKeyToMe()) 1477 { 1478 continue; 1479 } 1480 if ($oAttDef->IsExternalField()) 1481 { 1482 continue; 1483 } 1484 $sAttValue = $oObj->GetAsHTML($sAttCode); 1485 if (strlen($sAttValue) > 0) 1486 { 1487 $aAttributes[] = $sAttValue; 1488 } 1489 } 1490 $sAttributes = implode(', ', $aAttributes); 1491 $aItems[] = $sAttributes; 1492 } 1493 1494 return implode('<br/>', $aItems); 1495 } 1496 1497 return null; 1498 } 1499 1500 /** 1501 * @param string $sValue 1502 * @param \DBObject $oHostObject 1503 * @param bool $bLocalize 1504 * 1505 * @return string 1506 * 1507 * @throws \CoreException 1508 */ 1509 public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) 1510 { 1511 if (is_object($sValue) && ($sValue instanceof ormLinkSet)) 1512 { 1513 $sValue->Rewind(); 1514 $sRes = "<Set>\n"; 1515 while ($oObj = $sValue->Fetch()) 1516 { 1517 $sObjClass = get_class($oObj); 1518 $sRes .= "<$sObjClass id=\"".$oObj->GetKey()."\">\n"; 1519 // Show only relevant information (hide the external key to the current object) 1520 foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) 1521 { 1522 if ($sAttCode == 'finalclass') 1523 { 1524 if ($sObjClass == $this->GetLinkedClass()) 1525 { 1526 // Simplify the output if the exact class could be determined implicitely 1527 continue; 1528 } 1529 } 1530 if ($sAttCode == $this->GetExtKeyToMe()) 1531 { 1532 continue; 1533 } 1534 if ($oAttDef->IsExternalField()) 1535 { 1536 /** @var \AttributeExternalField $oAttDef */ 1537 if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) 1538 { 1539 continue; 1540 } 1541 /** @var AttributeExternalField $oAttDef */ 1542 if ($oAttDef->IsFriendlyName()) 1543 { 1544 continue; 1545 } 1546 } 1547 if ($oAttDef instanceof AttributeFriendlyName) 1548 { 1549 continue; 1550 } 1551 if (!$oAttDef->IsScalar()) 1552 { 1553 continue; 1554 } 1555 $sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize); 1556 $sRes .= "<$sAttCode>$sAttValue</$sAttCode>\n"; 1557 } 1558 $sRes .= "</$sObjClass>\n"; 1559 } 1560 $sRes .= "</Set>\n"; 1561 } 1562 else 1563 { 1564 $sRes = ''; 1565 } 1566 1567 return $sRes; 1568 } 1569 1570 /** 1571 * @param $sValue 1572 * @param string $sSeparator 1573 * @param string $sTextQualifier 1574 * @param \DBObject $oHostObject 1575 * @param bool $bLocalize 1576 * @param bool $bConvertToPlainText 1577 * 1578 * @return mixed|string 1579 * @throws \CoreException 1580 */ 1581 public function GetAsCSV( 1582 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 1583 $bConvertToPlainText = false 1584 ) { 1585 $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); 1586 $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); 1587 $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); 1588 $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); 1589 1590 if (is_object($sValue) && ($sValue instanceof ormLinkSet)) 1591 { 1592 $sValue->Rewind(); 1593 $aItems = array(); 1594 while ($oObj = $sValue->Fetch()) 1595 { 1596 $sObjClass = get_class($oObj); 1597 // Show only relevant information (hide the external key to the current object) 1598 $aAttributes = array(); 1599 foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) 1600 { 1601 if ($sAttCode == 'finalclass') 1602 { 1603 if ($sObjClass == $this->GetLinkedClass()) 1604 { 1605 // Simplify the output if the exact class could be determined implicitely 1606 continue; 1607 } 1608 } 1609 if ($sAttCode == $this->GetExtKeyToMe()) 1610 { 1611 continue; 1612 } 1613 if ($oAttDef->IsExternalField()) 1614 { 1615 continue; 1616 } 1617 if (!$oAttDef->IsBasedOnDBColumns()) 1618 { 1619 continue; 1620 } 1621 if (!$oAttDef->IsScalar()) 1622 { 1623 continue; 1624 } 1625 $sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize); 1626 if (strlen($sAttValue) > 0) 1627 { 1628 $sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier, 1629 $sAttCode.$sSepValue.$sAttValue); 1630 $aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier; 1631 } 1632 } 1633 $sAttributes = implode($sSepAttribute, $aAttributes); 1634 $aItems[] = $sAttributes; 1635 } 1636 $sRes = implode($sSepItem, $aItems); 1637 } 1638 else 1639 { 1640 $sRes = ''; 1641 } 1642 $sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes); 1643 $sRes = $sTextQualifier.$sRes.$sTextQualifier; 1644 1645 return $sRes; 1646 } 1647 1648 /** 1649 * List the available verbs for 'GetForTemplate' 1650 */ 1651 public function EnumTemplateVerbs() 1652 { 1653 return array( 1654 '' => 'Plain text (unlocalized) representation', 1655 'html' => 'HTML representation (unordered list)', 1656 ); 1657 } 1658 1659 /** 1660 * Get various representations of the value, for insertion into a template (e.g. in Notifications) 1661 * 1662 * @param mixed $value The current value of the field 1663 * @param string $sVerb The verb specifying the representation of the value 1664 * @param DBObject $oHostObject The object 1665 * @param bool $bLocalize Whether or not to localize the value 1666 * 1667 * @return string 1668 * @throws \Exception 1669 */ 1670 public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) 1671 { 1672 $sRemoteName = $this->IsIndirect() ? 1673 /** @var \AttributeLinkedSetIndirect $this */ 1674 $this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname'; 1675 1676 $oLinkSet = clone $value; // Workaround/Safety net for Trac #887 1677 $iLimit = MetaModel::GetConfig()->Get('max_linkset_output'); 1678 $iCount = 0; 1679 $aNames = array(); 1680 foreach($oLinkSet as $oItem) 1681 { 1682 if (($iLimit > 0) && ($iCount == $iLimit)) 1683 { 1684 $iTotal = $oLinkSet->Count(); 1685 $aNames[] = '... '.Dict::Format('UI:TruncatedResults', $iCount, $iTotal); 1686 break; 1687 } 1688 $aNames[] = $oItem->Get($sRemoteName); 1689 $iCount++; 1690 } 1691 1692 switch ($sVerb) 1693 { 1694 case '': 1695 return implode("\n", $aNames); 1696 1697 case 'html': 1698 return '<ul><li>'.implode("</li><li>", $aNames).'</li></ul>'; 1699 1700 default: 1701 throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); 1702 } 1703 } 1704 1705 public function DuplicatesAllowed() 1706 { 1707 return false; 1708 } // No duplicates for 1:n links, never 1709 1710 public function GetImportColumns() 1711 { 1712 $aColumns = array(); 1713 $aColumns[$this->GetCode()] = 'TEXT'; 1714 1715 return $aColumns; 1716 } 1717 1718 /** 1719 * @param string $sProposedValue 1720 * @param bool $bLocalizedValue 1721 * @param string $sSepItem 1722 * @param string $sSepAttribute 1723 * @param string $sSepValue 1724 * @param string $sAttributeQualifier 1725 * 1726 * @return \DBObjectSet|mixed 1727 * @throws \CSVParserException 1728 * @throws \CoreException 1729 * @throws \CoreUnexpectedValue 1730 * @throws \MissingQueryArgument 1731 * @throws \MySQLException 1732 * @throws \MySQLHasGoneAwayException 1733 * @throws \Exception 1734 */ 1735 public function MakeValueFromString( 1736 $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, 1737 $sAttributeQualifier = null 1738 ) { 1739 if (is_null($sSepItem) || empty($sSepItem)) 1740 { 1741 $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); 1742 } 1743 if (is_null($sSepAttribute) || empty($sSepAttribute)) 1744 { 1745 $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); 1746 } 1747 if (is_null($sSepValue) || empty($sSepValue)) 1748 { 1749 $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); 1750 } 1751 if (is_null($sAttributeQualifier) || empty($sAttributeQualifier)) 1752 { 1753 $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); 1754 } 1755 1756 $sTargetClass = $this->Get('linked_class'); 1757 1758 $sInput = str_replace($sSepItem, "\n", $sProposedValue); 1759 $oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier); 1760 1761 $aInput = $oCSVParser->ToArray(0 /* do not skip lines */); 1762 1763 $aLinks = array(); 1764 foreach($aInput as $aRow) 1765 { 1766 // 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value 1767 $aExtKeys = array(); 1768 $aValues = array(); 1769 foreach($aRow as $sCell) 1770 { 1771 $iSepPos = strpos($sCell, $sSepValue); 1772 if ($iSepPos === false) 1773 { 1774 // Houston... 1775 throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell)); 1776 } 1777 1778 $sAttCode = trim(substr($sCell, 0, $iSepPos)); 1779 $sValue = substr($sCell, $iSepPos + strlen($sSepValue)); 1780 1781 if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches)) 1782 { 1783 $sKeyAttCode = $aMatches[1]; 1784 $sRemoteAttCode = $aMatches[2]; 1785 $aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue; 1786 if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode)) 1787 { 1788 throw new CoreException('Wrong attribute code for link attribute specification', 1789 array('class' => $sTargetClass, 'attcode' => $sKeyAttCode)); 1790 } 1791 /** @var \AttributeExternalKey $oKeyAttDef */ 1792 $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); 1793 $sRemoteClass = $oKeyAttDef->GetTargetClass(); 1794 if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode)) 1795 { 1796 throw new CoreException('Wrong attribute code for link attribute specification', 1797 array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode)); 1798 } 1799 } 1800 else 1801 { 1802 if (!MetaModel::IsValidAttCode($sTargetClass, $sAttCode)) 1803 { 1804 throw new CoreException('Wrong attribute code for link attribute specification', 1805 array('class' => $sTargetClass, 'attcode' => $sAttCode)); 1806 } 1807 $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode); 1808 $aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem, 1809 $sSepAttribute, $sSepValue, $sAttributeQualifier); 1810 } 1811 } 1812 1813 // 2nd - Instanciate the object and set the value 1814 if (isset($aValues['finalclass'])) 1815 { 1816 $sLinkClass = $aValues['finalclass']; 1817 if (!is_subclass_of($sLinkClass, $sTargetClass)) 1818 { 1819 throw new CoreException('Wrong class for link attribute specification', 1820 array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); 1821 } 1822 } 1823 elseif (MetaModel::IsAbstract($sTargetClass)) 1824 { 1825 throw new CoreException('Missing finalclass for link attribute specification'); 1826 } 1827 else 1828 { 1829 $sLinkClass = $sTargetClass; 1830 } 1831 1832 $oLink = MetaModel::NewObject($sLinkClass); 1833 foreach($aValues as $sAttCode => $sValue) 1834 { 1835 $oLink->Set($sAttCode, $sValue); 1836 } 1837 1838 // 3rd - Set external keys from search conditions 1839 foreach($aExtKeys as $sKeyAttCode => $aReconciliation) 1840 { 1841 $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); 1842 $sKeyClass = $oKeyAttDef->GetTargetClass(); 1843 $oExtKeyFilter = new DBObjectSearch($sKeyClass); 1844 $aReconciliationDesc = array(); 1845 foreach($aReconciliation as $sRemoteAttCode => $sValue) 1846 { 1847 $oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '='); 1848 $aReconciliationDesc[] = "$sRemoteAttCode=$sValue"; 1849 } 1850 $oExtKeySet = new DBObjectSet($oExtKeyFilter); 1851 switch ($oExtKeySet->Count()) 1852 { 1853 case 0: 1854 $sReconciliationDesc = implode(', ', $aReconciliationDesc); 1855 throw new CoreException("Found no match", 1856 array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); 1857 break; 1858 case 1: 1859 $oRemoteObj = $oExtKeySet->Fetch(); 1860 $oLink->Set($sKeyAttCode, $oRemoteObj->GetKey()); 1861 break; 1862 default: 1863 $sReconciliationDesc = implode(', ', $aReconciliationDesc); 1864 throw new CoreException("Found several matches", 1865 array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); 1866 // Found several matches, ambiguous 1867 } 1868 } 1869 1870 // Check (roughly) if such a link is valid 1871 $aErrors = array(); 1872 foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) 1873 { 1874 if ($oAttDef->IsExternalKey()) 1875 { 1876 /** @var \AttributeExternalKey $oAttDef */ 1877 if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), 1878 $oAttDef->GetTargetClass()))) 1879 { 1880 continue; // Don't check the key to self 1881 } 1882 } 1883 1884 if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) 1885 { 1886 $aErrors[] = $sAttCode; 1887 } 1888 } 1889 if (count($aErrors) > 0) 1890 { 1891 throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); 1892 } 1893 1894 $aLinks[] = $oLink; 1895 } 1896 $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); 1897 1898 return $oSet; 1899 } 1900 1901 /** 1902 * Helper to get a value that will be JSON encoded 1903 * The operation is the opposite to FromJSONToValue 1904 * 1905 * @param \ormLinkSet $value 1906 * 1907 * @return array 1908 * @throws \CoreException 1909 */ 1910 public function GetForJSON($value) 1911 { 1912 $aRet = array(); 1913 if (is_object($value) && ($value instanceof ormLinkSet)) 1914 { 1915 $value->Rewind(); 1916 while ($oObj = $value->Fetch()) 1917 { 1918 $sObjClass = get_class($oObj); 1919 // Show only relevant information (hide the external key to the current object) 1920 $aAttributes = array(); 1921 foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) 1922 { 1923 if ($sAttCode == 'finalclass') 1924 { 1925 if ($sObjClass == $this->GetLinkedClass()) 1926 { 1927 // Simplify the output if the exact class could be determined implicitely 1928 continue; 1929 } 1930 } 1931 if ($sAttCode == $this->GetExtKeyToMe()) 1932 { 1933 continue; 1934 } 1935 if ($oAttDef->IsExternalField()) 1936 { 1937 continue; 1938 } 1939 if (!$oAttDef->IsBasedOnDBColumns()) 1940 { 1941 continue; 1942 } 1943 if (!$oAttDef->IsScalar()) 1944 { 1945 continue; 1946 } 1947 $attValue = $oObj->Get($sAttCode); 1948 $aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue); 1949 } 1950 $aRet[] = $aAttributes; 1951 } 1952 } 1953 1954 return $aRet; 1955 } 1956 1957 /** 1958 * Helper to form a value, given JSON decoded data 1959 * The operation is the opposite to GetForJSON 1960 * 1961 * @param $json 1962 * 1963 * @return \DBObjectSet 1964 * @throws \CoreException 1965 * @throws \CoreUnexpectedValue 1966 * @throws \Exception 1967 */ 1968 public function FromJSONToValue($json) 1969 { 1970 $sTargetClass = $this->Get('linked_class'); 1971 1972 $aLinks = array(); 1973 foreach($json as $aValues) 1974 { 1975 if (isset($aValues['finalclass'])) 1976 { 1977 $sLinkClass = $aValues['finalclass']; 1978 if (!is_subclass_of($sLinkClass, $sTargetClass)) 1979 { 1980 throw new CoreException('Wrong class for link attribute specification', 1981 array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); 1982 } 1983 } 1984 elseif (MetaModel::IsAbstract($sTargetClass)) 1985 { 1986 throw new CoreException('Missing finalclass for link attribute specification'); 1987 } 1988 else 1989 { 1990 $sLinkClass = $sTargetClass; 1991 } 1992 1993 $oLink = MetaModel::NewObject($sLinkClass); 1994 foreach($aValues as $sAttCode => $sValue) 1995 { 1996 $oLink->Set($sAttCode, $sValue); 1997 } 1998 1999 // Check (roughly) if such a link is valid 2000 $aErrors = array(); 2001 foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) 2002 { 2003 if ($oAttDef->IsExternalKey()) 2004 { 2005 /** @var AttributeExternalKey $oAttDef */ 2006 if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), 2007 $oAttDef->GetTargetClass()))) 2008 { 2009 continue; // Don't check the key to self 2010 } 2011 } 2012 2013 if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) 2014 { 2015 $aErrors[] = $sAttCode; 2016 } 2017 } 2018 if (count($aErrors) > 0) 2019 { 2020 throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); 2021 } 2022 2023 $aLinks[] = $oLink; 2024 } 2025 $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); 2026 2027 return $oSet; 2028 } 2029 2030 /** 2031 * @param $proposedValue 2032 * @param $oHostObj 2033 * 2034 * @return mixed 2035 * @throws \Exception 2036 */ 2037 public function MakeRealValue($proposedValue, $oHostObj) 2038 { 2039 if ($proposedValue === null) 2040 { 2041 $sLinkedClass = $this->GetLinkedClass(); 2042 $aLinkedObjectsArray = array(); 2043 $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); 2044 2045 return new ormLinkSet( 2046 get_class($oHostObj), 2047 $this->GetCode(), 2048 $oSet 2049 ); 2050 } 2051 2052 return $proposedValue; 2053 } 2054 2055 /** 2056 * @param ormLinkSet $val1 2057 * @param ormLinkSet $val2 2058 * 2059 * @return bool 2060 */ 2061 public function Equals($val1, $val2) 2062 { 2063 if ($val1 === $val2) 2064 { 2065 $bAreEquivalent = true; 2066 } 2067 else 2068 { 2069 $bAreEquivalent = ($val2->HasDelta() === false); 2070 } 2071 2072 return $bAreEquivalent; 2073 } 2074 2075 /** 2076 * Find the corresponding "link" attribute on the target class, if any 2077 * 2078 * @return null | AttributeDefinition 2079 * @throws \Exception 2080 */ 2081 public function GetMirrorLinkAttribute() 2082 { 2083 $oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe()); 2084 2085 return $oRemoteAtt; 2086 } 2087 2088 static public function GetFormFieldClass() 2089 { 2090 return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField'; 2091 } 2092 2093 /** 2094 * @param \DBObject $oObject 2095 * @param \Combodo\iTop\Form\Field\LinkedSetField $oFormField 2096 * 2097 * @return \Combodo\iTop\Form\Field\LinkedSetField 2098 * @throws \CoreException 2099 * @throws \DictExceptionMissingString 2100 * @throws \Exception 2101 */ 2102 public function MakeFormField(DBObject $oObject, $oFormField = null) 2103 { 2104 if ($oFormField === null) 2105 { 2106 $sFormFieldClass = static::GetFormFieldClass(); 2107 $oFormField = new $sFormFieldClass($this->GetCode()); 2108 } 2109 2110 // Setting target class 2111 if (!$this->IsIndirect()) 2112 { 2113 $sTargetClass = $this->GetLinkedClass(); 2114 } 2115 else 2116 { 2117 /** @var \AttributeExternalKey $oRemoteAttDef */ 2118 /** @var \AttributeLinkedSetIndirect $this */ 2119 $oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); 2120 $sTargetClass = $oRemoteAttDef->GetTargetClass(); 2121 2122 /** @var \AttributeLinkedSetIndirect $this */ 2123 $oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote()); 2124 } 2125 $oFormField->SetTargetClass($sTargetClass); 2126 $oFormField->SetIndirect($this->IsIndirect()); 2127 // Setting attcodes to display 2128 $aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list')); 2129 // - Adding friendlyname attribute to the list is not already in it 2130 $sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass); 2131 if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) 2132 { 2133 $aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay); 2134 } 2135 // - Adding attribute labels 2136 $aAttributesToDisplay = array(); 2137 foreach($aAttCodesToDisplay as $sAttCodeToDisplay) 2138 { 2139 $oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay); 2140 $aAttributesToDisplay[$sAttCodeToDisplay] = $oAttDefToDisplay->GetLabel(); 2141 } 2142 $oFormField->SetAttributesToDisplay($aAttributesToDisplay); 2143 2144 parent::MakeFormField($oObject, $oFormField); 2145 2146 return $oFormField; 2147 } 2148 2149 public function IsPartOfFingerprint() 2150 { 2151 return false; 2152 } 2153} 2154 2155/** 2156 * Set of objects linked to an object (n-n), and being part of its definition 2157 * 2158 * @package iTopORM 2159 */ 2160class AttributeLinkedSetIndirect extends AttributeLinkedSet 2161{ 2162 static public function ListExpectedParams() 2163 { 2164 return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote")); 2165 } 2166 2167 public function IsIndirect() 2168 { 2169 return true; 2170 } 2171 2172 public function GetExtKeyToRemote() 2173 { 2174 return $this->Get('ext_key_to_remote'); 2175 } 2176 2177 public function GetEditClass() 2178 { 2179 return "LinkedSet"; 2180 } 2181 2182 public function DuplicatesAllowed() 2183 { 2184 return $this->GetOptional("duplicates", false); 2185 } // The same object may be linked several times... or not... 2186 2187 public function GetTrackingLevel() 2188 { 2189 return $this->GetOptional('tracking_level', 2190 MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default')); 2191 } 2192 2193 /** 2194 * Find the corresponding "link" attribute on the target class, if any 2195 * 2196 * @return null | AttributeDefinition 2197 * @throws \CoreException 2198 */ 2199 public function GetMirrorLinkAttribute() 2200 { 2201 $oRet = null; 2202 /** @var \AttributeExternalKey $oExtKeyToRemote */ 2203 $oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); 2204 $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); 2205 foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) 2206 { 2207 if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect) 2208 { 2209 continue; 2210 } 2211 if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass()) 2212 { 2213 continue; 2214 } 2215 if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote()) 2216 { 2217 continue; 2218 } 2219 if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe()) 2220 { 2221 continue; 2222 } 2223 $oRet = $oRemoteAttDef; 2224 break; 2225 } 2226 2227 return $oRet; 2228 } 2229} 2230 2231/** 2232 * Abstract class implementing default filters for a DB column 2233 * 2234 * @package iTopORM 2235 */ 2236class AttributeDBFieldVoid extends AttributeDefinition 2237{ 2238 static public function ListExpectedParams() 2239 { 2240 return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql")); 2241 } 2242 2243 // To be overriden, used in GetSQLColumns 2244 protected function GetSQLCol($bFullSpec = false) 2245 { 2246 return 'VARCHAR(255)' 2247 .CMDBSource::GetSqlStringColumnDefinition() 2248 .($bFullSpec ? $this->GetSQLColSpec() : ''); 2249 } 2250 2251 protected function GetSQLColSpec() 2252 { 2253 $default = $this->ScalarToSQL($this->GetDefaultValue()); 2254 if (is_null($default)) 2255 { 2256 $sRet = ''; 2257 } 2258 else 2259 { 2260 if (is_numeric($default)) 2261 { 2262 // Though it is a string in PHP, it will be considered as a numeric value in MySQL 2263 // Then it must not be quoted here, to preserve the compatibility with the value returned by CMDBSource::GetFieldSpec 2264 $sRet = " DEFAULT $default"; 2265 } 2266 else 2267 { 2268 $sRet = " DEFAULT ".CMDBSource::Quote($default); 2269 } 2270 } 2271 2272 return $sRet; 2273 } 2274 2275 public function GetEditClass() 2276 { 2277 return "String"; 2278 } 2279 2280 public function GetValuesDef() 2281 { 2282 return $this->Get("allowed_values"); 2283 } 2284 2285 public function GetPrerequisiteAttributes($sClass = null) 2286 { 2287 return $this->Get("depends_on"); 2288 } 2289 2290 static public function IsBasedOnDBColumns() 2291 { 2292 return true; 2293 } 2294 2295 static public function IsScalar() 2296 { 2297 return true; 2298 } 2299 2300 public function IsWritable() 2301 { 2302 return !$this->IsMagic(); 2303 } 2304 2305 public function GetSQLExpr() 2306 { 2307 return $this->Get("sql"); 2308 } 2309 2310 public function GetDefaultValue(DBObject $oHostObject = null) 2311 { 2312 return $this->MakeRealValue("", $oHostObject); 2313 } 2314 2315 public function IsNullAllowed() 2316 { 2317 return false; 2318 } 2319 2320 // 2321 protected function ScalarToSQL($value) 2322 { 2323 return $value; 2324 } // format value as a valuable SQL literal (quoted outside) 2325 2326 public function GetSQLExpressions($sPrefix = '') 2327 { 2328 $aColumns = array(); 2329 // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix 2330 $aColumns[''] = $this->Get("sql"); 2331 2332 return $aColumns; 2333 } 2334 2335 public function FromSQLToValue($aCols, $sPrefix = '') 2336 { 2337 $value = $this->MakeRealValue($aCols[$sPrefix.''], null); 2338 2339 return $value; 2340 } 2341 2342 public function GetSQLValues($value) 2343 { 2344 $aValues = array(); 2345 $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); 2346 2347 return $aValues; 2348 } 2349 2350 public function GetSQLColumns($bFullSpec = false) 2351 { 2352 $aColumns = array(); 2353 $aColumns[$this->Get("sql")] = $this->GetSQLCol($bFullSpec); 2354 2355 return $aColumns; 2356 } 2357 2358 public function GetFilterDefinitions() 2359 { 2360 return array($this->GetCode() => new FilterFromAttribute($this)); 2361 } 2362 2363 public function GetBasicFilterOperators() 2364 { 2365 return array("=" => "equals", "!=" => "differs from"); 2366 } 2367 2368 public function GetBasicFilterLooseOperator() 2369 { 2370 return "="; 2371 } 2372 2373 public function GetBasicFilterSQLExpr($sOpCode, $value) 2374 { 2375 $sQValue = CMDBSource::Quote($value); 2376 switch ($sOpCode) 2377 { 2378 case '!=': 2379 return $this->GetSQLExpr()." != $sQValue"; 2380 break; 2381 case '=': 2382 default: 2383 return $this->GetSQLExpr()." = $sQValue"; 2384 } 2385 } 2386} 2387 2388/** 2389 * Base class for all kind of DB attributes, with the exception of external keys 2390 * 2391 * @package iTopORM 2392 */ 2393class AttributeDBField extends AttributeDBFieldVoid 2394{ 2395 static public function ListExpectedParams() 2396 { 2397 return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed")); 2398 } 2399 2400 public function GetDefaultValue(DBObject $oHostObject = null) 2401 { 2402 return $this->MakeRealValue($this->Get("default_value"), $oHostObject); 2403 } 2404 2405 public function IsNullAllowed() 2406 { 2407 return $this->Get("is_null_allowed"); 2408 } 2409} 2410 2411/** 2412 * Map an integer column to an attribute 2413 * 2414 * @package iTopORM 2415 */ 2416class AttributeInteger extends AttributeDBField 2417{ 2418 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; 2419 2420 static public function ListExpectedParams() 2421 { 2422 return parent::ListExpectedParams(); 2423 //return array_merge(parent::ListExpectedParams(), array()); 2424 } 2425 2426 public function GetEditClass() 2427 { 2428 return "String"; 2429 } 2430 2431 protected function GetSQLCol($bFullSpec = false) 2432 { 2433 return "INT(11)".($bFullSpec ? $this->GetSQLColSpec() : ''); 2434 } 2435 2436 public function GetValidationPattern() 2437 { 2438 return "^[0-9]+$"; 2439 } 2440 2441 public function GetBasicFilterOperators() 2442 { 2443 return array( 2444 "!=" => "differs from", 2445 "=" => "equals", 2446 ">" => "greater (strict) than", 2447 ">=" => "greater than", 2448 "<" => "less (strict) than", 2449 "<=" => "less than", 2450 "in" => "in" 2451 ); 2452 } 2453 2454 public function GetBasicFilterLooseOperator() 2455 { 2456 // Unless we implement an "equals approximately..." or "same order of magnitude" 2457 return "="; 2458 } 2459 2460 public function GetBasicFilterSQLExpr($sOpCode, $value) 2461 { 2462 $sQValue = CMDBSource::Quote($value); 2463 switch ($sOpCode) 2464 { 2465 case '!=': 2466 return $this->GetSQLExpr()." != $sQValue"; 2467 break; 2468 case '>': 2469 return $this->GetSQLExpr()." > $sQValue"; 2470 break; 2471 case '>=': 2472 return $this->GetSQLExpr()." >= $sQValue"; 2473 break; 2474 case '<': 2475 return $this->GetSQLExpr()." < $sQValue"; 2476 break; 2477 case '<=': 2478 return $this->GetSQLExpr()." <= $sQValue"; 2479 break; 2480 case 'in': 2481 if (!is_array($value)) 2482 { 2483 throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); 2484 } 2485 2486 return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; 2487 break; 2488 2489 case '=': 2490 default: 2491 return $this->GetSQLExpr()." = \"$value\""; 2492 } 2493 } 2494 2495 public function GetNullValue() 2496 { 2497 return null; 2498 } 2499 2500 public function IsNull($proposedValue) 2501 { 2502 return is_null($proposedValue); 2503 } 2504 2505 public function MakeRealValue($proposedValue, $oHostObj) 2506 { 2507 if (is_null($proposedValue)) 2508 { 2509 return null; 2510 } 2511 if ($proposedValue === '') 2512 { 2513 return null; 2514 } // 0 is transformed into '' ! 2515 2516 return (int)$proposedValue; 2517 } 2518 2519 public function ScalarToSQL($value) 2520 { 2521 assert(is_numeric($value) || is_null($value)); 2522 2523 return $value; // supposed to be an int 2524 } 2525} 2526 2527/** 2528 * An external key for which the class is defined as the value of another attribute 2529 * 2530 * @package iTopORM 2531 */ 2532class AttributeObjectKey extends AttributeDBFieldVoid 2533{ 2534 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; 2535 2536 static public function ListExpectedParams() 2537 { 2538 return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed')); 2539 } 2540 2541 public function GetEditClass() 2542 { 2543 return "String"; 2544 } 2545 2546 protected function GetSQLCol($bFullSpec = false) 2547 { 2548 return "INT(11)".($bFullSpec ? " DEFAULT 0" : ""); 2549 } 2550 2551 public function GetDefaultValue(DBObject $oHostObject = null) 2552 { 2553 return 0; 2554 } 2555 2556 public function IsNullAllowed() 2557 { 2558 return $this->Get("is_null_allowed"); 2559 } 2560 2561 2562 public function GetBasicFilterOperators() 2563 { 2564 return parent::GetBasicFilterOperators(); 2565 } 2566 2567 public function GetBasicFilterLooseOperator() 2568 { 2569 return parent::GetBasicFilterLooseOperator(); 2570 } 2571 2572 public function GetBasicFilterSQLExpr($sOpCode, $value) 2573 { 2574 return parent::GetBasicFilterSQLExpr($sOpCode, $value); 2575 } 2576 2577 public function GetNullValue() 2578 { 2579 return 0; 2580 } 2581 2582 public function IsNull($proposedValue) 2583 { 2584 return ($proposedValue == 0); 2585 } 2586 2587 public function MakeRealValue($proposedValue, $oHostObj) 2588 { 2589 if (is_null($proposedValue)) 2590 { 2591 return 0; 2592 } 2593 if ($proposedValue === '') 2594 { 2595 return 0; 2596 } 2597 if (MetaModel::IsValidObject($proposedValue)) 2598 { 2599 /** @var \DBObject $proposedValue */ 2600 return $proposedValue->GetKey(); 2601 } 2602 2603 return (int)$proposedValue; 2604 } 2605} 2606 2607/** 2608 * Display an integer between 0 and 100 as a percentage / horizontal bar graph 2609 * 2610 * @package iTopORM 2611 */ 2612class AttributePercentage extends AttributeInteger 2613{ 2614 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; 2615 2616 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 2617 { 2618 $iWidth = 5; // Total width of the percentage bar graph, in em... 2619 $iValue = (int)$sValue; 2620 if ($iValue > 100) 2621 { 2622 $iValue = 100; 2623 } 2624 else 2625 { 2626 if ($iValue < 0) 2627 { 2628 $iValue = 0; 2629 } 2630 } 2631 if ($iValue > 90) 2632 { 2633 $sColor = "#cc3300"; 2634 } 2635 else 2636 { 2637 if ($iValue > 50) 2638 { 2639 $sColor = "#cccc00"; 2640 } 2641 else 2642 { 2643 $sColor = "#33cc00"; 2644 } 2645 } 2646 $iPercentWidth = ($iWidth * $iValue) / 100; 2647 2648 return "<div style=\"width:{$iWidth}em;-moz-border-radius: 3px;-webkit-border-radius: 3px;border-radius: 3px;display:inline-block;border: 1px #ccc solid;\"><div style=\"width:{$iPercentWidth}em; display:inline-block;background-color:$sColor;\"> </div></div> $sValue %"; 2649 } 2650} 2651 2652/** 2653 * Map a decimal value column (suitable for financial computations) to an attribute 2654 * internally in PHP such numbers are represented as string. Should you want to perform 2655 * a calculation on them, it is recommended to use the BC Math functions in order to 2656 * retain the precision 2657 * 2658 * @package iTopORM 2659 */ 2660class AttributeDecimal extends AttributeDBField 2661{ 2662 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; 2663 2664 static public function ListExpectedParams() 2665 { 2666 return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */)); 2667 } 2668 2669 public function GetEditClass() 2670 { 2671 return "String"; 2672 } 2673 2674 protected function GetSQLCol($bFullSpec = false) 2675 { 2676 return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")".($bFullSpec ? $this->GetSQLColSpec() : ''); 2677 } 2678 2679 public function GetValidationPattern() 2680 { 2681 $iNbDigits = $this->Get('digits'); 2682 $iPrecision = $this->Get('decimals'); 2683 $iNbIntegerDigits = $iNbDigits - $iPrecision - 1; // -1 because the first digit is treated separately in the pattern below 2684 2685 return "^[-+]?[0-9]\d{0,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$"; 2686 } 2687 2688 public function GetBasicFilterOperators() 2689 { 2690 return array( 2691 "!=" => "differs from", 2692 "=" => "equals", 2693 ">" => "greater (strict) than", 2694 ">=" => "greater than", 2695 "<" => "less (strict) than", 2696 "<=" => "less than", 2697 "in" => "in" 2698 ); 2699 } 2700 2701 public function GetBasicFilterLooseOperator() 2702 { 2703 // Unless we implement an "equals approximately..." or "same order of magnitude" 2704 return "="; 2705 } 2706 2707 public function GetBasicFilterSQLExpr($sOpCode, $value) 2708 { 2709 $sQValue = CMDBSource::Quote($value); 2710 switch ($sOpCode) 2711 { 2712 case '!=': 2713 return $this->GetSQLExpr()." != $sQValue"; 2714 break; 2715 case '>': 2716 return $this->GetSQLExpr()." > $sQValue"; 2717 break; 2718 case '>=': 2719 return $this->GetSQLExpr()." >= $sQValue"; 2720 break; 2721 case '<': 2722 return $this->GetSQLExpr()." < $sQValue"; 2723 break; 2724 case '<=': 2725 return $this->GetSQLExpr()." <= $sQValue"; 2726 break; 2727 case 'in': 2728 if (!is_array($value)) 2729 { 2730 throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); 2731 } 2732 2733 return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; 2734 break; 2735 2736 case '=': 2737 default: 2738 return $this->GetSQLExpr()." = \"$value\""; 2739 } 2740 } 2741 2742 public function GetNullValue() 2743 { 2744 return null; 2745 } 2746 2747 public function IsNull($proposedValue) 2748 { 2749 return is_null($proposedValue); 2750 } 2751 2752 public function MakeRealValue($proposedValue, $oHostObj) 2753 { 2754 if (is_null($proposedValue)) 2755 { 2756 return null; 2757 } 2758 if ($proposedValue === '') 2759 { 2760 return null; 2761 } 2762 2763 return $this->ScalarToSQL($proposedValue); 2764 } 2765 2766 public function ScalarToSQL($value) 2767 { 2768 assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value)); 2769 2770 if (!is_null($value) && ($value !== '')) 2771 { 2772 $value = sprintf("%01.".$this->Get('decimals')."f", $value); 2773 } 2774 return $value; // null or string 2775 } 2776} 2777 2778/** 2779 * Map a boolean column to an attribute 2780 * 2781 * @package iTopORM 2782 */ 2783class AttributeBoolean extends AttributeInteger 2784{ 2785 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 2786 2787 static public function ListExpectedParams() 2788 { 2789 return parent::ListExpectedParams(); 2790 //return array_merge(parent::ListExpectedParams(), array()); 2791 } 2792 2793 public function GetEditClass() 2794 { 2795 return "Integer"; 2796 } 2797 2798 protected function GetSQLCol($bFullSpec = false) 2799 { 2800 return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : ''); 2801 } 2802 2803 public function MakeRealValue($proposedValue, $oHostObj) 2804 { 2805 if (is_null($proposedValue)) 2806 { 2807 return null; 2808 } 2809 if ($proposedValue === '') 2810 { 2811 return null; 2812 } 2813 if ((int)$proposedValue) 2814 { 2815 return true; 2816 } 2817 2818 return false; 2819 } 2820 2821 public function ScalarToSQL($value) 2822 { 2823 if ($value) 2824 { 2825 return 1; 2826 } 2827 2828 return 0; 2829 } 2830 2831 public function GetValueLabel($bValue) 2832 { 2833 if (is_null($bValue)) 2834 { 2835 $sLabel = Dict::S('Core:'.get_class($this).'/Value:null'); 2836 } 2837 else 2838 { 2839 $sValue = $bValue ? 'yes' : 'no'; 2840 $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue); 2841 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/); 2842 } 2843 2844 return $sLabel; 2845 } 2846 2847 public function GetValueDescription($bValue) 2848 { 2849 if (is_null($bValue)) 2850 { 2851 $sDescription = Dict::S('Core:'.get_class($this).'/Value:null+'); 2852 } 2853 else 2854 { 2855 $sValue = $bValue ? 'yes' : 'no'; 2856 $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+'); 2857 $sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault, 2858 true /*user lang*/); 2859 } 2860 2861 return $sDescription; 2862 } 2863 2864 public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true) 2865 { 2866 if (is_null($bValue)) 2867 { 2868 $sRes = ''; 2869 } 2870 elseif ($bLocalize) 2871 { 2872 $sLabel = $this->GetValueLabel($bValue); 2873 $sDescription = $this->GetValueDescription($bValue); 2874 // later, we could imagine a detailed description in the title 2875 $sRes = "<span title=\"$sDescription\">".parent::GetAsHtml($sLabel)."</span>"; 2876 } 2877 else 2878 { 2879 $sRes = $bValue ? 'yes' : 'no'; 2880 } 2881 2882 return $sRes; 2883 } 2884 2885 public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true) 2886 { 2887 if (is_null($bValue)) 2888 { 2889 $sFinalValue = ''; 2890 } 2891 elseif ($bLocalize) 2892 { 2893 $sFinalValue = $this->GetValueLabel($bValue); 2894 } 2895 else 2896 { 2897 $sFinalValue = $bValue ? 'yes' : 'no'; 2898 } 2899 $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); 2900 2901 return $sRes; 2902 } 2903 2904 public function GetAsCSV( 2905 $bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 2906 $bConvertToPlainText = false 2907 ) { 2908 if (is_null($bValue)) 2909 { 2910 $sFinalValue = ''; 2911 } 2912 elseif ($bLocalize) 2913 { 2914 $sFinalValue = $this->GetValueLabel($bValue); 2915 } 2916 else 2917 { 2918 $sFinalValue = $bValue ? 'yes' : 'no'; 2919 } 2920 $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); 2921 2922 return $sRes; 2923 } 2924 2925 static public function GetFormFieldClass() 2926 { 2927 return '\\Combodo\\iTop\\Form\\Field\\SelectField'; 2928 } 2929 2930 /** 2931 * @param \DBObject $oObject 2932 * @param \Combodo\iTop\Form\Field\SelectField $oFormField 2933 * 2934 * @return \Combodo\iTop\Form\Field\SelectField 2935 * @throws \CoreException 2936 */ 2937 public function MakeFormField(DBObject $oObject, $oFormField = null) 2938 { 2939 if ($oFormField === null) 2940 { 2941 $sFormFieldClass = static::GetFormFieldClass(); 2942 $oFormField = new $sFormFieldClass($this->GetCode()); 2943 } 2944 2945 $oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false))); 2946 parent::MakeFormField($oObject, $oFormField); 2947 2948 return $oFormField; 2949 } 2950 2951 public function GetEditValue($value, $oHostObj = null) 2952 { 2953 if (is_null($value)) 2954 { 2955 return ''; 2956 } 2957 else 2958 { 2959 return $this->GetValueLabel($value); 2960 } 2961 } 2962 2963 /** 2964 * Helper to get a value that will be JSON encoded 2965 * The operation is the opposite to FromJSONToValue 2966 * 2967 * @param $value 2968 * 2969 * @return bool 2970 */ 2971 public function GetForJSON($value) 2972 { 2973 return (bool)$value; 2974 } 2975 2976 public function MakeValueFromString( 2977 $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, 2978 $sAttributeQualifier = null 2979 ) { 2980 $sInput = strtolower(trim($sProposedValue)); 2981 if ($bLocalizedValue) 2982 { 2983 switch ($sInput) 2984 { 2985 case '1': // backward compatibility 2986 case $this->GetValueLabel(true): 2987 $value = true; 2988 break; 2989 case '0': // backward compatibility 2990 case 'no': 2991 case $this->GetValueLabel(false): 2992 $value = false; 2993 break; 2994 default: 2995 $value = null; 2996 } 2997 } 2998 else 2999 { 3000 switch ($sInput) 3001 { 3002 case '1': // backward compatibility 3003 case 'yes': 3004 $value = true; 3005 break; 3006 case '0': // backward compatibility 3007 case 'no': 3008 $value = false; 3009 break; 3010 default: 3011 $value = null; 3012 } 3013 } 3014 3015 return $value; 3016 } 3017} 3018 3019/** 3020 * Map a varchar column (size < ?) to an attribute 3021 * 3022 * @package iTopORM 3023 */ 3024class AttributeString extends AttributeDBField 3025{ 3026 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 3027 3028 static public function ListExpectedParams() 3029 { 3030 return parent::ListExpectedParams(); 3031 //return array_merge(parent::ListExpectedParams(), array()); 3032 } 3033 3034 public function GetEditClass() 3035 { 3036 return "String"; 3037 } 3038 3039 protected function GetSQLCol($bFullSpec = false) 3040 { 3041 return 'VARCHAR(255)' 3042 .CMDBSource::GetSqlStringColumnDefinition() 3043 .($bFullSpec ? $this->GetSQLColSpec() : ''); 3044 } 3045 3046 public function GetValidationPattern() 3047 { 3048 $sPattern = $this->GetOptional('validation_pattern', ''); 3049 if (empty($sPattern)) 3050 { 3051 return parent::GetValidationPattern(); 3052 } 3053 else 3054 { 3055 return $sPattern; 3056 } 3057 } 3058 3059 public function CheckFormat($value) 3060 { 3061 $sRegExp = $this->GetValidationPattern(); 3062 if (empty($sRegExp)) 3063 { 3064 return true; 3065 } 3066 else 3067 { 3068 $sRegExp = str_replace('/', '\\/', $sRegExp); 3069 3070 return preg_match("/$sRegExp/", $value); 3071 } 3072 } 3073 3074 public function GetMaxSize() 3075 { 3076 return 255; 3077 } 3078 3079 public function GetBasicFilterOperators() 3080 { 3081 return array( 3082 "=" => "equals", 3083 "!=" => "differs from", 3084 "Like" => "equals (no case)", 3085 "NotLike" => "differs from (no case)", 3086 "Contains" => "contains", 3087 "Begins with" => "begins with", 3088 "Finishes with" => "finishes with" 3089 ); 3090 } 3091 3092 public function GetBasicFilterLooseOperator() 3093 { 3094 return "Contains"; 3095 } 3096 3097 public function GetBasicFilterSQLExpr($sOpCode, $value) 3098 { 3099 $sQValue = CMDBSource::Quote($value); 3100 switch ($sOpCode) 3101 { 3102 case '=': 3103 case '!=': 3104 return $this->GetSQLExpr()." $sOpCode $sQValue"; 3105 case 'Begins with': 3106 return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("$value%"); 3107 case 'Finishes with': 3108 return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value"); 3109 case 'Contains': 3110 return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); 3111 case 'NotLike': 3112 return $this->GetSQLExpr()." NOT LIKE $sQValue"; 3113 case 'Like': 3114 default: 3115 return $this->GetSQLExpr()." LIKE $sQValue"; 3116 } 3117 } 3118 3119 public function GetNullValue() 3120 { 3121 return ''; 3122 } 3123 3124 public function IsNull($proposedValue) 3125 { 3126 return ($proposedValue == ''); 3127 } 3128 3129 public function MakeRealValue($proposedValue, $oHostObj) 3130 { 3131 if (is_null($proposedValue)) 3132 { 3133 return ''; 3134 } 3135 3136 return (string)$proposedValue; 3137 } 3138 3139 public function ScalarToSQL($value) 3140 { 3141 if (!is_string($value) && !is_null($value)) 3142 { 3143 throw new CoreWarning('Expected the attribute value to be a string', array( 3144 'found_type' => gettype($value), 3145 'value' => $value, 3146 'class' => $this->GetHostClass(), 3147 'attribute' => $this->GetCode() 3148 )); 3149 } 3150 3151 return $value; 3152 } 3153 3154 public function GetAsCSV( 3155 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 3156 $bConvertToPlainText = false 3157 ) { 3158 $sFrom = array("\r\n", $sTextQualifier); 3159 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 3160 $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); 3161 3162 return $sTextQualifier.$sEscaped.$sTextQualifier; 3163 } 3164 3165 public function GetDisplayStyle() 3166 { 3167 return $this->GetOptional('display_style', 'select'); 3168 } 3169 3170 static public function GetFormFieldClass() 3171 { 3172 return '\\Combodo\\iTop\\Form\\Field\\StringField'; 3173 } 3174 3175 public function MakeFormField(DBObject $oObject, $oFormField = null) 3176 { 3177 if ($oFormField === null) 3178 { 3179 $sFormFieldClass = static::GetFormFieldClass(); 3180 $oFormField = new $sFormFieldClass($this->GetCode()); 3181 } 3182 parent::MakeFormField($oObject, $oFormField); 3183 3184 return $oFormField; 3185 } 3186 3187} 3188 3189/** 3190 * An attribute that matches an object class 3191 * 3192 * @package iTopORM 3193 */ 3194class AttributeClass extends AttributeString 3195{ 3196 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; 3197 3198 static public function ListExpectedParams() 3199 { 3200 return array_merge(parent::ListExpectedParams(), array("class_category", "more_values")); 3201 } 3202 3203 public function __construct($sCode, $aParams) 3204 { 3205 $this->m_sCode = $sCode; 3206 $aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']); 3207 parent::__construct($sCode, $aParams); 3208 } 3209 3210 public function GetDefaultValue(DBObject $oHostObject = null) 3211 { 3212 $sDefault = parent::GetDefaultValue($oHostObject); 3213 if (!$this->IsNullAllowed() && $this->IsNull($sDefault)) 3214 { 3215 // For this kind of attribute specifying null as default value 3216 // is authorized even if null is not allowed 3217 3218 // Pick the first one... 3219 $aClasses = $this->GetAllowedValues(); 3220 $sDefault = key($aClasses); 3221 } 3222 3223 return $sDefault; 3224 } 3225 3226 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 3227 { 3228 if (empty($sValue)) 3229 { 3230 return ''; 3231 } 3232 3233 return MetaModel::GetName($sValue); 3234 } 3235 3236 public function RequiresIndex() 3237 { 3238 return true; 3239 } 3240 3241 public function GetBasicFilterLooseOperator() 3242 { 3243 return '='; 3244 } 3245 3246} 3247 3248 3249/** 3250 * An attribute that matches a class state 3251 * 3252 * @package iTopORM 3253 */ 3254class AttributeClassState extends AttributeString 3255{ 3256 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 3257 3258 static public function ListExpectedParams() 3259 { 3260 return array_merge(parent::ListExpectedParams(), array('class_field')); 3261 } 3262 3263 public function GetAllowedValues($aArgs = array(), $sContains = '') 3264 { 3265 if (isset($aArgs['this'])) 3266 { 3267 $oHostObj = $aArgs['this']; 3268 $sTargetClass = $this->Get('class_field'); 3269 $sClass = $oHostObj->Get($sTargetClass); 3270 3271 $aAllowedStates = array(); 3272 foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) 3273 { 3274 $aValues = MetaModel::EnumStates($sChildClass); 3275 foreach (array_keys($aValues) as $sState) 3276 { 3277 $aAllowedStates[$sState] = $sState.' ('.MetaModel::GetStateLabel($sChildClass, $sState).')'; 3278 } 3279 } 3280 return $aAllowedStates; 3281 } 3282 3283 return null; 3284 } 3285 3286 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 3287 { 3288 if (empty($sValue)) 3289 { 3290 return ''; 3291 } 3292 3293 if (!empty($oHostObject)) 3294 { 3295 $sTargetClass = $this->Get('class_field'); 3296 $sClass = $oHostObject->Get($sTargetClass); 3297 foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) 3298 { 3299 $aValues = MetaModel::EnumStates($sChildClass); 3300 if (in_array($sValue, $aValues)) 3301 { 3302 $sHTML = '<span class="attribute-set-item" data-code="'.$sValue.'" data-label="'.$sValue.' ('.MetaModel::GetStateLabel($sChildClass, $sValue).')'.'" data-description="">'.$sValue.'</span>'; 3303 return $sHTML; 3304 } 3305 } 3306 } 3307 3308 return $sValue; 3309 } 3310 3311} 3312 3313/** 3314 * An attibute that matches one of the language codes availables in the dictionnary 3315 * 3316 * @package iTopORM 3317 */ 3318class AttributeApplicationLanguage extends AttributeString 3319{ 3320 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 3321 3322 static public function ListExpectedParams() 3323 { 3324 return parent::ListExpectedParams(); 3325 } 3326 3327 public function __construct($sCode, $aParams) 3328 { 3329 $this->m_sCode = $sCode; 3330 $aAvailableLanguages = Dict::GetLanguages(); 3331 $aLanguageCodes = array(); 3332 foreach($aAvailableLanguages as $sLangCode => $aInfo) 3333 { 3334 $aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')'; 3335 } 3336 $aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes); 3337 parent::__construct($sCode, $aParams); 3338 } 3339 3340 public function RequiresIndex() 3341 { 3342 return true; 3343 } 3344 3345 public function GetBasicFilterLooseOperator() 3346 { 3347 return '='; 3348 } 3349} 3350 3351/** 3352 * The attribute dedicated to the finalclass automatic attribute 3353 * 3354 * @package iTopORM 3355 */ 3356class AttributeFinalClass extends AttributeString 3357{ 3358 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 3359 public $m_sValue; 3360 3361 public function __construct($sCode, $aParams) 3362 { 3363 $this->m_sCode = $sCode; 3364 $aParams["allowed_values"] = null; 3365 parent::__construct($sCode, $aParams); 3366 3367 $this->m_sValue = $this->Get("default_value"); 3368 } 3369 3370 public function IsWritable() 3371 { 3372 return false; 3373 } 3374 3375 public function IsMagic() 3376 { 3377 return true; 3378 } 3379 3380 public function RequiresIndex() 3381 { 3382 return true; 3383 } 3384 3385 public function SetFixedValue($sValue) 3386 { 3387 $this->m_sValue = $sValue; 3388 } 3389 3390 public function GetDefaultValue(DBObject $oHostObject = null) 3391 { 3392 return $this->m_sValue; 3393 } 3394 3395 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 3396 { 3397 if (empty($sValue)) 3398 { 3399 return ''; 3400 } 3401 if ($bLocalize) 3402 { 3403 return MetaModel::GetName($sValue); 3404 } 3405 else 3406 { 3407 return $sValue; 3408 } 3409 } 3410 3411 /** 3412 * An enum can be localized 3413 * 3414 * @param string $sProposedValue 3415 * @param bool $bLocalizedValue 3416 * @param string $sSepItem 3417 * @param string $sSepAttribute 3418 * @param string $sSepValue 3419 * @param string $sAttributeQualifier 3420 * 3421 * @return mixed|null|string 3422 * @throws \CoreException 3423 * @throws \OQLException 3424 */ 3425 public function MakeValueFromString( 3426 $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, 3427 $sAttributeQualifier = null 3428 ) { 3429 if ($bLocalizedValue) 3430 { 3431 // Lookup for the value matching the input 3432 // 3433 $sFoundValue = null; 3434 $aRawValues = self::GetAllowedValues(); 3435 if (!is_null($aRawValues)) 3436 { 3437 foreach($aRawValues as $sKey => $sValue) 3438 { 3439 if ($sProposedValue == $sValue) 3440 { 3441 $sFoundValue = $sKey; 3442 break; 3443 } 3444 } 3445 } 3446 if (is_null($sFoundValue)) 3447 { 3448 return null; 3449 } 3450 3451 return $this->MakeRealValue($sFoundValue, null); 3452 } 3453 else 3454 { 3455 return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, 3456 $sAttributeQualifier); 3457 } 3458 } 3459 3460 3461 // Because this is sometimes used to get a localized/string version of an attribute... 3462 public function GetEditValue($sValue, $oHostObj = null) 3463 { 3464 if (empty($sValue)) 3465 { 3466 return ''; 3467 } 3468 3469 return MetaModel::GetName($sValue); 3470 } 3471 3472 /** 3473 * Helper to get a value that will be JSON encoded 3474 * The operation is the opposite to FromJSONToValue 3475 * 3476 * @param $value 3477 * 3478 * @return string 3479 */ 3480 public function GetForJSON($value) 3481 { 3482 // JSON values are NOT localized 3483 return $value; 3484 } 3485 3486 /** 3487 * @param $value 3488 * @param string $sSeparator 3489 * @param string $sTextQualifier 3490 * @param \DBObject $oHostObject 3491 * @param bool $bLocalize 3492 * @param bool $bConvertToPlainText 3493 * 3494 * @return string 3495 * @throws \CoreException 3496 * @throws \DictExceptionMissingString 3497 */ 3498 public function GetAsCSV( 3499 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 3500 $bConvertToPlainText = false 3501 ) { 3502 if ($bLocalize && $value != '') 3503 { 3504 $sRawValue = MetaModel::GetName($value); 3505 } 3506 else 3507 { 3508 $sRawValue = $value; 3509 } 3510 3511 return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText); 3512 } 3513 3514 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 3515 { 3516 if (empty($value)) 3517 { 3518 return ''; 3519 } 3520 if ($bLocalize) 3521 { 3522 $sRawValue = MetaModel::GetName($value); 3523 } 3524 else 3525 { 3526 $sRawValue = $value; 3527 } 3528 3529 return Str::pure2xml($sRawValue); 3530 } 3531 3532 public function GetBasicFilterLooseOperator() 3533 { 3534 return '='; 3535 } 3536 3537 public function GetValueLabel($sValue) 3538 { 3539 if (empty($sValue)) 3540 { 3541 return ''; 3542 } 3543 3544 return MetaModel::GetName($sValue); 3545 } 3546 3547 public function GetAllowedValues($aArgs = array(), $sContains = '') 3548 { 3549 $aRawValues = MetaModel::EnumChildClasses($this->GetHostClass(), ENUM_CHILD_CLASSES_ALL); 3550 $aLocalizedValues = array(); 3551 foreach($aRawValues as $sClass) 3552 { 3553 $aLocalizedValues[$sClass] = MetaModel::GetName($sClass); 3554 } 3555 3556 return $aLocalizedValues; 3557 } 3558} 3559 3560 3561/** 3562 * Map a varchar column (size < ?) to an attribute that must never be shown to the user 3563 * 3564 * @package iTopORM 3565 */ 3566class AttributePassword extends AttributeString 3567{ 3568 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 3569 3570 static public function ListExpectedParams() 3571 { 3572 return parent::ListExpectedParams(); 3573 //return array_merge(parent::ListExpectedParams(), array()); 3574 } 3575 3576 public function GetEditClass() 3577 { 3578 return "Password"; 3579 } 3580 3581 protected function GetSQLCol($bFullSpec = false) 3582 { 3583 return "VARCHAR(64)" 3584 .CMDBSource::GetSqlStringColumnDefinition() 3585 .($bFullSpec ? $this->GetSQLColSpec() : ''); 3586 } 3587 3588 public function GetMaxSize() 3589 { 3590 return 64; 3591 } 3592 3593 public function GetFilterDefinitions() 3594 { 3595 // Note: due to this, you will get an error if a password is being declared as a search criteria (see ZLists) 3596 // not allowed to search on passwords! 3597 return array(); 3598 } 3599 3600 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 3601 { 3602 if (strlen($sValue) == 0) 3603 { 3604 return ''; 3605 } 3606 else 3607 { 3608 return '******'; 3609 } 3610 } 3611 3612 public function IsPartOfFingerprint() 3613 { 3614 return false; 3615 } // Cannot reliably compare two encrypted passwords since the same password will be encrypted in diffferent manners depending on the random 'salt' 3616} 3617 3618/** 3619 * Map a text column (size < 255) to an attribute that is encrypted in the database 3620 * The encryption is based on a key set per iTop instance. Thus if you export your 3621 * database (in SQL) to someone else without providing the key at the same time 3622 * the encrypted fields will remain encrypted 3623 * 3624 * @package iTopORM 3625 */ 3626class AttributeEncryptedString extends AttributeString 3627{ 3628 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 3629 3630 static $sKey = null; // Encryption key used for all encrypted fields 3631 static $sLibrary = null; // Encryption library used for all encrypted fields 3632 3633 public function __construct($sCode, $aParams) 3634 { 3635 parent::__construct($sCode, $aParams); 3636 if (self::$sKey == null) 3637 { 3638 self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); 3639 } 3640 if (self::$sLibrary == null) 3641 { 3642 self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary(); 3643 } 3644 } 3645 3646 /** 3647 * When the attribute definitions are stored in APC cache: 3648 * 1) The static class variable $sKey is NOT serialized 3649 * 2) The object's constructor is NOT called upon wakeup 3650 * 3) mcrypt may crash the server if passed an empty key !! 3651 * 3652 * So let's restore the key (if needed) when waking up 3653 **/ 3654 public function __wakeup() 3655 { 3656 if (self::$sKey == null) 3657 { 3658 self::$sKey = MetaModel::GetConfig()->GetEncryptionKey(); 3659 } 3660 if (self::$sLibrary == null) 3661 { 3662 self::$sLibrary = MetaModel::GetConfig()->GetEncryptionLibrary(); 3663 } 3664 } 3665 3666 3667 protected function GetSQLCol($bFullSpec = false) 3668 { 3669 return "TINYBLOB"; 3670 } 3671 3672 public function GetMaxSize() 3673 { 3674 return 255; 3675 } 3676 3677 public function GetFilterDefinitions() 3678 { 3679 // Note: due to this, you will get an error if a an encrypted field is declared as a search criteria (see ZLists) 3680 // not allowed to search on encrypted fields ! 3681 return array(); 3682 } 3683 3684 public function MakeRealValue($proposedValue, $oHostObj) 3685 { 3686 if (is_null($proposedValue)) 3687 { 3688 return null; 3689 } 3690 3691 return (string)$proposedValue; 3692 } 3693 3694 /** 3695 * Decrypt the value when reading from the database 3696 * 3697 * @param array $aCols 3698 * @param string $sPrefix 3699 * 3700 * @return string 3701 * @throws \Exception 3702 */ 3703 public function FromSQLToValue($aCols, $sPrefix = '') 3704 { 3705 $oSimpleCrypt = new SimpleCrypt(self::$sLibrary); 3706 $sValue = $oSimpleCrypt->Decrypt(self::$sKey, $aCols[$sPrefix]); 3707 3708 return $sValue; 3709 } 3710 3711 /** 3712 * Encrypt the value before storing it in the database 3713 * 3714 * @param $value 3715 * 3716 * @return array 3717 * @throws \Exception 3718 */ 3719 public function GetSQLValues($value) 3720 { 3721 $oSimpleCrypt = new SimpleCrypt(self::$sLibrary); 3722 $encryptedValue = $oSimpleCrypt->Encrypt(self::$sKey, $value); 3723 3724 $aValues = array(); 3725 $aValues[$this->Get("sql")] = $encryptedValue; 3726 3727 return $aValues; 3728 } 3729} 3730 3731 3732/** 3733 * Wiki formatting - experimental 3734 * 3735 * [[<objClass>:<objName|objId>|<label>]] 3736 * <label> is optional 3737 * 3738 * Examples: 3739 * - [[Server:db1.tnut.com]] 3740 * - [[Server:123]] 3741 * - [[Server:db1.tnut.com|Production server]] 3742 * - [[Server:123|Production server]] 3743 */ 3744define('WIKI_OBJECT_REGEXP', '/\[\[(.+):(.+)(\|(.+))?\]\]/U'); 3745 3746 3747/** 3748 * Map a text column (size > ?) to an attribute 3749 * 3750 * @package iTopORM 3751 */ 3752class AttributeText extends AttributeString 3753{ 3754 public function GetEditClass() 3755 { 3756 return ($this->GetFormat() == 'text') ? 'Text' : "HTML"; 3757 } 3758 3759 protected function GetSQLCol($bFullSpec = false) 3760 { 3761 return "TEXT".CMDBSource::GetSqlStringColumnDefinition(); 3762 } 3763 3764 public function GetSQLColumns($bFullSpec = false) 3765 { 3766 $aColumns = array(); 3767 $aColumns[$this->Get('sql')] = $this->GetSQLCol($bFullSpec); 3768 if ($this->GetOptional('format', null) != null) 3769 { 3770 // Add the extra column only if the property 'format' is specified for the attribute 3771 $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')".CMDBSource::GetSqlStringColumnDefinition(); 3772 if ($bFullSpec) 3773 { 3774 $aColumns[$this->Get('sql').'_format'] .= " DEFAULT 'text'"; // default 'text' is for migrating old records 3775 } 3776 } 3777 3778 return $aColumns; 3779 } 3780 3781 public function GetSQLExpressions($sPrefix = '') 3782 { 3783 if ($sPrefix == '') 3784 { 3785 $sPrefix = $this->Get('sql'); 3786 } 3787 $aColumns = array(); 3788 // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix 3789 $aColumns[''] = $sPrefix; 3790 if ($this->GetOptional('format', null) != null) 3791 { 3792 // Add the extra column only if the property 'format' is specified for the attribute 3793 $aColumns['_format'] = $sPrefix.'_format'; 3794 } 3795 3796 return $aColumns; 3797 } 3798 3799 public function GetMaxSize() 3800 { 3801 // Is there a way to know the current limitation for mysql? 3802 // See mysql_field_len() 3803 return 65535; 3804 } 3805 3806 static public function RenderWikiHtml($sText, $bWikiOnly = false) 3807 { 3808 if (!$bWikiOnly) 3809 { 3810 $sPattern = '/'.str_replace('/', '\/', utils::GetConfig()->Get('url_validation_pattern')).'/i'; 3811 if (preg_match_all($sPattern, $sText, $aAllMatches, 3812 PREG_SET_ORDER /* important !*/ | PREG_OFFSET_CAPTURE /* important ! */)) 3813 { 3814 $i = count($aAllMatches); 3815 // Replace the URLs by an actual hyperlink <a href="...">...</a> 3816 // Let's do it backwards so that the initial positions are not modified by the replacement 3817 // This works if the matches are captured: in the order they occur in the string AND 3818 // with their offset (i.e. position) inside the string 3819 while ($i > 0) 3820 { 3821 $i--; 3822 $sUrl = $aAllMatches[$i][0][0]; // String corresponding to the main pattern 3823 $iPos = $aAllMatches[$i][0][1]; // Position of the main pattern 3824 $sText = substr_replace($sText, "<a href=\"$sUrl\">$sUrl</a>", $iPos, strlen($sUrl)); 3825 3826 } 3827 } 3828 } 3829 if (preg_match_all(WIKI_OBJECT_REGEXP, $sText, $aAllMatches, PREG_SET_ORDER)) 3830 { 3831 foreach($aAllMatches as $iPos => $aMatches) 3832 { 3833 $sClass = trim($aMatches[1]); 3834 $sName = trim($aMatches[2]); 3835 $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null; 3836 3837 if (MetaModel::IsValidClass($sClass)) 3838 { 3839 $bFound = false; 3840 3841 // Try to find by name, then by id 3842 if (is_object($oObj = MetaModel::GetObjectByName($sClass, $sName, false /* MustBeFound */))) 3843 { 3844 $bFound = true; 3845 } 3846 elseif(is_object($oObj = MetaModel::GetObject($sClass, (int) $sName, false /* MustBeFound */, true))) 3847 { 3848 $bFound = true; 3849 } 3850 3851 if($bFound === true) 3852 { 3853 // Propose a std link to the object 3854 $sHyperlinkLabel = (empty($sLabel)) ? $oObj->GetName() : $sLabel; 3855 $sText = str_replace($aMatches[0], $oObj->GetHyperlink(null, true, $sHyperlinkLabel), $sText); 3856 } 3857 else 3858 { 3859 // Propose a std link to the object 3860 $sClassLabel = MetaModel::GetName($sClass); 3861 $sReplacement = "<span class=\"wiki_broken_link\">$sClassLabel:$sName" . (!empty($sLabel) ? " ($sLabel)" : "") . "</span>"; 3862 $sText = str_replace($aMatches[0], $sReplacement, $sText); 3863 // Later: propose a link to create a new object 3864 // Anyhow... there is no easy way to suggest default values based on the given FRIENDLY name 3865 //$sText = preg_replace('/\[\[(.+):(.+)\]\]/', '<a href="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=new&class='.$sClass.'&default[att1]=xxx&default[att2]=yyy">'.$sName.'</a>', $sText); 3866 } 3867 } 3868 } 3869 } 3870 3871 return $sText; 3872 } 3873 3874 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 3875 { 3876 $aStyles = array(); 3877 if ($this->GetWidth() != '') 3878 { 3879 $aStyles[] = 'width:'.$this->GetWidth(); 3880 } 3881 if ($this->GetHeight() != '') 3882 { 3883 $aStyles[] = 'height:'.$this->GetHeight(); 3884 } 3885 $sStyle = ''; 3886 if (count($aStyles) > 0) 3887 { 3888 $aStyles[] = 'overflow:auto'; 3889 $sStyle = 'style="'.implode(';', $aStyles).'"'; 3890 } 3891 3892 if ($this->GetFormat() == 'text') 3893 { 3894 $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize); 3895 $sValue = self::RenderWikiHtml($sValue); 3896 3897 return "<div $sStyle>".str_replace("\n", "<br>\n", $sValue).'</div>'; 3898 } 3899 else 3900 { 3901 $sValue = self::RenderWikiHtml($sValue, true /* wiki only */); 3902 3903 return "<div class=\"HTML\" $sStyle>".InlineImage::FixUrls($sValue).'</div>'; 3904 } 3905 3906 } 3907 3908 public function GetEditValue($sValue, $oHostObj = null) 3909 { 3910 if ($this->GetFormat() == 'text') 3911 { 3912 if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) 3913 { 3914 foreach($aAllMatches as $iPos => $aMatches) 3915 { 3916 $sClass = trim($aMatches[1]); 3917 $sName = trim($aMatches[2]); 3918 $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null; 3919 3920 if (MetaModel::IsValidClass($sClass)) 3921 { 3922 $sClassLabel = MetaModel::GetName($sClass); 3923 $sReplacement = "[[$sClassLabel:$sName" . (!empty($sLabel) ? " | $sLabel" : "") . "]]"; 3924 $sValue = str_replace($aMatches[0], $sReplacement, $sValue); 3925 } 3926 } 3927 } 3928 } 3929 else 3930 { 3931 $sValue = str_replace('&', '&', $sValue); 3932 } 3933 3934 return $sValue; 3935 } 3936 3937 /** 3938 * For fields containing a potential markup, return the value without this markup 3939 * 3940 * @param string $sValue 3941 * @param \DBObject $oHostObj 3942 * 3943 * @return string 3944 */ 3945 public function GetAsPlainText($sValue, $oHostObj = null) 3946 { 3947 if ($this->GetFormat() == 'html') 3948 { 3949 return (string)utils::HtmlToText($this->GetEditValue($sValue, $oHostObj)); 3950 } 3951 else 3952 { 3953 return parent::GetAsPlainText($sValue, $oHostObj); 3954 } 3955 } 3956 3957 public function MakeRealValue($proposedValue, $oHostObj) 3958 { 3959 $sValue = $proposedValue; 3960 switch ($this->GetFormat()) 3961 { 3962 case 'html': 3963 if (($sValue !== null) && ($sValue !== '')) 3964 { 3965 $sValue = HTMLSanitizer::Sanitize($sValue); 3966 } 3967 break; 3968 3969 case 'text': 3970 default: 3971 if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) 3972 { 3973 foreach($aAllMatches as $iPos => $aMatches) 3974 { 3975 $sClassLabel = trim($aMatches[1]); 3976 $sName = trim($aMatches[2]); 3977 $sLabel = (!empty($aMatches[4])) ? trim($aMatches[4]) : null; 3978 3979 if (!MetaModel::IsValidClass($sClassLabel)) 3980 { 3981 $sClass = MetaModel::GetClassFromLabel($sClassLabel); 3982 if ($sClass) 3983 { 3984 $sReplacement = "[[$sClassLabel:$sName" . (!empty($sLabel) ? " | $sLabel" : "") . "]]"; 3985 $sValue = str_replace($aMatches[0], $sReplacement, $sValue); 3986 } 3987 } 3988 } 3989 } 3990 } 3991 3992 return $sValue; 3993 } 3994 3995 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 3996 { 3997 return Str::pure2xml($value); 3998 } 3999 4000 public function GetWidth() 4001 { 4002 return $this->GetOptional('width', ''); 4003 } 4004 4005 public function GetHeight() 4006 { 4007 return $this->GetOptional('height', ''); 4008 } 4009 4010 static public function GetFormFieldClass() 4011 { 4012 return '\\Combodo\\iTop\\Form\\Field\\TextAreaField'; 4013 } 4014 4015 /** 4016 * @param \DBObject $oObject 4017 * @param \Combodo\iTop\Form\Field\TextAreaField $oFormField 4018 * 4019 * @return \Combodo\iTop\Form\Field\TextAreaField 4020 * @throws \CoreException 4021 */ 4022 public function MakeFormField(DBObject $oObject, $oFormField = null) 4023 { 4024 if ($oFormField === null) 4025 { 4026 $sFormFieldClass = static::GetFormFieldClass(); 4027 /** @var \Combodo\iTop\Form\Field\TextAreaField $oFormField */ 4028 $oFormField = new $sFormFieldClass($this->GetCode(), null, $oObject); 4029 $oFormField->SetFormat($this->GetFormat()); 4030 } 4031 parent::MakeFormField($oObject, $oFormField); 4032 4033 return $oFormField; 4034 } 4035 4036 /** 4037 * The actual formatting of the field: either text (=plain text) or html (= text with HTML markup) 4038 * 4039 * @return string 4040 */ 4041 public function GetFormat() 4042 { 4043 return $this->GetOptional('format', 'text'); 4044 } 4045 4046 /** 4047 * Read the value from the row returned by the SQL query and transorms it to the appropriate 4048 * internal format (either text or html) 4049 * 4050 * @see AttributeDBFieldVoid::FromSQLToValue() 4051 * 4052 * @param array $aCols 4053 * @param string $sPrefix 4054 * 4055 * @return string 4056 */ 4057 public function FromSQLToValue($aCols, $sPrefix = '') 4058 { 4059 $value = $aCols[$sPrefix.'']; 4060 if ($this->GetOptional('format', null) != null) 4061 { 4062 // Read from the extra column only if the property 'format' is specified for the attribute 4063 $sFormat = $aCols[$sPrefix.'_format']; 4064 } 4065 else 4066 { 4067 $sFormat = $this->GetFormat(); 4068 } 4069 4070 switch ($sFormat) 4071 { 4072 case 'text': 4073 if ($this->GetFormat() == 'html') 4074 { 4075 $value = utils::TextToHtml($value); 4076 } 4077 break; 4078 4079 case 'html': 4080 if ($this->GetFormat() == 'text') 4081 { 4082 $value = utils::HtmlToText($value); 4083 } 4084 else 4085 { 4086 $value = InlineImage::FixUrls((string)$value); 4087 } 4088 break; 4089 4090 default: 4091 // unknown format ?? 4092 } 4093 4094 return $value; 4095 } 4096 4097 public function GetSQLValues($value) 4098 { 4099 $aValues = array(); 4100 $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); 4101 if ($this->GetOptional('format', null) != null) 4102 { 4103 // Add the extra column only if the property 'format' is specified for the attribute 4104 $aValues[$this->Get("sql").'_format'] = $this->GetFormat(); 4105 } 4106 4107 return $aValues; 4108 } 4109 4110 public function GetAsCSV( 4111 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 4112 $bConvertToPlainText = false 4113 ) { 4114 switch ($this->GetFormat()) 4115 { 4116 case 'html': 4117 if ($bConvertToPlainText) 4118 { 4119 $sValue = utils::HtmlToText((string)$sValue); 4120 } 4121 $sFrom = array("\r\n", $sTextQualifier); 4122 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 4123 $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); 4124 4125 return $sTextQualifier.$sEscaped.$sTextQualifier; 4126 break; 4127 4128 case 'text': 4129 default: 4130 return parent::GetAsCSV($sValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, 4131 $bConvertToPlainText); 4132 } 4133 } 4134} 4135 4136/** 4137 * Map a log to an attribute 4138 * 4139 * @package iTopORM 4140 */ 4141class AttributeLongText extends AttributeText 4142{ 4143 protected function GetSQLCol($bFullSpec = false) 4144 { 4145 return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); 4146 } 4147 4148 public function GetMaxSize() 4149 { 4150 // Is there a way to know the current limitation for mysql? 4151 // See mysql_field_len() 4152 return 65535 * 1024; // Limited... still 64 Mb! 4153 } 4154} 4155 4156/** 4157 * An attibute that stores a case log (i.e journal) 4158 * 4159 * @package iTopORM 4160 */ 4161class AttributeCaseLog extends AttributeLongText 4162{ 4163 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 4164 4165 public function GetNullValue() 4166 { 4167 return ''; 4168 } 4169 4170 public function IsNull($proposedValue) 4171 { 4172 if (!($proposedValue instanceof ormCaseLog)) 4173 { 4174 return ($proposedValue == ''); 4175 } 4176 4177 return ($proposedValue->GetText() == ''); 4178 } 4179 4180 public function ScalarToSQL($value) 4181 { 4182 if (!is_string($value) && !is_null($value)) 4183 { 4184 throw new CoreWarning('Expected the attribute value to be a string', array( 4185 'found_type' => gettype($value), 4186 'value' => $value, 4187 'class' => $this->GetCode(), 4188 'attribute' => $this->GetHostClass() 4189 )); 4190 } 4191 4192 return $value; 4193 } 4194 4195 public function GetEditClass() 4196 { 4197 return "CaseLog"; 4198 } 4199 4200 public function GetEditValue($sValue, $oHostObj = null) 4201 { 4202 if (!($sValue instanceOf ormCaseLog)) 4203 { 4204 return ''; 4205 } 4206 4207 return $sValue->GetModifiedEntry(); 4208 } 4209 4210 /** 4211 * For fields containing a potential markup, return the value without this markup 4212 * 4213 * @param mixed $value 4214 * @param \DBObject $oHostObj 4215 * 4216 * @return string 4217 */ 4218 public function GetAsPlainText($value, $oHostObj = null) 4219 { 4220 if ($value instanceOf ormCaseLog) 4221 { 4222 /** ormCaseLog $value */ 4223 return $value->GetAsPlainText(); 4224 } 4225 else 4226 { 4227 return (string)$value; 4228 } 4229 } 4230 4231 public function GetDefaultValue(DBObject $oHostObject = null) 4232 { 4233 return new ormCaseLog(); 4234 } 4235 4236 public function Equals($val1, $val2) 4237 { 4238 return ($val1->GetText() == $val2->GetText()); 4239 } 4240 4241 4242 /** 4243 * Facilitate things: allow the user to Set the value from a string 4244 * 4245 * @param $proposedValue 4246 * @param \DBObject $oHostObj 4247 * 4248 * @return mixed|null|\ormCaseLog|string 4249 * @throws \Exception 4250 */ 4251 public function MakeRealValue($proposedValue, $oHostObj) 4252 { 4253 if ($proposedValue instanceof ormCaseLog) 4254 { 4255 // Passthrough 4256 $ret = clone $proposedValue; 4257 } 4258 else 4259 { 4260 // Append the new value if an instance of the object is supplied 4261 // 4262 $oPreviousLog = null; 4263 if ($oHostObj != null) 4264 { 4265 $oPreviousLog = $oHostObj->Get($this->GetCode()); 4266 if (!is_object($oPreviousLog)) 4267 { 4268 $oPreviousLog = $oHostObj->GetOriginal($this->GetCode());; 4269 } 4270 4271 } 4272 if (is_object($oPreviousLog)) 4273 { 4274 $oCaseLog = clone($oPreviousLog); 4275 } 4276 else 4277 { 4278 $oCaseLog = new ormCaseLog(); 4279 } 4280 4281 if ($proposedValue instanceof stdClass) 4282 { 4283 $oCaseLog->AddLogEntryFromJSON($proposedValue); 4284 } 4285 else 4286 { 4287 if (strlen($proposedValue) > 0) 4288 { 4289 $oCaseLog->AddLogEntry($proposedValue); 4290 } 4291 } 4292 $ret = $oCaseLog; 4293 } 4294 4295 return $ret; 4296 } 4297 4298 public function GetSQLExpressions($sPrefix = '') 4299 { 4300 if ($sPrefix == '') 4301 { 4302 $sPrefix = $this->Get('sql'); 4303 } 4304 $aColumns = array(); 4305 // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix 4306 $aColumns[''] = $sPrefix; 4307 $aColumns['_index'] = $sPrefix.'_index'; 4308 4309 return $aColumns; 4310 } 4311 4312 /** 4313 * @param array $aCols 4314 * @param string $sPrefix 4315 * 4316 * @return \ormCaseLog 4317 * @throws \MissingColumnException 4318 */ 4319 public function FromSQLToValue($aCols, $sPrefix = '') 4320 { 4321 if (!array_key_exists($sPrefix, $aCols)) 4322 { 4323 $sAvailable = implode(', ', array_keys($aCols)); 4324 throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); 4325 } 4326 $sLog = $aCols[$sPrefix]; 4327 4328 if (isset($aCols[$sPrefix.'_index'])) 4329 { 4330 $sIndex = $aCols[$sPrefix.'_index']; 4331 } 4332 else 4333 { 4334 // For backward compatibility, allow the current state to be: 1 log, no index 4335 $sIndex = ''; 4336 } 4337 4338 if (strlen($sIndex) > 0) 4339 { 4340 $aIndex = unserialize($sIndex); 4341 $value = new ormCaseLog($sLog, $aIndex); 4342 } 4343 else 4344 { 4345 $value = new ormCaseLog($sLog); 4346 } 4347 4348 return $value; 4349 } 4350 4351 public function GetSQLValues($value) 4352 { 4353 if (!($value instanceOf ormCaseLog)) 4354 { 4355 $value = new ormCaseLog(''); 4356 } 4357 $aValues = array(); 4358 $aValues[$this->GetCode()] = $value->GetText(); 4359 $aValues[$this->GetCode().'_index'] = serialize($value->GetIndex()); 4360 4361 return $aValues; 4362 } 4363 4364 public function GetSQLColumns($bFullSpec = false) 4365 { 4366 $aColumns = array(); 4367 $aColumns[$this->GetCode()] = 'LONGTEXT' // 2^32 (4 Gb) 4368 .CMDBSource::GetSqlStringColumnDefinition(); 4369 $aColumns[$this->GetCode().'_index'] = 'BLOB'; 4370 4371 return $aColumns; 4372 } 4373 4374 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 4375 { 4376 if ($value instanceOf ormCaseLog) 4377 { 4378 $sContent = $value->GetAsHTML(null, false, array(__class__, 'RenderWikiHtml')); 4379 } 4380 else 4381 { 4382 $sContent = ''; 4383 } 4384 $aStyles = array(); 4385 if ($this->GetWidth() != '') 4386 { 4387 $aStyles[] = 'width:'.$this->GetWidth(); 4388 } 4389 if ($this->GetHeight() != '') 4390 { 4391 $aStyles[] = 'height:'.$this->GetHeight(); 4392 } 4393 $sStyle = ''; 4394 if (count($aStyles) > 0) 4395 { 4396 $sStyle = 'style="'.implode(';', $aStyles).'"'; 4397 } 4398 4399 return "<div class=\"caselog\" $sStyle>".$sContent.'</div>'; 4400 } 4401 4402 4403 public function GetAsCSV( 4404 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 4405 $bConvertToPlainText = false 4406 ) { 4407 if ($value instanceOf ormCaseLog) 4408 { 4409 return parent::GetAsCSV($value->GetText($bConvertToPlainText), $sSeparator, $sTextQualifier, $oHostObject, 4410 $bLocalize, $bConvertToPlainText); 4411 } 4412 else 4413 { 4414 return ''; 4415 } 4416 } 4417 4418 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 4419 { 4420 if ($value instanceOf ormCaseLog) 4421 { 4422 return parent::GetAsXML($value->GetText(), $oHostObject, $bLocalize); 4423 } 4424 else 4425 { 4426 return ''; 4427 } 4428 } 4429 4430 /** 4431 * List the available verbs for 'GetForTemplate' 4432 */ 4433 public function EnumTemplateVerbs() 4434 { 4435 return array( 4436 '' => 'Plain text representation of all the log entries', 4437 'head' => 'Plain text representation of the latest entry', 4438 'head_html' => 'HTML representation of the latest entry', 4439 'html' => 'HTML representation of all the log entries', 4440 ); 4441 } 4442 4443 /** 4444 * Get various representations of the value, for insertion into a template (e.g. in Notifications) 4445 * 4446 * @param $value mixed The current value of the field 4447 * @param $sVerb string The verb specifying the representation of the value 4448 * @param $oHostObject DBObject The object 4449 * @param $bLocalize bool Whether or not to localize the value 4450 * 4451 * @return mixed 4452 * @throws \Exception 4453 */ 4454 public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) 4455 { 4456 switch ($sVerb) 4457 { 4458 case '': 4459 return $value->GetText(true); 4460 4461 case 'head': 4462 return $value->GetLatestEntry('text'); 4463 4464 case 'head_html': 4465 return $value->GetLatestEntry('html'); 4466 4467 case 'html': 4468 return $value->GetAsEmailHtml(); 4469 4470 default: 4471 throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); 4472 } 4473 } 4474 4475 /** 4476 * Helper to get a value that will be JSON encoded 4477 * The operation is the opposite to FromJSONToValue 4478 */ 4479 public function GetForJSON($value) 4480 { 4481 return $value->GetForJSON(); 4482 } 4483 4484 /** 4485 * Helper to form a value, given JSON decoded data 4486 * The operation is the opposite to GetForJSON 4487 */ 4488 public function FromJSONToValue($json) 4489 { 4490 if (is_string($json)) 4491 { 4492 // Will be correctly handled in MakeRealValue 4493 $ret = $json; 4494 } 4495 else 4496 { 4497 if (isset($json->add_item)) 4498 { 4499 // Will be correctly handled in MakeRealValue 4500 $ret = $json->add_item; 4501 if (!isset($ret->message)) 4502 { 4503 throw new Exception("Missing mandatory entry: 'message'"); 4504 } 4505 } 4506 else 4507 { 4508 $ret = ormCaseLog::FromJSON($json); 4509 } 4510 } 4511 4512 return $ret; 4513 } 4514 4515 public function Fingerprint($value) 4516 { 4517 $sFingerprint = ''; 4518 if ($value instanceOf ormCaseLog) 4519 { 4520 $sFingerprint = $value->GetText(); 4521 } 4522 4523 return $sFingerprint; 4524 } 4525 4526 /** 4527 * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) 4528 * 4529 * @return string 4530 */ 4531 public function GetFormat() 4532 { 4533 return $this->GetOptional('format', 'html'); // default format for case logs is now HTML 4534 } 4535 4536 static public function GetFormFieldClass() 4537 { 4538 return '\\Combodo\\iTop\\Form\\Field\\CaseLogField'; 4539 } 4540 4541 public function MakeFormField(DBObject $oObject, $oFormField = null) 4542 { 4543 // First we call the parent so the field is build 4544 $oFormField = parent::MakeFormField($oObject, $oFormField); 4545 // Then only we set the value 4546 $oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode()))); 4547 // And we set the entries 4548 $oFormField->SetEntries($oObject->Get($this->GetCode())->GetAsArray()); 4549 4550 return $oFormField; 4551 } 4552} 4553 4554/** 4555 * Map a text column (size > ?), containing HTML code, to an attribute 4556 * 4557 * @package iTopORM 4558 */ 4559class AttributeHTML extends AttributeLongText 4560{ 4561 public function GetSQLColumns($bFullSpec = false) 4562 { 4563 $aColumns = array(); 4564 $aColumns[$this->Get('sql')] = $this->GetSQLCol(); 4565 if ($this->GetOptional('format', null) != null) 4566 { 4567 // Add the extra column only if the property 'format' is specified for the attribute 4568 $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; 4569 if ($bFullSpec) 4570 { 4571 $aColumns[$this->Get('sql').'_format'] .= " DEFAULT 'html'"; // default 'html' is for migrating old records 4572 } 4573 } 4574 4575 return $aColumns; 4576 } 4577 4578 /** 4579 * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) 4580 * 4581 * @return string 4582 */ 4583 public function GetFormat() 4584 { 4585 return $this->GetOptional('format', 'html'); // Defaults to HTML 4586 } 4587} 4588 4589/** 4590 * Specialization of a string: email 4591 * 4592 * @package iTopORM 4593 */ 4594class AttributeEmailAddress extends AttributeString 4595{ 4596 public function GetValidationPattern() 4597 { 4598 return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('email_validation_pattern').'$'); 4599 } 4600 4601 static public function GetFormFieldClass() 4602 { 4603 return '\\Combodo\\iTop\\Form\\Field\\EmailField'; 4604 } 4605 4606 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 4607 { 4608 if (empty($sValue)) 4609 { 4610 return ''; 4611 } 4612 4613 $sUrlDecorationClass = utils::GetConfig()->Get('email_decoration_class'); 4614 4615 return '<a class="mailto" href="mailto:'.$sValue.'"><span class="text_decoration '.$sUrlDecorationClass.'"></span>'.parent::GetAsHTML($sValue).'</a>'; 4616 } 4617} 4618 4619/** 4620 * Specialization of a string: IP address 4621 * 4622 * @package iTopORM 4623 */ 4624class AttributeIPAddress extends AttributeString 4625{ 4626 public function GetValidationPattern() 4627 { 4628 $sNum = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])'; 4629 4630 return "^($sNum\\.$sNum\\.$sNum\\.$sNum)$"; 4631 } 4632 4633 public function GetOrderBySQLExpressions($sClassAlias) 4634 { 4635 // Note: This is the responsibility of this function to place backticks around column aliases 4636 return array('INET_ATON(`'.$sClassAlias.$this->GetCode().'`)'); 4637 } 4638} 4639 4640/** 4641 * Specialization of a string: phone number 4642 * 4643 * @package iTopORM 4644 */ 4645class AttributePhoneNumber extends AttributeString 4646{ 4647 public function GetValidationPattern() 4648 { 4649 return $this->GetOptional('validation_pattern', 4650 '^'.utils::GetConfig()->Get('phone_number_validation_pattern').'$'); 4651 } 4652 4653 static public function GetFormFieldClass() 4654 { 4655 return '\\Combodo\\iTop\\Form\\Field\\PhoneField'; 4656 } 4657 4658 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 4659 { 4660 if (empty($sValue)) 4661 { 4662 return ''; 4663 } 4664 4665 $sUrlDecorationClass = utils::GetConfig()->Get('phone_number_decoration_class'); 4666 $sUrlPattern = utils::GetConfig()->Get('phone_number_url_pattern'); 4667 $sUrl = sprintf($sUrlPattern, $sValue); 4668 4669 return '<a class="tel" href="'.$sUrl.'"><span class="text_decoration '.$sUrlDecorationClass.'"></span>'.parent::GetAsHTML($sValue).'</a>'; 4670 } 4671} 4672 4673/** 4674 * Specialization of a string: OQL expression 4675 * 4676 * @package iTopORM 4677 */ 4678class AttributeOQL extends AttributeText 4679{ 4680 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 4681 4682 public function GetEditClass() 4683 { 4684 return "OQLExpression"; 4685 } 4686} 4687 4688/** 4689 * Specialization of a string: template (contains iTop placeholders like $current_contact_id$ or $this->name$) 4690 * 4691 * @package iTopORM 4692 */ 4693class AttributeTemplateString extends AttributeString 4694{ 4695 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 4696} 4697 4698/** 4699 * Specialization of a text: template (contains iTop placeholders like $current_contact_id$ or $this->name$) 4700 * 4701 * @package iTopORM 4702 */ 4703class AttributeTemplateText extends AttributeText 4704{ 4705 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 4706} 4707 4708/** 4709 * Specialization of a HTML: template (contains iTop placeholders like $current_contact_id$ or $this->name$) 4710 * 4711 * @package iTopORM 4712 */ 4713class AttributeTemplateHTML extends AttributeText 4714{ 4715 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 4716 4717 public function GetSQLColumns($bFullSpec = false) 4718 { 4719 $aColumns = array(); 4720 $aColumns[$this->Get('sql')] = $this->GetSQLCol(); 4721 if ($this->GetOptional('format', null) != null) 4722 { 4723 // Add the extra column only if the property 'format' is specified for the attribute 4724 $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; 4725 if ($bFullSpec) 4726 { 4727 $aColumns[$this->Get('sql').'_format'] .= " DEFAULT 'html'"; // default 'html' is for migrating old records 4728 } 4729 } 4730 4731 return $aColumns; 4732 } 4733 4734 /** 4735 * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) 4736 * 4737 * @return string 4738 */ 4739 public function GetFormat() 4740 { 4741 return $this->GetOptional('format', 'html'); // Defaults to HTML 4742 } 4743} 4744 4745 4746/** 4747 * Map a enum column to an attribute 4748 * 4749 * @package iTopORM 4750 */ 4751class AttributeEnum extends AttributeString 4752{ 4753 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; 4754 4755 static public function ListExpectedParams() 4756 { 4757 return parent::ListExpectedParams(); 4758 //return array_merge(parent::ListExpectedParams(), array()); 4759 } 4760 4761 public function GetEditClass() 4762 { 4763 return "String"; 4764 } 4765 4766 protected function GetSQLCol($bFullSpec = false) 4767 { 4768 $oValDef = $this->GetValuesDef(); 4769 if ($oValDef) 4770 { 4771 $aValues = CMDBSource::Quote(array_keys($oValDef->GetValues(array(), "")), true); 4772 } 4773 else 4774 { 4775 $aValues = array(); 4776 } 4777 if (count($aValues) > 0) 4778 { 4779 // The syntax used here do matters 4780 // In particular, I had to remove unnecessary spaces to 4781 // make sure that this string will match the field type returned by the DB 4782 // (used to perform a comparison between the current DB format and the data model) 4783 return "ENUM(".implode(",", $aValues).")" 4784 .CMDBSource::GetSqlStringColumnDefinition() 4785 .($bFullSpec ? $this->GetSQLColSpec() : ''); 4786 } 4787 else 4788 { 4789 return "VARCHAR(255)" 4790 .CMDBSource::GetSqlStringColumnDefinition() 4791 .($bFullSpec ? " DEFAULT ''" : ""); // ENUM() is not an allowed syntax! 4792 } 4793 } 4794 4795 protected function GetSQLColSpec() 4796 { 4797 $default = $this->ScalarToSQL($this->GetDefaultValue()); 4798 if (is_null($default)) 4799 { 4800 $sRet = ''; 4801 } 4802 else 4803 { 4804 // ENUMs values are strings so the default value must be a string as well, 4805 // otherwise MySQL interprets the number as the zero-based index of the value in the list (i.e. the nth value in the list) 4806 $sRet = " DEFAULT ".CMDBSource::Quote($default); 4807 } 4808 4809 return $sRet; 4810 } 4811 4812 public function ScalarToSQL($value) 4813 { 4814 // Note: for strings, the null value is an empty string and it is recorded as such in the DB 4815 // but that wasn't working for enums, because '' is NOT one of the allowed values 4816 // that's why a null value must be forced to a real null 4817 $value = parent::ScalarToSQL($value); 4818 if ($this->IsNull($value)) 4819 { 4820 return null; 4821 } 4822 else 4823 { 4824 return $value; 4825 } 4826 } 4827 4828 public function RequiresIndex() 4829 { 4830 return false; 4831 } 4832 4833 public function GetBasicFilterOperators() 4834 { 4835 return parent::GetBasicFilterOperators(); 4836 } 4837 4838 public function GetBasicFilterLooseOperator() 4839 { 4840 return '='; 4841 } 4842 4843 public function GetBasicFilterSQLExpr($sOpCode, $value) 4844 { 4845 return parent::GetBasicFilterSQLExpr($sOpCode, $value); 4846 } 4847 4848 public function GetValueLabel($sValue) 4849 { 4850 if (is_null($sValue)) 4851 { 4852 // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label 4853 $sLabel = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue, 4854 Dict::S('Enum:Undefined')); 4855 } 4856 else 4857 { 4858 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, null, true /*user lang*/); 4859 if (is_null($sLabel)) 4860 { 4861 $sDefault = str_replace('_', ' ', $sValue); 4862 // Browse the hierarchy again, accepting default (english) translations 4863 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, false); 4864 } 4865 } 4866 4867 return $sLabel; 4868 } 4869 4870 public function GetValueDescription($sValue) 4871 { 4872 if (is_null($sValue)) 4873 { 4874 // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label 4875 $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', 4876 Dict::S('Enum:Undefined')); 4877 } 4878 else 4879 { 4880 $sDescription = Dict::S('Class:'.$this->GetHostClass().'/Attribute:'.$this->GetCode().'/Value:'.$sValue.'+', 4881 '', true /* user language only */); 4882 if (strlen($sDescription) == 0) 4883 { 4884 $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); 4885 if ($sParentClass) 4886 { 4887 if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) 4888 { 4889 $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); 4890 $sDescription = $oAttDef->GetValueDescription($sValue); 4891 } 4892 } 4893 } 4894 } 4895 4896 return $sDescription; 4897 } 4898 4899 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 4900 { 4901 if ($bLocalize) 4902 { 4903 $sLabel = $this->GetValueLabel($sValue); 4904 $sDescription = $this->GetValueDescription($sValue); 4905 // later, we could imagine a detailed description in the title 4906 $sRes = "<span title=\"$sDescription\">".parent::GetAsHtml($sLabel)."</span>"; 4907 } 4908 else 4909 { 4910 $sRes = parent::GetAsHtml($sValue, $oHostObject, $bLocalize); 4911 } 4912 4913 return $sRes; 4914 } 4915 4916 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 4917 { 4918 if (is_null($value)) 4919 { 4920 $sFinalValue = ''; 4921 } 4922 elseif ($bLocalize) 4923 { 4924 $sFinalValue = $this->GetValueLabel($value); 4925 } 4926 else 4927 { 4928 $sFinalValue = $value; 4929 } 4930 $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); 4931 4932 return $sRes; 4933 } 4934 4935 public function GetAsCSV( 4936 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 4937 $bConvertToPlainText = false 4938 ) { 4939 if (is_null($sValue)) 4940 { 4941 $sFinalValue = ''; 4942 } 4943 elseif ($bLocalize) 4944 { 4945 $sFinalValue = $this->GetValueLabel($sValue); 4946 } 4947 else 4948 { 4949 $sFinalValue = $sValue; 4950 } 4951 $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); 4952 4953 return $sRes; 4954 } 4955 4956 static public function GetFormFieldClass() 4957 { 4958 return '\\Combodo\\iTop\\Form\\Field\\SelectField'; 4959 } 4960 4961 public function MakeFormField(DBObject $oObject, $oFormField = null) 4962 { 4963 if ($oFormField === null) 4964 { 4965 // Later : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value 4966 $sFormFieldClass = static::GetFormFieldClass(); 4967 $oFormField = new $sFormFieldClass($this->GetCode()); 4968 } 4969 4970 $oFormField->SetChoices($this->GetAllowedValues($oObject->ToArgsForQuery())); 4971 parent::MakeFormField($oObject, $oFormField); 4972 4973 return $oFormField; 4974 } 4975 4976 public function GetEditValue($sValue, $oHostObj = null) 4977 { 4978 if (is_null($sValue)) 4979 { 4980 return ''; 4981 } 4982 else 4983 { 4984 return $this->GetValueLabel($sValue); 4985 } 4986 } 4987 4988 /** 4989 * Helper to get a value that will be JSON encoded 4990 * The operation is the opposite to FromJSONToValue 4991 */ 4992 public function GetForJSON($value) 4993 { 4994 return $value; 4995 } 4996 4997 public function GetAllowedValues($aArgs = array(), $sContains = '') 4998 { 4999 $aRawValues = parent::GetAllowedValues($aArgs, $sContains); 5000 if (is_null($aRawValues)) 5001 { 5002 return null; 5003 } 5004 $aLocalizedValues = array(); 5005 foreach($aRawValues as $sKey => $sValue) 5006 { 5007 $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey); 5008 } 5009 5010 return $aLocalizedValues; 5011 } 5012 5013 public function GetMaxSize() 5014 { 5015 return null; 5016 } 5017 5018 /** 5019 * An enum can be localized 5020 */ 5021 public function MakeValueFromString( 5022 $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, 5023 $sAttributeQualifier = null 5024 ) { 5025 if ($bLocalizedValue) 5026 { 5027 // Lookup for the value matching the input 5028 // 5029 $sFoundValue = null; 5030 $aRawValues = parent::GetAllowedValues(); 5031 if (!is_null($aRawValues)) 5032 { 5033 foreach($aRawValues as $sKey => $sValue) 5034 { 5035 $sRefValue = $this->GetValueLabel($sKey); 5036 if ($sProposedValue == $sRefValue) 5037 { 5038 $sFoundValue = $sKey; 5039 break; 5040 } 5041 } 5042 } 5043 if (is_null($sFoundValue)) 5044 { 5045 return null; 5046 } 5047 5048 return $this->MakeRealValue($sFoundValue, null); 5049 } 5050 else 5051 { 5052 return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, 5053 $sAttributeQualifier); 5054 } 5055 } 5056 5057 /** 5058 * Processes the input value to align it with the values supported 5059 * by this type of attribute. In this case: turns empty strings into nulls 5060 * 5061 * @param mixed $proposedValue The value to be set for the attribute 5062 * 5063 * @return mixed The actual value that will be set 5064 */ 5065 public function MakeRealValue($proposedValue, $oHostObj) 5066 { 5067 if ($proposedValue == '') 5068 { 5069 return null; 5070 } 5071 5072 return parent::MakeRealValue($proposedValue, $oHostObj); 5073 } 5074 5075 public function GetOrderByHint() 5076 { 5077 $aValues = $this->GetAllowedValues(); 5078 5079 return Dict::Format('UI:OrderByHint_Values', implode(', ', $aValues)); 5080 } 5081} 5082 5083/** 5084 * A meta enum is an aggregation of enum from subclasses into an enum of a base class 5085 * It has been designed is to cope with the fact that statuses must be defined in leaf classes, while it makes sense to 5086 * have a superstatus available on the root classe(s) 5087 * 5088 * @package iTopORM 5089 */ 5090class AttributeMetaEnum extends AttributeEnum 5091{ 5092 static public function ListExpectedParams() 5093 { 5094 return array('allowed_values', 'sql', 'default_value', 'mapping'); 5095 } 5096 5097 public function IsNullAllowed() 5098 { 5099 return false; // Well... this actually depends on the mapping 5100 } 5101 5102 public function IsWritable() 5103 { 5104 return false; 5105 } 5106 5107 public function RequiresIndex() 5108 { 5109 return true; 5110 } 5111 5112 public function GetPrerequisiteAttributes($sClass = null) 5113 { 5114 if (is_null($sClass)) 5115 { 5116 $sClass = $this->GetHostClass(); 5117 } 5118 $aMappingData = $this->GetMapRule($sClass); 5119 if ($aMappingData == null) 5120 { 5121 $aRet = array(); 5122 } 5123 else 5124 { 5125 $aRet = array($aMappingData['attcode']); 5126 } 5127 5128 return $aRet; 5129 } 5130 5131 /** 5132 * Overload the standard so as to leave the data unsorted 5133 * 5134 * @param array $aArgs 5135 * @param string $sContains 5136 * 5137 * @return array|null 5138 */ 5139 public function GetAllowedValues($aArgs = array(), $sContains = '') 5140 { 5141 $oValSetDef = $this->GetValuesDef(); 5142 if (!$oValSetDef) 5143 { 5144 return null; 5145 } 5146 $aRawValues = $oValSetDef->GetValueList(); 5147 5148 if (is_null($aRawValues)) 5149 { 5150 return null; 5151 } 5152 $aLocalizedValues = array(); 5153 foreach($aRawValues as $sKey => $sValue) 5154 { 5155 $aLocalizedValues[$sKey] = Str::pure2html($this->GetValueLabel($sKey)); 5156 } 5157 5158 return $aLocalizedValues; 5159 } 5160 5161 /** 5162 * Returns the meta value for the given object. 5163 * See also MetaModel::RebuildMetaEnums() that must be maintained when MapValue changes 5164 * 5165 * @param $oObject 5166 * 5167 * @return mixed 5168 * @throws Exception 5169 */ 5170 public function MapValue($oObject) 5171 { 5172 $aMappingData = $this->GetMapRule(get_class($oObject)); 5173 if ($aMappingData == null) 5174 { 5175 $sRet = $this->GetDefaultValue(); 5176 } 5177 else 5178 { 5179 $sAttCode = $aMappingData['attcode']; 5180 $value = $oObject->Get($sAttCode); 5181 if (array_key_exists($value, $aMappingData['values'])) 5182 { 5183 $sRet = $aMappingData['values'][$value]; 5184 } 5185 elseif ($this->GetDefaultValue() != '') 5186 { 5187 $sRet = $this->GetDefaultValue(); 5188 } 5189 else 5190 { 5191 throw new Exception('AttributeMetaEnum::MapValue(): mapping not found for value "'.$value.'" in '.get_class($oObject).', on attribute '.MetaModel::GetAttributeOrigin($this->GetHostClass(), 5192 $this->GetCode()).'::'.$this->GetCode()); 5193 } 5194 } 5195 5196 return $sRet; 5197 } 5198 5199 public function GetMapRule($sClass) 5200 { 5201 $aMappings = $this->Get('mapping'); 5202 if (array_key_exists($sClass, $aMappings)) 5203 { 5204 $aMappingData = $aMappings[$sClass]; 5205 } 5206 else 5207 { 5208 $sParent = MetaModel::GetParentClass($sClass); 5209 if (is_null($sParent)) 5210 { 5211 $aMappingData = null; 5212 } 5213 else 5214 { 5215 $aMappingData = $this->GetMapRule($sParent); 5216 } 5217 } 5218 5219 return $aMappingData; 5220 } 5221} 5222 5223/** 5224 * Map a date+time column to an attribute 5225 * 5226 * @package iTopORM 5227 */ 5228class AttributeDateTime extends AttributeDBField 5229{ 5230 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE_TIME; 5231 5232 static $oFormat = null; 5233 5234 /** 5235 * 5236 * @return DateTimeFormat 5237 */ 5238 static public function GetFormat() 5239 { 5240 if (self::$oFormat == null) 5241 { 5242 static::LoadFormatFromConfig(); 5243 } 5244 5245 return self::$oFormat; 5246 } 5247 5248 /** 5249 * Load the 3 settings: date format, time format and data_time format from the configuration 5250 */ 5251 protected static function LoadFormatFromConfig() 5252 { 5253 $aFormats = MetaModel::GetConfig()->Get('date_and_time_format'); 5254 $sLang = Dict::GetUserLanguage(); 5255 $sDateFormat = isset($aFormats[$sLang]['date']) ? $aFormats[$sLang]['date'] : (isset($aFormats['default']['date']) ? $aFormats['default']['date'] : 'Y-m-d'); 5256 $sTimeFormat = isset($aFormats[$sLang]['time']) ? $aFormats[$sLang]['time'] : (isset($aFormats['default']['time']) ? $aFormats['default']['time'] : 'H:i:s'); 5257 $sDateAndTimeFormat = isset($aFormats[$sLang]['date_time']) ? $aFormats[$sLang]['date_time'] : (isset($aFormats['default']['date_time']) ? $aFormats['default']['date_time'] : '$date $time'); 5258 5259 $sFullFormat = str_replace(array('$date', '$time'), array($sDateFormat, $sTimeFormat), $sDateAndTimeFormat); 5260 5261 self::SetFormat(new DateTimeFormat($sFullFormat)); 5262 AttributeDate::SetFormat(new DateTimeFormat($sDateFormat)); 5263 } 5264 5265 /** 5266 * Returns the format string used for the date & time stored in memory 5267 * 5268 * @return string 5269 */ 5270 static public function GetInternalFormat() 5271 { 5272 return 'Y-m-d H:i:s'; 5273 } 5274 5275 /** 5276 * Returns the format string used for the date & time written to MySQL 5277 * 5278 * @return string 5279 */ 5280 static public function GetSQLFormat() 5281 { 5282 return 'Y-m-d H:i:s'; 5283 } 5284 5285 static public function SetFormat(DateTimeFormat $oDateTimeFormat) 5286 { 5287 self::$oFormat = $oDateTimeFormat; 5288 } 5289 5290 static public function GetSQLTimeFormat() 5291 { 5292 return 'H:i:s'; 5293 } 5294 5295 /** 5296 * Parses a search string coming from user input 5297 * 5298 * @param string $sSearchString 5299 * 5300 * @return string 5301 */ 5302 public function ParseSearchString($sSearchString) 5303 { 5304 try 5305 { 5306 $oDateTime = $this->GetFormat()->Parse($sSearchString); 5307 $sSearchString = $oDateTime->format($this->GetInternalFormat()); 5308 } catch (Exception $e) 5309 { 5310 $sFormatString = '!'.(string)AttributeDate::GetFormat(); // BEWARE: ! is needed to set non-parsed fields to zero !!! 5311 $oDateTime = DateTime::createFromFormat($sFormatString, $sSearchString); 5312 if ($oDateTime !== false) 5313 { 5314 $sSearchString = $oDateTime->format($this->GetInternalFormat()); 5315 } 5316 } 5317 5318 return $sSearchString; 5319 } 5320 5321 static public function GetFormFieldClass() 5322 { 5323 return '\\Combodo\\iTop\\Form\\Field\\DateTimeField'; 5324 } 5325 5326 /** 5327 * Override to specify Field class 5328 * 5329 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the 5330 * $oFormField is passed, MakeFormField behave more like a Prepare. 5331 */ 5332 public function MakeFormField(DBObject $oObject, $oFormField = null) 5333 { 5334 if ($oFormField === null) 5335 { 5336 $sFormFieldClass = static::GetFormFieldClass(); 5337 $oFormField = new $sFormFieldClass($this->GetCode()); 5338 } 5339 $oFormField->SetPHPDateTimeFormat((string)$this->GetFormat()); 5340 $oFormField->SetJSDateTimeFormat($this->GetFormat()->ToMomentJS()); 5341 5342 $oFormField = parent::MakeFormField($oObject, $oFormField); 5343 5344 // After call to the parent as it sets the current value 5345 $oFormField->SetCurrentValue($this->GetFormat()->Format($oObject->Get($this->GetCode()))); 5346 5347 return $oFormField; 5348 } 5349 5350 /** 5351 * @inheritdoc 5352 */ 5353 public function EnumTemplateVerbs() 5354 { 5355 return array( 5356 '' => 'Formatted representation', 5357 'raw' => 'Not formatted representation', 5358 ); 5359 } 5360 5361 /** 5362 * @inheritdoc 5363 */ 5364 public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) 5365 { 5366 switch ($sVerb) 5367 { 5368 case '': 5369 case 'text': 5370 return static::GetFormat()->format($value); 5371 break; 5372 case 'html': 5373 // Note: Not passing formatted value as the method will format it. 5374 return $this->GetAsHTML($value); 5375 break; 5376 case 'raw': 5377 return $value; 5378 break; 5379 default: 5380 return parent::GetForTemplate($value, $sVerb, $oHostObject, $bLocalize); 5381 break; 5382 } 5383 } 5384 5385 static public function ListExpectedParams() 5386 { 5387 return parent::ListExpectedParams(); 5388 //return array_merge(parent::ListExpectedParams(), array()); 5389 } 5390 5391 public function GetEditClass() 5392 { 5393 return "DateTime"; 5394 } 5395 5396 5397 public function GetEditValue($sValue, $oHostObj = null) 5398 { 5399 return (string)static::GetFormat()->format($sValue); 5400 } 5401 5402 public function GetValueLabel($sValue, $oHostObj = null) 5403 { 5404 return (string)static::GetFormat()->format($sValue); 5405 } 5406 5407 protected function GetSQLCol($bFullSpec = false) 5408 { 5409 return "DATETIME"; 5410 } 5411 5412 public function GetImportColumns() 5413 { 5414 // Allow an empty string to be a valid value (synonym for "reset") 5415 $aColumns = array(); 5416 $aColumns[$this->GetCode()] = 'VARCHAR(19)'; 5417 5418 return $aColumns; 5419 } 5420 5421 public static function GetAsUnixSeconds($value) 5422 { 5423 $oDeadlineDateTime = new DateTime($value); 5424 $iUnixSeconds = $oDeadlineDateTime->format('U'); 5425 5426 return $iUnixSeconds; 5427 } 5428 5429 public function GetDefaultValue(DBObject $oHostObject = null) 5430 { 5431 // null value will be replaced by the current date, if not already set, in DoComputeValues 5432 return $this->GetNullValue(); 5433 } 5434 5435 public function GetValidationPattern() 5436 { 5437 return static::GetFormat()->ToRegExpr(); 5438 } 5439 5440 public function GetBasicFilterOperators() 5441 { 5442 return array( 5443 "=" => "equals", 5444 "!=" => "differs from", 5445 "<" => "before", 5446 "<=" => "before", 5447 ">" => "after (strictly)", 5448 ">=" => "after", 5449 "SameDay" => "same day (strip time)", 5450 "SameMonth" => "same year/month", 5451 "SameYear" => "same year", 5452 "Today" => "today", 5453 ">|" => "after today + N days", 5454 "<|" => "before today + N days", 5455 "=|" => "equals today + N days", 5456 ); 5457 } 5458 5459 public function GetBasicFilterLooseOperator() 5460 { 5461 // Unless we implement a "same xxx, depending on given precision" ! 5462 return "="; 5463 } 5464 5465 public function GetBasicFilterSQLExpr($sOpCode, $value) 5466 { 5467 $sQValue = CMDBSource::Quote($value); 5468 5469 switch ($sOpCode) 5470 { 5471 case '=': 5472 case '!=': 5473 case '<': 5474 case '<=': 5475 case '>': 5476 case '>=': 5477 return $this->GetSQLExpr()." $sOpCode $sQValue"; 5478 case 'SameDay': 5479 return "DATE(".$this->GetSQLExpr().") = DATE($sQValue)"; 5480 case 'SameMonth': 5481 return "DATE_FORMAT(".$this->GetSQLExpr().", '%Y-%m') = DATE_FORMAT($sQValue, '%Y-%m')"; 5482 case 'SameYear': 5483 return "MONTH(".$this->GetSQLExpr().") = MONTH($sQValue)"; 5484 case 'Today': 5485 return "DATE(".$this->GetSQLExpr().") = CURRENT_DATE()"; 5486 case '>|': 5487 return "DATE(".$this->GetSQLExpr().") > DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; 5488 case '<|': 5489 return "DATE(".$this->GetSQLExpr().") < DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; 5490 case '=|': 5491 return "DATE(".$this->GetSQLExpr().") = DATE_ADD(CURRENT_DATE(), INTERVAL $sQValue DAY)"; 5492 default: 5493 return $this->GetSQLExpr()." = $sQValue"; 5494 } 5495 } 5496 5497 public function MakeRealValue($proposedValue, $oHostObj) 5498 { 5499 if (is_null($proposedValue)) 5500 { 5501 return null; 5502 } 5503 if (is_string($proposedValue) && ($proposedValue == "") && $this->IsNullAllowed()) 5504 { 5505 return null; 5506 } 5507 if (!is_numeric($proposedValue)) 5508 { 5509 // Check the format 5510 try 5511 { 5512 $oFormat = new DateTimeFormat($this->GetInternalFormat()); 5513 $oFormat->Parse($proposedValue); 5514 } catch (Exception $e) 5515 { 5516 throw new Exception('Wrong format for date attribute '.$this->GetCode().', expecting "'.$this->GetInternalFormat().'" and got "'.$proposedValue.'"'); 5517 } 5518 5519 return $proposedValue; 5520 } 5521 5522 return date(static::GetInternalFormat(), $proposedValue); 5523 } 5524 5525 public function ScalarToSQL($value) 5526 { 5527 if (empty($value)) 5528 { 5529 return null; 5530 } 5531 5532 return $value; 5533 } 5534 5535 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 5536 { 5537 return Str::pure2html(static::GetFormat()->format($value)); 5538 } 5539 5540 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 5541 { 5542 return Str::pure2xml($value); 5543 } 5544 5545 public function GetAsCSV( 5546 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 5547 $bConvertToPlainText = false 5548 ) { 5549 if (empty($sValue) || ($sValue === '0000-00-00 00:00:00') || ($sValue === '0000-00-00')) 5550 { 5551 return ''; 5552 } 5553 else 5554 { 5555 if ((string)static::GetFormat() !== static::GetInternalFormat()) 5556 { 5557 // Format conversion 5558 $oDate = new DateTime($sValue); 5559 if ($oDate !== false) 5560 { 5561 $sValue = static::GetFormat()->format($oDate); 5562 } 5563 } 5564 } 5565 $sFrom = array("\r\n", $sTextQualifier); 5566 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 5567 $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); 5568 5569 return $sTextQualifier.$sEscaped.$sTextQualifier; 5570 } 5571 5572 /** 5573 * Parses a string to find some smart search patterns and build the corresponding search/OQL condition 5574 * Each derived class is reponsible for defining and processing their own smart patterns, the base class 5575 * does nothing special, and just calls the default (loose) operator 5576 * 5577 * @param string $sSearchText The search string to analyze for smart patterns 5578 * @param FieldExpression $oField The FieldExpression representing the atttribute code in this OQL query 5579 * @param array $aParams Values of the query parameters 5580 * @param bool $bParseSearchString 5581 * 5582 * @return Expression The search condition to be added (AND) to the current search 5583 * @throws \CoreException 5584 */ 5585 public function GetSmartConditionExpression( 5586 $sSearchText, FieldExpression $oField, &$aParams, $bParseSearchString = false 5587 ) { 5588 // Possible smart patterns 5589 $aPatterns = array( 5590 'between' => array('pattern' => '/^\[(.*),(.*)\]$/', 'operator' => 'n/a'), 5591 'greater than or equal' => array('pattern' => '/^>=(.*)$/', 'operator' => '>='), 5592 'greater than' => array('pattern' => '/^>(.*)$/', 'operator' => '>'), 5593 'less than or equal' => array('pattern' => '/^<=(.*)$/', 'operator' => '<='), 5594 'less than' => array('pattern' => '/^<(.*)$/', 'operator' => '<'), 5595 ); 5596 5597 $sPatternFound = ''; 5598 $aMatches = array(); 5599 foreach($aPatterns as $sPatName => $sPattern) 5600 { 5601 if (preg_match($sPattern['pattern'], $sSearchText, $aMatches)) 5602 { 5603 $sPatternFound = $sPatName; 5604 break; 5605 } 5606 } 5607 5608 switch ($sPatternFound) 5609 { 5610 case 'between': 5611 5612 $sParamName1 = $oField->GetParent().'_'.$oField->GetName().'_1'; 5613 $oRightExpr = new VariableExpression($sParamName1); 5614 if ($bParseSearchString) 5615 { 5616 $aParams[$sParamName1] = $this->ParseSearchString($aMatches[1]); 5617 } 5618 else 5619 { 5620 $aParams[$sParamName1] = $aMatches[1]; 5621 } 5622 $oCondition1 = new BinaryExpression($oField, '>=', $oRightExpr); 5623 5624 $sParamName2 = $oField->GetParent().'_'.$oField->GetName().'_2'; 5625 $oRightExpr = new VariableExpression($sParamName2); 5626 if ($bParseSearchString) 5627 { 5628 $aParams[$sParamName2] = $this->ParseSearchString($aMatches[2]); 5629 } 5630 else 5631 { 5632 $aParams[$sParamName2] = $aMatches[2]; 5633 } 5634 $oCondition2 = new BinaryExpression($oField, '<=', $oRightExpr); 5635 5636 $oNewCondition = new BinaryExpression($oCondition1, 'AND', $oCondition2); 5637 break; 5638 5639 case 'greater than': 5640 case 'greater than or equal': 5641 case 'less than': 5642 case 'less than or equal': 5643 $sSQLOperator = $aPatterns[$sPatternFound]['operator']; 5644 $sParamName = $oField->GetParent().'_'.$oField->GetName(); 5645 $oRightExpr = new VariableExpression($sParamName); 5646 if ($bParseSearchString) 5647 { 5648 $aParams[$sParamName] = $this->ParseSearchString($aMatches[1]); 5649 } 5650 else 5651 { 5652 $aParams[$sParamName] = $aMatches[1]; 5653 } 5654 $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); 5655 5656 break; 5657 5658 default: 5659 $oNewCondition = parent::GetSmartConditionExpression($sSearchText, $oField, $aParams); 5660 5661 } 5662 5663 return $oNewCondition; 5664 } 5665 5666 5667 public function GetHelpOnSmartSearch() 5668 { 5669 $sDict = parent::GetHelpOnSmartSearch(); 5670 5671 $oFormat = static::GetFormat(); 5672 $sExample = $oFormat->Format(new DateTime('2015-07-19 18:40:00')); 5673 5674 return vsprintf($sDict, array($oFormat->ToPlaceholder(), $sExample)); 5675 } 5676} 5677 5678/** 5679 * Store a duration as a number of seconds 5680 * 5681 * @package iTopORM 5682 */ 5683class AttributeDuration extends AttributeInteger 5684{ 5685 public function GetEditClass() 5686 { 5687 return "Duration"; 5688 } 5689 5690 protected function GetSQLCol($bFullSpec = false) 5691 { 5692 return "INT(11) UNSIGNED"; 5693 } 5694 5695 public function GetNullValue() 5696 { 5697 return '0'; 5698 } 5699 5700 public function MakeRealValue($proposedValue, $oHostObj) 5701 { 5702 if (is_null($proposedValue)) 5703 { 5704 return null; 5705 } 5706 if (!is_numeric($proposedValue)) 5707 { 5708 return null; 5709 } 5710 if (((int)$proposedValue) < 0) 5711 { 5712 return null; 5713 } 5714 5715 return (int)$proposedValue; 5716 } 5717 5718 public function ScalarToSQL($value) 5719 { 5720 if (is_null($value)) 5721 { 5722 return null; 5723 } 5724 5725 return $value; 5726 } 5727 5728 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 5729 { 5730 return Str::pure2html(self::FormatDuration($value)); 5731 } 5732 5733 public static function FormatDuration($duration) 5734 { 5735 $aDuration = self::SplitDuration($duration); 5736 5737 if ($duration < 60) 5738 { 5739 // Less than 1 min 5740 $sResult = Dict::Format('Core:Duration_Seconds', $aDuration['seconds']); 5741 } 5742 else 5743 { 5744 if ($duration < 3600) 5745 { 5746 // less than 1 hour, display it in minutes/seconds 5747 $sResult = Dict::Format('Core:Duration_Minutes_Seconds', $aDuration['minutes'], $aDuration['seconds']); 5748 } 5749 else 5750 { 5751 if ($duration < 86400) 5752 { 5753 // Less than 1 day, display it in hours/minutes/seconds 5754 $sResult = Dict::Format('Core:Duration_Hours_Minutes_Seconds', $aDuration['hours'], 5755 $aDuration['minutes'], $aDuration['seconds']); 5756 } 5757 else 5758 { 5759 // more than 1 day, display it in days/hours/minutes/seconds 5760 $sResult = Dict::Format('Core:Duration_Days_Hours_Minutes_Seconds', $aDuration['days'], 5761 $aDuration['hours'], $aDuration['minutes'], $aDuration['seconds']); 5762 } 5763 } 5764 } 5765 5766 return $sResult; 5767 } 5768 5769 static function SplitDuration($duration) 5770 { 5771 $duration = (int)$duration; 5772 $days = floor($duration / 86400); 5773 $hours = floor(($duration - (86400 * $days)) / 3600); 5774 $minutes = floor(($duration - (86400 * $days + 3600 * $hours)) / 60); 5775 $seconds = ($duration % 60); // modulo 5776 5777 return array('days' => $days, 'hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds); 5778 } 5779 5780 static public function GetFormFieldClass() 5781 { 5782 return '\\Combodo\\iTop\\Form\\Field\\DurationField'; 5783 } 5784 5785 public function MakeFormField(DBObject $oObject, $oFormField = null) 5786 { 5787 if ($oFormField === null) 5788 { 5789 $sFormFieldClass = static::GetFormFieldClass(); 5790 $oFormField = new $sFormFieldClass($this->GetCode()); 5791 } 5792 parent::MakeFormField($oObject, $oFormField); 5793 5794 // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition 5795 $sAttCode = $this->GetCode(); 5796 $oFormField->SetCurrentValue($oObject->Get($sAttCode)); 5797 $oFormField->SetReadOnly(true); 5798 5799 return $oFormField; 5800 } 5801 5802} 5803 5804/** 5805 * Map a date+time column to an attribute 5806 * 5807 * @package iTopORM 5808 */ 5809class AttributeDate extends AttributeDateTime 5810{ 5811 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE; 5812 5813 static $oDateFormat = null; 5814 5815 static public function GetFormat() 5816 { 5817 if (self::$oDateFormat == null) 5818 { 5819 AttributeDateTime::LoadFormatFromConfig(); 5820 } 5821 5822 return self::$oDateFormat; 5823 } 5824 5825 static public function SetFormat(DateTimeFormat $oDateFormat) 5826 { 5827 self::$oDateFormat = $oDateFormat; 5828 } 5829 5830 /** 5831 * Returns the format string used for the date & time stored in memory 5832 * 5833 * @return string 5834 */ 5835 static public function GetInternalFormat() 5836 { 5837 return 'Y-m-d'; 5838 } 5839 5840 /** 5841 * Returns the format string used for the date & time written to MySQL 5842 * 5843 * @return string 5844 */ 5845 static public function GetSQLFormat() 5846 { 5847 return 'Y-m-d'; 5848 } 5849 5850 static public function ListExpectedParams() 5851 { 5852 return parent::ListExpectedParams(); 5853 //return array_merge(parent::ListExpectedParams(), array()); 5854 } 5855 5856 public function GetEditClass() 5857 { 5858 return "Date"; 5859 } 5860 5861 protected function GetSQLCol($bFullSpec = false) 5862 { 5863 return "DATE"; 5864 } 5865 5866 public function GetImportColumns() 5867 { 5868 // Allow an empty string to be a valid value (synonym for "reset") 5869 $aColumns = array(); 5870 $aColumns[$this->GetCode()] = 'VARCHAR(10)'; 5871 5872 return $aColumns; 5873 } 5874 5875 5876 /** 5877 * Override to specify Field class 5878 * 5879 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the 5880 * $oFormField is passed, MakeFormField behave more like a Prepare. 5881 */ 5882 public function MakeFormField(DBObject $oObject, $oFormField = null) 5883 { 5884 $oFormField = parent::MakeFormField($oObject, $oFormField); 5885 $oFormField->SetDateOnly(true); 5886 5887 return $oFormField; 5888 } 5889 5890} 5891 5892/** 5893 * A dead line stored as a date & time 5894 * The only difference with the DateTime attribute is the display: 5895 * relative to the current time 5896 */ 5897class AttributeDeadline extends AttributeDateTime 5898{ 5899 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 5900 { 5901 $sResult = self::FormatDeadline($value); 5902 5903 return $sResult; 5904 } 5905 5906 public static function FormatDeadline($value) 5907 { 5908 $sResult = ''; 5909 if ($value !== null) 5910 { 5911 $iValue = AttributeDateTime::GetAsUnixSeconds($value); 5912 $sDate = AttributeDateTime::GetFormat()->Format($value); 5913 $difference = $iValue - time(); 5914 5915 if ($difference >= 0) 5916 { 5917 $sDifference = self::FormatDuration($difference); 5918 } 5919 else 5920 { 5921 $sDifference = Dict::Format('UI:DeadlineMissedBy_duration', self::FormatDuration(-$difference)); 5922 } 5923 $sFormat = MetaModel::GetConfig()->Get('deadline_format'); 5924 $sResult = str_replace(array('$date$', '$difference$'), array($sDate, $sDifference), $sFormat); 5925 } 5926 5927 return $sResult; 5928 } 5929 5930 static function FormatDuration($duration) 5931 { 5932 $days = floor($duration / 86400); 5933 $hours = floor(($duration - (86400 * $days)) / 3600); 5934 $minutes = floor(($duration - (86400 * $days + 3600 * $hours)) / 60); 5935 5936 if ($duration < 60) 5937 { 5938 // Less than 1 min 5939 $sResult = Dict::S('UI:Deadline_LessThan1Min'); 5940 } 5941 else 5942 { 5943 if ($duration < 3600) 5944 { 5945 // less than 1 hour, display it in minutes 5946 $sResult = Dict::Format('UI:Deadline_Minutes', $minutes); 5947 } 5948 else 5949 { 5950 if ($duration < 86400) 5951 { 5952 // Less that 1 day, display it in hours/minutes 5953 $sResult = Dict::Format('UI:Deadline_Hours_Minutes', $hours, $minutes); 5954 } 5955 else 5956 { 5957 // Less that 1 day, display it in hours/minutes 5958 $sResult = Dict::Format('UI:Deadline_Days_Hours_Minutes', $days, $hours, $minutes); 5959 } 5960 } 5961 } 5962 5963 return $sResult; 5964 } 5965} 5966 5967/** 5968 * Map a foreign key to an attribute 5969 * AttributeExternalKey and AttributeExternalField may be an external key 5970 * the difference is that AttributeExternalKey corresponds to a column into the defined table 5971 * where an AttributeExternalField corresponds to a column into another table (class) 5972 * 5973 * @package iTopORM 5974 */ 5975class AttributeExternalKey extends AttributeDBFieldVoid 5976{ 5977 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; 5978 5979 5980 /** 5981 * Return the search widget type corresponding to this attribute 5982 * 5983 * @return string 5984 */ 5985 public function GetSearchType() 5986 { 5987 try 5988 { 5989 $oRemoteAtt = $this->GetFinalAttDef(); 5990 $sTargetClass = $oRemoteAtt->GetTargetClass(); 5991 if (MetaModel::IsHierarchicalClass($sTargetClass)) 5992 { 5993 return self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; 5994 } 5995 5996 return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; 5997 } catch (CoreException $e) 5998 { 5999 } 6000 6001 return self::SEARCH_WIDGET_TYPE_RAW; 6002 } 6003 6004 static public function ListExpectedParams() 6005 { 6006 return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete")); 6007 } 6008 6009 public function GetEditClass() 6010 { 6011 return "ExtKey"; 6012 } 6013 6014 protected function GetSQLCol($bFullSpec = false) 6015 { 6016 return "INT(11)".($bFullSpec ? " DEFAULT 0" : ""); 6017 } 6018 6019 public function RequiresIndex() 6020 { 6021 return true; 6022 } 6023 6024 public function IsExternalKey($iType = EXTKEY_RELATIVE) 6025 { 6026 return true; 6027 } 6028 6029 public function GetTargetClass($iType = EXTKEY_RELATIVE) 6030 { 6031 return $this->Get("targetclass"); 6032 } 6033 6034 public function GetKeyAttDef($iType = EXTKEY_RELATIVE) 6035 { 6036 return $this; 6037 } 6038 6039 public function GetKeyAttCode() 6040 { 6041 return $this->GetCode(); 6042 } 6043 6044 public function GetDisplayStyle() 6045 { 6046 return $this->GetOptional('display_style', 'select'); 6047 } 6048 6049 6050 public function GetDefaultValue(DBObject $oHostObject = null) 6051 { 6052 return 0; 6053 } 6054 6055 public function IsNullAllowed() 6056 { 6057 if (MetaModel::GetConfig()->Get('disable_mandatory_ext_keys')) 6058 { 6059 return true; 6060 } 6061 6062 return $this->Get("is_null_allowed"); 6063 } 6064 6065 6066 public function GetBasicFilterOperators() 6067 { 6068 return parent::GetBasicFilterOperators(); 6069 } 6070 6071 public function GetBasicFilterLooseOperator() 6072 { 6073 return parent::GetBasicFilterLooseOperator(); 6074 } 6075 6076 public function GetBasicFilterSQLExpr($sOpCode, $value) 6077 { 6078 return parent::GetBasicFilterSQLExpr($sOpCode, $value); 6079 } 6080 6081 // overloaded here so that an ext key always have the answer to 6082 // "what are your possible values?" 6083 public function GetValuesDef() 6084 { 6085 $oValSetDef = $this->Get("allowed_values"); 6086 if (!$oValSetDef) 6087 { 6088 // Let's propose every existing value 6089 $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); 6090 } 6091 6092 return $oValSetDef; 6093 } 6094 6095 public function GetAllowedValues($aArgs = array(), $sContains = '') 6096 { 6097 //throw new Exception("GetAllowedValues on ext key has been deprecated"); 6098 try 6099 { 6100 return parent::GetAllowedValues($aArgs, $sContains); 6101 } catch (Exception $e) 6102 { 6103 // Some required arguments could not be found, enlarge to any existing value 6104 $oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass()); 6105 6106 return $oValSetDef->GetValues($aArgs, $sContains); 6107 } 6108 } 6109 6110 public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) 6111 { 6112 $oValSetDef = $this->GetValuesDef(); 6113 $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue); 6114 6115 return $oSet; 6116 } 6117 6118 public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) 6119 { 6120 return DBObjectSearch::FromOQL($this->GetValuesDef()->GetFilterExpression()); 6121 } 6122 6123 public function GetDeletionPropagationOption() 6124 { 6125 return $this->Get("on_target_delete"); 6126 } 6127 6128 public function GetNullValue() 6129 { 6130 return 0; 6131 } 6132 6133 public function IsNull($proposedValue) 6134 { 6135 return ($proposedValue == 0); 6136 } 6137 6138 public function MakeRealValue($proposedValue, $oHostObj) 6139 { 6140 if (is_null($proposedValue)) 6141 { 6142 return 0; 6143 } 6144 if ($proposedValue === '') 6145 { 6146 return 0; 6147 } 6148 if (MetaModel::IsValidObject($proposedValue)) 6149 { 6150 return $proposedValue->GetKey(); 6151 } 6152 6153 return (int)$proposedValue; 6154 } 6155 6156 public function GetMaximumComboLength() 6157 { 6158 return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length')); 6159 } 6160 6161 public function GetMinAutoCompleteChars() 6162 { 6163 return $this->GetOptional('min_autocomplete_chars', MetaModel::GetConfig()->Get('min_autocomplete_chars')); 6164 } 6165 6166 public function AllowTargetCreation() 6167 { 6168 return $this->GetOptional('allow_target_creation', MetaModel::GetConfig()->Get('allow_target_creation')); 6169 } 6170 6171 /** 6172 * Find the corresponding "link" attribute on the target class, if any 6173 * 6174 * @return null | AttributeDefinition 6175 * @throws \CoreException 6176 */ 6177 public function GetMirrorLinkAttribute() 6178 { 6179 $oRet = null; 6180 $sRemoteClass = $this->GetTargetClass(); 6181 foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) 6182 { 6183 if (!$oRemoteAttDef->IsLinkSet()) 6184 { 6185 continue; 6186 } 6187 if (!is_subclass_of($this->GetHostClass(), 6188 $oRemoteAttDef->GetLinkedClass()) && $oRemoteAttDef->GetLinkedClass() != $this->GetHostClass()) 6189 { 6190 continue; 6191 } 6192 if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetCode()) 6193 { 6194 continue; 6195 } 6196 $oRet = $oRemoteAttDef; 6197 break; 6198 } 6199 6200 return $oRet; 6201 } 6202 6203 static public function GetFormFieldClass() 6204 { 6205 return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField'; 6206 } 6207 6208 public function MakeFormField(DBObject $oObject, $oFormField = null) 6209 { 6210 if ($oFormField === null) 6211 { 6212 // Later : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value 6213 $sFormFieldClass = static::GetFormFieldClass(); 6214 $oFormField = new $sFormFieldClass($this->GetCode()); 6215 } 6216 6217 // Setting params 6218 $oFormField->SetMaximumComboLength($this->GetMaximumComboLength()); 6219 $oFormField->SetMinAutoCompleteChars($this->GetMinAutoCompleteChars()); 6220 $oFormField->SetHierarchical(MetaModel::IsHierarchicalClass($this->GetTargetClass())); 6221 // Setting choices regarding the field dependencies 6222 $aFieldDependencies = $this->GetPrerequisiteAttributes(); 6223 if (!empty($aFieldDependencies)) 6224 { 6225 $oTmpAttDef = $this; 6226 $oTmpField = $oFormField; 6227 $oFormField->SetOnFinalizeCallback(function () use ($oTmpField, $oTmpAttDef, $oObject) { 6228 /** @var $oTmpField \Combodo\iTop\Form\Field\Field */ 6229 /** @var $oTmpAttDef \AttributeDefinition */ 6230 /** @var $oObject \DBObject */ 6231 6232 // We set search object only if it has not already been set (overrided) 6233 if ($oTmpField->GetSearch() === null) 6234 { 6235 $oSearch = DBSearch::FromOQL($oTmpAttDef->GetValuesDef()->GetFilterExpression()); 6236 $oSearch->SetInternalParams(array('this' => $oObject)); 6237 $oTmpField->SetSearch($oSearch); 6238 } 6239 }); 6240 } 6241 else 6242 { 6243 $oSearch = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression()); 6244 $oSearch->SetInternalParams(array('this' => $oObject)); 6245 $oFormField->SetSearch($oSearch); 6246 } 6247 6248 // If ExtKey is mandatory, we add a validator to ensure that the value 0 is not selected 6249 if ($oObject->GetAttributeFlags($this->GetCode()) & OPT_ATT_MANDATORY) 6250 { 6251 $oFormField->AddValidator(new \Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator()); 6252 } 6253 6254 parent::MakeFormField($oObject, $oFormField); 6255 6256 return $oFormField; 6257 } 6258 6259 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 6260 { 6261 if (!is_null($oHostObject)) 6262 { 6263 return $oHostObject->GetAsHTML($this->GetCode(), $oHostObject); 6264 } 6265 6266 return DBObject::MakeHyperLink($this->GetTargetClass(), $sValue); 6267 } 6268} 6269 6270/** 6271 * Special kind of External Key to manage a hierarchy of objects 6272 */ 6273class AttributeHierarchicalKey extends AttributeExternalKey 6274{ 6275 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY; 6276 6277 protected $m_sTargetClass; 6278 6279 static public function ListExpectedParams() 6280 { 6281 $aParams = parent::ListExpectedParams(); 6282 $idx = array_search('targetclass', $aParams); 6283 unset($aParams[$idx]); 6284 $idx = array_search('jointype', $aParams); 6285 unset($aParams[$idx]); 6286 6287 return $aParams; // Later: mettre les bons parametres ici !! 6288 } 6289 6290 public function GetEditClass() 6291 { 6292 return "ExtKey"; 6293 } 6294 6295 public function RequiresIndex() 6296 { 6297 return true; 6298 } 6299 6300 /* 6301 * The target class is the class for which the attribute has been defined first 6302 */ 6303 public function SetHostClass($sHostClass) 6304 { 6305 if (!isset($this->m_sTargetClass)) 6306 { 6307 $this->m_sTargetClass = $sHostClass; 6308 } 6309 parent::SetHostClass($sHostClass); 6310 } 6311 6312 static public function IsHierarchicalKey() 6313 { 6314 return true; 6315 } 6316 6317 public function GetTargetClass($iType = EXTKEY_RELATIVE) 6318 { 6319 return $this->m_sTargetClass; 6320 } 6321 6322 public function GetKeyAttDef($iType = EXTKEY_RELATIVE) 6323 { 6324 return $this; 6325 } 6326 6327 public function GetKeyAttCode() 6328 { 6329 return $this->GetCode(); 6330 } 6331 6332 public function GetBasicFilterOperators() 6333 { 6334 return parent::GetBasicFilterOperators(); 6335 } 6336 6337 public function GetBasicFilterLooseOperator() 6338 { 6339 return parent::GetBasicFilterLooseOperator(); 6340 } 6341 6342 public function GetSQLColumns($bFullSpec = false) 6343 { 6344 $aColumns = array(); 6345 $aColumns[$this->GetCode()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); 6346 $aColumns[$this->GetSQLLeft()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); 6347 $aColumns[$this->GetSQLRight()] = 'INT(11)'.($bFullSpec ? ' DEFAULT 0' : ''); 6348 6349 return $aColumns; 6350 } 6351 6352 public function GetSQLRight() 6353 { 6354 return $this->GetCode().'_right'; 6355 } 6356 6357 public function GetSQLLeft() 6358 { 6359 return $this->GetCode().'_left'; 6360 } 6361 6362 public function GetSQLValues($value) 6363 { 6364 if (!is_array($value)) 6365 { 6366 $aValues[$this->GetCode()] = $value; 6367 } 6368 else 6369 { 6370 $aValues = array(); 6371 $aValues[$this->GetCode()] = $value[$this->GetCode()]; 6372 $aValues[$this->GetSQLRight()] = $value[$this->GetSQLRight()]; 6373 $aValues[$this->GetSQLLeft()] = $value[$this->GetSQLLeft()]; 6374 } 6375 6376 return $aValues; 6377 } 6378 6379 public function GetAllowedValues($aArgs = array(), $sContains = '') 6380 { 6381 $oFilter = $this->GetHierachicalFilter($aArgs, $sContains); 6382 if ($oFilter) 6383 { 6384 $oValSetDef = $this->GetValuesDef(); 6385 $oValSetDef->AddCondition($oFilter); 6386 6387 return $oValSetDef->GetValues($aArgs, $sContains); 6388 } 6389 else 6390 { 6391 return parent::GetAllowedValues($aArgs, $sContains); 6392 } 6393 } 6394 6395 public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '', $iAdditionalValue = null) 6396 { 6397 $oValSetDef = $this->GetValuesDef(); 6398 $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue); 6399 if ($oFilter) 6400 { 6401 $oValSetDef->AddCondition($oFilter); 6402 } 6403 $oSet = $oValSetDef->ToObjectSet($aArgs, $sContains, $iAdditionalValue); 6404 6405 return $oSet; 6406 } 6407 6408 public function GetAllowedValuesAsFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) 6409 { 6410 $oFilter = $this->GetHierachicalFilter($aArgs, $sContains, $iAdditionalValue); 6411 if ($oFilter) 6412 { 6413 return $oFilter; 6414 } 6415 6416 return parent::GetAllowedValuesAsFilter($aArgs, $sContains, $iAdditionalValue); 6417 } 6418 6419 private function GetHierachicalFilter($aArgs = array(), $sContains = '', $iAdditionalValue = null) 6420 { 6421 if (array_key_exists('this', $aArgs)) 6422 { 6423 // Hierarchical keys have one more constraint: the "parent value" cannot be 6424 // "under" themselves 6425 $iRootId = $aArgs['this']->GetKey(); 6426 if ($iRootId > 0) // ignore objects that do no exist in the database... 6427 { 6428 $sClass = $this->m_sTargetClass; 6429 6430 return DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node.".$this->GetCode()." NOT BELOW root.id WHERE root.id = $iRootId"); 6431 } 6432 } 6433 6434 return false; 6435 } 6436 6437 /** 6438 * Find the corresponding "link" attribute on the target class, if any 6439 * 6440 * @return null | AttributeDefinition 6441 */ 6442 public function GetMirrorLinkAttribute() 6443 { 6444 return null; 6445 } 6446} 6447 6448/** 6449 * An attribute which corresponds to an external key (direct or indirect) 6450 * 6451 * @package iTopORM 6452 */ 6453class AttributeExternalField extends AttributeDefinition 6454{ 6455 /** 6456 * Return the search widget type corresponding to this attribute 6457 * 6458 * @return string 6459 * @throws \CoreException 6460 */ 6461 public function GetSearchType() 6462 { 6463 // Not necessary the external key is already present 6464 if ($this->IsFriendlyName()) 6465 { 6466 return self::SEARCH_WIDGET_TYPE_RAW; 6467 } 6468 6469 try 6470 { 6471 $oRemoteAtt = $this->GetFinalAttDef(); 6472 switch (true) 6473 { 6474 case ($oRemoteAtt instanceof AttributeString): 6475 return self::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD; 6476 case ($oRemoteAtt instanceof AttributeExternalKey): 6477 return self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; 6478 } 6479 } catch (CoreException $e) 6480 { 6481 } 6482 6483 return self::SEARCH_WIDGET_TYPE_RAW; 6484 } 6485 6486 6487 static public function ListExpectedParams() 6488 { 6489 return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode")); 6490 } 6491 6492 public function GetEditClass() 6493 { 6494 return "ExtField"; 6495 } 6496 6497 /** 6498 * @return \AttributeDefinition 6499 * @throws \CoreException 6500 */ 6501 public function GetFinalAttDef() 6502 { 6503 $oExtAttDef = $this->GetExtAttDef(); 6504 6505 return $oExtAttDef->GetFinalAttDef(); 6506 } 6507 6508 protected function GetSQLCol($bFullSpec = false) 6509 { 6510 // throw new CoreException("external attribute: does it make any sense to request its type ?"); 6511 $oExtAttDef = $this->GetExtAttDef(); 6512 6513 return $oExtAttDef->GetSQLCol($bFullSpec); 6514 } 6515 6516 public function GetSQLExpressions($sPrefix = '') 6517 { 6518 if ($sPrefix == '') 6519 { 6520 return array('' => $this->GetCode()); // Warning: Use GetCode() since AttributeExternalField does not have any 'sql' property 6521 } 6522 else 6523 { 6524 return $sPrefix; 6525 } 6526 } 6527 6528 public function GetLabel($sDefault = null) 6529 { 6530 if ($this->IsFriendlyName()) 6531 { 6532 $sKeyAttCode = $this->Get("extkey_attcode"); 6533 $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); 6534 $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); 6535 } 6536 else 6537 { 6538 $sLabel = parent::GetLabel(''); 6539 if (strlen($sLabel) == 0) 6540 { 6541 $oRemoteAtt = $this->GetExtAttDef(); 6542 $sLabel = $oRemoteAtt->GetLabel($this->m_sCode); 6543 $oKeyAtt = $this->GetKeyAttDef(); 6544 $sKeyLabel = $oKeyAtt->GetLabel($this->GetKeyAttCode()); 6545 $sLabel = "{$sKeyLabel}->{$sLabel}"; 6546 } 6547 } 6548 6549 return $sLabel; 6550 } 6551 6552 public function GetLabelForSearchField() 6553 { 6554 $sLabel = parent::GetLabel(''); 6555 if (strlen($sLabel) == 0) 6556 { 6557 $sKeyAttCode = $this->Get("extkey_attcode"); 6558 $oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode); 6559 $sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode); 6560 6561 $oRemoteAtt = $this->GetExtAttDef(); 6562 $sLabel .= '->'.$oRemoteAtt->GetLabel($this->m_sCode); 6563 } 6564 6565 return $sLabel; 6566 } 6567 6568 public function GetDescription($sDefault = null) 6569 { 6570 $sLabel = parent::GetDescription(''); 6571 if (strlen($sLabel) == 0) 6572 { 6573 $oRemoteAtt = $this->GetExtAttDef(); 6574 $sLabel = $oRemoteAtt->GetDescription(''); 6575 } 6576 6577 return $sLabel; 6578 } 6579 6580 public function GetHelpOnEdition($sDefault = null) 6581 { 6582 $sLabel = parent::GetHelpOnEdition(''); 6583 if (strlen($sLabel) == 0) 6584 { 6585 $oRemoteAtt = $this->GetExtAttDef(); 6586 $sLabel = $oRemoteAtt->GetHelpOnEdition(''); 6587 } 6588 6589 return $sLabel; 6590 } 6591 6592 public function IsExternalKey($iType = EXTKEY_RELATIVE) 6593 { 6594 switch ($iType) 6595 { 6596 case EXTKEY_ABSOLUTE: 6597 // see further 6598 $oRemoteAtt = $this->GetExtAttDef(); 6599 6600 return $oRemoteAtt->IsExternalKey($iType); 6601 6602 case EXTKEY_RELATIVE: 6603 return false; 6604 6605 default: 6606 throw new CoreException("Unexpected value for argument iType: '$iType'"); 6607 } 6608 } 6609 6610 /** 6611 * @return bool 6612 * @throws \CoreException 6613 */ 6614 public function IsFriendlyName() 6615 { 6616 $oRemoteAtt = $this->GetExtAttDef(); 6617 if ($oRemoteAtt instanceof AttributeExternalField) 6618 { 6619 $bRet = $oRemoteAtt->IsFriendlyName(); 6620 } 6621 elseif ($oRemoteAtt instanceof AttributeFriendlyName) 6622 { 6623 $bRet = true; 6624 } 6625 else 6626 { 6627 $bRet = false; 6628 } 6629 6630 return $bRet; 6631 } 6632 6633 public function GetTargetClass($iType = EXTKEY_RELATIVE) 6634 { 6635 return $this->GetKeyAttDef($iType)->GetTargetClass(); 6636 } 6637 6638 static public function IsExternalField() 6639 { 6640 return true; 6641 } 6642 6643 public function GetKeyAttCode() 6644 { 6645 return $this->Get("extkey_attcode"); 6646 } 6647 6648 public function GetExtAttCode() 6649 { 6650 return $this->Get("target_attcode"); 6651 } 6652 6653 /** 6654 * @param int $iType 6655 * 6656 * @return \AttributeExternalKey 6657 * @throws \CoreException 6658 * @throws \Exception 6659 */ 6660 public function GetKeyAttDef($iType = EXTKEY_RELATIVE) 6661 { 6662 switch ($iType) 6663 { 6664 case EXTKEY_ABSOLUTE: 6665 // see further 6666 /** @var \AttributeExternalKey $oRemoteAtt */ 6667 $oRemoteAtt = $this->GetExtAttDef(); 6668 if ($oRemoteAtt->IsExternalField()) 6669 { 6670 return $oRemoteAtt->GetKeyAttDef(EXTKEY_ABSOLUTE); 6671 } 6672 else 6673 { 6674 if ($oRemoteAtt->IsExternalKey()) 6675 { 6676 return $oRemoteAtt; 6677 } 6678 } 6679 6680 return $this->GetKeyAttDef(EXTKEY_RELATIVE); // which corresponds to the code hereafter ! 6681 6682 case EXTKEY_RELATIVE: 6683 /** @var \AttributeExternalKey $oAttDef */ 6684 $oAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $this->Get("extkey_attcode")); 6685 6686 return $oAttDef; 6687 6688 default: 6689 throw new CoreException("Unexpected value for argument iType: '$iType'"); 6690 } 6691 } 6692 6693 public function GetPrerequisiteAttributes($sClass = null) 6694 { 6695 return array($this->Get("extkey_attcode")); 6696 } 6697 6698 6699 /** 6700 * @return \AttributeExternalField 6701 * @throws \CoreException 6702 * @throws \Exception 6703 */ 6704 public function GetExtAttDef() 6705 { 6706 $oKeyAttDef = $this->GetKeyAttDef(); 6707 /** @var \AttributeExternalField $oExtAttDef */ 6708 $oExtAttDef = MetaModel::GetAttributeDef($oKeyAttDef->GetTargetClass(), $this->Get("target_attcode")); 6709 if (!is_object($oExtAttDef)) 6710 { 6711 throw new CoreException("Invalid external field ".$this->GetCode()." in class ".$this->GetHostClass().". The class ".$oKeyAttDef->GetTargetClass()." has no attribute ".$this->Get("target_attcode")); 6712 } 6713 6714 return $oExtAttDef; 6715 } 6716 6717 /** 6718 * @return mixed 6719 * @throws \CoreException 6720 */ 6721 public function GetSQLExpr() 6722 { 6723 $oExtAttDef = $this->GetExtAttDef(); 6724 6725 return $oExtAttDef->GetSQLExpr(); 6726 } 6727 6728 public function GetDefaultValue(DBObject $oHostObject = null) 6729 { 6730 $oExtAttDef = $this->GetExtAttDef(); 6731 6732 return $oExtAttDef->GetDefaultValue(); 6733 } 6734 6735 public function IsNullAllowed() 6736 { 6737 $oExtAttDef = $this->GetExtAttDef(); 6738 6739 return $oExtAttDef->IsNullAllowed(); 6740 } 6741 6742 static public function IsScalar() 6743 { 6744 return true; 6745 } 6746 6747 public function GetFilterDefinitions() 6748 { 6749 return array($this->GetCode() => new FilterFromAttribute($this)); 6750 } 6751 6752 public function GetBasicFilterOperators() 6753 { 6754 $oExtAttDef = $this->GetExtAttDef(); 6755 6756 return $oExtAttDef->GetBasicFilterOperators(); 6757 } 6758 6759 public function GetBasicFilterLooseOperator() 6760 { 6761 $oExtAttDef = $this->GetExtAttDef(); 6762 6763 return $oExtAttDef->GetBasicFilterLooseOperator(); 6764 } 6765 6766 public function GetBasicFilterSQLExpr($sOpCode, $value) 6767 { 6768 $oExtAttDef = $this->GetExtAttDef(); 6769 6770 return $oExtAttDef->GetBasicFilterSQLExpr($sOpCode, $value); 6771 } 6772 6773 public function GetNullValue() 6774 { 6775 $oExtAttDef = $this->GetExtAttDef(); 6776 6777 return $oExtAttDef->GetNullValue(); 6778 } 6779 6780 public function IsNull($proposedValue) 6781 { 6782 $oExtAttDef = $this->GetExtAttDef(); 6783 6784 return $oExtAttDef->IsNull($proposedValue); 6785 } 6786 6787 public function MakeRealValue($proposedValue, $oHostObj) 6788 { 6789 $oExtAttDef = $this->GetExtAttDef(); 6790 6791 return $oExtAttDef->MakeRealValue($proposedValue, $oHostObj); 6792 } 6793 6794 public function ScalarToSQL($value) 6795 { 6796 // This one could be used in case of filtering only 6797 $oExtAttDef = $this->GetExtAttDef(); 6798 6799 return $oExtAttDef->ScalarToSQL($value); 6800 } 6801 6802 6803 // Do not overload GetSQLExpression here because this is handled in the joins 6804 //public function GetSQLExpressions($sPrefix = '') {return array();} 6805 6806 // Here, we get the data... 6807 public function FromSQLToValue($aCols, $sPrefix = '') 6808 { 6809 $oExtAttDef = $this->GetExtAttDef(); 6810 6811 return $oExtAttDef->FromSQLToValue($aCols, $sPrefix); 6812 } 6813 6814 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 6815 { 6816 $oExtAttDef = $this->GetExtAttDef(); 6817 6818 return $oExtAttDef->GetAsHTML($value, null, $bLocalize); 6819 } 6820 6821 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 6822 { 6823 $oExtAttDef = $this->GetExtAttDef(); 6824 6825 return $oExtAttDef->GetAsXML($value, null, $bLocalize); 6826 } 6827 6828 public function GetAsCSV( 6829 $value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true, 6830 $bConvertToPlainText = false 6831 ) { 6832 $oExtAttDef = $this->GetExtAttDef(); 6833 6834 return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize, $bConvertToPlainText); 6835 } 6836 6837 static public function GetFormFieldClass() 6838 { 6839 return '\\Combodo\\iTop\\Form\\Field\\LabelField'; 6840 } 6841 6842 /** 6843 * @param \DBObject $oObject 6844 * @param \Combodo\iTop\Form\Field\Field $oFormField 6845 * 6846 * @return null 6847 * @throws \CoreException 6848 */ 6849 public function MakeFormField(DBObject $oObject, $oFormField = null) 6850 { 6851 // Retrieving AttDef from the remote attribute 6852 $oRemoteAttDef = $this->GetExtAttDef(); 6853 6854 if ($oFormField === null) 6855 { 6856 // ExternalField's FormField are actually based on the FormField from the target attribute. 6857 // Except for the AttributeExternalKey because we have no OQL and stuff 6858 if ($oRemoteAttDef instanceof AttributeExternalKey) 6859 { 6860 $sFormFieldClass = static::GetFormFieldClass(); 6861 } 6862 else 6863 { 6864 $sFormFieldClass = $oRemoteAttDef::GetFormFieldClass(); 6865 } 6866 $oFormField = new $sFormFieldClass($this->GetCode()); 6867 } 6868 parent::MakeFormField($oObject, $oFormField); 6869 6870 // Manually setting for remote ExternalKey, otherwise, the id would be displayed. 6871 if ($oRemoteAttDef instanceof AttributeExternalKey) 6872 { 6873 $oFormField->SetCurrentValue($oObject->Get($this->GetCode().'_friendlyname')); 6874 } 6875 6876 // Readonly field because we can't update external fields 6877 $oFormField->SetReadOnly(true); 6878 6879 return $oFormField; 6880 } 6881 6882 public function IsPartOfFingerprint() 6883 { 6884 return false; 6885 } 6886} 6887 6888 6889/** 6890 * Map a varchar column to an URL (formats the ouput in HMTL) 6891 * 6892 * @package iTopORM 6893 */ 6894class AttributeURL extends AttributeString 6895{ 6896 static public function ListExpectedParams() 6897 { 6898 //return parent::ListExpectedParams(); 6899 return array_merge(parent::ListExpectedParams(), array("target")); 6900 } 6901 6902 protected function GetSQLCol($bFullSpec = false) 6903 { 6904 return "VARCHAR(2048)" 6905 .CMDBSource::GetSqlStringColumnDefinition() 6906 .($bFullSpec ? $this->GetSQLColSpec() : ''); 6907 } 6908 6909 public function GetMaxSize() 6910 { 6911 return 2048; 6912 } 6913 6914 public function GetEditClass() 6915 { 6916 return "String"; 6917 } 6918 6919 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 6920 { 6921 $sTarget = $this->Get("target"); 6922 if (empty($sTarget)) 6923 { 6924 $sTarget = "_blank"; 6925 } 6926 $sLabel = Str::pure2html($sValue); 6927 if (strlen($sLabel) > 128) 6928 { 6929 // Truncate the length to 128 characters, by removing the middle 6930 $sLabel = substr($sLabel, 0, 100).'.....'.substr($sLabel, -20); 6931 } 6932 6933 return "<a target=\"$sTarget\" href=\"$sValue\">$sLabel</a>"; 6934 } 6935 6936 public function GetValidationPattern() 6937 { 6938 return $this->GetOptional('validation_pattern', '^'.utils::GetConfig()->Get('url_validation_pattern').'$'); 6939 } 6940 6941 static public function GetFormFieldClass() 6942 { 6943 return '\\Combodo\\iTop\\Form\\Field\\UrlField'; 6944 } 6945 6946 /** 6947 * @param \DBObject $oObject 6948 * @param \Combodo\iTop\Form\Field\UrlField $oFormField 6949 * 6950 * @return null 6951 * @throws \CoreException 6952 */ 6953 public function MakeFormField(DBObject $oObject, $oFormField = null) 6954 { 6955 if ($oFormField === null) 6956 { 6957 $sFormFieldClass = static::GetFormFieldClass(); 6958 $oFormField = new $sFormFieldClass($this->GetCode()); 6959 } 6960 parent::MakeFormField($oObject, $oFormField); 6961 6962 $oFormField->SetTarget($this->Get('target')); 6963 6964 return $oFormField; 6965 } 6966} 6967 6968/** 6969 * A blob is an ormDocument, it is stored as several columns in the database 6970 * 6971 * @package iTopORM 6972 */ 6973class AttributeBlob extends AttributeDefinition 6974{ 6975 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 6976 6977 static public function ListExpectedParams() 6978 { 6979 return array_merge(parent::ListExpectedParams(), array("depends_on")); 6980 } 6981 6982 public function GetEditClass() 6983 { 6984 return "Document"; 6985 } 6986 6987 static public function IsBasedOnDBColumns() 6988 { 6989 return true; 6990 } 6991 6992 static public function IsScalar() 6993 { 6994 return true; 6995 } 6996 6997 public function IsWritable() 6998 { 6999 return true; 7000 } 7001 7002 public function GetDefaultValue(DBObject $oHostObject = null) 7003 { 7004 return ""; 7005 } 7006 7007 public function IsNullAllowed(DBObject $oHostObject = null) 7008 { 7009 return $this->GetOptional("is_null_allowed", false); 7010 } 7011 7012 public function GetEditValue($sValue, $oHostObj = null) 7013 { 7014 return ''; 7015 } 7016 7017 /** 7018 * Users can provide the document from an URL (including an URL on iTop itself) 7019 * for CSV import. Administrators can even provide the path to a local file 7020 * {@inheritDoc} 7021 * 7022 * @see AttributeDefinition::MakeRealValue() 7023 */ 7024 public function MakeRealValue($proposedValue, $oHostObj) 7025 { 7026 if ($proposedValue === null) 7027 { 7028 return null; 7029 } 7030 7031 if (is_object($proposedValue)) 7032 { 7033 $proposedValue = clone $proposedValue; 7034 } 7035 else 7036 { 7037 try 7038 { 7039 // Read the file from iTop, an URL (or the local file system - for admins only) 7040 $proposedValue = Utils::FileGetContentsAndMIMEType($proposedValue); 7041 } catch (Exception $e) 7042 { 7043 IssueLog::Warning(get_class($this)."::MakeRealValue - ".$e->getMessage()); 7044 // Not a real document !! store is as text !!! (This was the default behavior before) 7045 $proposedValue = new ormDocument($e->getMessage()." \n".$proposedValue, 'text/plain'); 7046 } 7047 } 7048 7049 return $proposedValue; 7050 } 7051 7052 public function GetSQLExpressions($sPrefix = '') 7053 { 7054 if ($sPrefix == '') 7055 { 7056 $sPrefix = $this->GetCode(); 7057 } 7058 $aColumns = array(); 7059 // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix 7060 $aColumns[''] = $sPrefix.'_mimetype'; 7061 $aColumns['_data'] = $sPrefix.'_data'; 7062 $aColumns['_filename'] = $sPrefix.'_filename'; 7063 7064 return $aColumns; 7065 } 7066 7067 public function FromSQLToValue($aCols, $sPrefix = '') 7068 { 7069 if (!array_key_exists($sPrefix, $aCols)) 7070 { 7071 $sAvailable = implode(', ', array_keys($aCols)); 7072 throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); 7073 } 7074 $sMimeType = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; 7075 7076 if (!array_key_exists($sPrefix.'_data', $aCols)) 7077 { 7078 $sAvailable = implode(', ', array_keys($aCols)); 7079 throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}"); 7080 } 7081 $data = isset($aCols[$sPrefix.'_data']) ? $aCols[$sPrefix.'_data'] : null; 7082 7083 if (!array_key_exists($sPrefix.'_filename', $aCols)) 7084 { 7085 $sAvailable = implode(', ', array_keys($aCols)); 7086 throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}"); 7087 } 7088 $sFileName = isset($aCols[$sPrefix.'_filename']) ? $aCols[$sPrefix.'_filename'] : ''; 7089 7090 $value = new ormDocument($data, $sMimeType, $sFileName); 7091 7092 return $value; 7093 } 7094 7095 public function GetSQLValues($value) 7096 { 7097 // #@# Optimization: do not load blobs anytime 7098 // As per mySQL doc, selecting blob columns will prevent mySQL from 7099 // using memory in case a temporary table has to be created 7100 // (temporary tables created on disk) 7101 // We will have to remove the blobs from the list of attributes when doing the select 7102 // then the use of Get() should finalize the load 7103 if ($value instanceOf ormDocument && !$value->IsEmpty()) 7104 { 7105 $aValues = array(); 7106 $aValues[$this->GetCode().'_data'] = $value->GetData(); 7107 $aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType(); 7108 $aValues[$this->GetCode().'_filename'] = $value->GetFileName(); 7109 } 7110 else 7111 { 7112 $aValues = array(); 7113 $aValues[$this->GetCode().'_data'] = ''; 7114 $aValues[$this->GetCode().'_mimetype'] = ''; 7115 $aValues[$this->GetCode().'_filename'] = ''; 7116 } 7117 7118 return $aValues; 7119 } 7120 7121 public function GetSQLColumns($bFullSpec = false) 7122 { 7123 $aColumns = array(); 7124 $aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb) 7125 $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); 7126 $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); 7127 7128 return $aColumns; 7129 } 7130 7131 public function GetFilterDefinitions() 7132 { 7133 return array(); 7134 } 7135 7136 public function GetBasicFilterOperators() 7137 { 7138 return array(); 7139 } 7140 7141 public function GetBasicFilterLooseOperator() 7142 { 7143 return '='; 7144 } 7145 7146 public function GetBasicFilterSQLExpr($sOpCode, $value) 7147 { 7148 return 'true'; 7149 } 7150 7151 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 7152 { 7153 if (is_object($value)) 7154 { 7155 return $value->GetAsHTML(); 7156 } 7157 7158 return ''; 7159 } 7160 7161 /** 7162 * @param string $sValue 7163 * @param string $sSeparator 7164 * @param string $sTextQualifier 7165 * @param \DBObject $oHostObject 7166 * @param bool $bLocalize 7167 * @param bool $bConvertToPlainText 7168 * 7169 * @return string 7170 */ 7171 public function GetAsCSV( 7172 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 7173 $bConvertToPlainText = false 7174 ) { 7175 $sAttCode = $this->GetCode(); 7176 if ($sValue instanceof ormDocument && !$sValue->IsEmpty()) 7177 { 7178 return $sValue->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $sAttCode); 7179 } 7180 7181 return ''; // Not exportable in CSV ! 7182 } 7183 7184 /** 7185 * @param $value 7186 * @param \DBObject $oHostObject 7187 * @param bool $bLocalize 7188 * 7189 * @return mixed|string 7190 */ 7191 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 7192 { 7193 $sRet = ''; 7194 if (is_object($value)) 7195 { 7196 if (!$value->IsEmpty()) 7197 { 7198 $sRet = '<mimetype>'.$value->GetMimeType().'</mimetype>'; 7199 $sRet .= '<filename>'.$value->GetFileName().'</filename>'; 7200 $sRet .= '<data>'.base64_encode($value->GetData()).'</data>'; 7201 } 7202 } 7203 7204 return $sRet; 7205 } 7206 7207 /** 7208 * Helper to get a value that will be JSON encoded 7209 * The operation is the opposite to FromJSONToValue 7210 */ 7211 public function GetForJSON($value) 7212 { 7213 if ($value instanceOf ormDocument) 7214 { 7215 $aValues = array(); 7216 $aValues['data'] = base64_encode($value->GetData()); 7217 $aValues['mimetype'] = $value->GetMimeType(); 7218 $aValues['filename'] = $value->GetFileName(); 7219 } 7220 else 7221 { 7222 $aValues = null; 7223 } 7224 7225 return $aValues; 7226 } 7227 7228 /** 7229 * Helper to form a value, given JSON decoded data 7230 * The operation is the opposite to GetForJSON 7231 */ 7232 public function FromJSONToValue($json) 7233 { 7234 if (isset($json->data)) 7235 { 7236 $data = base64_decode($json->data); 7237 $value = new ormDocument($data, $json->mimetype, $json->filename); 7238 } 7239 else 7240 { 7241 $value = null; 7242 } 7243 7244 return $value; 7245 } 7246 7247 public function Fingerprint($value) 7248 { 7249 $sFingerprint = ''; 7250 if ($value instanceOf ormDocument) 7251 { 7252 $sFingerprint = md5($value->GetData()); 7253 } 7254 7255 return $sFingerprint; 7256 } 7257 7258 static public function GetFormFieldClass() 7259 { 7260 return '\\Combodo\\iTop\\Form\\Field\\BlobField'; 7261 } 7262 7263 public function MakeFormField(DBObject $oObject, $oFormField = null) 7264 { 7265 if ($oFormField === null) 7266 { 7267 $sFormFieldClass = static::GetFormFieldClass(); 7268 $oFormField = new $sFormFieldClass($this->GetCode()); 7269 } 7270 7271 // Note: As of today we want this field to always be read-only 7272 $oFormField->SetReadOnly(true); 7273 7274 // Generating urls 7275 $value = $oObject->Get($this->GetCode()); 7276 $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); 7277 $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); 7278 7279 parent::MakeFormField($oObject, $oFormField); 7280 7281 return $oFormField; 7282 } 7283 7284} 7285 7286/** 7287 * An image is a specific type of document, it is stored as several columns in the database 7288 * 7289 * @package iTopORM 7290 */ 7291class AttributeImage extends AttributeBlob 7292{ 7293 public function GetEditClass() 7294 { 7295 return "Image"; 7296 } 7297 7298 /** 7299 * {@inheritDoc} 7300 * @see AttributeBlob::MakeRealValue() 7301 */ 7302 public function MakeRealValue($proposedValue, $oHostObj) 7303 { 7304 $oDoc = parent::MakeRealValue($proposedValue, $oHostObj); 7305 7306 // The validation of the MIME Type is done by CheckFormat below 7307 return $oDoc; 7308 } 7309 7310 /** 7311 * Check that the supplied ormDocument actually contains an image 7312 * {@inheritDoc} 7313 * 7314 * @see AttributeDefinition::CheckFormat() 7315 */ 7316 public function CheckFormat($value) 7317 { 7318 if ($value instanceof ormDocument && !$value->IsEmpty()) 7319 { 7320 return ($value->GetMainMimeType() == 'image'); 7321 } 7322 7323 return true; 7324 } 7325 7326 /** 7327 * @param \ormDocument $value 7328 * @param \DBObject $oHostObject 7329 * @param bool $bLocalize 7330 * 7331 * @return string 7332 * 7333 * @see edit_image.js for JS generated markup in form edition 7334 */ 7335 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 7336 { 7337 $sRet = ''; 7338 $bIsCustomImage = false; 7339 7340 $iMaxWidthPx = $this->Get('display_max_width').'px'; 7341 $iMaxHeightPx = $this->Get('display_max_height').'px'; 7342 7343 $sDefaultImageUrl = $this->Get('default_image'); 7344 if ($sDefaultImageUrl !== null) { 7345 $sRet = $this->GetHtmlForImageUrl($sDefaultImageUrl, $iMaxWidthPx, $iMaxHeightPx); 7346 } 7347 7348 $sCustomImageUrl = $this->GetAttributeImageFileUrl($value, $oHostObject); 7349 if ($sCustomImageUrl !== null) { 7350 $bIsCustomImage = true; 7351 $sRet = $this->GetHtmlForImageUrl($sCustomImageUrl, $iMaxWidthPx, $iMaxHeightPx); 7352 } 7353 7354 $sCssClasses = 'view-image attribute-image'; 7355 $sCssClasses .= ' '.(($bIsCustomImage) ? 'attribute-image-custom' : 'attribute-image-default'); 7356 7357 return '<div class="'.$sCssClasses.'" style="width: '.$iMaxWidthPx.'; height: '.$iMaxHeightPx.';"><span class="helper-middle"></span>'.$sRet.'</div>'; 7358 } 7359 7360 private function GetHtmlForImageUrl($sUrl, $iMaxWidthPx, $iMaxHeightPx) { 7361 return '<img src="'.$sUrl.'" style="max-width: '.$iMaxWidthPx.'; max-height: '.$iMaxHeightPx.'">'; 7362 } 7363 7364 /** 7365 * @param \ormDocument $value 7366 * @param \DBObject $oHostObject 7367 * 7368 * @return null|string 7369 */ 7370 private function GetAttributeImageFileUrl($value, $oHostObject) { 7371 if (!is_object($value)) { 7372 return null; 7373 } 7374 if ($value->IsEmpty()) { 7375 return null; 7376 } 7377 7378 $bExistingImageModified = ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges()))); 7379 if ($oHostObject->IsNew() || ($bExistingImageModified)) 7380 { 7381 // If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline 7382 // otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it 7383 return 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData()); 7384 } 7385 7386 return $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode()); 7387 } 7388 7389 static public function GetFormFieldClass() 7390 { 7391 return '\\Combodo\\iTop\\Form\\Field\\ImageField'; 7392 } 7393 7394 public function MakeFormField(DBObject $oObject, $oFormField = null) 7395 { 7396 if ($oFormField === null) 7397 { 7398 $sFormFieldClass = static::GetFormFieldClass(); 7399 $oFormField = new $sFormFieldClass($this->GetCode()); 7400 } 7401 7402 parent::MakeFormField($oObject, $oFormField); 7403 7404 // Generating urls 7405 $value = $oObject->Get($this->GetCode()); 7406 if (is_object($value) && !$value->IsEmpty()) 7407 { 7408 $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); 7409 $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); 7410 } 7411 else 7412 { 7413 $oFormField->SetDownloadUrl($this->Get('default_image')); 7414 $oFormField->SetDisplayUrl($this->Get('default_image')); 7415 } 7416 7417 return $oFormField; 7418 } 7419} 7420 7421/** 7422 * A stop watch is an ormStopWatch object, it is stored as several columns in the database 7423 * 7424 * @package iTopORM 7425 */ 7426class AttributeStopWatch extends AttributeDefinition 7427{ 7428 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 7429 7430 static public function ListExpectedParams() 7431 { 7432 // The list of thresholds must be an array of iPercent => array of 'option' => value 7433 return array_merge(parent::ListExpectedParams(), 7434 array("states", "goal_computing", "working_time_computing", "thresholds")); 7435 } 7436 7437 public function GetEditClass() 7438 { 7439 return "StopWatch"; 7440 } 7441 7442 static public function IsBasedOnDBColumns() 7443 { 7444 return true; 7445 } 7446 7447 static public function IsScalar() 7448 { 7449 return true; 7450 } 7451 7452 public function IsWritable() 7453 { 7454 return true; 7455 } 7456 7457 public function GetDefaultValue(DBObject $oHostObject = null) 7458 { 7459 return $this->NewStopWatch(); 7460 } 7461 7462 /** 7463 * @param \ormStopWatch $value 7464 * @param \DBObject $oHostObj 7465 * 7466 * @return string 7467 */ 7468 public function GetEditValue($value, $oHostObj = null) 7469 { 7470 return $value->GetTimeSpent(); 7471 } 7472 7473 public function GetStates() 7474 { 7475 return $this->Get('states'); 7476 } 7477 7478 public function AlwaysLoadInTables() 7479 { 7480 // Each and every stop watch is accessed for computing the highlight code (DBObject::GetHighlightCode()) 7481 return true; 7482 } 7483 7484 /** 7485 * Construct a brand new (but configured) stop watch 7486 */ 7487 public function NewStopWatch() 7488 { 7489 $oSW = new ormStopWatch(); 7490 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7491 { 7492 $oSW->DefineThreshold($iThreshold); 7493 } 7494 7495 return $oSW; 7496 } 7497 7498 // Facilitate things: allow the user to Set the value from a string 7499 public function MakeRealValue($proposedValue, $oHostObj) 7500 { 7501 if (!$proposedValue instanceof ormStopWatch) 7502 { 7503 return $this->NewStopWatch(); 7504 } 7505 7506 return $proposedValue; 7507 } 7508 7509 public function GetSQLExpressions($sPrefix = '') 7510 { 7511 if ($sPrefix == '') 7512 { 7513 $sPrefix = $this->GetCode(); // Warning: a stopwatch does not have any 'sql' property, so its SQL column is equal to its attribute code !! 7514 } 7515 $aColumns = array(); 7516 // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix 7517 $aColumns[''] = $sPrefix.'_timespent'; 7518 $aColumns['_started'] = $sPrefix.'_started'; 7519 $aColumns['_laststart'] = $sPrefix.'_laststart'; 7520 $aColumns['_stopped'] = $sPrefix.'_stopped'; 7521 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7522 { 7523 $sThPrefix = '_'.$iThreshold; 7524 $aColumns[$sThPrefix.'_deadline'] = $sPrefix.$sThPrefix.'_deadline'; 7525 $aColumns[$sThPrefix.'_passed'] = $sPrefix.$sThPrefix.'_passed'; 7526 $aColumns[$sThPrefix.'_triggered'] = $sPrefix.$sThPrefix.'_triggered'; 7527 $aColumns[$sThPrefix.'_overrun'] = $sPrefix.$sThPrefix.'_overrun'; 7528 } 7529 7530 return $aColumns; 7531 } 7532 7533 public static function DateToSeconds($sDate) 7534 { 7535 if (is_null($sDate)) 7536 { 7537 return null; 7538 } 7539 $oDateTime = new DateTime($sDate); 7540 $iSeconds = $oDateTime->format('U'); 7541 7542 return $iSeconds; 7543 } 7544 7545 public static function SecondsToDate($iSeconds) 7546 { 7547 if (is_null($iSeconds)) 7548 { 7549 return null; 7550 } 7551 7552 return date("Y-m-d H:i:s", $iSeconds); 7553 } 7554 7555 public function FromSQLToValue($aCols, $sPrefix = '') 7556 { 7557 $aExpectedCols = array($sPrefix, $sPrefix.'_started', $sPrefix.'_laststart', $sPrefix.'_stopped'); 7558 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7559 { 7560 $sThPrefix = '_'.$iThreshold; 7561 $aExpectedCols[] = $sPrefix.$sThPrefix.'_deadline'; 7562 $aExpectedCols[] = $sPrefix.$sThPrefix.'_passed'; 7563 $aExpectedCols[] = $sPrefix.$sThPrefix.'_triggered'; 7564 $aExpectedCols[] = $sPrefix.$sThPrefix.'_overrun'; 7565 } 7566 foreach($aExpectedCols as $sExpectedCol) 7567 { 7568 if (!array_key_exists($sExpectedCol, $aCols)) 7569 { 7570 $sAvailable = implode(', ', array_keys($aCols)); 7571 throw new MissingColumnException("Missing column '$sExpectedCol' from {$sAvailable}"); 7572 } 7573 } 7574 7575 $value = new ormStopWatch( 7576 $aCols[$sPrefix], 7577 self::DateToSeconds($aCols[$sPrefix.'_started']), 7578 self::DateToSeconds($aCols[$sPrefix.'_laststart']), 7579 self::DateToSeconds($aCols[$sPrefix.'_stopped']) 7580 ); 7581 7582 foreach($this->ListThresholds() as $iThreshold => $aDefinition) 7583 { 7584 $sThPrefix = '_'.$iThreshold; 7585 $value->DefineThreshold( 7586 $iThreshold, 7587 self::DateToSeconds($aCols[$sPrefix.$sThPrefix.'_deadline']), 7588 (bool)($aCols[$sPrefix.$sThPrefix.'_passed'] == 1), 7589 (bool)($aCols[$sPrefix.$sThPrefix.'_triggered'] == 1), 7590 $aCols[$sPrefix.$sThPrefix.'_overrun'], 7591 array_key_exists('highlight', $aDefinition) ? $aDefinition['highlight'] : null 7592 ); 7593 } 7594 7595 return $value; 7596 } 7597 7598 public function GetSQLValues($value) 7599 { 7600 if ($value instanceOf ormStopWatch) 7601 { 7602 $aValues = array(); 7603 $aValues[$this->GetCode().'_timespent'] = $value->GetTimeSpent(); 7604 $aValues[$this->GetCode().'_started'] = self::SecondsToDate($value->GetStartDate()); 7605 $aValues[$this->GetCode().'_laststart'] = self::SecondsToDate($value->GetLastStartDate()); 7606 $aValues[$this->GetCode().'_stopped'] = self::SecondsToDate($value->GetStopDate()); 7607 7608 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7609 { 7610 $sPrefix = $this->GetCode().'_'.$iThreshold; 7611 $aValues[$sPrefix.'_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold)); 7612 $aValues[$sPrefix.'_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0'; 7613 $aValues[$sPrefix.'_triggered'] = $value->IsThresholdTriggered($iThreshold) ? '1' : '0'; 7614 $aValues[$sPrefix.'_overrun'] = $value->GetOverrun($iThreshold); 7615 } 7616 } 7617 else 7618 { 7619 $aValues = array(); 7620 $aValues[$this->GetCode().'_timespent'] = ''; 7621 $aValues[$this->GetCode().'_started'] = ''; 7622 $aValues[$this->GetCode().'_laststart'] = ''; 7623 $aValues[$this->GetCode().'_stopped'] = ''; 7624 } 7625 7626 return $aValues; 7627 } 7628 7629 public function GetSQLColumns($bFullSpec = false) 7630 { 7631 $aColumns = array(); 7632 $aColumns[$this->GetCode().'_timespent'] = 'INT(11) UNSIGNED'; 7633 $aColumns[$this->GetCode().'_started'] = 'DATETIME'; 7634 $aColumns[$this->GetCode().'_laststart'] = 'DATETIME'; 7635 $aColumns[$this->GetCode().'_stopped'] = 'DATETIME'; 7636 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7637 { 7638 $sPrefix = $this->GetCode().'_'.$iThreshold; 7639 $aColumns[$sPrefix.'_deadline'] = 'DATETIME'; 7640 $aColumns[$sPrefix.'_passed'] = 'TINYINT(1) UNSIGNED'; 7641 $aColumns[$sPrefix.'_triggered'] = 'TINYINT(1)'; 7642 $aColumns[$sPrefix.'_overrun'] = 'INT(11) UNSIGNED'; 7643 } 7644 7645 return $aColumns; 7646 } 7647 7648 public function GetFilterDefinitions() 7649 { 7650 $aRes = array( 7651 $this->GetCode() => new FilterFromAttribute($this), 7652 $this->GetCode().'_started' => new FilterFromAttribute($this, '_started'), 7653 $this->GetCode().'_laststart' => new FilterFromAttribute($this, '_laststart'), 7654 $this->GetCode().'_stopped' => new FilterFromAttribute($this, '_stopped') 7655 ); 7656 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7657 { 7658 $sPrefix = $this->GetCode().'_'.$iThreshold; 7659 $aRes[$sPrefix.'_deadline'] = new FilterFromAttribute($this, '_deadline'); 7660 $aRes[$sPrefix.'_passed'] = new FilterFromAttribute($this, '_passed'); 7661 $aRes[$sPrefix.'_triggered'] = new FilterFromAttribute($this, '_triggered'); 7662 $aRes[$sPrefix.'_overrun'] = new FilterFromAttribute($this, '_overrun'); 7663 } 7664 7665 return $aRes; 7666 } 7667 7668 public function GetBasicFilterOperators() 7669 { 7670 return array(); 7671 } 7672 7673 public function GetBasicFilterLooseOperator() 7674 { 7675 return '='; 7676 } 7677 7678 public function GetBasicFilterSQLExpr($sOpCode, $value) 7679 { 7680 return 'true'; 7681 } 7682 7683 /** 7684 * @param \ormStopWatch $value 7685 * @param \DBObject $oHostObject 7686 * @param bool $bLocalize 7687 * 7688 * @return string 7689 */ 7690 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 7691 { 7692 if (is_object($value)) 7693 { 7694 return $value->GetAsHTML($this, $oHostObject); 7695 } 7696 7697 return ''; 7698 } 7699 7700 /** 7701 * @param ormStopWatch $value 7702 * @param string $sSeparator 7703 * @param string $sTextQualifier 7704 * @param null $oHostObject 7705 * @param bool $bLocalize 7706 * @param bool $bConvertToPlainText 7707 * 7708 * @return string 7709 */ 7710 public function GetAsCSV( 7711 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 7712 $bConvertToPlainText = false 7713 ) { 7714 return $value->GetTimeSpent(); 7715 } 7716 7717 /** 7718 * @param \ormStopWatch $value 7719 * @param \DBObject $oHostObject 7720 * @param bool $bLocalize 7721 * 7722 * @return mixed 7723 */ 7724 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 7725 { 7726 return $value->GetTimeSpent(); 7727 } 7728 7729 public function ListThresholds() 7730 { 7731 return $this->Get('thresholds'); 7732 } 7733 7734 public function Fingerprint($value) 7735 { 7736 $sFingerprint = ''; 7737 if (is_object($value)) 7738 { 7739 $sFingerprint = $value->GetAsHTML($this); 7740 } 7741 7742 return $sFingerprint; 7743 } 7744 7745 /** 7746 * To expose internal values: Declare an attribute AttributeSubItem 7747 * and implement the GetSubItemXXXX verbs 7748 * 7749 * @param string $sItemCode 7750 * 7751 * @return array 7752 * @throws \CoreException 7753 */ 7754 public function GetSubItemSQLExpression($sItemCode) 7755 { 7756 $sPrefix = $this->GetCode(); 7757 switch ($sItemCode) 7758 { 7759 case 'timespent': 7760 return array('' => $sPrefix.'_timespent'); 7761 case 'started': 7762 return array('' => $sPrefix.'_started'); 7763 case 'laststart': 7764 return array('' => $sPrefix.'_laststart'); 7765 case 'stopped': 7766 return array('' => $sPrefix.'_stopped'); 7767 } 7768 7769 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7770 { 7771 $sThPrefix = $iThreshold.'_'; 7772 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 7773 { 7774 // The current threshold is concerned 7775 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 7776 switch ($sThresholdCode) 7777 { 7778 case 'deadline': 7779 return array('' => $sPrefix.'_'.$iThreshold.'_deadline'); 7780 case 'passed': 7781 return array('' => $sPrefix.'_'.$iThreshold.'_passed'); 7782 case 'triggered': 7783 return array('' => $sPrefix.'_'.$iThreshold.'_triggered'); 7784 case 'overrun': 7785 return array('' => $sPrefix.'_'.$iThreshold.'_overrun'); 7786 } 7787 } 7788 } 7789 throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode()); 7790 } 7791 7792 /** 7793 * @param string $sItemCode 7794 * @param \ormStopWatch $value 7795 * @param \DBObject $oHostObject 7796 * 7797 * @return mixed 7798 * @throws \CoreException 7799 */ 7800 public function GetSubItemValue($sItemCode, $value, $oHostObject = null) 7801 { 7802 $oStopWatch = $value; 7803 switch ($sItemCode) 7804 { 7805 case 'timespent': 7806 return $oStopWatch->GetTimeSpent(); 7807 case 'started': 7808 return $oStopWatch->GetStartDate(); 7809 case 'laststart': 7810 return $oStopWatch->GetLastStartDate(); 7811 case 'stopped': 7812 return $oStopWatch->GetStopDate(); 7813 } 7814 7815 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7816 { 7817 $sThPrefix = $iThreshold.'_'; 7818 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 7819 { 7820 // The current threshold is concerned 7821 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 7822 switch ($sThresholdCode) 7823 { 7824 case 'deadline': 7825 return $oStopWatch->GetThresholdDate($iThreshold); 7826 case 'passed': 7827 return $oStopWatch->IsThresholdPassed($iThreshold); 7828 case 'triggered': 7829 return $oStopWatch->IsThresholdTriggered($iThreshold); 7830 case 'overrun': 7831 return $oStopWatch->GetOverrun($iThreshold); 7832 } 7833 } 7834 } 7835 7836 throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode()); 7837 } 7838 7839 protected function GetBooleanLabel($bValue) 7840 { 7841 $sDictKey = $bValue ? 'yes' : 'no'; 7842 7843 return Dict::S('BooleanLabel:'.$sDictKey, 'def:'.$sDictKey); 7844 } 7845 7846 public function GetSubItemAsHTMLForHistory($sItemCode, $sValue) 7847 { 7848 $sHtml = null; 7849 switch ($sItemCode) 7850 { 7851 case 'timespent': 7852 $sHtml = (int)$sValue ? Str::pure2html(AttributeDuration::FormatDuration($sValue)) : null; 7853 break; 7854 case 'started': 7855 case 'laststart': 7856 case 'stopped': 7857 $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), (int)$sValue) : null; 7858 break; 7859 7860 default: 7861 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7862 { 7863 $sThPrefix = $iThreshold.'_'; 7864 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 7865 { 7866 // The current threshold is concerned 7867 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 7868 switch ($sThresholdCode) 7869 { 7870 case 'deadline': 7871 $sHtml = (int)$sValue ? date((string)AttributeDateTime::GetFormat(), 7872 (int)$sValue) : null; 7873 break; 7874 case 'passed': 7875 $sHtml = $this->GetBooleanLabel((int)$sValue); 7876 break; 7877 case 'triggered': 7878 $sHtml = $this->GetBooleanLabel((int)$sValue); 7879 break; 7880 case 'overrun': 7881 $sHtml = (int)$sValue > 0 ? Str::pure2html(AttributeDuration::FormatDuration((int)$sValue)) : ''; 7882 } 7883 } 7884 } 7885 } 7886 7887 return $sHtml; 7888 } 7889 7890 public function GetSubItemAsPlainText($sItemCode, $value) 7891 { 7892 $sRet = $value; 7893 7894 switch ($sItemCode) 7895 { 7896 case 'timespent': 7897 $sRet = AttributeDuration::FormatDuration($value); 7898 break; 7899 case 'started': 7900 case 'laststart': 7901 case 'stopped': 7902 if (is_null($value)) 7903 { 7904 $sRet = ''; // Undefined 7905 } 7906 else 7907 { 7908 $oDateTime = new DateTime(); 7909 $oDateTime->setTimestamp($value); 7910 $oDateTimeFormat = AttributeDateTime::GetFormat(); 7911 $sRet = $oDateTimeFormat->Format($oDateTime); 7912 } 7913 break; 7914 7915 default: 7916 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7917 { 7918 $sThPrefix = $iThreshold.'_'; 7919 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 7920 { 7921 // The current threshold is concerned 7922 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 7923 switch ($sThresholdCode) 7924 { 7925 case 'deadline': 7926 if ($value) 7927 { 7928 $sDate = date(AttributeDateTime::GetInternalFormat(), $value); 7929 $sRet = AttributeDeadline::FormatDeadline($sDate); 7930 } 7931 else 7932 { 7933 $sRet = ''; 7934 } 7935 break; 7936 case 'passed': 7937 case 'triggered': 7938 $sRet = $this->GetBooleanLabel($value); 7939 break; 7940 case 'overrun': 7941 $sRet = AttributeDuration::FormatDuration($value); 7942 break; 7943 } 7944 } 7945 } 7946 } 7947 7948 return $sRet; 7949 } 7950 7951 public function GetSubItemAsHTML($sItemCode, $value) 7952 { 7953 $sHtml = $value; 7954 7955 switch ($sItemCode) 7956 { 7957 case 'timespent': 7958 $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value)); 7959 break; 7960 case 'started': 7961 case 'laststart': 7962 case 'stopped': 7963 if (is_null($value)) 7964 { 7965 $sHtml = ''; // Undefined 7966 } 7967 else 7968 { 7969 $oDateTime = new DateTime(); 7970 $oDateTime->setTimestamp($value); 7971 $oDateTimeFormat = AttributeDateTime::GetFormat(); 7972 $sHtml = Str::pure2html($oDateTimeFormat->Format($oDateTime)); 7973 } 7974 break; 7975 7976 default: 7977 foreach($this->ListThresholds() as $iThreshold => $aFoo) 7978 { 7979 $sThPrefix = $iThreshold.'_'; 7980 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 7981 { 7982 // The current threshold is concerned 7983 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 7984 switch ($sThresholdCode) 7985 { 7986 case 'deadline': 7987 if ($value) 7988 { 7989 $sDate = date(AttributeDateTime::GetInternalFormat(), $value); 7990 $sHtml = Str::pure2html(AttributeDeadline::FormatDeadline($sDate)); 7991 } 7992 else 7993 { 7994 $sHtml = ''; 7995 } 7996 break; 7997 case 'passed': 7998 case 'triggered': 7999 $sHtml = $this->GetBooleanLabel($value); 8000 break; 8001 case 'overrun': 8002 $sHtml = Str::pure2html(AttributeDuration::FormatDuration($value)); 8003 break; 8004 } 8005 } 8006 } 8007 } 8008 8009 return $sHtml; 8010 } 8011 8012 public function GetSubItemAsCSV( 8013 $sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"', $bConvertToPlainText = false 8014 ) { 8015 $sFrom = array("\r\n", $sTextQualifier); 8016 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 8017 $sEscaped = str_replace($sFrom, $sTo, (string)$value); 8018 $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; 8019 8020 switch ($sItemCode) 8021 { 8022 case 'timespent': 8023 $sRet = $sTextQualifier.AttributeDuration::FormatDuration($value).$sTextQualifier; 8024 break; 8025 case 'started': 8026 case 'laststart': 8027 case 'stopped': 8028 if ($value !== null) 8029 { 8030 $oDateTime = new DateTime(); 8031 $oDateTime->setTimestamp($value); 8032 $oDateTimeFormat = AttributeDateTime::GetFormat(); 8033 $sRet = $sTextQualifier.$oDateTimeFormat->Format($oDateTime).$sTextQualifier; 8034 } 8035 break; 8036 8037 default: 8038 foreach($this->ListThresholds() as $iThreshold => $aFoo) 8039 { 8040 $sThPrefix = $iThreshold.'_'; 8041 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 8042 { 8043 // The current threshold is concerned 8044 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 8045 switch ($sThresholdCode) 8046 { 8047 case 'deadline': 8048 if ($value != '') 8049 { 8050 $oDateTime = new DateTime(); 8051 $oDateTime->setTimestamp($value); 8052 $oDateTimeFormat = AttributeDateTime::GetFormat(); 8053 $sRet = $sTextQualifier.$oDateTimeFormat->Format($oDateTime).$sTextQualifier; 8054 } 8055 break; 8056 8057 case 'passed': 8058 case 'triggered': 8059 $sRet = $sTextQualifier.$this->GetBooleanLabel($value).$sTextQualifier; 8060 break; 8061 8062 case 'overrun': 8063 $sRet = $sTextQualifier.AttributeDuration::FormatDuration($value).$sTextQualifier; 8064 break; 8065 } 8066 } 8067 } 8068 } 8069 8070 return $sRet; 8071 } 8072 8073 public function GetSubItemAsXML($sItemCode, $value) 8074 { 8075 $sRet = Str::pure2xml((string)$value); 8076 8077 switch ($sItemCode) 8078 { 8079 case 'timespent': 8080 case 'started': 8081 case 'laststart': 8082 case 'stopped': 8083 break; 8084 8085 default: 8086 foreach($this->ListThresholds() as $iThreshold => $aFoo) 8087 { 8088 $sThPrefix = $iThreshold.'_'; 8089 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 8090 { 8091 // The current threshold is concerned 8092 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 8093 switch ($sThresholdCode) 8094 { 8095 case 'deadline': 8096 break; 8097 8098 case 'passed': 8099 case 'triggered': 8100 $sRet = $this->GetBooleanLabel($value); 8101 break; 8102 8103 case 'overrun': 8104 break; 8105 } 8106 } 8107 } 8108 } 8109 8110 return $sRet; 8111 } 8112 8113 /** 8114 * Implemented for the HTML spreadsheet format! 8115 * 8116 * @param string $sItemCode 8117 * @param \ormStopWatch $value 8118 * 8119 * @return false|string 8120 */ 8121 public function GetSubItemAsEditValue($sItemCode, $value) 8122 { 8123 $sRet = $value; 8124 8125 switch ($sItemCode) 8126 { 8127 case 'timespent': 8128 break; 8129 8130 case 'started': 8131 case 'laststart': 8132 case 'stopped': 8133 if (is_null($value)) 8134 { 8135 $sRet = ''; // Undefined 8136 } 8137 else 8138 { 8139 $sRet = date((string)AttributeDateTime::GetFormat(), $value); 8140 } 8141 break; 8142 8143 default: 8144 foreach($this->ListThresholds() as $iThreshold => $aFoo) 8145 { 8146 $sThPrefix = $iThreshold.'_'; 8147 if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix) 8148 { 8149 // The current threshold is concerned 8150 $sThresholdCode = substr($sItemCode, strlen($sThPrefix)); 8151 switch ($sThresholdCode) 8152 { 8153 case 'deadline': 8154 if ($value) 8155 { 8156 $sRet = date((string)AttributeDateTime::GetFormat(), $value); 8157 } 8158 else 8159 { 8160 $sRet = ''; 8161 } 8162 break; 8163 case 'passed': 8164 case 'triggered': 8165 $sRet = $this->GetBooleanLabel($value); 8166 break; 8167 case 'overrun': 8168 break; 8169 } 8170 } 8171 } 8172 } 8173 8174 return $sRet; 8175 } 8176} 8177 8178/** 8179 * View of a subvalue of another attribute 8180 * If an attribute implements the verbs GetSubItem.... then it can expose 8181 * internal values, each of them being an attribute and therefore they 8182 * can be displayed at different times in the object lifecycle, and used for 8183 * reporting (as a condition in OQL, or as an additional column in an export) 8184 * Known usages: Stop Watches can expose threshold statuses 8185 */ 8186class AttributeSubItem extends AttributeDefinition 8187{ 8188 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 8189 8190 static public function ListExpectedParams() 8191 { 8192 return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code')); 8193 } 8194 8195 public function GetParentAttCode() 8196 { 8197 return $this->Get("target_attcode"); 8198 } 8199 8200 /** 8201 * Helper : get the attribute definition to which the execution will be forwarded 8202 */ 8203 public function GetTargetAttDef() 8204 { 8205 $sClass = $this->GetHostClass(); 8206 $oParentAttDef = MetaModel::GetAttributeDef($sClass, $this->Get('target_attcode')); 8207 8208 return $oParentAttDef; 8209 } 8210 8211 public function GetEditClass() 8212 { 8213 return ""; 8214 } 8215 8216 public function GetValuesDef() 8217 { 8218 return null; 8219 } 8220 8221 static public function IsBasedOnDBColumns() 8222 { 8223 return true; 8224 } 8225 8226 static public function IsScalar() 8227 { 8228 return true; 8229 } 8230 8231 public function IsWritable() 8232 { 8233 return false; 8234 } 8235 8236 public function GetDefaultValue(DBObject $oHostObject = null) 8237 { 8238 return null; 8239 } 8240 8241// public function IsNullAllowed() {return false;} 8242 8243 static public function LoadInObject() 8244 { 8245 return false; 8246 } // if this verb returns false, then GetValues must be implemented 8247 8248 /** 8249 * Used by DBOBject::Get() 8250 * 8251 * @param \DBObject $oHostObject 8252 * 8253 * @return \AttributeSubItem 8254 * @throws \CoreException 8255 */ 8256 public function GetValue($oHostObject) 8257 { 8258 /** @var \AttributeStopWatch $oParent */ 8259 $oParent = $this->GetTargetAttDef(); 8260 $parentValue = $oHostObject->GetStrict($oParent->GetCode()); 8261 $res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject); 8262 8263 return $res; 8264 } 8265 8266 // 8267// protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside) 8268 8269 public function FromSQLToValue($aCols, $sPrefix = '') 8270 { 8271 } 8272 8273 public function GetSQLColumns($bFullSpec = false) 8274 { 8275 return array(); 8276 } 8277 8278 public function GetFilterDefinitions() 8279 { 8280 return array($this->GetCode() => new FilterFromAttribute($this)); 8281 } 8282 8283 public function GetBasicFilterOperators() 8284 { 8285 return array(); 8286 } 8287 8288 public function GetBasicFilterLooseOperator() 8289 { 8290 return "="; 8291 } 8292 8293 public function GetBasicFilterSQLExpr($sOpCode, $value) 8294 { 8295 $sQValue = CMDBSource::Quote($value); 8296 switch ($sOpCode) 8297 { 8298 case '!=': 8299 return $this->GetSQLExpr()." != $sQValue"; 8300 break; 8301 case '=': 8302 default: 8303 return $this->GetSQLExpr()." = $sQValue"; 8304 } 8305 } 8306 8307 public function GetSQLExpressions($sPrefix = '') 8308 { 8309 $oParent = $this->GetTargetAttDef(); 8310 $res = $oParent->GetSubItemSQLExpression($this->Get('item_code')); 8311 8312 return $res; 8313 } 8314 8315 public function GetAsPlainText($value, $oHostObject = null, $bLocalize = true) 8316 { 8317 $oParent = $this->GetTargetAttDef(); 8318 $res = $oParent->GetSubItemAsPlainText($this->Get('item_code'), $value); 8319 8320 return $res; 8321 } 8322 8323 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 8324 { 8325 $oParent = $this->GetTargetAttDef(); 8326 $res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value); 8327 8328 return $res; 8329 } 8330 8331 public function GetAsHTMLForHistory($value, $oHostObject = null, $bLocalize = true) 8332 { 8333 $oParent = $this->GetTargetAttDef(); 8334 $res = $oParent->GetSubItemAsHTMLForHistory($this->Get('item_code'), $value); 8335 8336 return $res; 8337 } 8338 8339 public function GetAsCSV( 8340 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 8341 $bConvertToPlainText = false 8342 ) { 8343 $oParent = $this->GetTargetAttDef(); 8344 $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier, 8345 $bConvertToPlainText); 8346 8347 return $res; 8348 } 8349 8350 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 8351 { 8352 $oParent = $this->GetTargetAttDef(); 8353 $res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value); 8354 8355 return $res; 8356 } 8357 8358 /** 8359 * As of now, this function must be implemented to have the value in spreadsheet format 8360 */ 8361 public function GetEditValue($value, $oHostObj = null) 8362 { 8363 $oParent = $this->GetTargetAttDef(); 8364 $res = $oParent->GetSubItemAsEditValue($this->Get('item_code'), $value); 8365 8366 return $res; 8367 } 8368 8369 public function IsPartOfFingerprint() 8370 { 8371 return false; 8372 } 8373 8374 static public function GetFormFieldClass() 8375 { 8376 return '\\Combodo\\iTop\\Form\\Field\\LabelField'; 8377 } 8378 8379 public function MakeFormField(DBObject $oObject, $oFormField = null) 8380 { 8381 if ($oFormField === null) 8382 { 8383 $sFormFieldClass = static::GetFormFieldClass(); 8384 $oFormField = new $sFormFieldClass($this->GetCode()); 8385 } 8386 parent::MakeFormField($oObject, $oFormField); 8387 8388 // Note : As of today, this attribute is -by nature- only supported in readonly mode, not edition 8389 $sAttCode = $this->GetCode(); 8390 $oFormField->SetCurrentValue(html_entity_decode($oObject->GetAsHTML($sAttCode), ENT_QUOTES, 'UTF-8')); 8391 $oFormField->SetReadOnly(true); 8392 8393 return $oFormField; 8394 } 8395 8396} 8397 8398/** 8399 * One way encrypted (hashed) password 8400 */ 8401class AttributeOneWayPassword extends AttributeDefinition 8402{ 8403 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 8404 8405 static public function ListExpectedParams() 8406 { 8407 return array_merge(parent::ListExpectedParams(), array("depends_on")); 8408 } 8409 8410 public function GetEditClass() 8411 { 8412 return "One Way Password"; 8413 } 8414 8415 static public function IsBasedOnDBColumns() 8416 { 8417 return true; 8418 } 8419 8420 static public function IsScalar() 8421 { 8422 return true; 8423 } 8424 8425 public function IsWritable() 8426 { 8427 return true; 8428 } 8429 8430 public function GetDefaultValue(DBObject $oHostObject = null) 8431 { 8432 return ""; 8433 } 8434 8435 public function IsNullAllowed() 8436 { 8437 return $this->GetOptional("is_null_allowed", false); 8438 } 8439 8440 // Facilitate things: allow the user to Set the value from a string or from an ormPassword (already encrypted) 8441 public function MakeRealValue($proposedValue, $oHostObj) 8442 { 8443 $oPassword = $proposedValue; 8444 if (is_object($oPassword)) 8445 { 8446 $oPassword = clone $proposedValue; 8447 } 8448 else 8449 { 8450 $oPassword = new ormPassword('', ''); 8451 $oPassword->SetPassword($proposedValue); 8452 } 8453 8454 return $oPassword; 8455 } 8456 8457 public function GetSQLExpressions($sPrefix = '') 8458 { 8459 if ($sPrefix == '') 8460 { 8461 $sPrefix = $this->GetCode(); // Warning: AttributeOneWayPassword does not have any sql property so code = sql ! 8462 } 8463 $aColumns = array(); 8464 // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix 8465 $aColumns[''] = $sPrefix.'_hash'; 8466 $aColumns['_salt'] = $sPrefix.'_salt'; 8467 8468 return $aColumns; 8469 } 8470 8471 public function FromSQLToValue($aCols, $sPrefix = '') 8472 { 8473 if (!array_key_exists($sPrefix, $aCols)) 8474 { 8475 $sAvailable = implode(', ', array_keys($aCols)); 8476 throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); 8477 } 8478 $hashed = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; 8479 8480 if (!array_key_exists($sPrefix.'_salt', $aCols)) 8481 { 8482 $sAvailable = implode(', ', array_keys($aCols)); 8483 throw new MissingColumnException("Missing column '".$sPrefix."_salt' from {$sAvailable}"); 8484 } 8485 $sSalt = isset($aCols[$sPrefix.'_salt']) ? $aCols[$sPrefix.'_salt'] : ''; 8486 8487 $value = new ormPassword($hashed, $sSalt); 8488 8489 return $value; 8490 } 8491 8492 public function GetSQLValues($value) 8493 { 8494 // #@# Optimization: do not load blobs anytime 8495 // As per mySQL doc, selecting blob columns will prevent mySQL from 8496 // using memory in case a temporary table has to be created 8497 // (temporary tables created on disk) 8498 // We will have to remove the blobs from the list of attributes when doing the select 8499 // then the use of Get() should finalize the load 8500 if ($value instanceOf ormPassword) 8501 { 8502 $aValues = array(); 8503 $aValues[$this->GetCode().'_hash'] = $value->GetHash(); 8504 $aValues[$this->GetCode().'_salt'] = $value->GetSalt(); 8505 } 8506 else 8507 { 8508 $aValues = array(); 8509 $aValues[$this->GetCode().'_hash'] = ''; 8510 $aValues[$this->GetCode().'_salt'] = ''; 8511 } 8512 8513 return $aValues; 8514 } 8515 8516 public function GetSQLColumns($bFullSpec = false) 8517 { 8518 $aColumns = array(); 8519 $aColumns[$this->GetCode().'_hash'] = 'TINYBLOB'; 8520 $aColumns[$this->GetCode().'_salt'] = 'TINYBLOB'; 8521 8522 return $aColumns; 8523 } 8524 8525 public function GetImportColumns() 8526 { 8527 $aColumns = array(); 8528 $aColumns[$this->GetCode()] = 'TINYTEXT'.CMDBSource::GetSqlStringColumnDefinition(); 8529 8530 return $aColumns; 8531 } 8532 8533 public function FromImportToValue($aCols, $sPrefix = '') 8534 { 8535 if (!isset($aCols[$sPrefix])) 8536 { 8537 $sAvailable = implode(', ', array_keys($aCols)); 8538 throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); 8539 } 8540 $sClearPwd = $aCols[$sPrefix]; 8541 8542 $oPassword = new ormPassword('', ''); 8543 $oPassword->SetPassword($sClearPwd); 8544 8545 return $oPassword; 8546 } 8547 8548 public function GetFilterDefinitions() 8549 { 8550 return array(); 8551 // still not working... see later... 8552 } 8553 8554 public function GetBasicFilterOperators() 8555 { 8556 return array(); 8557 } 8558 8559 public function GetBasicFilterLooseOperator() 8560 { 8561 return '='; 8562 } 8563 8564 public function GetBasicFilterSQLExpr($sOpCode, $value) 8565 { 8566 return 'true'; 8567 } 8568 8569 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 8570 { 8571 if (is_object($value)) 8572 { 8573 return $value->GetAsHTML(); 8574 } 8575 8576 return ''; 8577 } 8578 8579 public function GetAsCSV( 8580 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 8581 $bConvertToPlainText = false 8582 ) { 8583 return ''; // Not exportable in CSV 8584 } 8585 8586 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 8587 { 8588 return ''; // Not exportable in XML 8589 } 8590 8591 public function GetValueLabel($sValue, $oHostObj = null) 8592 { 8593 // Don't display anything in "group by" reports 8594 return '*****'; 8595 } 8596 8597} 8598 8599// Indexed array having two dimensions 8600class AttributeTable extends AttributeDBField 8601{ 8602 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 8603 8604 public function GetEditClass() 8605 { 8606 return "Table"; 8607 } 8608 8609 protected function GetSQLCol($bFullSpec = false) 8610 { 8611 return "LONGTEXT".CMDBSource::GetSqlStringColumnDefinition(); 8612 } 8613 8614 public function GetMaxSize() 8615 { 8616 return null; 8617 } 8618 8619 public function GetNullValue() 8620 { 8621 return array(); 8622 } 8623 8624 public function IsNull($proposedValue) 8625 { 8626 return (count($proposedValue) == 0); 8627 } 8628 8629 public function GetEditValue($sValue, $oHostObj = null) 8630 { 8631 return ''; 8632 } 8633 8634 // Facilitate things: allow the user to Set the value from a string 8635 public function MakeRealValue($proposedValue, $oHostObj) 8636 { 8637 if (is_null($proposedValue)) 8638 { 8639 return array(); 8640 } 8641 else 8642 { 8643 if (!is_array($proposedValue)) 8644 { 8645 return array(0 => array(0 => $proposedValue)); 8646 } 8647 } 8648 8649 return $proposedValue; 8650 } 8651 8652 public function FromSQLToValue($aCols, $sPrefix = '') 8653 { 8654 try 8655 { 8656 $value = @unserialize($aCols[$sPrefix.'']); 8657 if ($value === false) 8658 { 8659 $value = $this->MakeRealValue($aCols[$sPrefix.''], null); 8660 } 8661 } catch (Exception $e) 8662 { 8663 $value = $this->MakeRealValue($aCols[$sPrefix.''], null); 8664 } 8665 8666 return $value; 8667 } 8668 8669 public function GetSQLValues($value) 8670 { 8671 $aValues = array(); 8672 $aValues[$this->Get("sql")] = serialize($value); 8673 8674 return $aValues; 8675 } 8676 8677 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 8678 { 8679 if (!is_array($value)) 8680 { 8681 throw new CoreException('Expecting an array', array('found' => get_class($value))); 8682 } 8683 if (count($value) == 0) 8684 { 8685 return ""; 8686 } 8687 8688 $sRes = "<TABLE class=\"listResults\">"; 8689 $sRes .= "<TBODY>"; 8690 foreach($value as $iRow => $aRawData) 8691 { 8692 $sRes .= "<TR>"; 8693 foreach($aRawData as $iCol => $cell) 8694 { 8695 // Note: avoid the warning in case the cell is made of an array 8696 $sCell = @Str::pure2html((string)$cell); 8697 $sCell = str_replace("\n", "<br>\n", $sCell); 8698 $sRes .= "<TD>$sCell</TD>"; 8699 } 8700 $sRes .= "</TR>"; 8701 } 8702 $sRes .= "</TBODY>"; 8703 $sRes .= "</TABLE>"; 8704 8705 return $sRes; 8706 } 8707 8708 public function GetAsCSV( 8709 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 8710 $bConvertToPlainText = false 8711 ) { 8712 // Not implemented 8713 return ''; 8714 } 8715 8716 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 8717 { 8718 if (!is_array($value) || count($value) == 0) 8719 { 8720 return ""; 8721 } 8722 8723 $sRes = ""; 8724 foreach($value as $iRow => $aRawData) 8725 { 8726 $sRes .= "<row>"; 8727 foreach($aRawData as $iCol => $cell) 8728 { 8729 $sCell = Str::pure2xml((string)$cell); 8730 $sRes .= "<cell icol=\"$iCol\">$sCell</cell>"; 8731 } 8732 $sRes .= "</row>"; 8733 } 8734 8735 return $sRes; 8736 } 8737} 8738 8739// The PHP value is a hash array, it is stored as a TEXT column 8740class AttributePropertySet extends AttributeTable 8741{ 8742 public function GetEditClass() 8743 { 8744 return "PropertySet"; 8745 } 8746 8747 // Facilitate things: allow the user to Set the value from a string 8748 public function MakeRealValue($proposedValue, $oHostObj) 8749 { 8750 if (!is_array($proposedValue)) 8751 { 8752 return array('?' => (string)$proposedValue); 8753 } 8754 8755 return $proposedValue; 8756 } 8757 8758 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 8759 { 8760 if (!is_array($value)) 8761 { 8762 throw new CoreException('Expecting an array', array('found' => get_class($value))); 8763 } 8764 if (count($value) == 0) 8765 { 8766 return ""; 8767 } 8768 8769 $sRes = "<TABLE class=\"listResults\">"; 8770 $sRes .= "<TBODY>"; 8771 foreach($value as $sProperty => $sValue) 8772 { 8773 if ($sProperty == 'auth_pwd') 8774 { 8775 $sValue = '*****'; 8776 } 8777 $sRes .= "<TR>"; 8778 $sCell = str_replace("\n", "<br>\n", Str::pure2html((string)$sValue)); 8779 $sRes .= "<TD class=\"label\">$sProperty</TD><TD>$sCell</TD>"; 8780 $sRes .= "</TR>"; 8781 } 8782 $sRes .= "</TBODY>"; 8783 $sRes .= "</TABLE>"; 8784 8785 return $sRes; 8786 } 8787 8788 public function GetAsCSV( 8789 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 8790 $bConvertToPlainText = false 8791 ) { 8792 if (!is_array($value) || count($value) == 0) 8793 { 8794 return ""; 8795 } 8796 8797 $aRes = array(); 8798 foreach($value as $sProperty => $sValue) 8799 { 8800 if ($sProperty == 'auth_pwd') 8801 { 8802 $sValue = '*****'; 8803 } 8804 $sFrom = array(',', '='); 8805 $sTo = array('\,', '\='); 8806 $aRes[] = $sProperty.'='.str_replace($sFrom, $sTo, (string)$sValue); 8807 } 8808 $sRaw = implode(',', $aRes); 8809 8810 $sFrom = array("\r\n", $sTextQualifier); 8811 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 8812 $sEscaped = str_replace($sFrom, $sTo, $sRaw); 8813 8814 return $sTextQualifier.$sEscaped.$sTextQualifier; 8815 } 8816 8817 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 8818 { 8819 if (!is_array($value) || count($value) == 0) 8820 { 8821 return ""; 8822 } 8823 8824 $sRes = ""; 8825 foreach($value as $sProperty => $sValue) 8826 { 8827 if ($sProperty == 'auth_pwd') 8828 { 8829 $sValue = '*****'; 8830 } 8831 $sRes .= "<property id=\"$sProperty\">"; 8832 $sRes .= Str::pure2xml((string)$sValue); 8833 $sRes .= "</property>"; 8834 } 8835 8836 return $sRes; 8837 } 8838} 8839 8840/** 8841 * An unordered multi values attribute 8842 * Allowed values are mandatory for this attribute to be modified 8843 * 8844 * Class AttributeSet 8845 */ 8846abstract class AttributeSet extends AttributeDBFieldVoid 8847{ 8848 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 8849 const EDITABLE_INPUT_ID_SUFFIX = '-setwidget-values'; // used client side, see js/jquery.itop-set-widget.js 8850 8851 public function __construct($sCode, array $aParams) 8852 { 8853 parent::__construct($sCode, $aParams); 8854 $this->aCSSClasses[] = 'attribute-set'; 8855 } 8856 8857 static public function ListExpectedParams() 8858 { 8859 return array_merge(parent::ListExpectedParams(), array('is_null_allowed', 'max_items')); 8860 } 8861 8862 /** 8863 * Allowed values are mandatory for this attribute to be modified 8864 * 8865 * @param array $aArgs 8866 * @param string $sContains 8867 * 8868 * @return array|null 8869 * @throws \CoreException 8870 * @throws \OQLException 8871 */ 8872 public function GetAllowedValues($aArgs = array(), $sContains = '') 8873 { 8874 return parent::GetAllowedValues($aArgs, $sContains); 8875 } 8876 8877 /** 8878 * @param \ormSet $oValue 8879 * 8880 * @param $aArgs 8881 * 8882 * @return string JSON to be used in the itop.set_widget JQuery widget 8883 * @throws \CoreException 8884 * @throws \OQLException 8885 */ 8886 public function GetJsonForWidget($oValue, $aArgs = array()) 8887 { 8888 $aJson = array(); 8889 8890 // possible_values 8891 $aAllowedValues = $this->GetAllowedValues($aArgs); 8892 $aSetKeyValData = array(); 8893 foreach($aAllowedValues as $sCode => $sLabel) 8894 { 8895 $aSetKeyValData[] = [ 8896 'code' => $sCode, 8897 'label' => $sLabel, 8898 ]; 8899 } 8900 $aJson['possible_values'] = $aSetKeyValData; 8901 $aRemoved = array(); 8902 if (is_null($oValue)) 8903 { 8904 $aJson['partial_values'] = array(); 8905 $aJson['orig_value'] = array(); 8906 } 8907 else 8908 { 8909 $aPartialValues = $oValue->GetModified(); 8910 foreach ($aPartialValues as $key => $value) 8911 { 8912 if (!isset($aAllowedValues[$value])) 8913 { 8914 unset($aPartialValues[$key]); 8915 } 8916 } 8917 $aJson['partial_values'] = array_values($aPartialValues); 8918 $aOrigValues = array_merge($oValue->GetValues(), $oValue->GetModified()); 8919 foreach ($aOrigValues as $key => $value) 8920 { 8921 if (!isset($aAllowedValues[$value])) 8922 { 8923 // Remove unwanted values 8924 $aRemoved[] = $value; 8925 unset($aOrigValues[$key]); 8926 } 8927 } 8928 $aJson['orig_value'] = array_values($aOrigValues); 8929 } 8930 $aJson['added'] = array(); 8931 $aJson['removed'] = $aRemoved; 8932 8933 $iMaxTags = $this->GetMaxItems(); 8934 $aJson['max_items_allowed'] = $iMaxTags; 8935 8936 return json_encode($aJson); 8937 } 8938 8939 public function RequiresIndex() 8940 { 8941 return true; 8942 } 8943 8944 public function RequiresFullTextIndex() 8945 { 8946 return true; 8947 } 8948 8949 public function GetDefaultValue(DBObject $oHostObject = null) 8950 { 8951 return null; 8952 } 8953 8954 public function IsNullAllowed() 8955 { 8956 return $this->Get("is_null_allowed"); 8957 } 8958 8959 public function GetEditClass() 8960 { 8961 return "Set"; 8962 } 8963 8964 public function GetEditValue($value, $oHostObj = null) 8965 { 8966 if (is_string($value)) 8967 { 8968 return $value; 8969 } 8970 if ($value instanceof ormSet) 8971 { 8972 $value = $value->GetValues(); 8973 } 8974 if (is_array($value)) 8975 { 8976 return implode(', ', $value); 8977 } 8978 return ''; 8979 } 8980 8981 protected function GetSQLCol($bFullSpec = false) 8982 { 8983 $iLen = $this->GetMaxSize(); 8984 return "VARCHAR($iLen)" 8985 .CMDBSource::GetSqlStringColumnDefinition() 8986 .($bFullSpec ? $this->GetSQLColSpec() : ''); 8987 } 8988 8989 public function GetMaxSize() 8990 { 8991 return 255; 8992 } 8993 8994 public function FromStringToArray($proposedValue) 8995 { 8996 $aValues = array(); 8997 if (!empty($proposedValue)) 8998 { 8999 foreach(explode(',', $proposedValue) as $sCode) 9000 { 9001 $sValue = trim($sCode); 9002 $aValues[] = $sValue; 9003 } 9004 } 9005 return $aValues; 9006 } 9007 9008 /** 9009 * @param array $aCols 9010 * @param string $sPrefix 9011 * 9012 * @return mixed 9013 * @throws \Exception 9014 */ 9015 public function FromSQLToValue($aCols, $sPrefix = '') 9016 { 9017 $sValue = $aCols["$sPrefix"]; 9018 9019 return $this->MakeRealValue($sValue, null, true); 9020 } 9021 9022 /** 9023 * @param $aCols 9024 * @param string $sPrefix 9025 * 9026 * @return mixed 9027 * @throws \Exception 9028 */ 9029 public function FromImportToValue($aCols, $sPrefix = '') 9030 { 9031 $sValue = $aCols["$sPrefix"]; 9032 9033 return $this->MakeRealValue($sValue, null); 9034 } 9035 9036 /** 9037 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! 9038 * 9039 * @param $proposedValue 9040 * @param \DBObject $oHostObj 9041 * 9042 * @param bool $bIgnoreErrors 9043 * 9044 * @return mixed 9045 * @throws \CoreException 9046 * @throws \CoreUnexpectedValue 9047 */ 9048 public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false) 9049 { 9050 $oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems()); 9051 if (is_string($proposedValue) && !empty($proposedValue)) 9052 { 9053 $proposedValue = trim("$proposedValue"); 9054 $aValues = $this->FromStringToArray($proposedValue); 9055 $oSet->SetValues($aValues); 9056 } 9057 elseif ($proposedValue instanceof ormSet) 9058 { 9059 $oSet = $proposedValue; 9060 } 9061 9062 return $oSet; 9063 } 9064 9065 /** 9066 * Get the value from a given string (plain text, CSV import) 9067 * 9068 * @param string $sProposedValue 9069 * @param bool $bLocalizedValue 9070 * @param string $sSepItem 9071 * @param string $sSepAttribute 9072 * @param string $sSepValue 9073 * @param string $sAttributeQualifier 9074 * 9075 * @return mixed null if no match could be found 9076 * @throws \Exception 9077 */ 9078 public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) 9079 { 9080 return $this->MakeRealValue($sProposedValue, null); 9081 } 9082 9083 /** 9084 * @return null|\ormSet 9085 * @throws \CoreException 9086 * @throws \Exception 9087 */ 9088 public function GetNullValue() 9089 { 9090 return new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems()); 9091 } 9092 9093 public function IsNull($proposedValue) 9094 { 9095 if (empty($proposedValue)) 9096 { 9097 return true; 9098 } 9099 9100 /** @var \ormSet $proposedValue */ 9101 return $proposedValue->Count() == 0; 9102 } 9103 9104 /** 9105 * To be overloaded for localized enums 9106 * 9107 * @param $sValue 9108 * 9109 * @return string label corresponding to the given value (in plain text) 9110 * @throws \Exception 9111 */ 9112 public function GetValueLabel($sValue) 9113 { 9114 if ($sValue instanceof ormSet) 9115 { 9116 $sValue = $sValue->GetValues(); 9117 } 9118 if (is_array($sValue)) 9119 { 9120 return implode(', ', $sValue); 9121 } 9122 return $sValue; 9123 } 9124 9125 /** 9126 * @param string $sValue 9127 * @param null $oHostObj 9128 * 9129 * @return string 9130 * @throws \Exception 9131 */ 9132 public function GetAsPlainText($sValue, $oHostObj = null) 9133 { 9134 return $this->GetValueLabel($sValue); 9135 } 9136 9137 /** 9138 * @param $value 9139 * 9140 * @return string 9141 */ 9142 public function ScalarToSQL($value) 9143 { 9144 if (empty($value)) 9145 { 9146 return ''; 9147 } 9148 if ($value instanceof ormSet) 9149 { 9150 $value = $value->GetValues(); 9151 } 9152 if (is_array($value)) 9153 { 9154 return implode(', ', $value); 9155 } 9156 return $value; 9157 } 9158 9159 /** 9160 * @param $value 9161 * @param \DBObject $oHostObject 9162 * @param bool $bLocalize 9163 * 9164 * @return string|null 9165 * 9166 * @throws \Exception 9167 */ 9168 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 9169 { 9170 if ($value instanceof ormSet) 9171 { 9172 $value = $value->GetValues(); 9173 } 9174 if (is_array($value)) 9175 { 9176 return implode(', ', $value); 9177 } 9178 return $value; 9179 } 9180 9181 public function GetMaxItems() 9182 { 9183 return $this->Get('max_items'); 9184 } 9185 9186 static public function GetFormFieldClass() 9187 { 9188 return '\\Combodo\\iTop\\Form\\Field\\SetField'; 9189 } 9190} 9191 9192class AttributeClassAttCodeSet extends AttributeSet 9193{ 9194 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 9195 9196 const DEFAULT_PARAM_INCLUDE_CHILD_CLASSES_ATTRIBUTES = false; 9197 9198 public function __construct($sCode, array $aParams) 9199 { 9200 parent::__construct($sCode, $aParams); 9201 $this->aCSSClasses[] = 'attribute-class-attcode-set'; 9202 } 9203 9204 static public function ListExpectedParams() 9205 { 9206 return array_merge(parent::ListExpectedParams(), array('class_field', 'attribute_definition_list', 'attribute_definition_exclusion_list')); 9207 } 9208 9209 public function GetMaxSize() 9210 { 9211 return max(255, 15 * $this->GetMaxItems()); 9212 } 9213 9214 /** 9215 * @param array $aArgs 9216 * @param string $sContains 9217 * 9218 * @return array|null 9219 * @throws \CoreException 9220 */ 9221 public function GetAllowedValues($aArgs = array(), $sContains = '') 9222 { 9223 if (!isset($aArgs['this'])) 9224 { 9225 return null; 9226 } 9227 9228 $oHostObj = $aArgs['this']; 9229 $sTargetClass = $this->Get('class_field'); 9230 $sRootClass = $oHostObj->Get($sTargetClass); 9231 $bIncludeChildClasses = $this->GetOptional('include_child_classes_attributes', static::DEFAULT_PARAM_INCLUDE_CHILD_CLASSES_ATTRIBUTES); 9232 9233 $aExcludeDefs = array(); 9234 $sAttDefExclusionList = $this->Get('attribute_definition_exclusion_list'); 9235 if (!empty($sAttDefExclusionList)) 9236 { 9237 foreach(explode(',', $sAttDefExclusionList) as $sAttDefName) 9238 { 9239 $sAttDefName = trim($sAttDefName); 9240 $aExcludeDefs[$sAttDefName] = $sAttDefName; 9241 } 9242 } 9243 9244 $aAllowedDefs = array(); 9245 $sAttDefList = $this->Get('attribute_definition_list'); 9246 if (!empty($sAttDefList)) 9247 { 9248 foreach(explode(',', $sAttDefList) as $sAttDefName) 9249 { 9250 $sAttDefName = trim($sAttDefName); 9251 $aAllowedDefs[$sAttDefName] = $sAttDefName; 9252 } 9253 } 9254 9255 $aAllAttributes = array(); 9256 if (!empty($sRootClass)) 9257 { 9258 $aClasses = array($sRootClass); 9259 if($bIncludeChildClasses === true) 9260 { 9261 $aClasses = $aClasses + MetaModel::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_EXCLUDETOP); 9262 } 9263 9264 foreach($aClasses as $sClass) 9265 { 9266 foreach(MetaModel::GetAttributesList($sClass) as $sAttCode) 9267 { 9268 // Add attribute only if not already there (can be in leaf classes but not the root) 9269 if(!array_key_exists($sAttCode, $aAllAttributes)) 9270 { 9271 $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); 9272 $sAttDefClass = get_class($oAttDef); 9273 9274 // Skip excluded attdefs 9275 if(isset($aExcludeDefs[$sAttDefClass])) 9276 { 9277 continue; 9278 } 9279 // Skip not allowed attdefs only if list specified 9280 if(!empty($aAllowedDefs) && !isset($aAllowedDefs[$sAttDefClass])) 9281 { 9282 continue; 9283 } 9284 9285 $aAllAttributes[$sAttCode] = array( 9286 'classes' => array($sClass), 9287 ); 9288 } 9289 else 9290 { 9291 $aAllAttributes[$sAttCode]['classes'][] = $sClass; 9292 } 9293 } 9294 } 9295 } 9296 9297 $aAllowedAttributes = array(); 9298 foreach($aAllAttributes as $sAttCode => $aAttData) 9299 { 9300 $iAttClassesCount = count($aAttData['classes']); 9301 $sAttFirstClass = $aAttData['classes'][0]; 9302 $sAttLabel = MetaModel::GetLabel($sAttFirstClass, $sAttCode); 9303 9304 if($sAttFirstClass === $sRootClass) 9305 { 9306 $sLabel = Dict::Format('Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass', $sAttCode, $sAttLabel); 9307 } 9308 elseif($iAttClassesCount === 1) 9309 { 9310 $sLabel = Dict::Format('Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass', $sAttCode, $sAttLabel, MetaModel::GetName($sAttFirstClass)); 9311 } 9312 else 9313 { 9314 $sLabel = Dict::Format('Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses', $sAttCode, $sAttLabel); 9315 } 9316 $aAllowedAttributes[$sAttCode] = $sLabel; 9317 } 9318 return $aAllowedAttributes; 9319 } 9320 9321 /** 9322 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! 9323 * 9324 * @param $proposedValue 9325 * @param \DBObject $oHostObj 9326 * 9327 * @param bool $bIgnoreErrors 9328 * 9329 * @return mixed 9330 * @throws \CoreException 9331 * @throws \CoreUnexpectedValue 9332 * @throws \OQLException 9333 * @throws \Exception 9334 */ 9335 public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false) 9336 { 9337 $oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems()); 9338 $aArgs = array(); 9339 if (!empty($oHostObj)) 9340 { 9341 $aArgs['this'] = $oHostObj; 9342 } 9343 $aAllowedAttributes = $this->GetAllowedValues($aArgs); 9344 $aInvalidAttCodes = array(); 9345 if (is_string($proposedValue) && !empty($proposedValue)) 9346 { 9347 $aJsonFromWidget = json_decode($proposedValue, true); 9348 if (is_null($aJsonFromWidget)) 9349 { 9350 $proposedValue = trim($proposedValue); 9351 $aValues = array(); 9352 foreach(explode(',', $proposedValue) as $sValue) 9353 { 9354 $sAttCode = trim($sValue); 9355 if (empty($aAllowedAttributes) || isset($aAllowedAttributes[$sAttCode])) 9356 { 9357 $aValues[$sAttCode] = $sAttCode; 9358 } 9359 else 9360 { 9361 $aInvalidAttCodes[] = $sAttCode; 9362 } 9363 } 9364 $oSet->SetValues($aValues); 9365 } 9366 } 9367 elseif ($proposedValue instanceof ormSet) 9368 { 9369 $oSet = $proposedValue; 9370 } 9371 if (!empty($aInvalidAttCodes) && !$bIgnoreErrors) 9372 { 9373 $sTargetClass = $this->Get('class_field'); 9374 $sClass = $oHostObj->Get($sTargetClass); 9375 throw new CoreUnexpectedValue("The attribute(s) ".implode(', ', $aInvalidAttCodes)." are invalid for class {$sClass}"); 9376 } 9377 9378 return $oSet; 9379 } 9380 9381 /** 9382 * @param $value 9383 * @param \DBObject $oHostObject 9384 * @param bool $bLocalize 9385 * 9386 * @return string|null 9387 * 9388 * @throws \Exception 9389 */ 9390 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 9391 { 9392 if ($value instanceof ormSet) 9393 { 9394 $value = $value->GetValues(); 9395 } 9396 if (is_array($value)) 9397 { 9398 if (!empty($oHostObject) && $bLocalize) 9399 { 9400 $sTargetClass = $this->Get('class_field'); 9401 $sClass = $oHostObject->Get($sTargetClass); 9402 9403 $aLocalizedValues = array(); 9404 foreach($value as $sAttCode) 9405 { 9406 try 9407 { 9408 $sAttClass = $sClass; 9409 9410 // Look for the first class (current or children) that have this attcode 9411 foreach(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) 9412 { 9413 if(MetaModel::IsValidAttCode($sChildClass, $sAttCode)) 9414 { 9415 $sAttClass = $sChildClass; 9416 break; 9417 } 9418 } 9419 9420 $aLocalizedValues[] = '<span class="attribute-set-item" data-code="'.$sAttCode.'" data-label="'.MetaModel::GetLabel($sAttClass, $sAttCode)." ($sAttCode)".'" data-description="">'.$sAttCode.'</span>'; 9421 } catch (Exception $e) 9422 { 9423 // Ignore bad values 9424 } 9425 } 9426 $value = $aLocalizedValues; 9427 } 9428 $value = implode('', $value); 9429 } 9430 return '<span class="'.implode(' ', $this->aCSSClasses).'">'.$value.'</span>'; 9431 } 9432} 9433 9434class AttributeQueryAttCodeSet extends AttributeSet 9435{ 9436 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 9437 9438 public function __construct($sCode, array $aParams) 9439 { 9440 parent::__construct($sCode, $aParams); 9441 $this->aCSSClasses[] = 'attribute-query-attcode-set'; 9442 } 9443 9444 static public function ListExpectedParams() 9445 { 9446 return array_merge(parent::ListExpectedParams(), array('query_field')); 9447 } 9448 9449 protected function GetSQLCol($bFullSpec = false) 9450 { 9451 return "TEXT".CMDBSource::GetSqlStringColumnDefinition(); 9452 } 9453 9454 public function GetMaxSize() 9455 { 9456 return 65535; 9457 } 9458 9459 /** 9460 * Get a class array indexed by alias 9461 * @param $oHostObj 9462 * 9463 * @return array 9464 */ 9465 private function GetClassList($oHostObj) 9466 { 9467 try 9468 { 9469 $sQueryField = $this->Get('query_field'); 9470 $sQuery = $oHostObj->Get($sQueryField); 9471 if (empty($sQuery)) 9472 { 9473 return array(); 9474 } 9475 $oFilter = DBSearch::FromOQL($sQuery); 9476 return $oFilter->GetSelectedClasses(); 9477 9478 } catch (OQLException $e) 9479 { 9480 IssueLog::Warning($e->getMessage()); 9481 } 9482 return array(); 9483 } 9484 9485 public function GetAllowedValues($aArgs = array(), $sContains = '') 9486 { 9487 if (isset($aArgs['this'])) 9488 { 9489 $oHostObj = $aArgs['this']; 9490 $aClasses = $this->GetClassList($oHostObj); 9491 9492 $aAllowedAttributes = array(); 9493 $aAllAttributes = array(); 9494 9495 if ((count($aClasses) == 1) && (array_keys($aClasses)[0] == array_values($aClasses)[0])) 9496 { 9497 $sClass = reset($aClasses); 9498 $aAttributes = MetaModel::GetAttributesList($sClass); 9499 foreach($aAttributes as $sAttCode) 9500 { 9501 $aAllowedAttributes[$sAttCode] = "$sAttCode (".MetaModel::GetLabel($sClass, $sAttCode).')'; 9502 } 9503 } 9504 else 9505 { 9506 if (!empty($aClasses)) 9507 { 9508 ksort($aClasses); 9509 foreach($aClasses as $sAlias => $sClass) 9510 { 9511 $aAttributes = MetaModel::GetAttributesList($sClass); 9512 foreach($aAttributes as $sAttCode) 9513 { 9514 $aAllAttributes[] = array('alias' => $sAlias, 'class' => $sClass, 'att_code' => $sAttCode); 9515 } 9516 } 9517 } 9518 foreach($aAllAttributes as $aFullAttCode) 9519 { 9520 $sAttCode = $aFullAttCode['alias'].'.'.$aFullAttCode['att_code']; 9521 $sClass = $aFullAttCode['class']; 9522 $sLabel = "$sAttCode (".MetaModel::GetLabel($sClass, $aFullAttCode['att_code']).')'; 9523 $aAllowedAttributes[$sAttCode] = $sLabel; 9524 } 9525 } 9526 return $aAllowedAttributes; 9527 } 9528 9529 return null; 9530 } 9531 9532 /** 9533 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! 9534 * 9535 * @param $proposedValue 9536 * @param \DBObject $oHostObj 9537 * 9538 * @param bool $bIgnoreErrors 9539 * 9540 * @return mixed 9541 * @throws \CoreException 9542 * @throws \CoreUnexpectedValue 9543 * @throws \OQLException 9544 * @throws \Exception 9545 */ 9546 public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false) 9547 { 9548 $oSet = new ormSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems()); 9549 $aArgs = array(); 9550 if (!empty($oHostObj)) 9551 { 9552 $aArgs['this'] = $oHostObj; 9553 } 9554 $aAllowedAttributes = $this->GetAllowedValues($aArgs); 9555 $aInvalidAttCodes = array(); 9556 if (is_string($proposedValue) && !empty($proposedValue)) 9557 { 9558 $proposedValue = trim($proposedValue); 9559 $aValues = array(); 9560 foreach(explode(',', $proposedValue) as $sValue) 9561 { 9562 $sAttCode = trim($sValue); 9563 if (empty($aAllowedAttributes) || isset($aAllowedAttributes[$sAttCode])) 9564 { 9565 $aValues[$sAttCode] = $sAttCode; 9566 } 9567 else 9568 { 9569 $aInvalidAttCodes[] = $sAttCode; 9570 } 9571 } 9572 $oSet->SetValues($aValues); 9573 } 9574 elseif ($proposedValue instanceof ormSet) 9575 { 9576 $oSet = $proposedValue; 9577 } 9578 if (!empty($aInvalidAttCodes) && !$bIgnoreErrors) 9579 { 9580 throw new CoreUnexpectedValue("The attribute(s) ".implode(', ', $aInvalidAttCodes)." are invalid"); 9581 } 9582 9583 return $oSet; 9584 } 9585 9586 /** 9587 * @param $value 9588 * @param \DBObject $oHostObject 9589 * @param bool $bLocalize 9590 * 9591 * @return string|null 9592 * 9593 * @throws \Exception 9594 */ 9595 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 9596 { 9597 9598 if ($value instanceof ormSet) 9599 { 9600 $value = $value->GetValues(); 9601 } 9602 if (is_array($value)) 9603 { 9604 if (!empty($oHostObject) && $bLocalize) 9605 { 9606 $aArgs['this'] = $oHostObject; 9607 $aAllowedAttributes = $this->GetAllowedValues($aArgs); 9608 9609 $aLocalizedValues = array(); 9610 foreach($value as $sAttCode) 9611 { 9612 if (isset($aAllowedAttributes[$sAttCode])) 9613 { 9614 $aLocalizedValues[] = '<span class="attribute-set-item" data-code="'.$sAttCode.'" data-label="'.$aAllowedAttributes[$sAttCode].'" data-description="">'.$sAttCode.'</span>'; 9615 } 9616 } 9617 $value = $aLocalizedValues; 9618 } 9619 $value = implode('', $value); 9620 } 9621 9622 return '<span class="'.implode(' ', $this->aCSSClasses).'">'.$value.'</span>'; 9623 } 9624} 9625 9626/** 9627 * Multi value list of tags 9628 * 9629 * @see TagSetFieldData 9630 * @since 2.6 N°931 tag fields 9631 */ 9632class AttributeTagSet extends AttributeSet 9633{ 9634 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_TAG_SET; 9635 9636 public function __construct($sCode, array $aParams) 9637 { 9638 parent::__construct($sCode, $aParams); 9639 $this->aCSSClasses[] = 'attribute-tag-set'; 9640 } 9641 9642 public function GetEditClass() 9643 { 9644 return 'TagSet'; 9645 } 9646 9647 static public function ListExpectedParams() 9648 { 9649 return array_merge(parent::ListExpectedParams(), array('tag_code_max_len')); 9650 } 9651 9652 /** 9653 * @param \ormTagSet $oValue 9654 * 9655 * @param $aArgs 9656 * 9657 * @return string JSON to be used in the itop.tagset_widget JQuery widget 9658 * @throws \CoreException 9659 * @throws \OQLException 9660 */ 9661 public function GetJsonForWidget($oValue, $aArgs = array()) 9662 { 9663 $aJson = array(); 9664 9665 // possible_values 9666 $aTagSetObjectData = $this->GetAllowedValues($aArgs); 9667 $aTagSetKeyValData = array(); 9668 foreach($aTagSetObjectData as $sTagCode => $sTagLabel) 9669 { 9670 $aTagSetKeyValData[] = [ 9671 'code' => $sTagCode, 9672 'label' => $sTagLabel, 9673 ]; 9674 } 9675 $aJson['possible_values'] = $aTagSetKeyValData; 9676 9677 if (is_null($oValue)) 9678 { 9679 $aJson['partial_values'] = array(); 9680 $aJson['orig_value'] = array(); 9681 $aJson['added'] = array(); 9682 $aJson['removed'] = array(); 9683 } 9684 else 9685 { 9686 $aJson['orig_value'] = array_merge($oValue->GetValues(), $oValue->GetModified()); 9687 $aJson['added'] = $oValue->GetAdded(); 9688 $aJson['removed'] = $oValue->GetRemoved(); 9689 9690 if ($oValue->DisplayPartial()) 9691 { 9692 // For bulk updates 9693 $aJson['partial_values'] = $oValue->GetModified(); 9694 } 9695 else 9696 { 9697 // For simple updates 9698 $aJson['partial_values'] = array(); 9699 } 9700 } 9701 9702 9703 $iMaxTags = $this->GetMaxItems(); 9704 $aJson['max_items_allowed'] = $iMaxTags; 9705 9706 return json_encode($aJson); 9707 } 9708 9709 public function FromStringToArray($proposedValue) 9710 { 9711 $aValues = array(); 9712 if (!empty($proposedValue)) 9713 { 9714 foreach(explode(' ', $proposedValue) as $sCode) 9715 { 9716 $sValue = trim($sCode); 9717 $aValues[] = $sValue; 9718 } 9719 } 9720 return $aValues; 9721 } 9722 9723 /** 9724 * Extract all existing tags from a string and ignore bad tags 9725 * 9726 * @param $sValue 9727 * @param bool $bNoLimit : don't apply the maximum tag limit 9728 * 9729 * @return \ormTagSet 9730 * @throws \CoreException 9731 * @throws \CoreUnexpectedValue 9732 */ 9733 public function GetExistingTagsFromString($sValue, $bNoLimit = false) 9734 { 9735 $aTagCodes = $this->FromStringToArray("$sValue"); 9736 $sAttCode = $this->GetCode(); 9737 $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode); 9738 if ($bNoLimit) 9739 { 9740 $oTagSet = new ormTagSet($sClass, $sAttCode, 0); 9741 } 9742 else 9743 { 9744 $oTagSet = new ormTagSet($sClass, $sAttCode, $this->GetMaxItems()); 9745 } 9746 $aGoodTags = array(); 9747 foreach($aTagCodes as $sTagCode) 9748 { 9749 if ($sTagCode === '') 9750 { 9751 continue; 9752 } 9753 if ($oTagSet->IsValidTag($sTagCode)) 9754 { 9755 $aGoodTags[] = $sTagCode; 9756 if (!$bNoLimit && (count($aGoodTags) === $this->GetMaxItems())) 9757 { 9758 // extra and bad tags are ignored 9759 break; 9760 } 9761 } 9762 } 9763 $oTagSet->SetValues($aGoodTags); 9764 9765 return $oTagSet; 9766 } 9767 9768 public function GetTagCodeMaxLength() 9769 { 9770 return $this->Get('tag_code_max_len'); 9771 } 9772 9773 public function GetEditValue($value, $oHostObj = null) 9774 { 9775 if (empty($value)) 9776 { 9777 return ''; 9778 } 9779 if ($value instanceof ormTagSet) 9780 { 9781 $aValues = $value->GetValues(); 9782 9783 return implode(' ', $aValues); 9784 } 9785 9786 return ''; 9787 } 9788 9789 public function GetMaxSize() 9790 { 9791 return max(255, ($this->GetMaxItems() * $this->GetTagCodeMaxLength()) + 1); 9792 } 9793 9794 public function Equals($val1, $val2) 9795 { 9796 if (($val1 instanceof ormTagSet) && ($val2 instanceof ormTagSet)) 9797 { 9798 return $val1->Equals($val2); 9799 } 9800 9801 return ($val1 == $val2); 9802 } 9803 9804 public function GetAllowedValues($aArgs = array(), $sContains = '') 9805 { 9806 $sAttCode = $this->GetCode(); 9807 $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode); 9808 $aAllowedTags = TagSetFieldData::GetAllowedValues($sClass, $sAttCode); 9809 $aAllowedValues = array(); 9810 foreach($aAllowedTags as $oAllowedTag) 9811 { 9812 $aAllowedValues[$oAllowedTag->Get('code')] = $oAllowedTag->Get('label'); 9813 } 9814 9815 return $aAllowedValues; 9816 } 9817 9818 /** 9819 * @param array $aCols 9820 * @param string $sPrefix 9821 * 9822 * @return mixed 9823 * @throws \CoreException 9824 * @throws \Exception 9825 */ 9826 public function FromSQLToValue($aCols, $sPrefix = '') 9827 { 9828 $sValue = $aCols["$sPrefix"]; 9829 9830 return $this->GetExistingTagsFromString($sValue); 9831 } 9832 9833 /** 9834 * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! 9835 * 9836 * @param $proposedValue 9837 * @param $oHostObj 9838 * 9839 * @param bool $bIgnoreErrors 9840 * 9841 * @return mixed 9842 * @throws \CoreException 9843 * @throws \CoreUnexpectedValue 9844 */ 9845 public function MakeRealValue($proposedValue, $oHostObj, $bIgnoreErrors = false) 9846 { 9847 $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems()); 9848 if (is_string($proposedValue) && !empty($proposedValue)) 9849 { 9850 $sJsonFromWidget = json_decode($proposedValue, true); 9851 if (is_null($sJsonFromWidget)) 9852 { 9853 $proposedValue = trim("$proposedValue"); 9854 $aTagCodes = $this->FromStringToArray($proposedValue); 9855 $oTagSet->SetValues($aTagCodes); 9856 } 9857 } 9858 elseif ($proposedValue instanceof ormTagSet) 9859 { 9860 $oTagSet = $proposedValue; 9861 } 9862 9863 return $oTagSet; 9864 } 9865 9866 /** 9867 * Get the value from a given string (plain text, CSV import) 9868 * 9869 * @param string $sProposedValue 9870 * @param bool $bLocalizedValue 9871 * @param string $sSepItem 9872 * @param string $sSepAttribute 9873 * @param string $sSepValue 9874 * @param string $sAttributeQualifier 9875 * 9876 * @return mixed null if no match could be found 9877 * @throws \Exception 9878 */ 9879 public function MakeValueFromString($sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, $sAttributeQualifier = null) 9880 { 9881 if (is_null($sSepItem) || empty($sSepItem)) 9882 { 9883 $sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator'); 9884 } 9885 if (!empty($sProposedValue)) 9886 { 9887 $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), 9888 $this->GetCode(), $this->GetMaxItems()); 9889 $aLabels = explode($sSepItem, $sProposedValue); 9890 $aCodes = array(); 9891 foreach($aLabels as $sTagLabel) 9892 { 9893 if (!empty($sTagLabel)) 9894 { 9895 $aCodes[] = ($bLocalizedValue) ? $oTagSet->GetTagFromLabel($sTagLabel) : $sTagLabel; 9896 } 9897 } 9898 $sProposedValue = implode(' ', $aCodes); 9899 } 9900 9901 return $this->MakeRealValue($sProposedValue, null); 9902 } 9903 9904 public function GetNullValue() 9905 { 9906 return new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode(), $this->GetMaxItems()); 9907 } 9908 9909 public function IsNull($proposedValue) 9910 { 9911 if (is_null($proposedValue)) 9912 { 9913 return true; 9914 } 9915 9916 /** @var \ormTagSet $proposedValue */ 9917 return count($proposedValue->GetValues()) == 0; 9918 } 9919 9920 /** 9921 * To be overloaded for localized enums 9922 * 9923 * @param $sValue 9924 * 9925 * @return string label corresponding to the given value (in plain text) 9926 * @throws \CoreWarning 9927 * @throws \Exception 9928 */ 9929 public function GetValueLabel($sValue) 9930 { 9931 if (empty($sValue)) 9932 { 9933 return ''; 9934 } 9935 if (is_string($sValue)) 9936 { 9937 $sValue = $this->GetExistingTagsFromString($sValue); 9938 } 9939 if ($sValue instanceof ormTagSet) 9940 { 9941 $aValues = $sValue->GetLabels(); 9942 9943 return implode(', ', $aValues); 9944 } 9945 throw new CoreWarning('Expected the attribute value to be a TagSet', array( 9946 'found_type' => gettype($sValue), 9947 'value' => $sValue, 9948 'class' => $this->GetHostClass(), 9949 'attribute' => $this->GetCode() 9950 )); 9951 } 9952 9953 /** 9954 * @param $value 9955 * 9956 * @return string 9957 * @throws \CoreWarning 9958 */ 9959 public function ScalarToSQL($value) 9960 { 9961 if (empty($value)) 9962 { 9963 return ''; 9964 } 9965 if ($value instanceof ormTagSet) 9966 { 9967 $aValues = $value->GetValues(); 9968 9969 return implode(' ', $aValues); 9970 } 9971 throw new CoreWarning('Expected the attribute value to be a TagSet', array( 9972 'found_type' => gettype($value), 9973 'value' => $value, 9974 'class' => $this->GetHostClass(), 9975 'attribute' => $this->GetCode() 9976 )); 9977 } 9978 9979 /** 9980 * @param $value 9981 * @param \DBObject $oHostObject 9982 * @param bool $bLocalize 9983 * 9984 * @return string|null 9985 * 9986 * @throws \CoreException 9987 * @throws \Exception 9988 */ 9989 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 9990 { 9991 if ($value instanceof ormTagSet) 9992 { 9993 if ($bLocalize) 9994 { 9995 $aValues = $value->GetTags(); 9996 } 9997 else 9998 { 9999 $aValues = $value->GetValues(); 10000 } 10001 if (empty($aValues)) 10002 { 10003 return ''; 10004 } 10005 10006 return $this->GenerateViewHtmlForValues($aValues); 10007 } 10008 if (is_string($value)) 10009 { 10010 try 10011 { 10012 $oValue = $this->MakeRealValue($value, $oHostObject); 10013 10014 return $this->GetAsHTML($oValue, $oHostObject, $bLocalize); 10015 } catch (Exception $e) 10016 { 10017 // unknown tags are present display the code instead 10018 } 10019 $aTagCodes = $this->FromStringToArray($value); 10020 $aValues = array(); 10021 $oTagSet = new ormTagSet(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), 10022 $this->GetCode(), $this->GetMaxItems()); 10023 foreach($aTagCodes as $sTagCode) 10024 { 10025 try 10026 { 10027 $oTagSet->Add($sTagCode); 10028 } catch (Exception $e) 10029 { 10030 $aValues[] = $sTagCode; 10031 } 10032 } 10033 $sHTML = ''; 10034 if (!empty($aValues)) 10035 { 10036 $sHTML .= $this->GenerateViewHtmlForValues($aValues, 'attribute-set-item-undefined'); 10037 } 10038 $aValues = $oTagSet->GetTags(); 10039 if (!empty($aValues)) 10040 { 10041 $sHTML .= $this->GenerateViewHtmlForValues($aValues); 10042 } 10043 10044 return $sHTML; 10045 } 10046 10047 return parent::GetAsHTML($value, $oHostObject, $bLocalize); 10048 } 10049 10050 // Do not display friendly names in the history of change 10051 public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) 10052 { 10053 $sResult = Dict::Format('Change:AttName_Changed', $this->GetLabel()).", "; 10054 10055 $aNewValues = $this->FromStringToArray($sNewValue); 10056 $aOldValues = $this->FromStringToArray($sOldValue); 10057 10058 $aDelta['removed'] = array_diff($aOldValues, $aNewValues); 10059 $aDelta['added'] = array_diff($aNewValues, $aOldValues); 10060 10061 $aAllowedTags = TagSetFieldData::GetAllowedValues(MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()), $this->GetCode()); 10062 10063 if (!empty($aDelta['removed'])) 10064 { 10065 $aRemoved = array(); 10066 foreach($aDelta['removed'] as $idx => $sTagCode) 10067 { 10068 if (empty($sTagCode)) {continue;} 10069 $sTagLabel = $sTagCode; 10070 foreach($aAllowedTags as $oTag) 10071 { 10072 if ($sTagCode === $oTag->Get('code')) 10073 { 10074 $sTagLabel = $oTag->Get('label'); 10075 } 10076 } 10077 $aRemoved[] = $sTagLabel; 10078 } 10079 10080 $sRemoved = $this->GenerateViewHtmlForValues($aRemoved, 'history-removed'); 10081 if (!empty($sRemoved)) 10082 { 10083 $sResult .= Dict::Format('Change:LinkSet:Removed', $sRemoved); 10084 } 10085 } 10086 10087 if (!empty($aDelta['added'])) 10088 { 10089 if (!empty($sRemoved)) 10090 { 10091 $sResult .= ', '; 10092 } 10093 10094 $aAdded = array(); 10095 foreach($aDelta['added'] as $idx => $sTagCode) 10096 { 10097 if (empty($sTagCode)) {continue;} 10098 $sTagLabel = $sTagCode; 10099 foreach($aAllowedTags as $oTag) 10100 { 10101 if ($sTagCode === $oTag->Get('code')) 10102 { 10103 $sTagLabel = $oTag->Get('label'); 10104 } 10105 } 10106 $aAdded[] = $sTagLabel; 10107 } 10108 10109 $sAdded = $this->GenerateViewHtmlForValues($aAdded, 'history-added'); 10110 if (!empty($sAdded)) 10111 { 10112 $sResult .= Dict::Format('Change:LinkSet:Added', $sAdded); 10113 } 10114 } 10115 10116 return $sResult; 10117 } 10118 10119 /** 10120 * HTML representation of a list of tags (read-only) 10121 * accept a list of strings or a list of TagSetFieldData 10122 * 10123 * @param array $aValues 10124 * @param string $sCssClass 10125 * @param bool $bWithLink if true will generate a link, otherwise just a "a" tag without href 10126 * 10127 * @return string 10128 * @throws \CoreException 10129 * @throws \OQLException 10130 */ 10131 public function GenerateViewHtmlForValues($aValues, $sCssClass = '', $bWithLink = true) 10132 { 10133 if (empty($aValues)) {return '';} 10134 $sHtml = '<span class="'.$sCssClass.' '.implode(' ', $this->aCSSClasses).'">'; 10135 foreach($aValues as $oTag) 10136 { 10137 if ($oTag instanceof TagSetFieldData) 10138 { 10139 $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $this->GetCode()); 10140 $sAttCode = $this->GetCode(); 10141 $sTagCode = $oTag->Get('code'); 10142 $sTagLabel = $oTag->Get('label'); 10143 $sTagDescription = $oTag->Get('description'); 10144 $oFilter = DBSearch::FromOQL("SELECT $sClass WHERE $sAttCode MATCHES '$sTagCode'"); 10145 $oAppContext = new ApplicationContext(); 10146 $sContext = $oAppContext->GetForLink(); 10147 $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($oFilter->GetClass()); 10148 $sFilter = rawurlencode($oFilter->serialize()); 10149 10150 $sLink = ''; 10151 if ($bWithLink) 10152 { 10153 $sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}"; 10154 $sLink = ' href="'.$sUrl.'"'; 10155 } 10156 10157 $sHtml .= '<a'.$sLink.' class="attribute-set-item attribute-set-item-'.$sTagCode.'" data-code="'.$sTagCode.'" data-label="'.htmlentities($sTagLabel, 10158 ENT_QUOTES, 'UTF-8').'" data-description="'.htmlentities($sTagDescription, ENT_QUOTES, 10159 'UTF-8').'">'.htmlentities($sTagLabel, ENT_QUOTES, 'UTF-8').'</a>'; 10160 } 10161 else 10162 { 10163 $sHtml .= '<span class="attribute-set-item">'.$oTag.'</span>'; 10164 } 10165 } 10166 $sHtml .= '</span>'; 10167 10168 return $sHtml; 10169 } 10170 10171 /** 10172 * @param $value 10173 * @param \DBObject $oHostObject 10174 * @param bool $bLocalize 10175 * 10176 * @return string 10177 * 10178 */ 10179 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 10180 { 10181 if (is_object($value) && ($value instanceof ormTagSet)) 10182 { 10183 $sRes = "<Set>\n"; 10184 if ($bLocalize) 10185 { 10186 $aValues = $value->GetLabels(); 10187 } 10188 else 10189 { 10190 $aValues = $value->GetValues(); 10191 } 10192 if (!empty($aValues)) 10193 { 10194 $sRes .= '<Tag>'.implode('</Tag><Tag>', $aValues).'</Tag>'; 10195 } 10196 $sRes .= "</Set>\n"; 10197 } 10198 else 10199 { 10200 $sRes = ''; 10201 } 10202 10203 return $sRes; 10204 } 10205 10206 /** 10207 * @param $value 10208 * @param string $sSeparator 10209 * @param string $sTextQualifier 10210 * @param \DBObject $oHostObject 10211 * @param bool $bLocalize 10212 * @param bool $bConvertToPlainText 10213 * 10214 * @return mixed|string 10215 */ 10216 public function GetAsCSV( 10217 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 10218 $bConvertToPlainText = false 10219 ) { 10220 $sSepItem = MetaModel::GetConfig()->Get('tag_set_item_separator'); 10221 if (is_object($value) && ($value instanceof ormTagSet)) 10222 { 10223 if ($bLocalize) 10224 { 10225 $aValues = $value->GetLabels(); 10226 } 10227 else 10228 { 10229 $aValues = $value->GetValues(); 10230 } 10231 $sRes = implode($sSepItem, $aValues); 10232 } 10233 else 10234 { 10235 $sRes = ''; 10236 } 10237 10238 return "{$sTextQualifier}{$sRes}{$sTextQualifier}"; 10239 } 10240 10241 /** 10242 * List the available verbs for 'GetForTemplate' 10243 */ 10244 public function EnumTemplateVerbs() 10245 { 10246 return array( 10247 '' => 'Plain text representation', 10248 'html' => 'HTML representation (unordered list)', 10249 ); 10250 } 10251 10252 /** 10253 * Get various representations of the value, for insertion into a template (e.g. in Notifications) 10254 * 10255 * @param mixed $value The current value of the field 10256 * @param string $sVerb The verb specifying the representation of the value 10257 * @param DBObject $oHostObject The object 10258 * @param bool $bLocalize Whether or not to localize the value 10259 * 10260 * @return string 10261 * @throws \Exception 10262 */ 10263 public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) 10264 { 10265 if (is_object($value) && ($value instanceof ormTagSet)) 10266 { 10267 if ($bLocalize) 10268 { 10269 $aValues = $value->GetLabels(); 10270 $sSep = ', '; 10271 } 10272 else 10273 { 10274 $aValues = $value->GetValues(); 10275 $sSep = ' '; 10276 } 10277 10278 switch ($sVerb) 10279 { 10280 case '': 10281 return implode($sSep, $aValues); 10282 10283 case 'html': 10284 return '<ul><li>'.implode("</li><li>", $aValues).'</li></ul>'; 10285 10286 default: 10287 throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); 10288 } 10289 } 10290 throw new CoreUnexpectedValue("Bad value '$value' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); 10291 } 10292 10293 /** 10294 * Helper to get a value that will be JSON encoded 10295 * The operation is the opposite to FromJSONToValue 10296 * 10297 * @param \ormTagSet $value 10298 * 10299 * @return array 10300 */ 10301 public function GetForJSON($value) 10302 { 10303 $aRet = array(); 10304 if (is_object($value) && ($value instanceof ormTagSet)) 10305 { 10306 $aRet = $value->GetValues(); 10307 } 10308 10309 return $aRet; 10310 } 10311 10312 /** 10313 * Helper to form a value, given JSON decoded data 10314 * The operation is the opposite to GetForJSON 10315 * 10316 * @param $json 10317 * 10318 * @return \ormTagSet 10319 * @throws \CoreException 10320 * @throws \CoreUnexpectedValue 10321 * @throws \Exception 10322 */ 10323 public function FromJSONToValue($json) 10324 { 10325 $oSet = new ormTagSet($this->GetHostClass(), $this->GetCode(), $this->GetMaxItems()); 10326 $oSet->SetValues($json); 10327 10328 return $oSet; 10329 } 10330 10331 /** 10332 * The part of the current attribute in the object's signature, for the supplied value 10333 * 10334 * @param mixed $value The value of this attribute for the object 10335 * 10336 * @return string The "signature" for this field/attribute 10337 */ 10338 public function Fingerprint($value) 10339 { 10340 if ($value instanceof ormTagSet) 10341 { 10342 $aValues = $value->GetValues(); 10343 10344 return implode(' ', $aValues); 10345 } 10346 10347 return parent::Fingerprint($value); 10348 } 10349 10350 static public function GetFormFieldClass() 10351 { 10352 return '\\Combodo\\iTop\\Form\\Field\\TagSetField'; 10353 } 10354} 10355 10356/** 10357 * The attribute dedicated to the friendly name automatic attribute (not written) 10358 * 10359 * @package iTopORM 10360 */ 10361class AttributeFriendlyName extends AttributeDefinition 10362{ 10363 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; 10364 public $m_sValue; 10365 10366 public function __construct($sCode) 10367 { 10368 $this->m_sCode = $sCode; 10369 $aParams = array(); 10370 $aParams["default_value"] = ''; 10371 parent::__construct($sCode, $aParams); 10372 10373 $this->m_sValue = $this->Get("default_value"); 10374 } 10375 10376 10377 public function GetEditClass() 10378 { 10379 return ""; 10380 } 10381 10382 public function GetValuesDef() 10383 { 10384 return null; 10385 } 10386 10387 public function GetPrerequisiteAttributes($sClass = null) 10388 { 10389 return $this->GetOptional("depends_on", array()); 10390 } 10391 10392 static public function IsScalar() 10393 { 10394 return true; 10395 } 10396 10397 public function IsNullAllowed() 10398 { 10399 return false; 10400 } 10401 10402 public function GetSQLExpressions($sPrefix = '') 10403 { 10404 if ($sPrefix == '') 10405 { 10406 $sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property 10407 } 10408 10409 return array('' => $sPrefix); 10410 } 10411 10412 static public function IsBasedOnOQLExpression() 10413 { 10414 return true; 10415 } 10416 10417 public function GetOQLExpression() 10418 { 10419 return MetaModel::GetNameExpression($this->GetHostClass()); 10420 } 10421 10422 public function GetLabel($sDefault = null) 10423 { 10424 $sLabel = parent::GetLabel(''); 10425 if (strlen($sLabel) == 0) 10426 { 10427 $sLabel = Dict::S('Core:FriendlyName-Label'); 10428 } 10429 10430 return $sLabel; 10431 } 10432 10433 public function GetDescription($sDefault = null) 10434 { 10435 $sLabel = parent::GetDescription(''); 10436 if (strlen($sLabel) == 0) 10437 { 10438 $sLabel = Dict::S('Core:FriendlyName-Description'); 10439 } 10440 10441 return $sLabel; 10442 } 10443 10444 public function FromSQLToValue($aCols, $sPrefix = '') 10445 { 10446 $sValue = $aCols[$sPrefix]; 10447 10448 return $sValue; 10449 } 10450 10451 public function IsWritable() 10452 { 10453 return false; 10454 } 10455 10456 public function IsMagic() 10457 { 10458 return true; 10459 } 10460 10461 static public function IsBasedOnDBColumns() 10462 { 10463 return false; 10464 } 10465 10466 public function SetFixedValue($sValue) 10467 { 10468 $this->m_sValue = $sValue; 10469 } 10470 10471 public function GetDefaultValue(DBObject $oHostObject = null) 10472 { 10473 return $this->m_sValue; 10474 } 10475 10476 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 10477 { 10478 return Str::pure2html((string)$sValue); 10479 } 10480 10481 public function GetAsCSV( 10482 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 10483 $bConvertToPlainText = false 10484 ) { 10485 $sFrom = array("\r\n", $sTextQualifier); 10486 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 10487 $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); 10488 10489 return $sTextQualifier.$sEscaped.$sTextQualifier; 10490 } 10491 10492 static function GetFormFieldClass() 10493 { 10494 return '\\Combodo\\iTop\\Form\\Field\\StringField'; 10495 } 10496 10497 public function MakeFormField(DBObject $oObject, $oFormField = null) 10498 { 10499 if ($oFormField === null) 10500 { 10501 $sFormFieldClass = static::GetFormFieldClass(); 10502 $oFormField = new $sFormFieldClass($this->GetCode()); 10503 } 10504 $oFormField->SetReadOnly(true); 10505 parent::MakeFormField($oObject, $oFormField); 10506 10507 return $oFormField; 10508 } 10509 10510 // Do not display friendly names in the history of change 10511 public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) 10512 { 10513 return ''; 10514 } 10515 10516 public function GetFilterDefinitions() 10517 { 10518 return array($this->GetCode() => new FilterFromAttribute($this)); 10519 } 10520 10521 public function GetBasicFilterOperators() 10522 { 10523 return array("=" => "equals", "!=" => "differs from"); 10524 } 10525 10526 public function GetBasicFilterLooseOperator() 10527 { 10528 return "Contains"; 10529 } 10530 10531 public function GetBasicFilterSQLExpr($sOpCode, $value) 10532 { 10533 $sQValue = CMDBSource::Quote($value); 10534 switch ($sOpCode) 10535 { 10536 case '=': 10537 case '!=': 10538 return $this->GetSQLExpr()." $sOpCode $sQValue"; 10539 case 'Contains': 10540 return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); 10541 case 'NotLike': 10542 return $this->GetSQLExpr()." NOT LIKE $sQValue"; 10543 case 'Like': 10544 default: 10545 return $this->GetSQLExpr()." LIKE $sQValue"; 10546 } 10547 } 10548 10549 public function IsPartOfFingerprint() 10550 { 10551 return false; 10552 } 10553} 10554 10555/** 10556 * Holds the setting for the redundancy on a specific relation 10557 * Its value is a string, containing either: 10558 * - 'disabled' 10559 * - 'n', where n is a positive integer value giving the minimum count of items upstream 10560 * - 'n%', where n is a positive integer value, giving the minimum as a percentage of the total count of items upstream 10561 * 10562 * @package iTopORM 10563 */ 10564class AttributeRedundancySettings extends AttributeDBField 10565{ 10566 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 10567 10568 static public function ListExpectedParams() 10569 { 10570 return array( 10571 'sql', 10572 'relation_code', 10573 'from_class', 10574 'neighbour_id', 10575 'enabled', 10576 'enabled_mode', 10577 'min_up', 10578 'min_up_type', 10579 'min_up_mode' 10580 ); 10581 } 10582 10583 public function GetValuesDef() 10584 { 10585 return null; 10586 } 10587 10588 public function GetPrerequisiteAttributes($sClass = null) 10589 { 10590 return array(); 10591 } 10592 10593 public function GetEditClass() 10594 { 10595 return "RedundancySetting"; 10596 } 10597 10598 protected function GetSQLCol($bFullSpec = false) 10599 { 10600 return "VARCHAR(20)" 10601 .CMDBSource::GetSqlStringColumnDefinition() 10602 .($bFullSpec ? $this->GetSQLColSpec() : ''); 10603 } 10604 10605 10606 public function GetValidationPattern() 10607 { 10608 return "^[0-9]{1,3}|[0-9]{1,2}%|disabled$"; 10609 } 10610 10611 public function GetMaxSize() 10612 { 10613 return 20; 10614 } 10615 10616 public function GetDefaultValue(DBObject $oHostObject = null) 10617 { 10618 $sRet = 'disabled'; 10619 if ($this->Get('enabled')) 10620 { 10621 if ($this->Get('min_up_type') == 'count') 10622 { 10623 $sRet = (string)$this->Get('min_up'); 10624 } 10625 else // percent 10626 { 10627 $sRet = $this->Get('min_up').'%'; 10628 } 10629 } 10630 10631 return $sRet; 10632 } 10633 10634 public function IsNullAllowed() 10635 { 10636 return false; 10637 } 10638 10639 public function GetNullValue() 10640 { 10641 return ''; 10642 } 10643 10644 public function IsNull($proposedValue) 10645 { 10646 return ($proposedValue == ''); 10647 } 10648 10649 public function MakeRealValue($proposedValue, $oHostObj) 10650 { 10651 if (is_null($proposedValue)) 10652 { 10653 return ''; 10654 } 10655 10656 return (string)$proposedValue; 10657 } 10658 10659 public function ScalarToSQL($value) 10660 { 10661 if (!is_string($value)) 10662 { 10663 throw new CoreException('Expected the attribute value to be a string', array( 10664 'found_type' => gettype($value), 10665 'value' => $value, 10666 'class' => $this->GetHostClass(), 10667 'attribute' => $this->GetCode() 10668 )); 10669 } 10670 10671 return $value; 10672 } 10673 10674 public function GetRelationQueryData() 10675 { 10676 foreach(MetaModel::EnumRelationQueries($this->GetHostClass(), $this->Get('relation_code'), 10677 false) as $sDummy => $aQueryInfo) 10678 { 10679 if ($aQueryInfo['sFromClass'] == $this->Get('from_class')) 10680 { 10681 if ($aQueryInfo['sNeighbour'] == $this->Get('neighbour_id')) 10682 { 10683 return $aQueryInfo; 10684 } 10685 } 10686 } 10687 10688 return array(); 10689 } 10690 10691 /** 10692 * Find the user option label 10693 * 10694 * @param string $sUserOption possible values : disabled|cout|percent 10695 * @param string $sDefault 10696 * 10697 * @return string 10698 * @throws \Exception 10699 */ 10700 public function GetUserOptionFormat($sUserOption, $sDefault = null) 10701 { 10702 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, null, true /*user lang*/); 10703 if (is_null($sLabel)) 10704 { 10705 // If no default value is specified, let's define the most relevant one for developping purposes 10706 if (is_null($sDefault)) 10707 { 10708 $sDefault = str_replace('_', ' ', $this->m_sCode.':'.$sUserOption.'(%1$s)'); 10709 } 10710 // Browse the hierarchy again, accepting default (english) translations 10711 $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/'.$sUserOption, $sDefault, false); 10712 } 10713 10714 return $sLabel; 10715 } 10716 10717 /** 10718 * Override to display the value in the GUI 10719 * 10720 * @param string $sValue 10721 * @param \DBObject $oHostObject 10722 * @param bool $bLocalize 10723 * 10724 * @return string 10725 * @throws \CoreException 10726 * @throws \DictExceptionMissingString 10727 */ 10728 public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) 10729 { 10730 $sCurrentOption = $this->GetCurrentOption($sValue); 10731 $sClass = $oHostObject ? get_class($oHostObject) : $this->m_sHostClass; 10732 10733 return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue), 10734 MetaModel::GetName($sClass)); 10735 } 10736 10737 public function GetAsCSV( 10738 $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 10739 $bConvertToPlainText = false 10740 ) { 10741 $sFrom = array("\r\n", $sTextQualifier); 10742 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 10743 $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); 10744 10745 return $sTextQualifier.$sEscaped.$sTextQualifier; 10746 } 10747 10748 /** 10749 * Helper to interpret the value, given the current settings and string representation of the attribute 10750 */ 10751 public function IsEnabled($sValue) 10752 { 10753 if ($this->get('enabled_mode') == 'fixed') 10754 { 10755 $bRet = $this->get('enabled'); 10756 } 10757 else 10758 { 10759 $bRet = ($sValue != 'disabled'); 10760 } 10761 10762 return $bRet; 10763 } 10764 10765 /** 10766 * Helper to interpret the value, given the current settings and string representation of the attribute 10767 */ 10768 public function GetMinUpType($sValue) 10769 { 10770 if ($this->get('min_up_mode') == 'fixed') 10771 { 10772 $sRet = $this->get('min_up_type'); 10773 } 10774 else 10775 { 10776 $sRet = 'count'; 10777 if (substr(trim($sValue), -1, 1) == '%') 10778 { 10779 $sRet = 'percent'; 10780 } 10781 } 10782 10783 return $sRet; 10784 } 10785 10786 /** 10787 * Helper to interpret the value, given the current settings and string representation of the attribute 10788 */ 10789 public function GetMinUpValue($sValue) 10790 { 10791 if ($this->get('min_up_mode') == 'fixed') 10792 { 10793 $iRet = (int)$this->Get('min_up'); 10794 } 10795 else 10796 { 10797 $sRefValue = $sValue; 10798 if (substr(trim($sValue), -1, 1) == '%') 10799 { 10800 $sRefValue = substr(trim($sValue), 0, -1); 10801 } 10802 $iRet = (int)trim($sRefValue); 10803 } 10804 10805 return $iRet; 10806 } 10807 10808 /** 10809 * Helper to determine if the redundancy can be viewed/edited by the end-user 10810 */ 10811 public function IsVisible() 10812 { 10813 $bRet = false; 10814 if ($this->Get('enabled_mode') == 'fixed') 10815 { 10816 $bRet = $this->Get('enabled'); 10817 } 10818 elseif ($this->Get('enabled_mode') == 'user') 10819 { 10820 $bRet = true; 10821 } 10822 10823 return $bRet; 10824 } 10825 10826 public function IsWritable() 10827 { 10828 if (($this->Get('enabled_mode') == 'fixed') && ($this->Get('min_up_mode') == 'fixed')) 10829 { 10830 return false; 10831 } 10832 10833 return true; 10834 } 10835 10836 /** 10837 * Returns an HTML form that can be read by ReadValueFromPostedForm 10838 */ 10839 public function GetDisplayForm($sCurrentValue, $oPage, $bEditMode = false, $sFormPrefix = '') 10840 { 10841 $sRet = ''; 10842 $aUserOptions = $this->GetUserOptions($sCurrentValue); 10843 if (count($aUserOptions) < 2) 10844 { 10845 $bEditOption = false; 10846 } 10847 else 10848 { 10849 $bEditOption = $bEditMode; 10850 } 10851 $sCurrentOption = $this->GetCurrentOption($sCurrentValue); 10852 foreach($aUserOptions as $sUserOption) 10853 { 10854 $bSelected = ($sUserOption == $sCurrentOption); 10855 $sRet .= '<div>'; 10856 $sRet .= $this->GetDisplayOption($sCurrentValue, $oPage, $sFormPrefix, $bEditOption, $sUserOption, 10857 $bSelected); 10858 $sRet .= '</div>'; 10859 } 10860 10861 return $sRet; 10862 } 10863 10864 const USER_OPTION_DISABLED = 'disabled'; 10865 const USER_OPTION_ENABLED_COUNT = 'count'; 10866 const USER_OPTION_ENABLED_PERCENT = 'percent'; 10867 10868 /** 10869 * Depending on the xxx_mode parameters, build the list of options that are allowed to the end-user 10870 */ 10871 protected function GetUserOptions($sValue) 10872 { 10873 $aRet = array(); 10874 if ($this->Get('enabled_mode') == 'user') 10875 { 10876 $aRet[] = self::USER_OPTION_DISABLED; 10877 } 10878 10879 if ($this->Get('min_up_mode') == 'user') 10880 { 10881 $aRet[] = self::USER_OPTION_ENABLED_COUNT; 10882 $aRet[] = self::USER_OPTION_ENABLED_PERCENT; 10883 } 10884 else 10885 { 10886 if ($this->GetMinUpType($sValue) == 'count') 10887 { 10888 $aRet[] = self::USER_OPTION_ENABLED_COUNT; 10889 } 10890 else 10891 { 10892 $aRet[] = self::USER_OPTION_ENABLED_PERCENT; 10893 } 10894 } 10895 10896 return $aRet; 10897 } 10898 10899 /** 10900 * Convert the string representation into one of the existing options 10901 */ 10902 protected function GetCurrentOption($sValue) 10903 { 10904 $sRet = self::USER_OPTION_DISABLED; 10905 if ($this->IsEnabled($sValue)) 10906 { 10907 if ($this->GetMinUpType($sValue) == 'count') 10908 { 10909 $sRet = self::USER_OPTION_ENABLED_COUNT; 10910 } 10911 else 10912 { 10913 $sRet = self::USER_OPTION_ENABLED_PERCENT; 10914 } 10915 } 10916 10917 return $sRet; 10918 } 10919 10920 /** 10921 * Display an option (form, or current value) 10922 * 10923 * @param string $sCurrentValue 10924 * @param \WebPage $oPage 10925 * @param string $sFormPrefix 10926 * @param bool $bEditMode 10927 * @param string $sUserOption 10928 * @param bool $bSelected 10929 * 10930 * @return string 10931 * @throws \CoreException 10932 * @throws \DictExceptionMissingString 10933 * @throws \Exception 10934 */ 10935 protected function GetDisplayOption( 10936 $sCurrentValue, $oPage, $sFormPrefix, $bEditMode, $sUserOption, $bSelected = true 10937 ) { 10938 $sRet = ''; 10939 10940 $iCurrentValue = $this->GetMinUpValue($sCurrentValue); 10941 if ($bEditMode) 10942 { 10943 $sValue = null; 10944 $sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id'); 10945 switch ($sUserOption) 10946 { 10947 case self::USER_OPTION_DISABLED: 10948 $sValue = ''; // Empty placeholder 10949 break; 10950 10951 case self::USER_OPTION_ENABLED_COUNT: 10952 if ($bEditMode) 10953 { 10954 $sName = $sHtmlNamesPrefix.'_min_up_count'; 10955 $sEditValue = $bSelected ? $iCurrentValue : ''; 10956 $sValue = '<input class="redundancy-min-up-count" type="string" size="3" name="'.$sName.'" value="'.$sEditValue.'">'; 10957 // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option) 10958 $oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});"); 10959 } 10960 else 10961 { 10962 $sValue = $iCurrentValue; 10963 } 10964 break; 10965 10966 case self::USER_OPTION_ENABLED_PERCENT: 10967 if ($bEditMode) 10968 { 10969 $sName = $sHtmlNamesPrefix.'_min_up_percent'; 10970 $sEditValue = $bSelected ? $iCurrentValue : ''; 10971 $sValue = '<input class="redundancy-min-up-percent" type="string" size="3" name="'.$sName.'" value="'.$sEditValue.'">'; 10972 // To fix an issue on Firefox: focus set to the option (because the input is within the label for the option) 10973 $oPage->add_ready_script("\$('[name=\"$sName\"]').click(function(){var me=this; setTimeout(function(){\$(me).focus();}, 100);});"); 10974 } 10975 else 10976 { 10977 $sValue = $iCurrentValue; 10978 } 10979 break; 10980 } 10981 $sLabel = sprintf($this->GetUserOptionFormat($sUserOption), $sValue, 10982 MetaModel::GetName($this->GetHostClass())); 10983 10984 $sOptionName = $sHtmlNamesPrefix.'_user_option'; 10985 $sOptionId = $sOptionName.'_'.$sUserOption; 10986 $sChecked = $bSelected ? 'checked' : ''; 10987 $sRet = '<input type="radio" name="'.$sOptionName.'" id="'.$sOptionId.'" value="'.$sUserOption.'" '.$sChecked.'> <label for="'.$sOptionId.'">'.$sLabel.'</label>'; 10988 } 10989 else 10990 { 10991 // Read-only: display only the currently selected option 10992 if ($bSelected) 10993 { 10994 $sRet = sprintf($this->GetUserOptionFormat($sUserOption), $iCurrentValue, 10995 MetaModel::GetName($this->GetHostClass())); 10996 } 10997 } 10998 10999 return $sRet; 11000 } 11001 11002 /** 11003 * Makes the string representation out of the values given by the form defined in GetDisplayForm 11004 */ 11005 public function ReadValueFromPostedForm($sFormPrefix) 11006 { 11007 $sHtmlNamesPrefix = 'rddcy_'.$this->Get('relation_code').'_'.$this->Get('from_class').'_'.$this->Get('neighbour_id'); 11008 11009 $iMinUpCount = (int)utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_count', null, 'raw_data'); 11010 $iMinUpPercent = (int)utils::ReadPostedParam($sHtmlNamesPrefix.'_min_up_percent', null, 'raw_data'); 11011 $sSelectedOption = utils::ReadPostedParam($sHtmlNamesPrefix.'_user_option', null, 'raw_data'); 11012 switch ($sSelectedOption) 11013 { 11014 case self::USER_OPTION_ENABLED_COUNT: 11015 $sRet = $iMinUpCount; 11016 break; 11017 11018 case self::USER_OPTION_ENABLED_PERCENT: 11019 $sRet = $iMinUpPercent.'%'; 11020 break; 11021 11022 case self::USER_OPTION_DISABLED: 11023 default: 11024 $sRet = 'disabled'; 11025 break; 11026 } 11027 11028 return $sRet; 11029 } 11030} 11031 11032/** 11033 * Custom fields managed by an external implementation 11034 * 11035 * @package iTopORM 11036 */ 11037class AttributeCustomFields extends AttributeDefinition 11038{ 11039 const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; 11040 11041 static public function ListExpectedParams() 11042 { 11043 return array_merge(parent::ListExpectedParams(), array("handler_class")); 11044 } 11045 11046 public function GetEditClass() 11047 { 11048 return "CustomFields"; 11049 } 11050 11051 public function IsWritable() 11052 { 11053 return true; 11054 } 11055 11056 static public function LoadFromDB() 11057 { 11058 return false; 11059 } // See ReadValue... 11060 11061 public function GetDefaultValue(DBObject $oHostObject = null) 11062 { 11063 return new ormCustomFieldsValue($oHostObject, $this->GetCode()); 11064 } 11065 11066 public function GetBasicFilterOperators() 11067 { 11068 return array(); 11069 } 11070 11071 public function GetBasicFilterLooseOperator() 11072 { 11073 return ''; 11074 } 11075 11076 public function GetBasicFilterSQLExpr($sOpCode, $value) 11077 { 11078 return ''; 11079 } 11080 11081 /** 11082 * @param DBObject $oHostObject 11083 * @param array|null $aValues 11084 * 11085 * @return CustomFieldsHandler 11086 */ 11087 public function GetHandler($aValues = null) 11088 { 11089 $sHandlerClass = $this->Get('handler_class'); 11090 $oHandler = new $sHandlerClass($this->GetCode()); 11091 if (!is_null($aValues)) 11092 { 11093 $oHandler->SetCurrentValues($aValues); 11094 } 11095 11096 return $oHandler; 11097 } 11098 11099 public function GetPrerequisiteAttributes($sClass = null) 11100 { 11101 $sHandlerClass = $this->Get('handler_class'); 11102 11103 return $sHandlerClass::GetPrerequisiteAttributes($sClass); 11104 } 11105 11106 public function GetEditValue($sValue, $oHostObj = null) 11107 { 11108 return $this->GetForTemplate($sValue, '', $oHostObj, true); 11109 } 11110 11111 /** 11112 * Makes the string representation out of the values given by the form defined in GetDisplayForm 11113 */ 11114 public function ReadValueFromPostedForm($oHostObject, $sFormPrefix) 11115 { 11116 $aRawData = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$this->GetCode()}", '{}', 'raw_data'), 11117 true); 11118 11119 return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aRawData); 11120 } 11121 11122 public function MakeRealValue($proposedValue, $oHostObject) 11123 { 11124 if (is_object($proposedValue) && ($proposedValue instanceof ormCustomFieldsValue)) 11125 { 11126 return $proposedValue; 11127 } 11128 elseif (is_string($proposedValue)) 11129 { 11130 $aValues = json_decode($proposedValue, true); 11131 11132 return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); 11133 } 11134 elseif (is_array($proposedValue)) 11135 { 11136 return new ormCustomFieldsValue($oHostObject, $this->GetCode(), $proposedValue); 11137 } 11138 elseif (is_null($proposedValue)) 11139 { 11140 return new ormCustomFieldsValue($oHostObject, $this->GetCode()); 11141 } 11142 throw new Exception('Unexpected type for the value of a custom fields attribute: '.gettype($proposedValue)); 11143 } 11144 11145 static public function GetFormFieldClass() 11146 { 11147 return '\\Combodo\\iTop\\Form\\Field\\SubFormField'; 11148 } 11149 11150 /** 11151 * Override to build the relevant form field 11152 * 11153 * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the 11154 * $oFormField is passed, MakeFormField behaves more like a Prepare. 11155 */ 11156 public function MakeFormField(DBObject $oObject, $oFormField = null) 11157 { 11158 if ($oFormField === null) 11159 { 11160 $sFormFieldClass = static::GetFormFieldClass(); 11161 $oFormField = new $sFormFieldClass($this->GetCode()); 11162 $oFormField->SetForm($this->GetForm($oObject)); 11163 } 11164 parent::MakeFormField($oObject, $oFormField); 11165 11166 return $oFormField; 11167 } 11168 11169 /** 11170 * @param DBObject $oHostObject 11171 * @param null $sFormPrefix 11172 * 11173 * @return Combodo\iTop\Form\Form 11174 * @throws \Exception 11175 */ 11176 public function GetForm(DBObject $oHostObject, $sFormPrefix = null) 11177 { 11178 try 11179 { 11180 $oValue = $oHostObject->Get($this->GetCode()); 11181 $oHandler = $this->GetHandler($oValue->GetValues()); 11182 $sFormId = is_null($sFormPrefix) ? 'cf_'.$this->GetCode() : $sFormPrefix.'_cf_'.$this->GetCode(); 11183 $oHandler->BuildForm($oHostObject, $sFormId); 11184 $oForm = $oHandler->GetForm(); 11185 } catch (Exception $e) 11186 { 11187 $oForm = new \Combodo\iTop\Form\Form(''); 11188 $oField = new \Combodo\iTop\Form\Field\LabelField(''); 11189 $oField->SetLabel('Custom field error: '.$e->getMessage()); 11190 $oForm->AddField($oField); 11191 $oForm->Finalize(); 11192 } 11193 11194 return $oForm; 11195 } 11196 11197 /** 11198 * Read the data from where it has been stored. This verb must be implemented as soon as LoadFromDB returns false 11199 * and LoadInObject returns true 11200 * 11201 * @param $oHostObject 11202 * 11203 * @return ormCustomFieldsValue 11204 */ 11205 public function ReadValue($oHostObject) 11206 { 11207 try 11208 { 11209 $oHandler = $this->GetHandler(); 11210 $aValues = $oHandler->ReadValues($oHostObject); 11211 $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode(), $aValues); 11212 } catch (Exception $e) 11213 { 11214 $oRet = new ormCustomFieldsValue($oHostObject, $this->GetCode()); 11215 } 11216 11217 return $oRet; 11218 } 11219 11220 /** 11221 * Record the data (currently in the processing of recording the host object) 11222 * It is assumed that the data has been checked prior to calling Write() 11223 * 11224 * @param DBObject $oHostObject 11225 * @param ormCustomFieldsValue|null $oValue (null is the default value) 11226 */ 11227 public function WriteValue(DBObject $oHostObject, ormCustomFieldsValue $oValue = null) 11228 { 11229 if (is_null($oValue)) 11230 { 11231 $oHandler = $this->GetHandler(); 11232 $aValues = array(); 11233 } 11234 else 11235 { 11236 // Pass the values through the form to make sure that they are correct 11237 $oHandler = $this->GetHandler($oValue->GetValues()); 11238 $oHandler->BuildForm($oHostObject, ''); 11239 $oForm = $oHandler->GetForm(); 11240 $aValues = $oForm->GetCurrentValues(); 11241 } 11242 11243 return $oHandler->WriteValues($oHostObject, $aValues); 11244 } 11245 11246 /** 11247 * The part of the current attribute in the object's signature, for the supplied value 11248 * 11249 * @param ormCustomFieldsValue $value The value of this attribute for the object 11250 * 11251 * @return string The "signature" for this field/attribute 11252 */ 11253 public function Fingerprint($value) 11254 { 11255 $oHandler = $this->GetHandler($value->GetValues()); 11256 11257 return $oHandler->GetValueFingerprint(); 11258 } 11259 11260 /** 11261 * Check the validity of the data 11262 * 11263 * @param DBObject $oHostObject 11264 * @param $value 11265 * 11266 * @return bool|string true or error message 11267 */ 11268 public function CheckValue(DBObject $oHostObject, $value) 11269 { 11270 try 11271 { 11272 $oHandler = $this->GetHandler($value->GetValues()); 11273 $oHandler->BuildForm($oHostObject, ''); 11274 $oForm = $oHandler->GetForm(); 11275 $oForm->Validate(); 11276 if ($oForm->GetValid()) 11277 { 11278 $ret = true; 11279 } 11280 else 11281 { 11282 $aMessages = array(); 11283 foreach($oForm->GetErrorMessages() as $sFieldId => $aFieldMessages) 11284 { 11285 $aMessages[] = $sFieldId.': '.implode(', ', $aFieldMessages); 11286 } 11287 $ret = 'Invalid value: '.implode(', ', $aMessages); 11288 } 11289 } catch (Exception $e) 11290 { 11291 $ret = $e->getMessage(); 11292 } 11293 11294 return $ret; 11295 } 11296 11297 /** 11298 * Cleanup data upon object deletion (object id still available here) 11299 * 11300 * @param DBObject $oHostObject 11301 * 11302 * @return 11303 * @throws \CoreException 11304 */ 11305 public function DeleteValue(DBObject $oHostObject) 11306 { 11307 $oValue = $oHostObject->Get($this->GetCode()); 11308 $oHandler = $this->GetHandler($oValue->GetValues()); 11309 11310 return $oHandler->DeleteValues($oHostObject); 11311 } 11312 11313 public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) 11314 { 11315 try 11316 { 11317 $sRet = $value->GetAsHTML($bLocalize); 11318 } catch (Exception $e) 11319 { 11320 $sRet = 'Custom field error: '.htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'); 11321 } 11322 11323 return $sRet; 11324 } 11325 11326 public function GetAsXML($value, $oHostObject = null, $bLocalize = true) 11327 { 11328 try 11329 { 11330 $sRet = $value->GetAsXML($bLocalize); 11331 } catch (Exception $e) 11332 { 11333 $sRet = Str::pure2xml('Custom field error: '.$e->getMessage()); 11334 } 11335 11336 return $sRet; 11337 } 11338 11339 public function GetAsCSV( 11340 $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, 11341 $bConvertToPlainText = false 11342 ) { 11343 try 11344 { 11345 $sRet = $value->GetAsCSV($sSeparator, $sTextQualifier, $bLocalize, $bConvertToPlainText); 11346 } catch (Exception $e) 11347 { 11348 $sFrom = array("\r\n", $sTextQualifier); 11349 $sTo = array("\n", $sTextQualifier.$sTextQualifier); 11350 $sEscaped = str_replace($sFrom, $sTo, 'Custom field error: '.$e->getMessage()); 11351 $sRet = $sTextQualifier.$sEscaped.$sTextQualifier; 11352 } 11353 11354 return $sRet; 11355 } 11356 11357 /** 11358 * List the available verbs for 'GetForTemplate' 11359 */ 11360 public function EnumTemplateVerbs() 11361 { 11362 $sHandlerClass = $this->Get('handler_class'); 11363 11364 return $sHandlerClass::EnumTemplateVerbs(); 11365 } 11366 11367 /** 11368 * Get various representations of the value, for insertion into a template (e.g. in Notifications) 11369 * 11370 * @param $value mixed The current value of the field 11371 * @param $sVerb string The verb specifying the representation of the value 11372 * @param $oHostObject DBObject The object 11373 * @param $bLocalize bool Whether or not to localize the value 11374 * 11375 * @return string 11376 */ 11377 public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) 11378 { 11379 try 11380 { 11381 $sRet = $value->GetForTemplate($sVerb, $bLocalize); 11382 } catch (Exception $e) 11383 { 11384 $sRet = 'Custom field error: '.$e->getMessage(); 11385 } 11386 11387 return $sRet; 11388 } 11389 11390 public function MakeValueFromString( 11391 $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, 11392 $sAttributeQualifier = null 11393 ) { 11394 return null; 11395 } 11396 11397 /** 11398 * Helper to get a value that will be JSON encoded 11399 * The operation is the opposite to FromJSONToValue 11400 * 11401 * @param $value 11402 * 11403 * @return string 11404 */ 11405 public function GetForJSON($value) 11406 { 11407 return null; 11408 } 11409 11410 /** 11411 * Helper to form a value, given JSON decoded data 11412 * The operation is the opposite to GetForJSON 11413 * 11414 * @param string $json 11415 * 11416 * @return array 11417 */ 11418 public function FromJSONToValue($json) 11419 { 11420 return null; 11421 } 11422 11423 public function Equals($val1, $val2) 11424 { 11425 try 11426 { 11427 $bEquals = $val1->Equals($val2); 11428 } catch (Exception $e) 11429 { 11430 $bEquals = false; 11431 } 11432 11433 return $bEquals; 11434 } 11435} 11436 11437class AttributeArchiveFlag extends AttributeBoolean 11438{ 11439 public function __construct($sCode) 11440 { 11441 parent::__construct($sCode, array( 11442 "allowed_values" => null, 11443 "sql" => $sCode, 11444 "default_value" => false, 11445 "is_null_allowed" => false, 11446 "depends_on" => array() 11447 )); 11448 } 11449 11450 public function RequiresIndex() 11451 { 11452 return true; 11453 } 11454 11455 public function CopyOnAllTables() 11456 { 11457 return true; 11458 } 11459 11460 public function IsWritable() 11461 { 11462 return false; 11463 } 11464 11465 public function IsMagic() 11466 { 11467 return true; 11468 } 11469 11470 public function GetLabel($sDefault = null) 11471 { 11472 $sDefault = Dict::S('Core:AttributeArchiveFlag/Label', $sDefault); 11473 11474 return parent::GetLabel($sDefault); 11475 } 11476 11477 public function GetDescription($sDefault = null) 11478 { 11479 $sDefault = Dict::S('Core:AttributeArchiveFlag/Label+', $sDefault); 11480 11481 return parent::GetDescription($sDefault); 11482 } 11483} 11484 11485class AttributeArchiveDate extends AttributeDate 11486{ 11487 public function GetLabel($sDefault = null) 11488 { 11489 $sDefault = Dict::S('Core:AttributeArchiveDate/Label', $sDefault); 11490 11491 return parent::GetLabel($sDefault); 11492 } 11493 11494 public function GetDescription($sDefault = null) 11495 { 11496 $sDefault = Dict::S('Core:AttributeArchiveDate/Label+', $sDefault); 11497 11498 return parent::GetDescription($sDefault); 11499 } 11500} 11501 11502class AttributeObsolescenceFlag extends AttributeBoolean 11503{ 11504 public function __construct($sCode) 11505 { 11506 parent::__construct($sCode, array( 11507 "allowed_values" => null, 11508 "sql" => $sCode, 11509 "default_value" => "", 11510 "is_null_allowed" => false, 11511 "depends_on" => array() 11512 )); 11513 } 11514 11515 public function IsWritable() 11516 { 11517 return false; 11518 } 11519 11520 public function IsMagic() 11521 { 11522 return true; 11523 } 11524 11525 static public function IsBasedOnDBColumns() 11526 { 11527 return false; 11528 } 11529 11530 /** 11531 * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via 11532 * GetOQLExpression) 11533 * 11534 * @return bool 11535 */ 11536 static public function IsBasedOnOQLExpression() 11537 { 11538 return true; 11539 } 11540 11541 public function GetOQLExpression() 11542 { 11543 return MetaModel::GetObsolescenceExpression($this->GetHostClass()); 11544 } 11545 11546 public function GetSQLExpressions($sPrefix = '') 11547 { 11548 return array(); 11549 } 11550 11551 public function GetSQLColumns($bFullSpec = false) 11552 { 11553 return array(); 11554 } // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) 11555 11556 public function GetSQLValues($value) 11557 { 11558 return array(); 11559 } // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update) 11560 11561 public function GetEditClass() 11562 { 11563 return ""; 11564 } 11565 11566 public function GetValuesDef() 11567 { 11568 return null; 11569 } 11570 11571 public function GetPrerequisiteAttributes($sClass = null) 11572 { 11573 return $this->GetOptional("depends_on", array()); 11574 } 11575 11576 public function IsDirectField() 11577 { 11578 return true; 11579 } 11580 11581 static public function IsScalar() 11582 { 11583 return true; 11584 } 11585 11586 public function GetSQLExpr() 11587 { 11588 return null; 11589 } 11590 11591 public function GetDefaultValue(DBObject $oHostObject = null) 11592 { 11593 return $this->MakeRealValue("", $oHostObject); 11594 } 11595 11596 public function IsNullAllowed() 11597 { 11598 return false; 11599 } 11600 11601 public function GetLabel($sDefault = null) 11602 { 11603 $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label', $sDefault); 11604 11605 return parent::GetLabel($sDefault); 11606 } 11607 11608 public function GetDescription($sDefault = null) 11609 { 11610 $sDefault = Dict::S('Core:AttributeObsolescenceFlag/Label+', $sDefault); 11611 11612 return parent::GetDescription($sDefault); 11613 } 11614} 11615 11616class AttributeObsolescenceDate extends AttributeDate 11617{ 11618 public function GetLabel($sDefault = null) 11619 { 11620 $sDefault = Dict::S('Core:AttributeObsolescenceDate/Label', $sDefault); 11621 11622 return parent::GetLabel($sDefault); 11623 } 11624 11625 public function GetDescription($sDefault = null) 11626 { 11627 $sDefault = Dict::S('Core:AttributeObsolescenceDate/Label+', $sDefault); 11628 11629 return parent::GetDescription($sDefault); 11630 } 11631} 11632