1<?php 2/** 3 * Group-Office 4 * 5 * Copyright Intermesh BV. 6 * This file is part of Group-Office. You should have received a copy of the 7 * Group-Office license along with Group-Office. See the file /LICENSE.TXT 8 * 9 * If you have questions write an e-mail to info@intermesh.nl 10 * 11 * @license AGPL/Proprietary http://www.group-office.com/LICENSE.TXT 12 * @link http://www.group-office.com 13 * @copyright Copyright Intermesh BV 14 * @version $Id: Number.php 7962 2011-08-24 14:48:45Z mschering $ 15 * @author Merijn Schering <mschering@intermesh.nl> 16 * @package GO.base.db 17 */ 18 19/** 20 * All Group-Office models should extend this ActiveRecord class. 21 * 22 * @package GO.base.db 23 * @version $Id: File.class.inc.php 7607 2011-06-15 09:17:42Z mschering $ 24 * @copyright Copyright Intermesh BV. 25 * @author Merijn Schering <mschering@intermesh.nl> 26 * @abstract 27 * 28 * @property \GO\Base\Model\User $user If this model has a user_id field it will automatically create this property 29 * @property \GO\Base\Model\Acl $acl If this model has an acl ID configured. See ActiveRecord::aclId it will automatically create this property. 30 * @property bool $isJoinedAclField 31 * @property int/array $pk Primary key value(s) for the model 32 * @property string $module Name of the module this model belongs to 33 * @property boolean $isNew Is the model new and not inserted in the database yet. 34 * @property String $localizedName The localized human friendly name of this model. 35 * @property int $permissionLevel @see \GO\Base\Model\Acl for available levels. Returns -1 if no aclField() is set in the model. 36 * 37 * @property GO\Files\Model\Folder $filesFolder The folder model that belongs to this model if hasFiles is true. 38 */ 39 40 41namespace GO\Base\Db; 42 43use GO\Base\Db\PDO; 44use GO; 45use go\core\db\Query; 46use go\core\orm\CustomFieldsTrait; 47use go\core\orm\SearchableTrait; 48use go\core\util\DateTime; 49 50abstract class ActiveRecord extends \GO\Base\Model{ 51 52 /** 53 * The mode for this model on how to output the attribute data. 54 * Can be "raw", "formatted" or "html"; 55 * 56 * @var StringHelper 57 */ 58 public static $attributeOutputMode='raw'; 59 60 /** 61 * Format attributes on input/output. We want to move to the situatation that 62 * the client does all the formatting and the server doesn't do this anymore. 63 * So on JSON payload requests this will be disabled in the controller. 64 * 65 * @var boolean 66 */ 67 public static $formatAttributesByDefault=true; 68 69 /** 70 * Spaces of all varchar attibutes of the record will be trimmed 71 * To prevent this, set this value to false 72 * @var boolean 73 */ 74 public static $trimOnSave = true; 75 76 /** 77 * This relation is used when the remote model's primary key is stored in a 78 * local attribute. 79 * 80 * Addressbook->user() for example 81 */ 82 const BELONGS_TO=1; // n:1 83 84 /** 85 * This relation type is used when this model has many related models. 86 * 87 * Addressbook->contacts() for example. 88 */ 89 const HAS_MANY=2; // 1:n 90 91 /** 92 * This relation type means that the relation is single and this model's primary 93 * key can be found in the remote model. 94 * 95 * User->Addressbook for example where user_id is in the addressbook table. 96 */ 97 const HAS_ONE=3; // 1:1 98 99 /* 100 * This relation type is used when this model has many related models. 101 * The relation makes use of a linked table that has a combined key of the related model and this model. 102 * 103 * Example use in the model class relationship array: 'users' => array('type'=>self::MANY_MANY, 'model'=>'GO\Base\Model\User', 'linkModel'=>'GO\Base\Model\UserGroups', 'field'=>'group_id', 'remoteField'=>'user_id'), 104 * 105 */ 106 const MANY_MANY=4; // n:n 107 108 /** 109 * Cascade delete relations. Only works on has_one and has_many relations. 110 */ 111 const DELETE_CASCADE = "CASCADE"; 112 113 /** 114 * Restrict delete relations. Only works on has_one and has_many relations. 115 */ 116 const DELETE_RESTRICT = "RESTRICT"; 117 118// /** 119// * The database connection of this record 120// * 121// * @var PDO 122// */ 123// private static $db; 124 125 126 127 private $_attributeLabels; 128 129 public static $db; //The database the active record should use 130 131 /** 132 * Force this activeRecord to save itself 133 * 134 * @var boolean 135 */ 136 private $_forceSave = false; 137 138 /** 139 * See http://dev.mysql.com/doc/refman/5.1/en/insert-delayed.html 140 * 141 * @var boolean 142 */ 143 protected $insertDelayed=false; 144 145 /** 146 * Indiciates that the ActiveRecord is being contructed by PDO. 147 * Used in setAttribute so it skips fancy features that we know will only 148 * cause overhead. 149 * 150 * @var boolean 151 */ 152 protected $loadingFromDatabase=true; 153 154 155 private static $_addedRelations=array(); 156 157 158 159 /** 160 * 161 * @var \GO\Base\Model\Acl 162 */ 163 private $_acl=false; 164 165 private $_isDeleted = false; 166 167 /** 168 * If this property is set the ACL of the model will be changed 169 * Possible values: 170 * - null: will not make any changes to the ACL 171 * - true: will create a new ACL and attach it to this model on save() 172 * - false: will remove the overwritten ACL if it is differend from its parent on save() 173 * and use the ACL from the parent 174 * @see setAcl_overwritten() 175 * @var boolean 176 */ 177 protected $overwriteAcl; 178 179 public function setAcl_overwritten($v) { 180 $this->overwriteAcl = $v; 181 } 182 183 /** 184 * 185 * @var int Link type of this Model used for the link system. See also the linkTo function 186 */ 187 public function modelTypeId(){ 188 return \GO\Base\Model\ModelType::model()->findByModelName($this->className()); 189 } 190 191 /** 192 * For compatibility with new framework 193 * @return type 194 */ 195 public static function entityType() { 196 return \go\core\orm\EntityType::findByClassName(static::class); 197 } 198 199 /** 200 * Get the localized human friendly name of this model. 201 * This function must be overriden. 202 * 203 * @return String 204 */ 205 protected function getLocalizedName(){ 206 207 $parts = explode('\\',$this->className()); 208 $lastPart = array_pop($parts); 209 210 $module = strtolower($parts[1]); 211 212 return GO::t($lastPart, $module); 213 } 214 215 /** 216 * For compatibility with new framework 217 * @return type 218 */ 219 public static function getClientName() { 220 $parts = explode('\\',static::class); 221 return array_pop($parts); 222 } 223 224 225 /** 226 * 227 * Define the relations for the model. 228 * 229 * NOTE: To get relations use getRelations() as it also includes dynamically added relations and automatic relations. 230 * 231 * Example return value: 232 * array( 233 'contacts' => array('type'=>self::HAS_MANY, 'model'=>'GO\Addressbook\Model\Contact', 'field'=>'addressbook_id', 'delete'=>self::DELETE_CASCADE //with this enabled the relation will be deleted along with the model), 234 'companies' => array('type'=>self::HAS_MANY, 'model'=>'GO\Addressbook\Model\Company', 'field'=>'addressbook_id', 'delete'=>self::DELETE_CASCADE), 235 'addressbook' => array( 236 * 'type'=>self::BELONGS_TO, 237 * 'model'=>'GO\Addressbook\Model\Addressbook', 238 * 'field'=>'addressbook_id', 239 * 'labelAttribute'=>function($model){return $model->relation->name;} //this will automatically supply the label for a combobox in a JSON request. 240 * ) 241 'users' => array('type'=>self::MANY_MANY, 'model'=>'GO\Base\Model\User', 'field'=>'group_id', 'linkModel' => 'GO\Base\Model\UserGroup'), // The "field" property is the key of the current model that is defined in the linkModel 242 ); 243 * 244 * The relations can be accessed as functions: 245 * 246 * Model->contacts() for example. They always return a PDO statement. 247 * You can supply FindParams as an optional parameter to narrow down the results. 248 * 249 * Note: relational queries do not check permissions! 250 * 251 * If you have a "user_id" field, an automatic relation model->user() is created that 252 * returns a \GO\Base\Model\User. 253 * 254 * "delete"=>true will automatically delete the relation along with the model. delete flags on BELONGS_TO relations are invalid and will be ignored. 255 * 256 * 257 * You can also select find parameters that will be applied to the relational query. eg.: 258 * 259 * findParams=>FindParams::newInstance()->order('sort_index'); 260 * 261 * @return array relational rules. 262 */ 263 public function relations(){ 264 return array(); 265 } 266 267 /** 268 * Dynamically add a relation to this ActiveRecord. See the relations() function 269 * for a description. 270 * 271 * Example to add the events relation to a user: 272 * 273 * \GO\Base\Model\User::model()->addRelation('events', array( 274 * 'type'=> ActiveRecord::HAS_MANY, 275 * 'model'=>'GO\Calendar\Model\Event', 276 * 'field'=>'user_id' 277 * )); 278 * 279 * @param array $config @see relations 280 */ 281 public function addRelation($name, $config){ 282 self::$_addedRelations[$name]=$config; 283 } 284 285 /** 286 * This is defined as a function because it's a only property that can be set 287 * by child classes. 288 * 289 * @return StringHelper The database table name 290 */ 291 public function tableName(){ 292 return false; 293 } 294 295 /** 296 * The name of the column that has the foreignkey the the ACL record 297 * If column 'acl_id' exists it default to this 298 * You can use field of a relation separated by a dot (eg: 'category.acl_id') 299 * @return StringHelper ACL to check for permissions. 300 */ 301 public function aclField(){ 302 return false; //return isset($this->columns['acl_id']) ? 'acl_id' : false; 303 } 304 305 /** 306 * If the ACL is joined but the table has it's own acl_id column it is 307 * possible to overwrite the ACL 308 * @return boolean|StringHelper the acl_id column name or false if not overwritable 309 */ 310 public function aclOverwrite() { 311 if(!$this->getIsJoinedAclField()) // is there is no dot in aclField() 312 return false; 313 return isset($this->columns['acl_id']) ? 'acl_id' : false; 314 } 315 316 /** 317 * Returns the fieldname that contains primary key of the database table of this model 318 * Can be an array of column names if the PK has more then one column 319 * @return mixed Primary key of database table. Can be a field name string or an array of fieldnames 320 */ 321 public function primaryKey() 322 { 323 return 'id'; 324 } 325 326 private $_relatedCache; 327 328 private $_joinRelationAttr; 329 330 protected $_attributes=array(); 331 332 private $_modifiedAttributes=array(); 333 334 private $_debugSql=false; 335 336 337 /** 338 * Set to true to enable a files module folder for this item. A files_folder_id 339 * column in the database is required. You will probably 340 * need to override buildFilesPath() to make it work properly. 341 * 342 * @return bool true if the Record has an files_folder_id column 343 */ 344 public function hasFiles(){ 345 return isset($this->columns['files_folder_id']); 346 } 347 348 /** 349 * Set to true to always create a files folder. Note that you may not use an auto increment ID in the buildFilesFolder() function when this is set to true. 350 * 351 * @return bool 352 */ 353 public function alwaysCreateFilesFolder() { 354 return (isset($this->acl_id) && !$this->aclOverwrite()); 355 } 356 357 /** 358 * Set to true to enable links for this model. A table go_links_$this->tableName() must be created 359 * with columns: id, model_id, model_type_id 360 * 361 * @return bool 362 */ 363 public function hasLinks(){return false;} 364 365 366 private $_filesFolder; 367 368 /** 369 * Get the folder model belonging to this model if it supports it. 370 * 371 * @param $autoCreate If the folder doesn't exist yet it will create it. 372 * @return \GO\Files\Model\Folder 373 */ 374 public function getFilesFolder($autoCreate=true){ 375 376 if(!$this->hasFiles()) 377 return false; 378 379 if(!isset($this->_filesFolder)){ 380 381 if($autoCreate){ 382 $c = new \GO\Files\Controller\FolderController(); 383 $folder_id = $c->checkModelFolder($this, true, true); 384 }elseif(empty($this->files_folder_id)){ 385 return false; 386 }else 387 { 388 $folder_id = $this->files_folder_id; 389 } 390 391 $this->_filesFolder=\GO\Files\Model\Folder::model()->findByPk($folder_id); 392 if(!$this->_filesFolder && $autoCreate) 393 throw new \Exception("Could not create files folder for ".$this->className()." ".$this->pk); 394 } 395 return $this->_filesFolder; 396 } 397 398 /** 399 * 400 * @return boolean Call $model->isJoinedAclField to check if the aclfield is joined. 401 */ 402 protected function getIsJoinedAclField (){ 403 return strpos($this->aclField(),'.')!==false; 404 } 405 406 /** 407 * Compares this ActiveRecord with $record. 408 * 409 * @param ActiveRecord $record record to compare to or an array of records 410 * @return boolean whether the active records are the same database row. 411 */ 412 public function equals($record) { 413 414 if(!is_array($record) && !($record instanceof \Traversable)){ 415 $record=array($record); 416 } 417 418 foreach($record as $r){ 419 if(get_class($r) != get_class($this)) { 420 return false; 421 } 422 423 if($this->tableName()===$r->tableName() && $this->getPk()===$r->getPk()) 424 { 425 return true; 426 } 427 } 428 return false; 429 } 430 431 /** 432 * The columns array is loaded automatically. Validator rules can be added by 433 * overriding the init() method. 434 * 435 * @var array Holds all the column properties indexed by the field name. 436 * 437 * eg: 'id'=>array( 438 * 'type'=>PDO::PARAM_INT, //Autodetected 439 * 'required'=>true, //Will be true automatically if field in database may not be null and doesn't have a default value 440 * 'length'=><max length of the value>, //Autodetected from db 441 * 'validator'=><a function to call to validate the value>, This may be an array: array("Class", "method", "error message") 442 * 'gotype'=>'number|textfield|textarea|unixtimestamp|unixdate|user|file(GO\Base\Fs\File can be set).', //Autodetected from db as far as possible. See loadColumns() 443 * 'filePathTemplate'=>'Only when gotype='file'. Eg. billing/templates/{id}.{extension} 444 * 'decimals'=>2//only for gotype=number) 445 * 'regex'=>'A preg_match expression for validation', 446 * 'dbtype'=>'varchar' //mysql database type 447 * 'unique'=>false //true|array to enforce a unique value value can me array of related attributes 448 * 'greater'=>'start_time' //this column must be greater than column start time 449 * 'greaterorequal'=>'start_time' //this column must be greater or equal to column start time 450 * The validator looks like this: 451 * 452 * function validate ($value){ 453 return true; 454 } 455 */ 456 protected $columns; 457 458// =array( 459// 'id'=>array('type'=>PDO::PARAM_INT,'required'=>true,'length'=>null, 'validator'=>null,) 460// ); 461// 462 private $_new=true; 463 464 private $_isStaticModel; 465 466 /** 467 * Constructor for the model 468 * 469 * @param boolean $newRecord true if this is a new model 470 * @param boolean true if this is the static model returned by \GO\Base\Model::model() 471 */ 472 public function __construct($newRecord=true, $isStaticModel=false){ 473 474 if(!empty(GO::session()->values['debugSql'])) 475 $this->_debugSql=true; 476 477 $this->_isStaticModel = $isStaticModel; 478 //$pk = $this->pk; 479 480 $this->columns=Columns::getColumns($this); 481 $this->setIsNew($newRecord); 482 483 $this->init(); 484 485 if($this->getIsNew()){ 486 $this->setAttributes($this->getDefaultAttributes(),false); 487 $this->loadingFromDatabase=false; 488 $this->afterCreate(); 489 }elseif(!$isStaticModel){ 490 $this->castMySqlValues(); 491 $this->_cacheRelatedAttributes(); 492 $this->afterLoad(); 493 494 $this->loadingFromDatabase=false; 495 } 496 497 $this->_modifiedAttributes=array(); 498 } 499 500 public function __wakeup() { 501 502 } 503 504 /** 505 * This function is called after the model is constructed by a find query 506 */ 507 protected function afterLoad(){ 508 509 } 510 511 /** 512 * This function is called after a new model is constructed 513 */ 514 protected function afterCreate(){ 515 516 } 517 518 519 /** 520 * When a model is joined on a find action and we need it for permissions, We 521 * select all the model attributes so we don't have to query it seperately later. 522 * eg. $contact->addressbook will work from the cache when it was already joined. 523 */ 524 private function _cacheRelatedAttributes(){ 525 foreach($this->_attributes as $name=>$value){ 526 $arr = explode('@',$name); 527 if(count($arr)>1){ 528 529 $cur = &$this->_joinRelationAttr; 530 531 foreach($arr as $part){ 532 $cur =& $cur[$part]; 533 //$this->_relatedCache[$arr[0]][$arr[1]]=$value; 534 } 535 $cur = $value; 536 537 unset($this->_attributes[$name]); 538 } 539 } 540 } 541 542 /** 543 * Returns localized attribute labels for each column. 544 * 545 * The default language variable name is modelColumn. 546 * 547 * eg.: \GO\Tasks\Model\Task column 'name' will look for: 548 * 549 * $l['taskName'] 550 * 551 * 'due_time' will be 552 * 553 * $l['taskDue_time'] 554 * 555 * If you don't like this you may also override this function in your model. 556 * 557 * @return array 558 * 559 * A key value array eg. array('name'=>'Name', 'due_time'=>'Due time') 560 * 561 */ 562 public function attributeLabels(){ 563 if(!isset($this->_attributeLabels)){ 564 $this->_attributeLabels = array(); 565 566// $classParts = explode('\\',$this->className()); 567// $prefix = strtolower(array_pop($classParts)); 568 569 foreach($this->columns as $columnName=>$columnData){ 570 571 $str = ucfirst(str_replace("_", " ", $columnName)); 572 573 $label = GO::t($str, $this->getModule()); 574 if($label == $str) { 575 $label = GO::t($str); 576 } 577 $this->_attributeLabels[$columnName] = $label; 578 if(!$str == $label) { 579 switch($columnName){ 580 case 'user_id': 581 $this->_attributeLabels[$columnName] = GO::t("Created by"); 582 break; 583 case 'muser_id': 584 $this->_attributeLabels[$columnName] = GO::t("Modified by"); 585 break; 586 587 case 'ctime': 588 $this->_attributeLabels[$columnName] = GO::t("Created at"); 589 break; 590 591 case 'mtime': 592 $this->_attributeLabels[$columnName] = GO::t("Modified at"); 593 break; 594 case 'name': 595 $this->_attributeLabels[$columnName] = GO::t("Name"); 596 break; 597 } 598 } 599 } 600 } 601 return $this->_attributeLabels; 602 } 603 604 605 606 /** 607 * Get the label of the asked attribute 608 * 609 * This function can be overridden in the model. 610 * 611 * @return String The label of the asked attribute 612 */ 613 public function getAttributeLabel($attribute) { 614 615 $labels = $this->attributeLabels(); 616 617 return isset($labels[$attribute]) ? $labels[$attribute] : GO::t(ucfirst(str_replace("_", " ", $attribute))); 618 } 619 620 /** 621 * Set the label of an attribute 622 * 623 * This function can be overridden in the model. 624 * 625 * @param type $attribute 626 * @param type $label 627 */ 628 public function setAttributeLabel($attribute,$label) { 629 $this->columns[$attribute]['label'] = $label; 630 } 631 632 public static function load($pk=null) { 633 $self = GO::getModel(get_called_class()); 634 if($pk !== null) 635 return $self->findByPk($pk); 636 $query = new Query($self); 637 return $query; 638 } 639 640 /** 641 * Can be overriden to initialize the model. Useful for setting attribute 642 * validators in the columns property for example. 643 */ 644 protected function init(){} 645 646 /** 647 * Get's the primary key value. Can also be accessed with $model->pk. 648 * 649 * @return mixed The primary key value 650 */ 651 public function getPk(){ 652 653 $ret = null; 654 655 if(is_array($this->primaryKey())){ 656 foreach($this->primaryKey() as $field){ 657 if(isset($this->_attributes[$field])){ 658 $ret[$field]=$this->_attributes[$field]; 659 }else 660 { 661 $ret[$field]=null; 662 } 663 } 664 }elseif(isset($this->_attributes[$this->primaryKey()])) 665 $ret = $this->_attributes[$this->primaryKey()]; 666 667 return $ret; 668 } 669 670 /** 671 * Check if this model is new and not stored in the database yet. 672 * 673 * @return bool 674 */ 675 public function getIsNew(){ 676 677 return $this->_new; 678 } 679 680 /** 681 * For compatibility with new framework. 682 * 683 * @return bool 684 */ 685 public function isNew() { 686 return $this->getIsNew(); 687 } 688 689 /** 690 * Set if this model is new and not stored in the database yet. 691 * Note: this function is generally only used by the framework internally. 692 * You don't need to set this boolean. The framework takes care of that. 693 * 694 * @param bool $new 695 */ 696 public function setIsNew($new){ 697 698 $this->_new=$new; 699 } 700 701 private $_pdo; 702 703 /** 704 * Returns the database connection used by active record. 705 * By default, the "db" application component is used as the database connection. 706 * You may override this method if you want to use a different database connection. 707 * @return PDO the database connection used by active record. 708 */ 709 public function getDbConnection() 710 { 711 if(isset($this->_pdo)) 712 return $this->_pdo; 713 else 714 return GO::getDbConnection(); 715 } 716 717 /** 718 * Connect the model to another database then the default. 719 * 720 * @param PDO $pdo 721 */ 722 public function setDbConnection($pdo) { 723 $this->_pdo=$pdo; 724 GO::modelCache()->remove($this->className()); 725 } 726 727 private function _getAclJoinProps(){ 728 $arr = explode('.',$this->aclField()); 729 if(count($arr)==2 && !$this->aclOverwrite()){ 730 $r= $this->getRelation($arr[0]); 731 732 return array('table'=>$r['name'], 'relation'=>$r, 'model'=>GO::getModel($r['model']), 'attribute'=>$arr[1]); 733 }else 734 { 735 return array('attribute'=>$this->aclOverwrite() ? $this->aclOverwrite() : $this->aclField(), 'table'=>'t'); 736 } 737 } 738 739 740// private function _joinAclTable(){ 741// $arr = explode('.',$this->aclField()); 742// if(count($arr)==2){ 743// //we need to join a table for the acl field 744// $r= $this->getRelation($arr[0]); 745// $model = GO::getModel($r['model']); 746// 747// $ret['relation']=$arr[0]; 748// $ret['aclField']=$arr[1]; 749// $ret['join']="\nINNER JOIN `".$model->tableName().'` '.$ret['relation'].' ON ('.$ret['relation'].'.`'.$model->primaryKey().'`=t.`'.$r['field'].'`) '; 750// $ret['fields']=''; 751// 752// $cols = $model->getColumns(); 753// 754// foreach($cols as $field=>$props){ 755// $ret['fields'].=', '.$ret['relation'].'.`'.$field.'` AS `'.$ret['relation'].'@'.$field.'`'; 756// } 757// $ret['table']=$ret['relation']; 758// 759// }else 760// { 761// return false; 762// } 763// 764// return $ret; 765// } 766 767 /** 768 * Makes an attribute unique in the table by adding a number behind the name. 769 * eg. Name becomes Name (1) if it already exists. 770 * 771 * @param String $attributeName 772 */ 773 public function makeAttributeUnique($attributeName){ 774 $x = 1; 775 776 $origValue = $value = $this->$attributeName; 777 778 while ($existing = $this->_findExisting($attributeName, $value)) { 779 780 $value = $origValue . ' (' . $x . ')'; 781 $x++; 782 } 783 $this->$attributeName=$value; 784 } 785 786 private function _findExisting($attributeName, $value){ 787 788 $criteria = FindCriteria::newInstance() 789 ->addModel(GO::getModel($this->className())) 790 ->addCondition($attributeName, $value); 791 792 if($this->pk) 793 $criteria->addCondition($this->primaryKey(), $this->pk, '!='); 794 795 $existing = $this->findSingle(FindParams::newInstance() 796 ->criteria($criteria)); 797 798 return $existing; 799 } 800 801 private $_permissionLevel; 802 803 private $_acl_id; 804 805 /** 806 * Find the model that controls permissions for this model. 807 * 808 * @return ActiveRecord 809 * @throws Exception 810 */ 811 public function findRelatedAclModel(){ 812 813 if (!$this->aclField()) 814 return false; 815 816 817 818 $arr = explode('.', $this->aclField()); 819 if (count($arr) > 1) { 820 $relation = $arr[0]; 821 822 //not really used. We use findAclId() of the model. 823 $aclField = array_pop($arr); 824 $modelWithAcl=$this; 825 826 while($relation = array_shift($arr)){ 827 if(!$modelWithAcl->$relation){ 828 throw new \Exception("Could not find relational ACL: ".$this->aclField()." ($relation) in ".$this->className()." with pk: ".$this->pk); 829 }else{ 830 $modelWithAcl=$modelWithAcl->$relation; 831 } 832 } 833 return $modelWithAcl; 834 }else 835 { 836 return false; 837 } 838 } 839 840 841 /** 842 * Check if the acl field is modified. 843 * 844 * Example: acl field is: addressbook.acl_id 845 * Then this function fill search for the addressbook relation and checks if the key is changed in this relation. 846 * If the key is changed then it will return true else it will return false. 847 * 848 * @return boolean 849 */ 850 private function _aclModified(){ 851 $aclFk = $this->_getAclFk(); 852 if($aclFk===false) 853 return false; 854 855 return $this->isModified($aclFk); 856 } 857 858 /** 859 * Get the FK field that link to the model containing the ACL 860 * eg. adressbook_id 861 * @return boolean|StringHelper field name or false if not an related ACL 862 */ 863 private function _getAclFk() { 864 if (!$this->aclField()) 865 return false; 866 867 $arr = explode('.', $this->aclField()); 868 869 if(count($arr)==1) 870 return false; 871 872 $relation = array_shift($arr); 873 $r = $this->getRelation($relation); 874 return $r['field']; 875 } 876 877 878 /** 879 * Find the acl_id integer value that applies to this model. 880 * 881 * @return int ACL id from core_acl_group_items table. 882 */ 883 public function findAclId() { 884 if (!$this->aclField()) { 885 $moduleName = $this->getModule(); 886 return \GO::modules()->{$moduleName}->acl_id; 887 } 888 889 //removed caching of _acl_id because the relation is cached already and when the relation changes the wrong acl_id is returned, 890 ////this happened when moving contacts from one acl to another. 891 //if(!isset($this->_acl_id)){ 892 //ACL is mapped to a relation. eg. $contact->addressbook->acl_id is defined as "addressbook.acl_id" in the contact model. 893 if(!$this->isAclOverwritten()){ 894 $modelWithAcl = $this->findRelatedAclModel(); 895 if($modelWithAcl){ 896 $this->_acl_id = $modelWithAcl->findAclId(); 897 } else { 898 $this->_acl_id = $this->{$this->aclField()}; 899 } 900 }else 901 { 902 $this->_acl_id = $this->{$this->aclOverwrite()}; 903 } 904 //} 905 906 return $this->_acl_id; 907 } 908 909 /** 910 * Returns the permission level for the current user when this model is new 911 * and does not have an ACL yet. This function can be overridden if you don't 912 * like the default action. 913 * By default it only allows new models by module admins. 914 * 915 * @return int 916 */ 917 protected function getPermissionLevelForNewModel(){ 918 //the new model has it's own ACL but it's not created yet. 919 //In this case we will check the module permissions. 920 $module = $this->getModule(); 921 if ($module == 'base') { 922 return GO::user()->isAdmin() ? \GO\Base\Model\Acl::MANAGE_PERMISSION : false; 923 }else 924 return GO::modules()->$module->permissionLevel; 925 } 926 927 /** 928 * Returns the permission level if an aclField is defined in the model. Otherwise 929 * it returns \GO\Base\Model\Acl::MANAGE_PERMISSION; 930 * 931 * @return int \GO\Base\Model\Acl::*_PERMISSION 932 */ 933 934 public function getPermissionLevel(){ 935 936 if(GO::$ignoreAclPermissions) 937 return \GO\Base\Model\Acl::MANAGE_PERMISSION; 938 939 if(!$this->aclField()) 940 return \GO\Base\Model\Acl::MANAGE_PERMISSION; 941 942 if(!GO::user()) 943 return false; 944 945 //if($this->isNew && !$this->joinAclField){ 946 if(empty($this->{$this->aclField()}) && !$this->isJoinedAclField){ 947 return $this->getPermissionLevelForNewModel(); 948 }else 949 { 950 if(!isset($this->_permissionLevel)){ 951 952 $acl_id = $this->findAclId(); 953 if(!$acl_id){ 954 throw new \Exception("Could not find ACL for ".$this->className()." with pk: ".$this->pk); 955 } 956 957 $this->_permissionLevel=\GO\Base\Model\Acl::getUserPermissionLevel($acl_id);// model()->findByPk($acl_id)->getUserPermissionLevel(); 958 } 959 return $this->_permissionLevel; 960 } 961 962 } 963 964 /** 965 * Returns an unique ID string for a find query. That is used to store the 966 * total number of rows in session. This way we don't need to calculate the 967 * total on each pagination page when limit 0,n is used. 968 * 969 * @param array $params 970 * @return StringHelper 971 */ 972 private function _getFindQueryUid($params){ 973 //create unique query id 974 975 unset($params['start'], $params['orderDirection'], $params['order'], $params['limit']); 976 if(isset($params['criteriaObject'])){ 977 $params['criteriaParams']=$params['criteriaObject']->getParams(); 978 $params['criteriaParams']=$params['criteriaObject']->getCondition(); 979 unset($params['criteriaObject']); 980 } 981 //GO::debug($params); 982 return md5(serialize($params).$this->className()); 983 } 984 985 /** 986 * Finds models by attribute and value 987 * This function uses find() to check permissions! 988 * 989 * @param StringHelper $attributeName column name you want to check a value for 990 * @param mixed $value the value to find (needs to be exact) 991 * @param FindParams $findParams Extra parameters to send to the find function. 992 * @return ActiveStatement 993 */ 994 public function findByAttribute($attributeName, $value, $findParams=false){ 995 return $this->findByAttributes(array($attributeName=>$value), $findParams); 996 } 997 998 /** 999 * Finds models by an attribute=>value array. 1000 * This function uses find() to check permissions! 1001 * 1002 * @param array $attributes 1003 * @param FindParams $findParams 1004 * @return static ActiveStatement 1005 */ 1006 public function findByAttributes($attributes, $findParams=false){ 1007 $newParams = FindParams::newInstance(); 1008 $criteria = $newParams->getCriteria()->addModel($this); 1009 1010 foreach($attributes as $attributeName=>$value) { 1011 if(is_array($value)) 1012 $criteria->addInCondition($attributeName, $value); 1013 else 1014 $criteria->addCondition($attributeName, $value); 1015 } 1016 1017 if($findParams) 1018 $newParams->mergeWith ($findParams); 1019 1020 $newParams->ignoreAcl(); 1021 1022 return $this->find($newParams); 1023 } 1024 1025 /** 1026 * Finds a single model by an attribute name and value. 1027 * 1028 * @param StringHelper $attributeName 1029 * @param mixed $value 1030 * @param FindParams $findParams Extra parameters to send to the find function. 1031 * @return static 1032 */ 1033 public function findSingleByAttribute($attributeName, $value, $findParams=false){ 1034 return $this->findSingleByAttributes(array($attributeName=>$value), $findParams); 1035 } 1036 1037 1038 /** 1039 * Finds a single model by an attribute=>value array. 1040 * 1041 * @param StringHelper $attributeName 1042 * @param mixed $value 1043 * @param array $findParams Extra parameters to send to the find function. 1044 * @return static 1045 */ 1046 public function findSingleByAttributes($attributes, $findParams=false){ 1047 1048 $cacheKey = md5(serialize($attributes)); 1049 1050 //Use cache so identical findByPk calls are only executed once per script request 1051 $cachedModel = GO::modelCache()->get($this->className(), $cacheKey); 1052 if($cachedModel) 1053 return $cachedModel; 1054 1055 $newParams = FindParams::newInstance(); 1056 $criteria = $newParams->getCriteria()->addModel($this); 1057 1058 foreach($attributes as $attributeName=>$value) { 1059 if(is_array($value)) 1060 $criteria->addInCondition($attributeName, $value); 1061 else 1062 $criteria->addCondition($attributeName, $value); 1063 } 1064 1065 if($findParams) 1066 $newParams->mergeWith ($findParams); 1067 1068 $newParams->ignoreAcl()->limit(1); 1069 1070 $stmt = $this->find($newParams); 1071 1072 $model = $stmt->fetch(); 1073 1074 GO::modelCache()->add($this->className(), $model, $cacheKey); 1075 1076 return $model; 1077 } 1078 1079 /** 1080 * Finds a single model by an attribute name and value. 1081 * This function does NOT check permissions. 1082 * 1083 * @todo FindSingleByAttributes should use this function when this one uses the FindParams object too. 1084 * 1085 * @param StringHelper $attributeName 1086 * @param mixed $value 1087 * @param FindParams $findParams Extra parameters to send to the find function. 1088 * @return static 1089 */ 1090 public function findSingle($findParams=array()){ 1091 1092 if(!is_array($findParams)) 1093 $findParams = $findParams->getParams(); 1094 1095 $defaultParams=array('limit'=>1, 'start'=>0,'ignoreAcl'=>true); 1096 $params = array_merge($findParams,$defaultParams); 1097 1098 $cacheKey = md5(serialize($params)); 1099 //Use cache so identical findByPk calls are only executed once per script request 1100 $cachedModel = empty($params['disableModelCache']) ? GO::modelCache()->get($this->className(), $cacheKey) : false; 1101 if($cachedModel) 1102 return $cachedModel; 1103 1104 $stmt = $this->find($params); 1105 $models = $stmt->fetchAll(); 1106 1107 $model = isset($models[0]) ? $models[0] : false; 1108 1109 GO::modelCache()->add($this->className(), $model, $cacheKey); 1110 1111 return $model; 1112 } 1113 1114 /** 1115 * Get all default select fields. It excludes BLOBS and TEXT fields. 1116 * This function is used by find. 1117 * 1118 * @param boolean $single 1119 * @param StringHelper $tableAlias 1120 * @return StringHelper 1121 */ 1122 public function getDefaultFindSelectFields($single=false, $tableAlias='t'){ 1123 1124 $fields = array(); 1125 1126 //when upgrading we must refresh columns 1127 if(Columns::$forceLoad) 1128 $this->columns = Columns::getColumns ($this); 1129 1130 if($single) 1131 return $tableAlias.'.*'; 1132 1133 foreach($this->columns as $name=>$attr){ 1134 if(isset($attr['gotype']) && $attr['gotype']!='blob' && $attr['gotype']!='textarea' && $attr['gotype']!='html') 1135 $fields[]=$name; 1136 } 1137 1138 // This is added so we can see the class when this error occurs 1139 if(empty($fields)){ 1140 throw new \Exception('Variable $fields is empty for class: '.self::className()); 1141 } 1142 1143 return "`$tableAlias`.`".implode('`, `'.$tableAlias.'`.`', $fields)."`"; 1144 } 1145 1146 /** 1147 * Create or find an ActiveRecord 1148 * when there is no PK supplied a new instance of the called class will be returned 1149 * else it will pass the PK value to findByPk() 1150 * When a multi column key is used it will create when not found 1151 * @param array $params PK or record to search for 1152 * @return ActiveRecord the called class 1153 * @throws \GO\Base\Exception\NotFound when no record found with supplied PK 1154 */ 1155 public function createOrFindByParams($params) { 1156 1157 $pkColumn = $this->primaryKey(); 1158 if (is_array($pkColumn)) { //if primaryKey excists of multiple columns 1159 $pk = array(); 1160 foreach ($pkColumn as $column) { 1161 if (isset($params[$column])) 1162 $pk[$column] = $this->formatInput($column, $params[$column]); 1163 } 1164 if (empty($pk)) 1165 $model = new static(); 1166 else { 1167 $model = $this->findByPk($pk); 1168 if (!$model) 1169 $model = new static(); 1170 } 1171 1172 if ($model->isNew) 1173 $model->setAttributes($params); 1174 1175 return $model; 1176 } 1177 else { 1178 $pk = isset($params[$this->primaryKey()]) ? $params[$this->primaryKey()] : null; 1179 if (empty($pk)) { 1180 $model = new static(); 1181 if ($model->isNew){ 1182 $model->setAttributes($params); 1183 } 1184 }else { 1185 $model = $this->findByPk($pk); 1186 if (!$model) 1187 $model = new static(); 1188 } 1189 return $model; 1190 } 1191 } 1192 1193 private $useSqlCalcFoundRows=true; 1194 1195 /** 1196 * Find models 1197 * 1198 * Example usage: 1199 * 1200 * <code> 1201 * //create new find params object 1202 * $params = FindParams::newInstance() 1203 * ->joinCustomFields() 1204 * ->order('due_time','ASC'); 1205 * 1206 * //select all from tasklist id = 1 1207 * $params->getCriteria()->addCondition('tasklist_id,1); 1208 * 1209 * //find the tasks 1210 * $stmt = \GO\Tasks\Model\Task::model()->find($params); 1211 * 1212 * //print the names 1213 * while($task = $stmt->fetch()){ 1214 * echo $task->name.'<br>'; 1215 * } 1216 * </code> 1217 * 1218 * 1219 * @param FindParams $params 1220 * @return static ActiveStatement 1221 */ 1222 public function find($params=array()){ 1223 1224 if(!is_array($params)) 1225 { 1226 if(!($params instanceof FindParams)) 1227 throw new \Exception('$params parameter for find() must be instance of FindParams'); 1228 1229 if($params->getParam("export")){ 1230 GO::session()->values[$params->getParam("export")]=array( 1231 'name'=>$params->getParam("export"), 1232 'model'=>$this->className(), 1233 'findParams'=>$params, 1234 'totalizeColumns'=>$params->getParam('export_totalize_columns')); 1235 } 1236 1237 //it must be a FindParams object 1238 $params = $params->getParams(); 1239 } 1240 1241 if(!empty($params['single'])){ 1242 unset($params['single']); 1243 return $this->findSingle($params); 1244 } 1245 1246 if(!empty($params['debugSql'])){ 1247 $this->_debugSql=true; 1248 //GO::debug($params); 1249 }else 1250 { 1251 $this->_debugSql=!empty(GO::session()->values['debugSql']); 1252 } 1253// $this->_debugSql=true; 1254 if(GO::$ignoreAclPermissions) 1255 $params['ignoreAcl']=true; 1256 1257 if(empty($params['userId'])){ 1258 $params['userId']=!empty(GO::session()->values['user_id']) ? GO::session()->values['user_id'] : 1; 1259 } 1260 1261 if($this->aclField() && (empty($params['ignoreAcl']) || !empty($params['joinAclFieldTable']))){ 1262 $aclJoinProps = $this->_getAclJoinProps(); 1263 1264 if(isset($aclJoinProps['relation'])) 1265 $params['joinRelations'][$aclJoinProps['relation']['name']]=array('name'=>$aclJoinProps['relation']['name'], 'type'=>'INNER'); 1266 } 1267 1268 $select = "SELECT "; 1269 1270 if(!empty($params['distinct'])) 1271 $select .= "DISTINCT "; 1272 1273 //Unique query ID for storing found rows in session 1274 $queryUid = $this->_getFindQueryUid($params); 1275 1276 if(!empty($params['calcFoundRows']) && !empty($params['limit']) && (empty($params['start']) || !isset(GO::session()->values[$queryUid]))){ 1277 1278 //TODO: This is MySQL only code 1279 if($this->useSqlCalcFoundRows) 1280 $select .= "SQL_CALC_FOUND_ROWS "; 1281 1282 $calcFoundRows=true; 1283 }else 1284 { 1285 $calcFoundRows=false; 1286 } 1287 1288// $select .= "SQL_NO_CACHE "; 1289 1290 1291 1292 if(empty($params['fields'])) 1293 $params['fields']=$this->getDefaultFindSelectFields(isset($params['limit']) && $params['limit']==1); 1294 else 1295 go()->debug($params['fields']); 1296 1297 1298 $fields = $params['fields'].' '; 1299 1300 $joinRelationSelectFields=''; 1301 $joinRelationjoins=''; 1302 if(!empty($params['joinRelations'])){ 1303 1304 foreach($params['joinRelations'] as $joinRelation){ 1305 1306 $names = explode('.', $joinRelation['name']); 1307 $relationModel = $this; 1308 $relationAlias='t'; 1309 $attributePrefix = ''; 1310 1311 foreach($names as $name){ 1312 $r = $relationModel->getRelation($name); 1313 1314 $attributePrefix.=$name.'@'; 1315 1316 if(!$r) 1317 throw new \Exception("Can't join non existing relation '".$name.'"'); 1318 1319 $model = GO::getModel($r['model']); 1320 $joinRelationjoins .= "\n".$joinRelation['type']." JOIN `".$model->tableName().'` `'.$name.'` ON ('; 1321 1322 switch($r['type']){ 1323 case self::BELONGS_TO: 1324 $joinRelationjoins .= '`'.$name.'`.`'.$model->primaryKey().'`=`'.$relationAlias.'`.`'.$r['field'].'`'; 1325 break; 1326 1327 case self::HAS_ONE: 1328 case self::HAS_MANY: 1329 if(is_array($r['field'])){ 1330 $conditions = array(); 1331 foreach($r['field'] as $my=>$foreign){ 1332 $conditions[]= '`'.$name.'`.`'.$foreign.'`=t.`'.$my.'`'; 1333 } 1334 $joinRelationjoins .= implode(' AND ', $conditions); 1335 }else{ 1336 $joinRelationjoins .= '`'.$name.'`.`'.$r['field'].'`=t.`'.$this->primaryKey().'`'; 1337 } 1338 break; 1339 1340 default: 1341 throw new \Exception("The relation type of ".$name." is not supported by joinRelation or groupRelation"); 1342 break; 1343 } 1344 1345 $joinRelationjoins .=') '; 1346 1347 //if a diffent fetch class is passed then we should not join the relational fields because it makes no sense. 1348 //\GO\Base\Model\Grouped does this for example. 1349 if(empty($params['fetchClass'])){ 1350 $cols = $model->getColumns(); 1351 1352 foreach($cols as $field=>$props){ 1353 $joinRelationSelectFields .=",\n`".$name.'`.`'.$field.'` AS `'.$attributePrefix.$field.'`'; 1354 } 1355 } 1356 1357 $relationModel=$model; 1358 $relationAlias=$name; 1359 1360 } 1361 } 1362 } 1363 1364 1365 $joinCf = !empty($params['joinCustomFields']) && $this->hasCustomFields(); 1366 1367 if($joinCf) { 1368 $cfFieldModels = array_filter(static::getCustomFieldModels(), function($f) { 1369 return $f->getDataType()->hasColumn(); 1370 }); 1371 1372 $names = array_map(function($f) { 1373 if(empty($f->databaseName)) { 1374 throw new Exception("Custom field ". $f->id ." has no databaseName"); 1375 } 1376 return "cf." . $f->databaseName; 1377 }, $cfFieldModels); 1378 1379 if(!empty($names)) { 1380 $fields .= ", " .implode(', ', $names); 1381 } 1382 } 1383 1384 $fields .= $joinRelationSelectFields; 1385 1386 if(!empty($params['groupRelationSelect'])){ 1387 $fields .= ",\n".$params['groupRelationSelect']; 1388 } 1389 1390 $from = "\nFROM `".$this->tableName()."` t ".$joinRelationjoins; 1391 1392 $joins = ""; 1393 if (!empty($params['linkModel'])) { //passed in case of a MANY_MANY relation query 1394 $linkModel = new $params['linkModel']; 1395 $primaryKeys = $linkModel->primaryKey(); 1396 1397 if(!is_array($primaryKeys)) 1398 throw new \Exception ("Fatal error: Primary key of linkModel '".$params['linkModel']."' in relation '".$params['relation']."' should be an array."); 1399 1400 $remoteField = $primaryKeys[0]==$params['linkModelLocalField'] ? $primaryKeys[1] : $primaryKeys[0]; 1401 $joins .= "\nINNER JOIN `".$linkModel->tableName()."` link_t ON t.`".$this->primaryKey()."`= link_t.".$remoteField.' '; 1402 } 1403 1404 1405 if($joinCf) 1406 $joins .= "\nLEFT JOIN `".$this->customFieldsTableName()."` cf ON cf.id=t.id "; 1407 1408 if(isset($aclJoinProps) && empty($params['ignoreAcl'])) 1409 $joins .= $this->_appendAclJoin($params, $aclJoinProps); 1410 1411 if(isset($params['join'])) 1412 $joins .= "\n".$params['join']; 1413 1414 $where = "\nWHERE 1 "; 1415 1416 if(isset($params['criteriaObject'])){ 1417 $conditionSql = $params['criteriaObject']->getCondition(); 1418 if(!empty($conditionSql)) 1419 $where .= "\nAND".$conditionSql; 1420 } 1421 1422 $where = self::_appendByParamsToSQL($where, $params); 1423 1424 if(isset($params['where'])) 1425 $where .= "\nAND ".$params['where']; 1426 1427 if(isset($linkModel)){ 1428 //$primaryKeys = $linkModel->primaryKey(); 1429 //$remoteField = $primaryKeys[0]==$params['linkModelLocalField'] ? $primaryKeys[1] : $primaryKeys[0]; 1430 $where .= " \nAND link_t.`".$params['linkModelLocalField']."` = ".intval($params['linkModelLocalPk'])." "; 1431 } 1432 1433 if(!empty($params['searchQuery'])){ 1434 $where .= " \nAND ("; 1435 1436 if(empty($params['searchQueryFields'])){ 1437 $searchFields = $this->getFindSearchQueryParamFields('t',$joinCf); 1438 }else{ 1439 $searchFields = $params['searchQueryFields']; 1440 } 1441 1442 1443 if(empty($searchFields)) 1444 throw new \Exception("No automatic search fields defined for ".$this->className().". Maybe this model has no varchar fields? You can override function getFindSearchQueryParamFields() or you can supply them with FindParams::searchFields()"); 1445 1446 //`name` LIKE "test" OR `content` LIKE "test" 1447 1448 $first = true; 1449 foreach($searchFields as $searchField){ 1450 if($first){ 1451 $first=false; 1452 }else 1453 { 1454 $where .= ' OR '; 1455 } 1456 $where .= $searchField.' LIKE '.$this->getDbConnection()->quote($params['searchQuery'], PDO::PARAM_STR); 1457 } 1458 1459 if($this->primaryKey()=='id'){ 1460 //Searc on exact ID match too. 1461 $idQuery = trim($params['searchQuery'],'% '); 1462 if(intval($idQuery)."" === $idQuery){ 1463 if($first){ 1464 $first=false; 1465 }else 1466 { 1467 $where .= ' OR '; 1468 } 1469 1470 $where .= 't.id='.intval($idQuery); 1471 } 1472 } 1473 1474 $where .= ') '; 1475 } 1476 1477 $group=""; 1478 if($this->aclField() && empty($params['ignoreAcl']) && (empty($params['limit']) || $params['limit']!=1)){ 1479 1480 //add group by pk so acl join won't return duplicate rows. Don't do this with limit=1 because that makes no sense and causes overhead. 1481 1482 $pk = is_array($this->primaryKey()) ? $this->primaryKey() : array($this->primaryKey()); 1483 1484 $group .= "\nGROUP BY t.`".implode('`,t.`', $pk)."` "; 1485 if(isset($params['group'])) 1486 $group .= ", "; 1487 1488 1489 }elseif(isset($params['group'])){ 1490 $group .= "\nGROUP BY "; 1491 } 1492 1493 if(isset($params['group'])){ 1494 if(!is_array($params['group'])) 1495 $params['group']=array($params['group']); 1496 1497 for($i=0;$i<count($params['group']);$i++){ 1498 if($i>0) 1499 $group .= ', '; 1500 1501 $group .= $this->_quoteColumnName($params['group'][$i]).' '; 1502 } 1503 } 1504 1505 if(isset($params['having'])) 1506 $group.="\nHAVING ".$params['having']; 1507 1508 1509 $order=""; 1510 if(!empty($params['order'])){ 1511 $order .= "\nORDER BY "; 1512 1513 if(!is_array($params['order'])) 1514 $params['order']=array($params['order']); 1515 1516 if(!isset($params['orderDirection'])){ 1517 $params['orderDirection']=array('ASC'); 1518 }elseif(!is_array($params['orderDirection'])){ 1519 $params['orderDirection']=array($params['orderDirection']); 1520 } 1521 1522 for($i=0;$i<count($params['order']);$i++){ 1523 if($i>0) 1524 $order .= ','; 1525 1526 if ($params['order'][$i] instanceof \go\core\db\Expression) { 1527 //if(strpos($params['order'][$i], '(')!==false) { 1528 $order .= $params['order'][$i].' '; 1529 } else { 1530 $order .= $this->_quoteColumnName($params['order'][$i]).' '; 1531 if(isset($params['orderDirection'][$i])){ 1532 $order .= strtoupper($params['orderDirection'][$i])=='ASC' ? 'ASC ' : 'DESC '; 1533 }else{ 1534 $order .= strtoupper($params['orderDirection'][0])=='ASC' ? 'ASC ' : 'DESC '; 1535 } 1536 } 1537 } 1538 } 1539 1540 $limit=""; 1541 if(!empty($params['limit'])){ 1542 if(!isset($params['start'])) 1543 $params['start']=0; 1544 1545 $limit .= "\nLIMIT ".intval($params['start']).','.intval($params['limit']); 1546 } 1547 1548 1549 $sql = $select.$fields.$from.$joins.$where.$group.$order.$limit; 1550 if($this->_debugSql) 1551 $this->_debugSql($params, $sql); 1552 1553 1554 try{ 1555 1556 1557 if($this->_debugSql) 1558 $start = \GO\Base\Util\Date::getmicrotime(); 1559 1560 $result = $this->getDbConnection()->prepare($sql); 1561 1562 if(isset($params['criteriaObject'])){ 1563 $criteriaObjectParams = $params['criteriaObject']->getParams(); 1564 1565 foreach($criteriaObjectParams as $param=>$value) 1566 $result->bindValue($param, $value[0], $value[1]); 1567 1568 $result->execute(); 1569 }elseif(isset($params['bindParams'])){ 1570 $result = $this->getDbConnection()->prepare($sql); 1571 $result->execute($params['bindParams']); 1572 }else 1573 { 1574 $result = $this->getDbConnection()->query($sql); 1575 } 1576 1577 if($this->_debugSql){ 1578 $end = \GO\Base\Util\Date::getmicrotime(); 1579 GO::debug("SQL Query took: ".($end-$start)); 1580 } 1581 1582 }catch(\Exception $e){ 1583 $msg = $e->getMessage(); 1584 1585 if(GO::config()->debug){ 1586 $msg .= "\n\nFull SQL Query: ".$sql; 1587 1588 if(isset($params['bindParams'])){ 1589 $msg .= "\nBind params: ".var_export($params['bindParams'], true); 1590 } 1591 1592 if(isset($criteriaObjectParams)){ 1593 $msg .= "\nBind params: ".var_export($criteriaObjectParams, true); 1594 } 1595 1596 $msg .= "\n\n".$e->getTraceAsString(); 1597 1598 GO::debug($msg); 1599 } 1600 1601 //SQLSTATE[42S22]: Column not found: 1054 Unknown column 'progress' in 'order clause 1602 if(strpos($msg, 'order clause')!==false && strpos($msg, 'Unknown column')!==false) 1603 { 1604 $msg = GO::t("Sorry, you can't sort on that column. Please click on another column header in the grid for sorting."); 1605 } 1606 1607 throw new \Exception($msg); 1608 } 1609 1610 $AS = new ActiveStatement($result, $this); 1611 1612 1613 if(!empty($params['calcFoundRows'])){ 1614 if(!empty($params['limit'])){ 1615 1616 //Total numbers are cached in session when browsing through pages. 1617 if($calcFoundRows){ 1618 1619 if($this->useSqlCalcFoundRows){ 1620// //TODO: This is MySQL only code 1621 $sql = "SELECT FOUND_ROWS() as found;"; 1622 $r2 = $this->getDbConnection()->query($sql); 1623 $record = $r2->fetch(PDO::FETCH_ASSOC); 1624 //$foundRows = intval($record['found']); 1625 $foundRows = GO::session()->values[$queryUid]=intval($record['found']); 1626 }else{ 1627 $countField = is_array($this->primaryKey()) ? '*' : 't.'.$this->primaryKey(); 1628 1629 $sql = $select.'COUNT('.$countField.') AS found '.$from.$joins.$where; 1630 1631// GO::debug($sql); 1632 1633 if($this->_debugSql){ 1634 $this->_debugSql($params, $sql); 1635 $start = \GO\Base\Util\Date::getmicrotime(); 1636 } 1637 1638 $r2 = $this->getDbConnection()->prepare($sql); 1639 1640 if(isset($params['criteriaObject'])){ 1641 $criteriaObjectParams = $params['criteriaObject']->getParams(); 1642 1643 foreach($criteriaObjectParams as $param=>$value) 1644 $r2->bindValue($param, $value[0], $value[1]); 1645 1646 $r2->execute(); 1647 }elseif(isset($params['bindParams'])){ 1648 $r2 = $this->getDbConnection()->prepare($sql); 1649 $r2->execute($params['bindParams']); 1650 }else 1651 { 1652 $r2 = $this->getDbConnection()->query($sql); 1653 } 1654 1655 if($this->_debugSql){ 1656 $end = \GO\Base\Util\Date::getmicrotime(); 1657 GO::debug("SQL Count Query took: ".($end-$start)); 1658 } 1659 1660 $record = $r2->fetch(PDO::FETCH_ASSOC); 1661 1662 1663 1664 1665 1666 //$foundRows = intval($record['found']); 1667 $foundRows = GO::session()->values[$queryUid]=intval($record['found']); 1668 } 1669 } 1670 else 1671 { 1672 $foundRows=GO::session()->values[$queryUid]; 1673 } 1674 1675 1676 $AS->foundRows=$foundRows; 1677 } 1678 } 1679 1680// //$result->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, $this->className()); 1681// if($fetchObject) 1682// $result->setFetchMode(PDO::FETCH_CLASS, $this->className(),array(false)); 1683// else 1684// $result->setFetchMode (PDO::FETCH_ASSOC); 1685 1686 //TODO these values should be set on findByPk too. 1687 $AS->findParams=$params; 1688 if(isset($params['relation'])) 1689 $AS->relation=$params['relation']; 1690 1691 1692 if(!empty($params['fetchClass'])){ 1693 $AS->stmt->setFetchMode(PDO::FETCH_CLASS, $params['fetchClass']); 1694 } 1695 1696 return $AS; 1697 } 1698 1699 public function hasCustomFields() { 1700 return method_exists($this, 'customFieldsTableName'); 1701 } 1702 1703 private function _debugSql($params, $sql){ 1704 1705 1706 $sqlParams = array(); 1707 1708 if(isset($params['criteriaObject'])){ 1709 1710 foreach(($params['criteriaObject']->getParams()) as $param=>$value){ 1711 $sqlParams[$param]=$value[0]; 1712 } 1713 } 1714 1715 if(isset($params['bindParams'])){ 1716 $sqlParams = array_merge($sqlParams, $params['bindParams']); 1717 } 1718 1719 //sort so that :param1 does not replace :param11 first. 1720 arsort($sqlParams); 1721 1722 foreach($sqlParams as $param=>$value){ 1723 $sql = str_replace($param, '"'.$value.'"', $sql); 1724 } 1725 1726 GO::debug($sql); 1727 } 1728 1729 private function _appendAclJoin($findParams, $aclJoinProps){ 1730 1731 1732 1733 $sql = "\nINNER JOIN core_acl_group ON (`".$aclJoinProps['table']."`.`".$aclJoinProps['attribute']."` = core_acl_group.aclId"; 1734 if(isset($findParams['permissionLevel']) && $findParams['permissionLevel']>\GO\Base\Model\Acl::READ_PERMISSION){ 1735 $sql .= " AND core_acl_group.level>=".intval($findParams['permissionLevel']); 1736 } 1737 1738 $groupIds = \GO\Base\Model\User::getGroupIds($findParams['userId']); 1739 1740 if(!empty($findParams['ignoreAdminGroup'])){ 1741 $key = array_search(GO::config()->group_root, $groupIds); 1742 if($key!==false) 1743 unset($groupIds[$key]); 1744 } 1745 1746 1747 $sql .= " AND core_acl_group.groupId IN (".implode(',',$groupIds).")) "; 1748 1749 return $sql; 1750 } 1751 1752 private function _quoteColumnName($name){ 1753 1754 //disallow \ ` and \00 : http://stackoverflow.com/questions/1542627/escaping-field-names-in-pdo-statements 1755 if(preg_match("/[`\\\\\\000\(\),]/", $name)) 1756 throw new \Exception("Invalid characters found in column name: ".$name); 1757 1758 $arr = explode('.',$name); 1759 1760// for($i=0,$max=count($arr);$i<$max;$i++) 1761// $arr[$i]=$this->getDbConnection ()->quote($arr[$i], PDO::PARAM_STR); 1762 1763 return '`'.implode('`.`',$arr).'`'; 1764 } 1765 1766 private function _appendByParamsToSQL($sql, $params){ 1767 if(!empty($params['by'])){ 1768 1769 if(!isset($params['byOperator'])) 1770 $params['byOperator']='AND'; 1771 1772 $first=true; 1773 $sql .= "\nAND ("; 1774 foreach($params['by'] as $arr){ 1775 1776 $field = $arr[0]; 1777 $value= $arr[1]; 1778 $comparator=isset($arr[2]) ? strtoupper($arr[2]) : '='; 1779 1780 if($first) 1781 { 1782 $first=false; 1783 }else 1784 { 1785 $sql .= $params['byOperator'].' '; 1786 } 1787 1788 if($comparator=='IN' || $comparator=='NOT IN'){ 1789 1790 //prevent sql error on empty value 1791 if(!count($value)) 1792 $value=array(0); 1793 1794 for($i=0;$i<count($value);$i++) 1795 $value[$i]=$this->getDbConnection()->quote($value[$i], $this->columns[$field]['type']); 1796 1797 $sql .= "t.`$field` $comparator (".implode(',',$value).") "; 1798 1799 1800 }else 1801 { 1802 if(!isset($this->columns[$field]['type'])) 1803 throw new \Exception($field.' not found in columns for model '.$this->className()); 1804 1805 $sql .= "t.`$field` $comparator ".$this->getDbConnection()->quote($value, $this->columns[$field]['type'])." "; 1806 } 1807 } 1808 1809 $sql .= ') '; 1810 } 1811 return $sql; 1812 } 1813 1814 /** 1815 * Override this method to supply the fields that the searchQuery argument 1816 * will usein the find function. 1817 * 1818 * By default all fields with type PDO::PARAM_STR are returned 1819 * 1820 * @return array Field names that should be used for the search query. 1821 */ 1822 public function getFindSearchQueryParamFields($prefixTable='t', $withCustomFields=false){ 1823 //throw new \Exception('Error: you supplied a searchQuery parameter to find but getFindSearchQueryParamFields() should be overriden in '.$this->className()); 1824 $fields = array(); 1825 foreach($this->columns as $field=>$attributes){ 1826 1827 if($field != 'uuid'){ 1828 if(isset($attributes['gotype']) && ($attributes['gotype']=='textfield' || ($attributes['gotype']=='customfield' && $attributes['customfield']->customfieldtype->includeInSearches()))){ 1829 $fields[]='`'.$prefixTable.'`.`'.$field.'`'; 1830 } 1831 } 1832 } 1833 1834// if($withCustomFields && GO::modules()->customfields && $this->customfieldsRecord && GO::modules()->customfields->permissionLevel) 1835// { 1836// $fields = array_merge($fields, $this->customfieldsRecord->getFindSearchQueryParamFields('cf')); 1837// } 1838 return $fields; 1839 } 1840 1841 private function _appendPkSQL($sql, $primaryKey=false){ 1842 if(!$primaryKey) 1843 $primaryKey=$this->pk; 1844 1845 if(is_array($this->primaryKey())){ 1846 1847 if(!is_array($primaryKey)){ 1848 throw new \Exception('Primary key should be an array for the model '.$this->className()); 1849 } 1850 1851 $first = true; 1852 foreach($primaryKey as $field=>$value){ 1853 //TODO: WHY ARE WE SETTING THIS???? 1854 $this->$field=$value; 1855 if(!$first) 1856 $sql .= ' AND '; 1857 else 1858 $first=false; 1859 1860 if(!isset($this->columns[$field])){ 1861 throw new \Exception($field.' not found in columns of '.$this->className()); 1862 } 1863 1864 $sql .= "`".$field.'`='.$this->getDbConnection()->quote($value, $this->columns[$field]['type']); 1865 } 1866 }else 1867 { 1868 1869 //TODO: WHY ARE WE SETTING THIS???? 1870 $this->{$this->primaryKey()}=$primaryKey; 1871 1872 $sql .= "`".$this->primaryKey().'`='.$this->getDbConnection()->quote($primaryKey, $this->columns[$this->primaryKey()]['type']); 1873 } 1874 return $sql; 1875 } 1876 1877 /** 1878 * Loads the model attributes from the database. It also automatically checks 1879 * read permission for the current user. 1880 * 1881 * @param int $primaryKey 1882 * @return static 1883 */ 1884 1885 public function findByPk($primaryKey, $findParams=false, $ignoreAcl=false, $noCache=false){ 1886 1887// if(GO::config()->debug && $findParams != false){ 1888// throw new \Exception('Adding findparams to findByPk is not yet available'); 1889// } 1890 1891// GO::debug($this->className()."::findByPk($primaryKey)"); 1892 if(empty($primaryKey)) 1893 return false; 1894 1895 //Use cache so identical findByPk calls are only executed once per script request 1896 if(!$noCache){ 1897 $cachedModel = GO::modelCache()->get($this->className(), $primaryKey); 1898// GO::debug("Cached : ".$this->className()."::findByPk($primaryKey)"); 1899 if($cachedModel){ 1900 1901 if($cachedModel && !$ignoreAcl && !$cachedModel->checkPermissionLevel(\GO\Base\Model\Acl::READ_PERMISSION)){ 1902 $msg = GO::config()->debug ? $this->className().' pk: '.var_export($this->getPk(), true) : ''; 1903 throw new \GO\Base\Exception\AccessDenied($msg); 1904 } 1905 1906 return $cachedModel; 1907 } 1908 } 1909 1910 $sql = "SELECT * FROM `".$this->tableName()."` WHERE "; 1911 1912 $sql = $this->_appendPkSQL($sql, $primaryKey); 1913 1914// GO::debug("DEBUG SQL: ".var_export($this->_debugSql, true)); 1915 1916 if($this->_debugSql) 1917 GO::debug($sql); 1918 1919 try{ 1920 $result = $this->getDbConnection()->query($sql); 1921 $result->model=$this; 1922 $result->findParams=$findParams; 1923 1924 $result->setFetchMode(PDO::FETCH_CLASS, $this->className(),array(false)); 1925 1926 $models = $result->fetchAll(); 1927 $model = isset($models[0]) ? $models[0] : false; 1928 }catch(PDOException $e){ 1929 $msg = $e->getMessage()."\n\nFull SQL Query: ".$sql; 1930 1931 throw new \Exception($msg); 1932 } 1933 1934 if($model && !$ignoreAcl && !$model->checkPermissionLevel(\GO\Base\Model\Acl::READ_PERMISSION)){ 1935 $msg = GO::config()->debug ? $this->className().' pk: '.var_export($this->getPk(), true) : ''; 1936 throw new \GO\Base\Exception\AccessDenied($msg); 1937 } 1938 1939 if($model) 1940 GO::modelCache()->add($this->className(), $model); 1941 1942 return $model; 1943 } 1944 1945 /** 1946 * Return the number of model records in the database. 1947 * 1948 * @return int 1949 */ 1950 public function count(){ 1951 $stmt = $this->getDbConnection()->query("SELECT count(*) AS count FROM `".$this->tableName()."`"); 1952 $record = $stmt->fetch(); 1953 return $record['count']; 1954 } 1955 1956 private function _relationExists($name){ 1957 $r= $this->getRelation($name); 1958 1959 return $r!=false; 1960 } 1961 1962 /** 1963 * Get all the relations of this activerecord. Incuding the automatic user and 1964 * mUser relation and dynamically added relations. 1965 * 1966 * @return array 1967 */ 1968 public function getRelations(){ 1969 $r= array_merge($this->relations(), self::$_addedRelations); 1970 1971 if(isset($this->columns['user_id']) && !isset($r['user'])){ 1972 $r['user']=array( 1973 'type'=>self::BELONGS_TO, 1974 'model'=>'GO\Base\Model\User', 1975 'field'=>'user_id', 1976 'labelAttribute'=>function($model){return $model->user->name;} 1977 ); 1978 } 1979 1980 if(isset($this->columns['muser_id']) && !isset($r['mUser'])){ 1981 $r['mUser']=array( 1982 'type'=>self::BELONGS_TO, 1983 'model'=>'GO\Base\Model\User', 1984 'field'=>'muser_id', 1985 'labelAttribute'=>function($model){return !empty($model->mUser) ? $model->mUser->name : '';} 1986 ); 1987 } 1988 1989 1990//\GO::debug($cfMod); 1991// if($this->customfieldsModel()){ 1992// $r['customfields']=array( 1993// 'type'=>self::BELONGS_TO, 1994// 'model'=>$this->customfieldsModel(), 1995// 'field'=>'id' 1996// ); 1997// } 1998 1999 return $r; 2000 } 2001 2002 public function getRelation($name){ 2003 2004 $r = $this->getRelations(); 2005 2006 $this->_checkRelations($r); 2007 2008 if(!isset($r[$name])) 2009 return false; 2010 2011 $r[$name]['name']=$name; 2012 2013 return $r[$name]; 2014 } 2015 2016 private function _checkRelations($r){ 2017 if(GO::config()->debug){ 2018 foreach($r as $name => $attr){ 2019 if(!isset($attr['model'])) 2020 throw new \Exception('model not set in relation '.$name.' '.var_export($attr, true)); 2021 2022 if(isset($this->columns[$name])) 2023 throw new \Exception("Relation $name conflicts with column attribute in ".$this->className()); 2024 2025 $method = 'get'.ucfirst($name); 2026 if($method != 'getType' && method_exists($this, $method)) 2027 throw new \Exception("Relation $name conflicts with getter function $method in ".$this->className()); 2028 2029 if($attr['type']==self::BELONGS_TO && !empty($attr['delete'])){ 2030 throw new \Exception("BELONGS_TO Relation $name may not have a delete flag in ".$this->className()); 2031 } 2032 } 2033 } 2034 } 2035 2036 /** 2037 * Get the findparams object used to query a defined relation. 2038 * 2039 * @param StringHelper $name 2040 * @return FindParams 2041 * @throws Exception 2042 */ 2043 public function getRelationFindParams($name, $extraFindParams=null){ 2044 2045 $r = $this->getRelation($name); 2046 2047 if(!isset($r['findParams'])) 2048 $r['findParams']=FindParams::newInstance(); 2049 2050 if($r['type']==self::HAS_MANY) 2051 { 2052 2053 2054 $findParams = FindParams::newInstance(); 2055 2056 2057 $findParams 2058 ->mergeWith($r['findParams']) 2059 ->ignoreAcl() 2060 ->relation($name); 2061 2062 //the extra find params supplied with call are merged last so that you 2063 //can override the defaults. 2064 if(isset($extraFindParams)) 2065 $findParams->mergeWith($extraFindParams); 2066 2067 2068 if(is_array($r['field'])){ 2069 foreach($r['field'] as $my=>$foreign){ 2070 $findParams->getCriteria() 2071 ->addCondition($my, $this->$foreign); 2072 } 2073 }else{ 2074 $remoteFieldThatHoldsMyPk = $r['field']; 2075 2076 $findParams->getCriteria() 2077 ->addCondition($remoteFieldThatHoldsMyPk, $this->pk); 2078 } 2079 2080 2081 }elseif($r['type']==self::MANY_MANY) 2082 { 2083 2084 $findParams = FindParams::newInstance(); 2085 2086 if(isset($extraFindParams)) 2087 $findParams->mergeWith($extraFindParams); 2088 2089 $findParams->mergeWith($r['findParams']) 2090 ->ignoreAcl() 2091 ->relation($name) 2092 ->linkModel($r['linkModel'], $r['field'], $this->pk); 2093 2094 2095 }else 2096 { 2097 throw new \Exception("getRelationFindParams not supported for ".$r[$name]['type']); 2098 } 2099 2100 return $findParams; 2101 } 2102 2103 2104 private function _getRelatedCacheKey($relation){ 2105 //append join attribute so cache is void automatically when this attribute changes. 2106 2107 if(is_array($relation['field'])) 2108 $relation['field']=implode(',', $relation['field']); 2109 2110 return $relation['name'].':'.(isset($this->_attributes[$relation['field']]) ? $this->_attributes[$relation['field']] : 0); 2111 2112 } 2113 2114 private function _getRelated($name, $extraFindParams=null){ 2115 2116 $r = $this->getRelation($name); 2117 2118 if(!$r) 2119 return false; 2120 2121 $model = $r['model']; 2122 2123 if(!class_exists($model)) //could be a missing module 2124 return false; 2125 2126 2127 2128 if($r['type']==self::BELONGS_TO){ 2129 2130 $joinAttribute = $r['field']; 2131 2132 if(GO::config()->debug && !isset($this->columns[$joinAttribute])){ 2133// var_dump($this->columns); 2134 throw new \Exception("You defined a non existing attribute in the 'field'='$joinAttribute' property in relation '$name' in model '".$this->className()."'"); 2135 } 2136 2137 /** 2138 * Related stuff can be put in the relatedCache array for when a relation is 2139 * accessed multiple times. 2140 * 2141 * Related stuff can also be joined in a query and be passed to the __set 2142 * function as relation@relation_attribute. This array will be used here to 2143 * construct the related model. 2144 */ 2145 2146 //append join attribute so cache is void automatically when this attribute changes. 2147 $cacheKey = $this->_getRelatedCacheKey($r); 2148 2149 if(isset($this->_joinRelationAttr[$name])){ 2150 2151 $attr = $this->_joinRelationAttr[$name]; 2152 2153 $model=new $model(false); 2154 $model->loadingFromDatabase = true; 2155 $model->setAttributes($attr, false); 2156 $model->castMySqlValues(); 2157 $model->loadingFromDatabase = false; 2158 2159 unset($this->_joinRelationAttr[$cacheKey]); 2160 2161 if(!GO::$disableModelCache){ 2162 $this->_relatedCache[$cacheKey] = $model; 2163 } 2164 2165 return $model; 2166 2167 }elseif(!isset($this->_relatedCache[$cacheKey])) 2168 { 2169 //In a belongs to relationship the primary key of the remote model is stored in this model in the attribute "field". 2170 if(!empty($this->_attributes[$joinAttribute])){ 2171 $model = GO::getModel($model)->findByPk($this->_attributes[$joinAttribute], array('relation'=>$name), true); 2172 2173 if(!GO::$disableModelCache){ 2174 $this->_relatedCache[$cacheKey] = $model; 2175 } 2176 2177 return $model; 2178 }else 2179 { 2180 return null; 2181 } 2182 }else 2183 { 2184 return $this->_relatedCache[$cacheKey]; 2185 } 2186 2187 }elseif($r['type']==self::HAS_ONE){ 2188 //We can't put this in the related cache because there's no reliable way to check if the situation has changed. 2189 2190 if(!isset($r['findParams'])) 2191 $r['findParams']=FindParams::newInstance(); 2192 2193 $params =$r['findParams']->relation($name); 2194 if(is_array($r['field'])) { 2195 $fieldKeys = array_keys($r['field']); 2196 $local_key = $fieldKeys[0]; 2197 $fieldValues = array_values($r['field']); 2198 $foreign_key = $fieldValues[0]; 2199 return empty($this->pk) ? false : GO::getModel($model)->findSingleByAttribute($foreign_key, $this->{$local_key}, $params); 2200 } else { 2201 //In a has one to relation ship the primary key of this model is stored in the "field" attribute of the related model. 2202 return empty($this->pk) ? false : GO::getModel($model)->findSingleByAttribute($r['field'], $this->pk, $params); 2203 } 2204 }else{ 2205 $findParams = $this->getRelationFindParams($name,$extraFindParams); 2206 2207 $stmt = GO::getModel($model)->find($findParams); 2208 return $stmt; 2209 } 2210 } 2211 2212 /** 2213 * Formats user input for the database. 2214 * 2215 * @param array $attributes 2216 * @return array 2217 */ 2218 protected function formatInputValues($attributes){ 2219 $formatted = array(); 2220 foreach($attributes as $key=>$value){ 2221 $formatted[$key]=$this->formatInput($key, $value); 2222 } 2223 return $formatted; 2224 } 2225 2226 /** 2227 * Formats user input for the database. 2228 * 2229 * @param StringHelper $column 2230 * @param mixed $value 2231 * @return array 2232 */ 2233 public function formatInput($column, $value){ 2234 if(!isset($this->columns[$column]['gotype'])){ 2235 //don't process unknown columns. But keep them for flexibility. 2236 return $value; 2237 } 2238 2239 switch($this->columns[$column]['gotype']){ 2240 2241 case 'time': 2242 return \GO\Base\Util\Date::toDbTime($value); 2243 break; 2244 2245 case 'unixdate': 2246 case 'unixtimestamp': 2247 if($this->columns[$column]['null'] && ($value=="" || $value==null)) 2248 return null; 2249 else 2250 return \GO\Base\Util\Date::to_unixtime($value); 2251 2252 break; 2253 case 'number': 2254 $value= \GO\Base\Util\Number::unlocalize($value); 2255 2256 if($value===null && !$this->columns[$column]['null']) 2257 $value=0; 2258 2259 return $value; 2260 break; 2261 2262 case 'phone': 2263 2264 //if it contains alpha chars then leave it alone. 2265 if(preg_match('/[a-z]+/i', $value)){ 2266 return $value; 2267 }else{ 2268 return trim(preg_replace('/[\s-_\(\)]+/','', $value)); 2269 } 2270 break; 2271 case 'boolean': 2272 $ret= empty($value) || $value==="false" ? 0 : 1; 2273 return $ret; 2274 break; 2275 case 'date': 2276 return \GO\Base\Util\Date::to_db_date($value); 2277 break; 2278 case 'datetime': 2279 if(empty($value)) 2280 { 2281 return null; 2282 } 2283 $time = \GO\Base\Util\Date::to_unixtime($value); 2284 if(!$time) 2285 { 2286 return null; 2287 } 2288 $date_format = 'Y-m-d H:i:s'; 2289 return date($date_format, $time); 2290 break; 2291 case 'textfield': 2292 return (string) $value; 2293 break; 2294 default: 2295 if($this->columns[$column]['type']==PDO::PARAM_INT){ 2296 if($this->columns[$column]['null'] && $value=="") 2297 $value=null; 2298 else 2299 $value = intval($value); 2300 } 2301 2302 return $value; 2303 break; 2304 } 2305 } 2306 2307 /** 2308 * Format database values for display in the user's locale. 2309 * 2310 * @param bool $html set to true if it's used for html output 2311 * @return array 2312 */ 2313 protected function formatOutputValues($html=false){ 2314 2315 $formatted = array(); 2316 foreach($this->_attributes as $attributeName=>$value){ 2317 $formatted[$attributeName]=$this->formatAttribute($attributeName, $value, $html); 2318 } 2319 2320 return $formatted; 2321 } 2322 2323 public function formatAttribute($attributeName, $value, $html=false){ 2324 if(!isset($this->columns[$attributeName]['gotype'])){ 2325 return $value; 2326 } 2327 2328 switch($this->columns[$attributeName]['gotype']){ 2329 2330 case 'time': 2331 return \GO\Base\Util\Date::formatTime($value); 2332 break; 2333 2334 case 'unixdate': 2335 return \GO\Base\Util\Date::get_timestamp($value, false); 2336 break; 2337 2338 case 'unixtimestamp': 2339 return \GO\Base\Util\Date::get_timestamp($value); 2340 break; 2341 2342 case 'textarea': 2343 if($html){ 2344 return \GO\Base\Util\StringHelper::text_to_html($value); 2345 }else 2346 { 2347 return $value; 2348 } 2349 break; 2350 2351 case 'date': 2352 //strtotime hangs a while on parsing 0000-00-00 from the database. There shouldn't be such a date in it but 2353 //the old system stored dates like this. 2354 2355 if($value == "0000-00-00" || empty($value)) 2356 return ""; 2357 2358 $date = new \DateTime($value); 2359 return $date->format(GO::user()?GO::user()->completeDateFormat:GO::config()->getCompleteDateFormat()); 2360 2361 //return $value != '0000-00-00' ? \GO\Base\Util\Date::get_timestamp(strtotime($value),false) : ''; 2362 break; 2363 2364 case 'datetime': 2365 2366 if($value == "0000-00-00" || empty($value)) 2367 return null; 2368 2369 $date = new \DateTime($value); 2370 return $date->format('c'); 2371 break; 2372 2373 case 'number': 2374 $decimals = isset($this->columns[$attributeName]['decimals']) ? $this->columns[$attributeName]['decimals'] : 2; 2375 return \GO\Base\Util\Number::localize($value, $decimals); 2376 break; 2377 2378 case 'boolean': 2379// Formatting as yes no breaks many functions 2380// if($html) 2381// return !empty($value) ? GO::t("Yes") : GO::t("No"); 2382// else 2383 return !empty($value); 2384 break; 2385 2386 case 'raw': 2387 case 'html': 2388 return $value; 2389 break; 2390 2391 case 'phone': 2392 if($html){ 2393 if(!preg_match('/[a-z]+/i', $value)){ 2394 if( preg_match( '/^(\+\d{2})(\d{2})(\d{3})(\d{4})$/', $value, $matches ) ) 2395 { 2396 return $matches[1] . ' ' .$matches[2] . ' ' . $matches[3].' ' . $matches[4]; 2397 }elseif(preg_match( '/^(\d*)(\d{3})(\d{4})$/', $value, $matches)){ 2398 return '('.$matches[1] . ') ' .$matches[2] . ' ' . $matches[3]; 2399 } 2400 } 2401 } 2402 return $value; 2403 2404 break; 2405 2406 default: 2407 if(substr($this->columns[$attributeName]['dbtype'],-3)=='int') 2408 return $value; 2409 else 2410 return $html ? htmlspecialchars($value, ENT_COMPAT,'UTF-8') : $value; 2411 break; 2412 } 2413 } 2414 2415 /** 2416 * This function is used to set attributes of this model from a controller. 2417 * Input may be in regional format and the model will translate it to the 2418 * database format. 2419 * 2420 * All attributes will be set even if the attributes don't exist in the model. 2421 * The only exception if for relations. You can't set an attribute named 2422 * "someRelation" if it exists in the relations. 2423 * 2424 * The attributes array may also contain custom fields. They will be saved 2425 * automatically. 2426 * 2427 * @param array $attributes attributes to set on this object 2428 */ 2429 2430 public function setAttributes($attributes, $format=null){ 2431 2432 if(!isset($format)){ 2433 $format = ActiveRecord::$formatAttributesByDefault; 2434 } 2435 2436 if($format) 2437 $attributes = $this->formatInputValues($attributes); 2438 2439 foreach($attributes as $key=>$value){ 2440 2441 //only set writable properties. It should either be a column or setter method. 2442 if(isset($this->columns[$key]) || property_exists($this, $key) || method_exists($this, 'set'.$key)){ 2443 $this->$key=$value; 2444 }elseif(is_array($value) && $this->getRelation($key)){ 2445 $this->_joinRelationAttr[$key]=$value; 2446 } 2447 } 2448 } 2449 2450 2451 2452 /** 2453 * Returns all column attribute values. 2454 * Note, related objects are not returned. 2455 * @param StringHelper $outputType Can be 2456 * 2457 * raw: return values as they are stored in the db 2458 * formatted: return the values formatted for an input form 2459 * html: Return the values formatted for HTML display 2460 * 2461 * @return array attribute values indexed by attribute names. 2462 */ 2463 public function getAttributes($outputType=null) 2464 { 2465 2466 if(!isset($outputType)){ 2467 $outputType = ActiveRecord::$formatAttributesByDefault ? 'formatted' : 'raw'; 2468 } 2469 2470 if($outputType=='raw') 2471 $att=$this->_attributes; 2472 else 2473 $att=$this->formatOutputValues($outputType=='html'); 2474 2475 if($this->aclOverwrite()) { 2476 $att['acl_overwritten']=$this->isAclOverwritten(); 2477 } 2478 foreach($this->_getMagicAttributeNames() as $attName){ 2479 $att[$attName]=$this->$attName; 2480 } 2481 2482 return $att; 2483 } 2484 2485 /** 2486 * Get a selection of attributes 2487 * 2488 * @param array $attributeNames 2489 * @param StringHelper $outputType 2490 * @return array 2491 */ 2492 public function getAttributeSelection($attributeNames, $outputType='formatted'){ 2493 $att=array(); 2494 foreach($attributeNames as $attName){ 2495 if(substr($attName, 0, 13) === 'customFields.') { 2496 $att[$attName]=$this->getCustomFields()[substr($attName, 13)] ?? null; 2497 }else if(isset($this->columns[$attName])){ 2498 $att[$attName]=$this->getAttribute($attName, $outputType); 2499 }elseif($this->hasAttribute($attName)){ 2500 $att[$attName]=$this->$attName; 2501 }else 2502 { 2503 $att[$attName]=null; 2504 } 2505 } 2506 return $att; 2507 } 2508 2509 private static $_magicAttributeNames; 2510 2511 private function _getMagicAttributeNames(){ 2512 2513 if(!isset(self::$_magicAttributeNames)) 2514 self::$_magicAttributeNames=GO::cache ()->get('magicattributes'); 2515 2516 if(!isset(self::$_magicAttributeNames[$this->className()])){ 2517 self::$_magicAttributeNames[$this->className()]=array(); 2518 $r = new \ReflectionObject($this); 2519 $publicProperties = $r->getProperties(\ReflectionProperty::IS_PUBLIC); 2520 foreach($publicProperties as $prop){ 2521 //$att[$prop->getName()]=$prop->getValue($this); 2522 //$prop = new \ReflectionProperty(); 2523 if(!$prop->isStatic()) { 2524 //$this->_magicAttributeNames[]=$prop->getName(); 2525 self::$_magicAttributeNames[$this->className()][]=$prop->name; 2526 } 2527 } 2528 2529// $methods = $r->getMethods(); 2530// 2531// foreach($methods as $method){ 2532// $methodName = $method->getName(); 2533// if(substr($methodName,0,3)=='get' && !$method->getNumberOfParameters()){ 2534// 2535// echo $propName = strtolower(substr($methodName,3,1)).substr($methodName,4); 2536// 2537// $this->_magicAttributeNames[]=$propName; 2538// } 2539// } 2540// 2541 GO::cache ()->set('magicattributes', self::$_magicAttributeNames); 2542 } 2543 return self::$_magicAttributeNames[$this->className()]; 2544 } 2545 2546 2547 /** 2548 * Returns all columns 2549 * 2550 * @see ActiveRecord::$columns 2551 * @return array 2552 */ 2553 public function getColumns() 2554 { 2555 return $this->columns; 2556 } 2557 2558 /** 2559 * Returns a column specification see $this->columns; 2560 * 2561 * @see ActiveRecord::$columns 2562 * @return array 2563 */ 2564 public function getColumn($name) 2565 { 2566 if(!isset($this->columns[$name])) 2567 return false; 2568 else 2569 return $this->columns[$name]; 2570 } 2571 2572 public function hasColumn($name) { 2573 return isset($this->columns[$name]); 2574 } 2575 2576 /** 2577 * Checks all the permissions 2578 * 2579 * @todo new item's which don't have ACL should check different ACL for adding new items. 2580 * @return boolean 2581 */ 2582 public function checkPermissionLevel($level){ 2583 2584 if(!$this->aclField()) 2585 return true; 2586 2587 if($this->getPermissionLevel()==-1) 2588 return true; 2589 2590 return $this->getPermissionLevel()>=$level; 2591 } 2592 2593 public function hasPermissionLevel($level) { 2594 return $this->checkPermissionLevel($level); 2595 } 2596 2597 /** 2598 * Check when the permissions level was before moving the object to a differend 2599 * related ACL object eg. moving contact to different addressbook 2600 * @param int $level permissio nlevel to check for 2601 * @return boolean if the user has the specified level 2602 * @throws Exception if the ACL is not found 2603 */ 2604 public function checkOldPermissionLevel($level) { 2605 2606 $arr = explode('.', $this->aclField()); 2607 $relation = array_shift($arr); 2608 $r = $this->getRelation($relation); 2609 $aclFKfield = $r['field']; 2610 2611 $oldValue = $this->getOldAttributeValue($aclFKfield); 2612 if(empty($oldValue)) 2613 return true; 2614 //TODO: check if above code is needed (test by moving contact to differend addresbook) 2615 2616 $acl_id = $this->_getOldParentAclId(); 2617 $result = \GO\Base\Model\Acl::getUserPermissionLevel($acl_id)>=$level; 2618 2619 return $result; 2620 } 2621 2622 /** 2623 * If the related object the contains the ACL is changed this function will 2624 * retrun the ACL of the relational object before it was changed (old ACL) 2625 * @return integer The ACL id 2626 * @throws \Exception 2627 */ 2628 private function _getOldParentAclId() { 2629 $arr = explode('.', $this->aclField()); 2630 $relation = array_shift($arr); 2631 $r = $this->getRelation($relation); 2632 $aclFKfield = $r['field']; 2633 2634 $oldValue = $this->getOldAttributeValue($aclFKfield); 2635 2636 if(empty($oldValue)) 2637 return $this->findAclId(); 2638 2639 $newValue = $this->{$aclFKfield}; 2640 $this->{$aclFKfield} = $oldValue; 2641 $acl_id = $this->findAclId(); 2642 $this->{$aclFKfield} = $newValue; 2643 2644 if(!$acl_id) 2645 throw new \Exception("Could not find ACL for ".$this->className()." with pk: ".$this->pk); 2646 2647 return $acl_id; 2648 } 2649 2650 public function isAclOverwritten() { 2651 if(!$this->aclField() || !$this->aclOverwrite() || $this->getIsNew() || !$this->isJoinedAclField){ 2652 return false; 2653 } 2654 2655 $relatedAclModel = $this->findRelatedAclModel(); 2656 2657 2658// if(!$relatedAclModel) 2659// throw new \Exception(var_export($relatedAclModel, true)); 2660 2661 return $relatedAclModel && $relatedAclModel->findAclId() != $this->{$this->aclOverwrite()}; 2662 } 2663 2664 /** 2665 * Returns a value indicating whether the attribute is required. 2666 * This is determined by checking if the attribute is associated with a 2667 * {@link CRequiredValidator} validation rule in the current {@link scenario}. 2668 * @param StringHelper $attribute attribute name 2669 * @return boolean whether the attribute is required 2670 */ 2671 public function isAttributeRequired($attribute) 2672 { 2673 if(!isset($this->columns[$attribute])) 2674 return false; 2675 return $this->columns[$attribute]['required']; 2676 } 2677 2678 /** 2679 * Do some things before the model will be validated. 2680 */ 2681 protected function beforeValidate(){ 2682 2683 } 2684 2685 /** 2686 * Add a custom validation rule for a column. 2687 * 2688 * Examples of rules: 2689 * 2690 * 'required'=>true, //Will be true automatically if field in database may not be null and doesn't have a default value 2691 * 'length'=><max length of the value>, //Autodetected from db 2692 * 'validator'=><a function to call to validate the value>, This may be an array: array("Class", "method", "error message"). 2693 * 'gotype'=>'number|textfield|textarea|unixtimestamp|unixdate|user', //Autodetected from db as far as possible. See loadColumns() 2694 * 'decimals'=>2//only for gotype=number) 2695 * 'regex'=>'A preg_match expression for validation', 2696 * 'unique'=>false //true to enforce a unique value 2697 * 'greater'=>'start_time' //this column must be greater than column start time 2698 * 'greaterorequal'=>'start_time' //this column must be greater or equal to column start time 2699 * 2700 * @param StringHelper $columnName 2701 * @param StringHelper $ruleName 2702 * @param mixed $value 2703 */ 2704 public function setValidationRule($columnName, $ruleName, $value){ 2705 if(!isset($this->columns[$columnName])) 2706 throw new \Exception("Column $columnName is unknown"); 2707 $this->columns[$columnName][$ruleName]=$value; 2708 2709 $this->_runTimeValidationRules[$columnName]=true; 2710 } 2711 2712 private $_runTimeValidationRules=array(); 2713 2714 /** 2715 * Validates all attributes of this model 2716 * 2717 * @return boolean 2718 */ 2719 2720 public function validate(){ 2721 2722 //foreach($this->columns as $field=>$attributes){ 2723 $this->beforeValidate(); 2724 2725 if($this->isNew){ 2726 //validate all columns 2727 $fieldsToCheck = array_keys($this->columns); 2728 }else 2729 { 2730 //validate modified columns 2731 $fieldsToCheck = array_keys($this->getModifiedAttributes()); 2732 2733 //validate columns with validation rules that were added by controllers 2734 //with setValidateionRule 2735 if(!empty($this->_runTimeValidationRules)){ 2736 $fieldsToCheck= array_unique(array_merge(array_keys($this->_runTimeValidationRules))); 2737 } 2738 } 2739 2740 foreach($fieldsToCheck as $field){ 2741 2742 $attributes=$this->columns[$field]; 2743 2744 if(!empty($attributes['required']) && empty($this->_attributes[$field]) && $this->_attributes[$field] !== '0'){ 2745 $this->setValidationError($field, sprintf(GO::t("Field '%s' is required"),$this->getAttributeLabel($field))); 2746 }elseif(!empty($attributes['length']) && !empty($this->_attributes[$field]) && \GO\Base\Util\StringHelper::length($this->_attributes[$field])>$attributes['length']) 2747 { 2748 $this->setValidationError($field, sprintf(GO::t("Field %s is longer than the maximum of %s characters"),$this->getAttributeLabel($field),$attributes['length'])); 2749 }elseif(!empty($attributes['regex']) && !empty($this->_attributes[$field]) && !preg_match($attributes['regex'], $this->_attributes[$field])) 2750 { 2751 $this->setValidationError($field, sprintf(GO::t("Field %s is formatted incorrectly"),$this->getAttributeLabel($field)).' ('.$this->$field.')'); 2752 }elseif(!empty($attributes['greater']) && !empty($this->_attributes[$field])){ 2753 if($this->_attributes[$field]<=$this->_attributes[$attributes['greater']]) 2754 $this->setValidationError($field, sprintf(GO::t("Field '%s' must be greater than '%s'"), $this->getAttributeLabel($field), $this->getAttributeLabel($attributes['greater']))); 2755 }elseif(!empty($attributes['greaterorequal']) && !empty($this->_attributes[$field])){ 2756 if($this->_attributes[$field]<$this->_attributes[$attributes['greaterorequal']]) 2757 $this->setValidationError($field, sprintf(GO::t("Field '%s' must be greater or equal than '%s'"), $this->getAttributeLabel($field), $this->getAttributeLabel($attributes['greaterorequal']))); 2758 }else { 2759 $this->_validateValidatorFunc ($attributes, $field); 2760 } 2761 } 2762 2763 $this->_validateUniqueColumns(); 2764 2765 $this->fireEvent('validate',array(&$this)); 2766 2767 return !$this->hasValidationErrors(); 2768 } 2769 2770 private function _validateValidatorFunc($attributes, $field){ 2771 $valid=true; 2772 if(!empty($attributes['validator']) && !empty($this->_attributes[$field])) 2773 { 2774 if(is_array($attributes['validator']) && count($attributes['validator'])==3){ 2775 $errorMsg = array_pop($attributes['validator']); 2776 }else 2777 { 2778 $errorMsg = GO::t("Field %s was invalid"); 2779 } 2780 2781 $valid = call_user_func($attributes['validator'], $this->_attributes[$field]); 2782 if(!$valid) 2783 $this->setValidationError($field, sprintf($errorMsg,$this->getAttributeLabel($field))); 2784 } 2785 2786 return $valid; 2787 } 2788 2789 private function _validateUniqueColumns(){ 2790 foreach($this->columns as $field=>$attributes){ 2791 2792 if(!empty($attributes['unique']) && !empty($this->_attributes[$field])){ 2793 2794 $relatedAttributes = array($field); 2795 if(is_array($attributes['unique'])) 2796 $relatedAttributes = array_merge($relatedAttributes,$attributes['unique']); 2797 2798 $modified = false; 2799 foreach($relatedAttributes as $relatedAttribute){ 2800 if($this->isModified($relatedAttribute)) 2801 $modified=true; 2802 } 2803 2804 2805 $where = array(); 2806 if($modified){ 2807 $criteria = FindCriteria::newInstance() 2808 ->addModel(GO::getModel($this->className())) 2809 ->addCondition($field, $this->_attributes[$field]); 2810 2811 if(is_array($attributes['unique'])){ 2812 foreach($attributes['unique'] as $f){ 2813 if(isset($this->_attributes[$f])){ 2814 $criteria->addCondition($f, $this->_attributes[$f]); 2815 $where[$f] = $this->_attributes[$f]; 2816 } 2817 } 2818 } 2819 2820 if(!$this->isNew){ 2821 $where[$this->primaryKey()] = $this->pk; 2822 $criteria->addCondition($this->primaryKey(), $this->pk, '!='); 2823 } 2824 2825 $existing = $this->findSingle(FindParams::newInstance() 2826 ->ignoreAcl() 2827 ->criteria($criteria) 2828 ); 2829 2830 if($existing) { 2831 2832 $msg = str_replace(array('%cf','%val'),array($this->getAttributeLabel($field), $this->_attributes[$field]),GO::t("The value \"%val\" entered for the field \"%cf\" already exists in the database. The field value must be unique. Please enter a different value in that field.", "customfields")); 2833 2834 if(\GO::config()->debug){ 2835 $msg .= var_export($where, true); 2836 } 2837 2838 $this->setValidationError($field, $msg); 2839// $this->setValidationError($field, sprintf(GO::t("%s \"%s\" already exists"),$this->localizedName, $this->_attributes[$field])); 2840 } 2841 } 2842 } 2843 } 2844 } 2845 2846 2847// public function getFilesFolder(){ 2848// if(!$this->hasFiles()) 2849// throw new \Exception("getFilesFolder() called on ".$this->className()." but hasFiles() is false for this model."); 2850// 2851// if($this->files_folder_id==0) 2852// return false; 2853// 2854// return \GO\Files\Model\Folder::model()->findByPk($this->files_folder_id); 2855// 2856// } 2857 2858 /** 2859 * Get the column name of the field this model sorts on. 2860 * It will automatically give the highest number to new models. 2861 * Useful in combination with \GO\Base\Controller\AbstractModelController::actionSubmitMultiple(). 2862 * Drag and drop actions will save the sort order in that action. 2863 * 2864 * @return StringHelper 2865 */ 2866 public function getSortOrderColumn(){ 2867 return false; 2868 } 2869 2870 /** 2871 * Just update the mtime timestamp 2872 */ 2873 public function touch(){ 2874 if ($this->getColumn('mtime')) { 2875 $time = time(); 2876 if($this->mtime==$time){ 2877 return true; 2878 }else{ 2879 $this->mtime=time(); 2880 return $this->_dbUpdate(); 2881 } 2882 } 2883 2884 if ($this->getColumn('modifiedAt')) { 2885 $this->modifiedAt = gmdate('Y-m-d H:i:s'); 2886 return $this->_dbUpdate(); 2887 } 2888 } 2889 2890 /** 2891 * Return true if an update qwery for this record is require override if needed 2892 * @return boolean true if dbupdate if required 2893 */ 2894 protected function dbUpdateRequired(){ 2895 return $this->_forceSave || $this->isNew || $this->isModified();// || ($this->customfieldsRecord !$this->customfieldsRecord->isModified()); 2896 } 2897 2898 /** 2899 * We need to get the modified file columns before save because we need the ID field 2900 * for the filePathTemplate. 2901 */ 2902 private function _getModifiedFileColumns(){ 2903 2904 $cols = array(); 2905 $modified = $this->isNew ? $this->columns : $this->getModifiedAttributes(); 2906 foreach($modified as $column=>$void){ 2907 if($this->columns[$column]['gotype']=='file'){ 2908 $cols[$column]=$this->_attributes[$column]; 2909 2910 if(!$this->isNew){ 2911 $this->resetAttribute($column); 2912 }else 2913 { 2914 $this->_attributes[$column]=""; 2915 } 2916 } 2917 } 2918 2919 return $cols; 2920 } 2921 2922 2923 private function _processFileColumns($cols){ 2924 2925 2926 foreach($cols as $column=>$newValue){ 2927 2928 $oldValue = $this->_attributes[$column]; 2929 2930 if(empty($newValue)){ 2931 2932 //unset of file column 2933 if(!empty($oldValue)){ 2934 $file = new \GO\Base\Fs\File(GO::config()->file_storage_path.$oldValue); 2935 $file->delete(); 2936 $this->$column=""; 2937 } 2938 }elseif($newValue instanceof \GO\Base\Fs\File) 2939 { 2940 if(!isset($this->columns[$column]['filePathTemplate'])){ 2941 throw new \Exception('For file columns you must set a filePathTemplate'); 2942 } 2943 $destination = $this->columns[$column]['filePathTemplate']; 2944 foreach($this->_attributes as $key=>$value){ 2945 $destination = str_replace('{'.$key.'}', $value, $destination); 2946 } 2947 $destination = str_replace('{extension}', $newValue->extension(), $destination); 2948 2949 $destinationFile = new \GO\Base\Fs\File(GO::config()->file_storage_path.$destination); 2950 $destinationFolder = $destinationFile->parent(); 2951 $destinationFolder->create(); 2952 2953 $newValue->move($destinationFolder, $destinationFile->name()); 2954 $this->$column=$destinationFile->stripFileStoragePath(); 2955 2956 2957 }else 2958 { 2959 throw new \Exception("Column $column must be an instance of GO\Base\Fs\File. ".var_export($newValue, true)); 2960 } 2961 } 2962 2963 return !empty($cols); 2964 } 2965 2966 private function _duplicateFileColumns(ActiveRecord $duplicate){ 2967 2968 2969 foreach($this->columns as $column=>$attr){ 2970 if($attr['gotype']=='file'){ 2971 if(!empty($this->_attributes[$column])){ 2972 2973 $file = new \GO\Base\Fs\File(GO::config()->file_storage_path.$this->_attributes[$column]); 2974 2975 $tmpFile = \GO\Base\Fs\File::tempFile('', $file->extension()); 2976 2977 $file->copy($tmpFile->parent(), $tmpFile->name()); 2978 2979 $duplicate->$column=$tmpFile; 2980 } 2981 } 2982 } 2983 2984 } 2985 2986 /** 2987 * Get the URL to download a file column 2988 * 2989 * @param StringHelper $column 2990 * @return StringHelper 2991 */ 2992 public function getFileColumnUrl($column){ 2993 2994 $value= isset($this->_attributes[$column]) ? $this->_attributes[$column] : null; 2995 if(empty($value)) 2996 return false; 2997 2998 if(substr($this->logo,0,7)=='public/'){ 2999 return GO::url('core/downloadPublicFile',array('path'=>substr($value,7))); 3000 }else 3001 { 3002 return GO::url('files/file/download',array('path'=>substr($value,7))); 3003 } 3004 3005 } 3006 3007 private function _moveAllowed() { 3008 $moveAllowed = $this->isNew || !$this->_aclModified() || $this->checkOldPermissionLevel(\GO\Base\Model\Acl::DELETE_PERMISSION); 3009 3010 if(!$moveAllowed){ 3011 3012 $allow = false; 3013 $this->fireEvent('moveallowed', array($this, &$allow)); 3014 return $allow; 3015 } 3016 3017 return $moveAllowed; 3018 } 3019 3020 protected function _trimSpacesFromAttributes() { 3021 if(!static::$trimOnSave) 3022 return; 3023 foreach($this->columns as $field=>$col){ 3024 3025 if(!isset($col['type'])) { 3026 throw new \Exception("Column $field has no type. Does it exist in the database?"); 3027 } 3028 if(isset($this->_attributes[$field]) && $col['type'] == \PDO::PARAM_STR){ 3029 $this->_attributes[$field] = trim($this->_attributes[$field]); 3030 } 3031 } 3032 } 3033 3034 3035 /** 3036 * Saves the model to the database 3037 * 3038 * @var boolean $ignoreAcl 3039 * @return boolean 3040 */ 3041 3042 public function save($ignoreAcl=false){ 3043 3044 //GO::debug('save'.$this->className()); 3045 3046 if(!$ignoreAcl && !$this->checkPermissionLevel($this->isNew?\GO\Base\Model\Acl::CREATE_PERMISSION:\GO\Base\Model\Acl::WRITE_PERMISSION)){ 3047 $msg = GO::config()->debug ? $this->className().' pk: '.var_export($this->pk, true).' acl_id: '.$this->_acl_id : ''; 3048 throw new \GO\Base\Exception\AccessDenied($msg); 3049 } 3050 3051 // when foreignkey to acl field changes check PermissionLevel of origional related ACL object as well 3052 if(!$this->_moveAllowed()){ 3053 $msg = GO::config()->debug ? $this->className().' pk: '.var_export($this->pk, true) : sprintf(GO::t("%s item(s) cannot be moved, you do not have the right permissions."),'1'); 3054 throw new \GO\Base\Exception\AccessDenied($msg); 3055 } 3056 3057 //use private customfields record so it's accessed only when accessed before 3058 if(!$this->validate()){ 3059 return false; 3060 } 3061 3062 3063 /* 3064 * Set some common column values 3065 */ 3066//GO::debug($this->mtime); 3067 3068 if($this->dbUpdateRequired()){ 3069 if(isset($this->columns['mtime']) && (!$this->isModified('mtime') || empty($this->mtime)))//Don't update if mtime was manually set. 3070 $this->mtime=time(); 3071 if(isset($this->columns['ctime']) && empty($this->ctime)){ 3072 $this->ctime=time(); 3073 } 3074 } 3075 3076 if (isset($this->columns['muser_id']) && isset($this->_modifiedAttributes['mtime'])) 3077 $this->muser_id=GO::user() ? GO::user()->id : 1; 3078 3079 if(isset($this->columns['modifiedBy'])) { 3080 $this->modifiedBy = GO::user() ? GO::user()->id : 1; 3081 } 3082 3083 if(isset($this->columns['modifiedAt'])) { 3084 $this->modifiedAt = gmdate("Y-m-d H:i:s"); 3085 } 3086 3087 if(isset($this->columns['createdAt']) && empty($this->createdAt)) { 3088 $this->createdAt = gmdate("Y-m-d H:i:s"); 3089 } 3090 3091 if(isset($this->columns['createdBy']) && empty($this->createdBy)) { 3092 $this->createdBy = GO::user() ? GO::user()->id : 1; 3093 } 3094 3095 //user id is set by defaultAttributes now. 3096 //do not use empty() here for checking the user id because some times it must be 0. eg. core_acl_group 3097// if(isset($this->columns['user_id']) && !isset($this->user_id)){ 3098// $this->user_id=GO::user() ? GO::user()->id : 1; 3099// } 3100 3101 3102 /** 3103 * Useful event for modules. For example custom fields can be loaded or a files folder. 3104 */ 3105 if($this->fireEvent('beforesave',array(&$this))===false) 3106 return false; 3107 3108 $fileColumns = $this->_getModifiedFileColumns(); 3109 3110 if($this->aclOverwrite()) { 3111 3112 if($this->overwriteAcl !== null) { 3113 if($this->overwriteAcl && !$this->isAclOverwritten()) { //Overwrite 3114 3115 3116 $oldAcl = $this->findRelatedAclModel()->acl; 3117 if($oldAcl->getUserLevel() < \GO\Base\Model\Acl::MANAGE_PERMISSION) { 3118 throw new \GO\Base\Exception\AccessDenied("You're not allowed to change permissions"); 3119 } 3120 3121 $user_id = !empty($this->user_id) ? $this->user_id : 0; 3122 $acl = new \GO\Base\Model\Acl(); 3123 $acl->usedIn=$this->tableName().'.'.$this->aclOverwrite(); 3124 $acl->ownedBy=$oldAcl->ownedBy; 3125 $acl->entityTypeId = $this->entityType()->getId(); 3126 $acl->entityId = $this->id; 3127 $acl->save(); 3128 3129 $oldAcl->copyPermissions($acl); 3130 3131 3132 // Attach new ACL id to this object 3133 $this->{$this->aclOverwrite()} = $acl->id; 3134 } elseif(!$this->overwriteAcl && $this->isAclOverwritten()) { // Disoverwrite 3135 $acl = \GO\Base\Model\Acl::model()->findByPk($this->{$this->aclOverwrite()}); 3136 $acl->delete(); 3137 $this->{$this->aclOverwrite()} = $this->findRelatedAclModel()->findAclId(); 3138 } 3139 } 3140// if(!$this->isAclOverwritten() && $this->isJoinedAclField) 3141// $this->{$this->aclOverwrite()} = $this->findRelatedAclModel()->findAclId(); 3142 } 3143 3144 $this->_trimSpacesFromAttributes(); 3145 3146 if($this->isNew){ 3147 3148 //automatically set sort order column 3149 if($this->getSortOrderColumn()) 3150 $this->{$this->getSortOrderColumn()}=$this->nextSortOrder(); 3151 3152 $wasNew=true; 3153 3154 if($this->aclField() && !$this->isJoinedAclField && empty($this->{$this->aclField()})){ 3155 //generate acl id 3156 if(!empty($this->user_id)) 3157 $newAcl = $this->setNewAcl($this->user_id); 3158 else 3159 $newAcl = $this->setNewAcl(GO::user() ? GO::user()->id : 1); 3160 } 3161 3162 3163 3164 if(!$this->beforeSave()){ 3165 GO::debug("WARNING: ".$this->className()."::beforeSave returned false or no value"); 3166 return false; 3167 } 3168 3169 if($this->hasFiles()){ 3170 $this->files_folder_id = 0; 3171 } 3172 3173 $this->_dbInsert(); 3174 $lastInsertId = $this->getDbConnection()->lastInsertId(); 3175 3176 if(isset($newAcl)) { 3177 $newAcl->entityId = $lastInsertId; 3178 $newAcl->save(); 3179 } 3180 3181 if(!is_array($this->primaryKey())){ 3182 if(empty($this->{$this->primaryKey()})){ 3183 3184 if(!$lastInsertId) { 3185 throw new \Exception("Could not get insert ID: $lastInsertId in ".$this->className()."; attributes: ".var_export($this->_attributes, true)); 3186 } 3187 $this->{$this->primaryKey()} = $lastInsertId; 3188 $this->castMySqlValues(array($this->primaryKey())); 3189 } 3190 3191 if(empty($this->{$this->primaryKey()})){ 3192 return false; 3193 } 3194 } 3195 3196 3197 if ($this->hasFiles() && GO::modules()->isInstalled('files')) { 3198 $this->checkModelFolder(); 3199 } 3200 3201 $this->setIsNew(false); 3202 3203 $changed = $this->_processFileColumns($fileColumns); 3204 if($changed || $this->afterDbInsert() || $this->isModified('files_folder_id')){ 3205 $this->_dbUpdate(); 3206 } 3207 }else 3208 { 3209 $wasNew=false; 3210 3211 $this->_processFileColumns($fileColumns); 3212 3213 3214 //change ACL owner 3215 if($this->aclField() && $this->isModified('user_id')) { 3216 $this->acl->ownedBy = $this->user_id; 3217 $this->acl->save(); 3218 } 3219 3220 3221 if ($this->hasFiles() && GO::modules()->isInstalled('files')) { 3222 //ACL must be generated here. 3223 $fc = new \GO\Files\Controller\FolderController(); 3224 $this->files_folder_id = $fc->checkModelFolder($this); 3225 } 3226 3227 if(!$this->beforeSave()){ 3228 GO::debug("WARNING: ".$this->className()."::beforeSave returned false or no value"); 3229 return false; 3230 } 3231 3232 3233 if($this->dbUpdateRequired() && !$this->_dbUpdate()) 3234 return false; 3235 } 3236 3237 //TODO modified custom fields attr? 3238 3239 $this->log($wasNew ? \GO\Log\Model\Log::ACTION_ADD : \GO\Log\Model\Log::ACTION_UPDATE,true, false); 3240 3241 if($this->hasCustomFields() && !$this->saveCustomFields()) { 3242 return false; 3243 } 3244 3245 if(!$this->afterSave($wasNew)){ 3246 GO::debug("WARNING: ".$this->className()."::afterSave returned false or no value"); 3247 return false; 3248 } 3249 3250 if(!$wasNew){ 3251 $this->_fixLinkedEmailAcls(); 3252 } 3253 3254 /** 3255 * Useful event for modules. For example custom fields can be loaded or a files folder. 3256 */ 3257 $this->fireEvent('save',array(&$this,$wasNew)); 3258 3259 3260 $this->cacheSearchRecord(); 3261 3262 $this->_modifiedAttributes = array(); 3263 3264 return true; 3265 } 3266 3267 protected function nextSortOrder() { 3268 return $this->count(); 3269 } 3270 3271 protected function checkModelFolder() { 3272 //ACL must be generated here. 3273 $fc = new \GO\Files\Controller\FolderController(); 3274 $this->files_folder_id = $fc->checkModelFolder($this); 3275 3276 } 3277 3278 /** 3279 * Get the message for the log module. Returns the contents of the first text column by default. 3280 * 3281 * @return StringHelper 3282 */ 3283 public function getLogMessage($action){ 3284 3285 $attr = $this->getCacheAttributes(); 3286 if($attr){ 3287 $msg = $attr['name']; 3288 if(isset($attr['description'])) 3289 $msg.="\n".$attr['description']; 3290 return $msg; 3291 }else 3292 return false; 3293 } 3294 3295 /** 3296 * Get the JSON data string for the given log action 3297 * 3298 * @param string $action 3299 * @return array Data for the JSON string 3300 */ 3301 public function getLogJSON($action,$modifiedCustomfieldAttrs=false){ 3302 3303 $cutoffString = ' ..Cut off at 500 chars.'; 3304 $cutoffLength = 500; 3305 3306 switch($action){ 3307 case \GO\Log\Model\Log::ACTION_DELETE: 3308 return $this->getAttributes(); 3309 case \GO\Log\Model\Log::ACTION_UPDATE: 3310 $oldValues = $this->getModifiedAttributes(); 3311 3312 $modifications = array(); 3313 foreach($oldValues as $key=>$oldVal){ 3314 3315 if(!is_scalar($oldVal)) { 3316 continue; 3317 } 3318 3319 $newVal = $this->getAttribute($key); 3320 3321 if(!is_scalar($newVal)) { 3322 continue; 3323 } 3324 3325// // Check if the value changed from false, to null 3326// if(is_null($newVal) && $oldVal === false){ 3327// continue; 3328// } 3329// 3330 // Check if the value changed from false, to null 3331 if(empty($newVal) && empty($oldVal)){ 3332 continue; 3333 } 3334 3335 if(strlen($newVal) > $cutoffLength){ 3336 $newVal = substr($newVal,0,$cutoffLength).$cutoffString; 3337 } 3338 3339 if(strlen($oldVal) > $cutoffLength){ 3340 $oldVal = substr($oldVal,0,$cutoffLength).$cutoffString; 3341 } 3342 3343 $modifications[$key]=array($oldVal,$newVal); 3344 } 3345 3346 // Also track customfieldsrecord changes 3347// if($this->customfieldsRecord && $modifiedCustomfieldAttrs){ 3348// 3349// foreach($modifiedCustomfieldAttrs as $key=>$oldVal){ 3350// $newVal = $this->customfieldsRecord->getAttribute($key); 3351// if(empty($newVal) && empty($oldVal)){ 3352// continue; 3353// } 3354// 3355// if(strlen($newVal) > $cutoffLength){ 3356// $newVal = substr($newVal,0,$cutoffLength).$cutoffString; 3357// } 3358// 3359// if(strlen($oldVal) > $cutoffLength){ 3360// $oldVal = substr($oldVal,0,$cutoffLength).$cutoffString; 3361// } 3362// 3363// $attrLabel = $this->getCustomfieldsRecord()->getAttributeLabelWithoutCategoryName($key); 3364// 3365// $modifications[$attrLabel.' ('.$key.')']=array($oldVal,$newVal); 3366// } 3367// } 3368 3369 3370 return $modifications; 3371 case \GO\Log\Model\Log::ACTION_ADD: 3372 $attrs = $this->getAttributes(); 3373 $logAttrs = array(); 3374 foreach($attrs as $attr=>$val){ 3375 3376 if(!is_scalar($val)) { 3377 continue; 3378 } 3379 3380 $newVal = $this->getAttribute($attr); 3381 3382 if(!is_scalar($newVal)) { 3383 continue; 3384 } 3385 3386 if(strlen($val) > $cutoffLength){ 3387 $newVal = substr($newVal,0,$cutoffLength).$cutoffString; 3388 } 3389 3390 $logAttrs[$attr] = $newVal; 3391 } 3392 3393 return $logAttrs; 3394 } 3395 3396 return array(); 3397 } 3398 3399 public static $log_enabled = true; 3400 3401 /** 3402 * Will all a log record in go_log 3403 * Made protected to be used in \GO\Files\Model\File 3404 * @param StringHelper $action 3405 * @param boolean $save set the false to not directly save the create Log record 3406 * @return boolean|\GO\Log\Model\Log returns the created log or succuss status when save is true 3407 */ 3408 protected function log($action, $save=true, $modifiedCustomfieldAttrs=false){ 3409 // jsonData field in go_log might not exist yet during upgrade 3410 if(!self::$log_enabled) { 3411 return true; 3412 } 3413 $message = $this->getLogMessage($action); 3414 if($message && GO::modules()->isInstalled('log')){ 3415 3416 $data = $this->getLogJSON($action,$modifiedCustomfieldAttrs); 3417 3418 $log = new \GO\Log\Model\Log(); 3419 3420 $pk = $this->pk; 3421 $log->model_id=is_array($pk) ? var_export($pk, true) : $pk; 3422 3423 $log->action=$action; 3424 $log->model=$this->className(); 3425 $log->message = $message; 3426 $log->object=$this; 3427 $log->jsonData = json_encode($data); 3428 if($save) 3429 return $log->save(); 3430 else 3431 return $log; 3432 } 3433 } 3434 3435 /** 3436 * Acl id's of linked emails are copies from the model they are linked too. 3437 * For example an e-mail linked to a contact will get the acl id of the addressbook. 3438 * When you move a contact to another contact all the acl id's must change. 3439 */ 3440 private function _fixLinkedEmailAcls(){ 3441 if($this->hasLinks() && GO::modules()->isInstalled('savemailas')){ 3442 $arr = explode('.', $this->aclField()); 3443 if (count($arr) > 1) { 3444 3445 $relation = $this->getRelation($arr[0]); 3446 3447 if($relation && $this->isModified($relation['field'])){ 3448 //acl relation changed. We must update linked emails 3449 3450 GO::debug("Fixing linked e-mail acl's because relation ".$arr[0]." changed."); 3451 3452 $stmt = \GO\Savemailas\Model\LinkedEmail::model()->findLinks($this); 3453 if($stmt->rowCount()) { 3454 $aclId = $this->findAclId(); 3455 while ($linkedEmail = $stmt->fetch()) { 3456 3457 GO::debug("Updating " . $linkedEmail->subject); 3458 3459 $linkedEmail->acl_id = $aclId; 3460 $linkedEmail->save(); 3461 } 3462 } 3463 } 3464 } 3465 } 3466 } 3467 3468 3469 /** 3470 * Sometimes you need the auto incremented primary key to generate another 3471 * property. Like the UUID of an event or task. 3472 * Or in a project number for example where you want to generate a number 3473 * like PR00023 where 23 is the id for example. 3474 * 3475 * @return boolean NOTE: Only return true if a database update is needed. 3476 */ 3477 protected function afterDbInsert(){ 3478 return false; 3479 } 3480 3481 3482 /** 3483 * Get a key value array of modified attribute names with their old values 3484 * that are not saved to the database yet. 3485 * 3486 * e. array('attributeName'=>'Old value') 3487 * 3488 * @return array 3489 */ 3490 public function getModifiedAttributes(){ 3491 return $this->_modifiedAttributes; 3492 } 3493 3494 /** 3495 * Reset modified attributes information. Useful when setting properties but 3496 * avoid a save to the database. 3497 */ 3498 public function clearModifiedAttributes(){ 3499 $this->_modifiedAttributes=array(); 3500 } 3501 3502 /** 3503 * Set a new ACL for this model. You need to save the model after calling this 3504 * function. 3505 * 3506 * @param StringHelper $user_id 3507 * @return \GO\Base\Model\Acl 3508 */ 3509 public function setNewAcl($user_id=0){ 3510 if($this->aclField()===false) 3511 throw new \Exception('Can not create a new ACL for an object that has no ACL field'); 3512 if(!$user_id) 3513 $user_id = GO::user() ? GO::user()->id : 1; 3514 3515 $acl = new \GO\Base\Model\Acl(); 3516 $acl->usedIn = $this->tableName().'.'.$this->aclField(); 3517 $acl->ownedBy=$user_id; 3518 $acl->entityTypeId = $this->entityType()->getId(); 3519 $acl->entityId = $this->id; 3520 if(!$acl->save()) { 3521 throw new \Exception("Could not save ACL: ".var_export($this->getValidationErrors(), true)); 3522 } 3523 3524 $this->{$this->aclField()}=$acl->id; 3525 3526 return $acl; 3527 } 3528 3529 /** 3530 * Check is this model or model attribute name has modifications not saved to 3531 * the database yet. 3532 * 3533 * @param string/array $attributeName 3534 * @return boolean 3535 */ 3536 public function isModified($attributeName=false){ 3537 if(!$attributeName){ 3538 return count($this->_modifiedAttributes)>0; 3539 }else 3540 { 3541 if(is_array($attributeName)){ 3542 foreach($attributeName as $a){ 3543 if(isset($this->_modifiedAttributes[$a])) 3544 { 3545 return true; 3546 } 3547 } 3548 return false; 3549 }else 3550 { 3551 return isset($this->_modifiedAttributes[$attributeName]); 3552 } 3553 } 3554 } 3555 3556 /** 3557 * Reset attribute to it's original value and clear the modified attribute. 3558 * 3559 * @param StringHelper $name 3560 */ 3561 public function resetAttribute($name){ 3562 $this->$name = $this->getOldAttributeValue($name); 3563 unset($this->_modifiedAttributes[$name]); 3564 } 3565 3566 /** 3567 * Reset attributes to it's original value and clear the modified attributes. 3568 */ 3569 public function resetAttributes(){ 3570 foreach($this->_modifiedAttributes as $name => $oldValue){ 3571 $this->$name = $oldValue; 3572 unset($this->_modifiedAttributes[$name]); 3573 } 3574 } 3575 3576 /** 3577 * Get the old value for a modified attribute. 3578 * 3579 * @param String $attributeName 3580 * @return mixed 3581 */ 3582 public function getOldAttributeValue($attributeName){ 3583 return isset($this->_modifiedAttributes[$attributeName]) ? $this->_modifiedAttributes[$attributeName] : false; 3584 } 3585 3586 /** 3587 * The files module will use this function. To create a files folder. 3588 * Override it if you don't like the default path. Make sure this path is unique! Appending the (<id>) would be wise. 3589 */ 3590 public function buildFilesPath() { 3591 3592 return isset($this->name) ? $this->getModule().'/' . \GO\Base\Fs\Base::stripInvalidChars($this->name) : false; 3593 } 3594 3595 /** 3596 * Put this model in the go_search_cache table as a \GO\Base\Model\SearchCacheRecord so it's searchable and linkable. 3597 * Generally you don't need to do this. It's called from the save function automatically when getCacheAttributes is overridden. 3598 * This method is only public so that the maintenance script can access it to rebuid the search cache. 3599 * 3600 * @return boolean 3601 */ 3602 public function cacheSearchRecord(){ 3603 3604 //don't do this on datbase checks. 3605 if(GO::router()->getControllerAction()=='checkdatabase') 3606 return false; 3607 3608 $attr = $this->getCacheAttributes(); 3609 if(!$attr) { 3610 return false; 3611 } 3612 3613 $search = \go\core\model\Search::find()->where('entityTypeId','=', static::entityType()->getId())->andWhere('entityId', '=', $this->id)->single(); 3614 if(!$search) { 3615 $search = new \go\core\model\Search(); 3616 $search->setEntity(static::entityType()); 3617 } 3618 3619 if(isset($attr['mtime'])) { 3620 $attr['modifiedAt'] = \DateTime::createFromFormat("U", $attr['mtime']); 3621 3622 } else { 3623 $attr['modifiedAt'] = \DateTime::createFromFormat("U", $this->mtime); 3624 } 3625 unset($attr['mtime']); 3626 3627 // Always unset ctime, we don't use it anymore in the searchcache table 3628 unset($attr['ctime']); 3629 unset($attr['type']); 3630 3631 if(!isset($attr['description'])) { 3632 $attr['description'] = ''; 3633 } 3634 $search->setValues($attr); 3635 unset($attr['modifiedAt']); 3636 3637 $search->entityId = $this->id; 3638 $search->setAclId(!empty($attr['aclId']) ? $attr['aclId'] : $this->findAclId()); 3639 //$search->createdAt = \DateTime::createFromFormat("U", $this->mtime); 3640 $search->setKeywords($this->getSearchCacheKeywords($this->localizedName.','.implode(',', $attr))); 3641 3642 //todo cut lengths 3643 3644 if(!$search->save()) { 3645 throw new \Exception("Could not save search cache!"); 3646 } 3647 3648// //GO::debug($attr); 3649// 3650// if($attr){ 3651// 3652// $model = \GO\Base\Model\SearchCacheRecord::model()->findByPk(array('model_id'=>$this->pk, 'model_type_id'=>$this->modelTypeId()),false,true); 3653// 3654// if(!$model) 3655// $model = new \GO\Base\Model\SearchCacheRecord(); 3656// 3657// $model->mtime=0; 3658// 3659// $acl_id = !empty($attr['acl_id']) ? $attr['acl_id'] : $this->findAclId(); 3660// 3661// //if model doesn't have an acl we use the acl of the module it belongs to. 3662// if(!$acl_id) 3663// $acl_id = GO::modules()->{$this->getModule ()}->acl_id; 3664// 3665// $defaultUserId = isset(GO::session()->values['user_id']) ? GO::session()->values['user_id'] : 1; 3666// 3667// //cache type in default system language. 3668// if(GO::user()) 3669// GO::language()->setLanguage(GO::config()->language); 3670// 3671// 3672// //GO::debug($model); 3673// $autoAttr = array( 3674// 'model_id'=>$this->pk, 3675// 'model_type_id'=>$this->modelTypeId(), 3676// 'user_id'=>isset($this->user_id) ? $this->user_id : $defaultUserId, 3677// 'module'=>$this->module, 3678// 'model_name'=>$this->className(), 3679// 'name' => '', 3680// 'description'=>'', 3681// 'type'=>$this->localizedName, //deprecated, for backwards compatibilty 3682// 'keywords'=>$this->getSearchCacheKeywords($this->localizedName.','.implode(',', $attr)), 3683// 'mtime'=>$this->mtime, 3684// 'ctime'=>$this->ctime, 3685// 'acl_id'=>$acl_id 3686// ); 3687// 3688// $attr = array_merge($autoAttr, $attr); 3689// 3690// if(GO::user()) 3691// GO::language()->setLanguage(GO::user()->language); 3692// 3693// if($attr['description']==null) 3694// $attr['description']=""; 3695// 3696// $model->setAttributes($attr, false); 3697// $model->cutAttributeLengths(); 3698//// $model->save(true); 3699// if(!$model->save(true)){ 3700// throw new \Exception("Error saving search cache record:\n".implode("\n", $model->getValidationErrors())); 3701// } 3702// 3703// return $model; 3704// 3705// } 3706// return false; 3707 3708 return true; 3709 } 3710 3711 3712 /** 3713 * Cut all attributes to their maximum lengths. Useful when importing stuff. 3714 */ 3715 public function cutAttributeLengths(){ 3716 $attr = $this->getModifiedAttributes(); 3717 foreach($attr as $attributeName=>$oldVal){ 3718// if(!empty($this->columns[$attribute]['length']) && \GO\Base\Util\StringHelper::length($this->_attributes[$attribute])>$this->columns[$attribute]['length']){ 3719// $this->_attributes[$attribute]=\GO\Base\Util\StringHelper::substr($this->_attributes[$attribute], 0, $this->columns[$attribute]['length']); 3720// } 3721 $this->cutAttributeLength($attributeName); 3722 } 3723 } 3724 3725 /** 3726 * Cut an attribute's value to it's maximum length in the database. 3727 * 3728 * @param StringHelper $attributeName 3729 */ 3730 public function cutAttributeLength($attributeName){ 3731 3732 if($this->columns[$attributeName]['dbtype'] == 'text' || $this->columns[$attributeName]['dbtype'] == 'mediumtext'){ 3733 $this->_attributes[$attributeName]= substr($this->_attributes[$attributeName], 0, $this->columns[$attributeName]['length']); 3734 } else if(!empty($this->columns[$attributeName]['length']) && \GO\Base\Util\StringHelper::length($this->_attributes[$attributeName]) > $this->columns[$attributeName]['length']){ 3735 $this->_attributes[$attributeName]=\GO\Base\Util\StringHelper::substr($this->_attributes[$attributeName], 0, $this->columns[$attributeName]['length']); 3736 } 3737 } 3738 3739 public function getCachedSearchRecord(){ 3740 $model = \GO\Base\Model\SearchCacheRecord::model()->findByPk(array('model_id'=>$this->pk, 'model_type_id'=>$this->modelTypeId())); 3741 if($model) 3742 return $model; 3743 else 3744 return $this->cacheSearchRecord (); 3745 } 3746 3747 /** 3748 * Override this function if you want to put your model in the search cache. 3749 * 3750 * @return array cache parameters with at least 'name', 'description' and 'type'. All are strings. See \GO\Base\ModelSearchCacheRecord for more info. 3751 */ 3752 protected function getCacheAttributes(){ 3753 return false; 3754 } 3755 3756 /** 3757 * Get keywords this model should be found on. 3758 * Returns all String properties in a concatenated string. 3759 * 3760 * @param String $prepend 3761 * @return String 3762 */ 3763 public function getSearchCacheKeywords($prepend=''){ 3764 $keywords=array(); 3765 3766 foreach($this->columns as $key=>$attr) 3767 { 3768 if(isset($this->$key)){ 3769 $value = $this->$key; 3770 3771 if(is_string($value) && ($attr['gotype']=='textfield' || $attr['gotype']=='customfield' || $attr['gotype']=='textarea') && !in_array($value,$keywords)){ 3772 if(!empty($value)) { 3773 if($attr['gotype'] == 'textarea') { 3774 $keywords = array_merge($keywords, SearchableTrait::splitTextKeywords($value)); 3775 } else { 3776 $keywords[] = $value; 3777 } 3778 } 3779 } 3780 } 3781 } 3782 3783 if (method_exists($this, 'getCustomFields')) { 3784 $keywords = array_merge($keywords, $this->getCustomFieldsSearchKeywords()); 3785 } 3786 3787 if($this->hasLinks()) { 3788 3789 $links = (new Query()) 3790 ->select('description') 3791 ->from('core_link') 3792 ->where('(toEntityTypeId = :e1 AND toId = :e2)') 3793 ->orWhere('(fromEntityTypeId = :e3 AND fromId = :e4)') 3794 ->bind([':e1' => static::entityType()->getId(), ':e2' => $this->id, ':e3' => static::entityType()->getId(), ':e4' => $this->id]); 3795 foreach ($links->all() as $link) { 3796 if (!empty($link['description']) && is_string($link['description'])) { 3797 $keywords[] = $link['description']; 3798 } 3799 } 3800 } 3801 3802 $keywords = $prepend.','.implode(',',$keywords); 3803 3804 3805 // Remove duplicate and empty entries 3806 $arr = explode(',', $keywords); 3807 $arr = array_filter(array_unique($arr), function($item){ 3808 return $item != ''; 3809 }); 3810 return implode(' ', $arr); 3811 } 3812 3813 protected function beforeSave(){ 3814 3815 return true; 3816 } 3817 3818 /** 3819 * May be overridden to do stuff after save 3820 * 3821 * @var bool $wasNew True if the model was new before saving 3822 * @return boolean 3823 */ 3824 protected function afterSave($wasNew){ 3825 return true; 3826 } 3827 3828 /** 3829 * Inserts the model into the database 3830 * 3831 * @return boolean 3832 */ 3833 protected function _dbInsert(){ 3834 3835 $fieldNames = array(); 3836 3837 //Build an array of fields that are set in the object. Unset columns will 3838 //not be in the SQL query so default values from the database are respected. 3839 foreach($this->columns as $field=>$col){ 3840 if(isset($this->_attributes[$field])){ 3841 $fieldNames[]=$field; 3842 } 3843 } 3844 3845 3846 $sql = "INSERT "; 3847 3848 if($this->insertDelayed) 3849 $sql .= "DELAYED "; 3850 3851 $sql .= "INTO `{$this->tableName()}` (`".implode('`,`', $fieldNames)."`) VALUES ". 3852 "(:ins".implode(',:ins', array_keys($fieldNames)).")"; 3853 3854 if($this->_debugSql){ 3855 $bindParams = array(); 3856 foreach($fieldNames as $field){ 3857 $bindParams[$field]=$this->_attributes[$field]; 3858 } 3859 $this->_debugSql(array('bindParams'=>$bindParams), $sql); 3860 } 3861 3862 try{ 3863 $stmt = $this->getDbConnection()->prepare($sql); 3864 3865 foreach($fieldNames as $i => $field){ 3866 3867 $attr = $this->columns[$field]; 3868 3869 $stmt->bindParam(':ins'.$i, $this->_attributes[$field], $attr['type'], empty($attr['length']) ? null : $attr['length']); 3870 } 3871 $ret = $stmt->execute(); 3872 }catch(\Exception $e){ 3873 3874 $msg = $e->getMessage(); 3875 3876 if(GO::config()->debug){ 3877 $msg .= "\n\nFull SQL Query: ".$sql."\n\nParams:\n".var_export($this->_attributes, true); 3878 3879 $msg .= "\n\n".$e->getTraceAsString(); 3880 3881 GO::debug($msg); 3882 } 3883 throw new \Exception($msg); 3884 } 3885 3886 return $ret; 3887 } 3888 3889 3890 private function _dbUpdate(){ 3891 3892 $updates=array(); 3893 3894 //$pks = is_array($this->primaryKey()) ? $this->primaryKey() : array($this->primaryKey()); 3895// foreach($this->columns as $field => $value) 3896// { 3897// if(!in_array($field,$pks)) 3898// { 3899// $updates[] = "`$field`=:".$field; 3900// } 3901// } 3902// 3903 $i = 0; 3904 $paramMap = []; 3905 $bindParams=array(); 3906 foreach($this->_modifiedAttributes as $field=>$oldValue) { 3907 $i++; 3908 $tag = ":upd".$i; 3909 $bindParams[$tag]=$field; 3910 3911 $updates[] = "`$field` = ".$tag; 3912 3913 $i++; 3914 } 3915 3916 if(!count($updates)) 3917 return true; 3918 3919 $sql = "UPDATE `{$this->tableName()}` SET ".implode(',',$updates)." WHERE "; 3920 3921 3922 $pk = $this->primaryKey(); 3923 if(!is_array($pk)){ 3924 $pk = [$pk]; 3925 } 3926 3927 $first=true; 3928 foreach($pk as $field){ 3929 if(!$first) 3930 $sql .= ' AND '; 3931 else 3932 $first=false; 3933 3934 $i++; 3935 $tag = ":upd".$i; 3936 $bindParams[$tag]=$field; 3937 $sql .= "`".$field."` = ".$tag; 3938 3939 } 3940 3941 3942 try{ 3943 $stmt = $this->getDbConnection()->prepare($sql); 3944 3945 //$pks = is_array($this->primaryKey()) ? $this->primaryKey() : array($this->primaryKey()); 3946 3947 foreach($bindParams as $tag => $field){ 3948 $attr = $this->getColumn($field); 3949 $stmt->bindParam($tag, $this->_attributes[$field], $attr['type'], empty($attr['length']) ? null : $attr['length']); 3950 } 3951 3952 if($this->_debugSql) 3953 $this->_debugSql(array('bindParams'=>$bindParams), $sql); 3954 3955 $ret = $stmt->execute(); 3956 if($this->_debugSql){ 3957 GO::debug("Affected rows: ".$ret); 3958 } 3959 }catch(\Exception $e){ 3960 $msg = $e->getMessage(); 3961 3962 if(GO::config()->debug){ 3963 $msg .= "\n\nFull SQL Query: ".$sql."\n\nParams:\n".var_export($bindParams, true); 3964 3965 $msg .= "\n\n".$e->getTraceAsString(); 3966 3967 GO::debug($msg); 3968 } 3969 throw new \Exception($msg); 3970 } 3971 return $ret; 3972 } 3973 3974 protected function beforeDelete(){ 3975 return true; 3976 } 3977 protected function afterDelete(){ 3978 return true; 3979 } 3980 3981 /** 3982 * Delete's the model from the database 3983 * @return PDOStatement 3984 */ 3985 public function delete($ignoreAcl=false){ 3986 3987 GO::setMaxExecutionTime(180); // Added this because the deletion of all relations sometimes takes a lot of time (3 minutes) 3988 3989 //GO::debug("Delete ".$this->className()." pk: ".$this->pk); 3990 3991 if($this->isNew) 3992 return true; 3993 3994 if(!$ignoreAcl && !$this->checkPermissionLevel(\GO\Base\Model\Acl::DELETE_PERMISSION)){ 3995 $msg = GO::config()->debug ? $this->className().' pk: '.var_export($this->pk, true) : ''; 3996 throw new \GO\Base\Exception\AccessDenied ($msg); 3997 } 3998 3999 4000 if(!$this->beforeDelete() || $this->fireEvent('beforedelete', array(&$this, $ignoreAcl))===false) 4001 return false; 4002 4003 $r= $this->relations(); 4004 4005 foreach($r as $name => $attr){ 4006 if (!GO::classExists($attr['model'])){ 4007 unset($r[$name]); 4008 continue; 4009 } 4010 4011 if(!empty($attr['delete']) && $attr['type']!=self::BELONGS_TO){ 4012 4013 //for backwards compatibility 4014 if($attr['delete']===true) 4015 $attr['delete']=ActiveRecord::DELETE_CASCADE; 4016 4017 switch($attr['delete']){ 4018 4019 case ActiveRecord::DELETE_CASCADE: 4020 $result = $this->$name; 4021 4022 if($result instanceof ActiveStatement){ 4023 //has_many relations result in a statement. 4024 while($child = $result->fetch()){ 4025 if($child->className()!=$this->className() || $child->pk != $this->pk)//prevent delete of self 4026 $child->delete($ignoreAcl); 4027 } 4028 }elseif($result) 4029 { 4030 //single relations return a model. 4031 $result->delete($ignoreAcl); 4032 } 4033 break; 4034 4035 case ActiveRecord::DELETE_RESTRICT: 4036 if($attr['type']==self::HAS_ONE) 4037 $result = $this->$name; 4038 else 4039 $result = $this->$name(FindParams::newInstance()->single()); 4040 4041 if($result){ 4042 throw new \GO\Base\Exception\RelationDeleteRestrict($this, $attr); 4043 } 4044 4045 break; 4046 } 4047 } 4048 4049 //clean up link models for many_many relations 4050 if($attr['type']==self::MANY_MANY){// && class_exists($attr['linkModel'])){ 4051 $stmt = GO::getModel($attr['linkModel'])->find( 4052 FindParams::newInstance() 4053 ->criteria(FindCriteria::newInstance() 4054 ->addModel(GO::getModel($attr['linkModel'])) 4055 ->addCondition($attr['field'], $this->pk) 4056 ) 4057 ); 4058 $stmt->callOnEach('delete'); 4059 unset($stmt); 4060 } 4061 } 4062 4063 //Set the foreign fields of the deleted relations to 0 because the relation doesn't exist anymore. 4064 //We do this in a separate loop because relations that should be deleted should be processed first. 4065 //Consider these relation definitions: 4066 // 4067 // 'messagesCustomer' => array('type'=>self::HAS_MANY, 'model'=>'GO\Tickets\Model\Message', 'field'=>'ticket_id', 'findParams'=>FindParams::newInstance()->order('id','DESC')->select('t.*')->criteria(FindCriteria::newInstance()->addCondition('is_note', 0))), 4068 // 'messagesNotes' => array('type'=>self::HAS_MANY, 'model'=>'GO\Tickets\Model\Message', 'field'=>'ticket_id', 'findParams'=>FindParams::newInstance()->order('id','DESC')->select('t.*')->criteria(FindCriteria::newInstance()->addCondition('is_note', 0))), 4069 // 'messages' => array('type'=>self::HAS_MANY, 'model'=>'GO\Tickets\Model\Message', 'field'=>'ticket_id','delete'=>true, 'findParams'=>FindParams::newInstance()->order('id','DESC')->select('t.*')), 4070 // 4071 // messagesCustomer and messagesNotes are just subsets of the messages 4072 // relation that must all be deleted anyway. We don't want to clear foreign keys first and then fail to delete them. 4073 4074 foreach($r as $name => $attr){ 4075 if(empty($attr['delete'])){ 4076 if($attr['type']==self::HAS_ONE){ 4077 //set the foreign field to 0. Because it doesn't exist anymore. 4078 $model = $this->$name; 4079 if($model){ 4080 4081 $columns = $model->getColumns(); 4082 4083 $model->{$attr['field']}=$columns[$attr['field']]['null'] ? null : 0; 4084 $model->save(); 4085 } 4086 }elseif($attr['type']==self::HAS_MANY){ 4087 //set the foreign field to 0 because it doesn't exist anymore. 4088 $stmt = $this->$name; 4089 4090 while($model = $stmt->fetch()){ 4091 4092 $columns = $model->getColumns(); 4093 4094 $model->{$attr['field']}=$columns[$attr['field']]['null'] ? null : 0; 4095 $model->save(); 4096 } 4097 } 4098 } 4099 } 4100 4101 $sql = "DELETE FROM `".$this->tableName()."` WHERE "; 4102 $sql = $this->_appendPkSQL($sql); 4103 4104 //remove cached models 4105 GO::modelCache()->remove($this->className()); 4106 4107 4108 if($this->_debugSql) 4109 GO::debug($sql); 4110 4111 $success = $this->getDbConnection()->query($sql); 4112 if(!$success) 4113 throw new \Exception("Could not delete from database"); 4114 4115 $this->_isDeleted = true; 4116 4117 $this->log(\GO\Log\Model\Log::ACTION_DELETE); 4118 4119 $attr = $this->getCacheAttributes(); 4120 4121 if($attr){ 4122 \go\core\model\Search::delete(['entityId' => $this->pk, 'entityTypeId'=>$this->modelTypeId()]); 4123 } 4124 4125 if($this->hasFiles() && $this->files_folder_id > 0 && GO::modules()->isInstalled('files')){ 4126 $folder = \GO\Files\Model\Folder::model()->findByPk($this->files_folder_id,false,true); 4127 if($folder) 4128 $folder->delete(true); 4129 } 4130 4131 4132 if($this->aclField() && (!$this->isJoinedAclField || $this->isAclOverwritten())){ 4133 //echo 'Deleting acl '.$this->{$this->aclField()}.' '.$this->aclField().'<br />'; 4134 $aclField = $this->isAclOverwritten() ? $this->aclOverwrite() : $this->aclField(); 4135 4136 $acl = \GO\Base\Model\Acl::model()->findByPk($this->{$aclField}); 4137 if($acl) { 4138 $acl->delete(); 4139 } 4140 } 4141 4142 4143 $this->_deleteLinks(); 4144 4145 4146 if(!$this->afterDelete()) 4147 return false; 4148 4149 if($this->hasLinks() && !is_array($this->pk)) { 4150 $this->deleteReminders(); 4151 } 4152 4153 $this->fireEvent('delete', array(&$this)); 4154 4155 return true; 4156 } 4157 4158 public function isDeleted(){ 4159 return $this->_isDeleted; 4160 } 4161 4162 4163 private function _deleteLinks(){ 4164 //cleanup links 4165 if($this->hasLinks()){ 4166 4167 $sql = "DELETE FROM core_link WHERE fromEntityTypeId=".intval($this->modelTypeId()).' AND fromId='.intval($this->pk); 4168 $this->getDbConnection()->query($sql); 4169 4170 $sql = "DELETE FROM core_link WHERE toEntityTypeId=".intval($this->modelTypeId()).' AND toId='.intval($this->pk); 4171 $this->getDbConnection()->query($sql); 4172 } 4173 } 4174 4175// /** 4176// * Set the output mode for this model. The default value can be set globally 4177// * too with ActiveRecord::$attributeOutputMode. 4178// * It can be 'raw', 'formatted' or 'html'. 4179// * 4180// * @param type $mode 4181// */ 4182// public function setAttributeOutputMode($mode){ 4183// if($mode!='raw' && $mode!='formatted' && $mode!='html') 4184// throw new \Exception("Invalid mode ".$mode." supplied to setAttributeOutputMode in ".$this->className()); 4185// 4186// $this->_attributeOutputMode=$mode; 4187// } 4188 4189// /** 4190// *Get the current attributeOutputmode 4191// * 4192// * @return string 4193// */ 4194// public function getAttributeOutputMode(){ 4195// 4196// return $this->_attributeOutputMode; 4197// } 4198 /** 4199 * PHP getter magic method. 4200 * This method is overridden so that AR attributes can be accessed like properties. 4201 * @param StringHelper $name property name 4202 * @return mixed property value 4203 * @see getAttribute 4204 */ 4205 public function __get($name) 4206 { 4207 return $this->_getMagicAttribute($name); 4208 } 4209 4210 private function _getMagicAttribute($name){ 4211 if(key_exists($name, $this->_attributes)){ 4212 return $this->getAttribute($name, self::$attributeOutputMode); 4213 }elseif(isset($this->columns[$name])){ 4214 //it's a db column but it's not set in the attributes array. 4215 return null; 4216 }elseif($this->_relationExists($name)){ 4217 return $this->_getRelated($name); 4218 }else{ 4219// if(!isset($this->columns[$name])) 4220// return null; 4221 return parent::__get($name); 4222 } 4223 } 4224 /** 4225 * Get a single attibute raw like in the database or formatted using the \ 4226 * Group-Office user preferences. 4227 * 4228 * @param String $attributeName 4229 * @param String $outputType raw, formatted or html 4230 * @return mixed 4231 */ 4232 public function getAttribute($attributeName, $outputType='raw'){ 4233 if(!isset($this->_attributes[$attributeName])){ 4234 return null; 4235 } 4236 4237 return $outputType=='raw' ? $this->_attributes[$attributeName] : $this->formatAttribute($attributeName, $this->_attributes[$attributeName],$outputType=='html'); 4238 } 4239 4240 public function resolveAttribute($path, $outputType='raw'){ 4241 4242 if(substr($path, 0, 13) === 'customFields.') { 4243 $cf = $this->getCustomFields($outputType === 'formatted'); 4244 return $cf[substr($path, 13)] ?? null; 4245 } 4246 4247 $parts = explode('.', $path); 4248 4249 $model = $this; 4250 if(count($parts)>1){ 4251 $last = array_pop($parts); 4252 4253 while($part = array_shift($parts)){ 4254 $model = $model->$part; 4255 if(!$model){ 4256 return null; 4257 } 4258 } 4259 4260 return $model->getAttribute($last, $outputType); 4261 4262 }else 4263 { 4264 return $this->getAttribute($parts[0], $outputType); 4265 } 4266 } 4267 4268 4269 /** 4270 * Calls the named method which is not a class method. 4271 * Do not call this method. This is a PHP magic method that we override 4272 * to implement the named scope feature. 4273 * 4274 * @param StringHelper $name the method name 4275 * @param array $parameters method parameters 4276 * @return mixed the method return value 4277 */ 4278 public function __call($name,$parameters) 4279 { 4280 //todo find relation 4281 4282 $extraFindParams=isset($parameters[0]) ?$parameters[0] : array(); 4283 if($this->_relationExists($name)) 4284 return $this->_getRelated($name,$extraFindParams); 4285 else 4286 throw new \Exception("function {$this->className()}:$name does not exist"); 4287 //return parent::__call($name,$parameters); 4288 } 4289 4290 /** 4291 * PHP setter magic method. 4292 * This method is overridden so that AR attributes can be accessed like properties. 4293 * 4294 * @param StringHelper $name property name 4295 * @param mixed $value property value 4296 */ 4297 public function __set($name,$value) 4298 { 4299 $this->setAttribute($name,$value); 4300 } 4301 4302 public function __isset($name){ 4303 return isset($this->_attributes[$name]) || 4304 //isset($this->columns[$name]) || MS: removed this because it returns true when attribute is null. This might break something but it shouldn't return true. 4305 ($this->_relationExists($name) && $this->_getRelated($name)) || 4306 parent::__isset($name); 4307 } 4308 4309 /** 4310 * Check if this model has a named attribute 4311 * @param StringHelper $name 4312 * @return boolean 4313 */ 4314 public function hasAttribute($name){ 4315 4316 if(isset($this->columns[$name])) 4317 return true; 4318 4319 if($this->_relationExists($name)) 4320 return true; 4321 4322 if(method_exists($this, 'get'.$name)) 4323 return true; 4324 4325 return false; 4326 } 4327 4328 /** 4329 * Sets a component property to be null. 4330 * This method overrides the parent implementation by clearing 4331 * the specified attribute value. 4332 * 4333 * @param StringHelper $name the property name 4334 */ 4335 public function __unset($name) 4336 { 4337 unset($this->_modifiedAttributes[$name]); 4338 unset($this->_attributes[$name]); 4339 } 4340 4341 /** 4342 * Mysql always returns strings. We want strict types in our model to clearly 4343 * detect modifications 4344 * 4345 * @param array $columns 4346 * @return void 4347 */ 4348 public function castMySqlValues($columns=false){ 4349 4350 if(!$columns) 4351 $columns = array_keys($this->columns); 4352 4353 foreach($columns as $column){ 4354 if(isset($this->_attributes[$column]) && isset($this->columns[$column]['dbtype'])){ 4355 switch ($this->columns[$column]['dbtype']) { 4356 case 'int': 4357 case 'tinyint': 4358 case 'bigint': 4359 //must use floatval because of ints greater then 32 bit 4360 $this->_attributes[$column]=floatval($this->_attributes[$column]); 4361 break; 4362 4363 case 'float': 4364 case 'double': 4365 case 'decimal': 4366 $this->_attributes[$column]=floatval($this->_attributes[$column]); 4367 break; 4368 } 4369 } 4370 } 4371 } 4372 4373 4374 /** 4375 * Sets the named attribute value. It can also set BELONGS_TO and HAS_ONE 4376 * relations if you pass a ActiveRecord 4377 * 4378 * You may also use $this->AttributeName to set the attribute value. 4379 * 4380 * @param StringHelper $name the attribute name 4381 * @param mixed $value the attribute value. 4382 * @return boolean whether the attribute exists and the assignment is conducted successfully 4383 * @see hasAttribute 4384 */ 4385 public function setAttribute($name,$value, $format=false) 4386 { 4387// TODO 4388// if($this->_isStaticModel) { 4389// throw new \Exception("Don't set on static model!"); 4390// } 4391 if($this->loadingFromDatabase){ 4392 //skip fancy features when loading from the database. 4393 $this->_attributes[$name]=$value; 4394 return true; 4395 } 4396 4397 if($format) 4398 $value = $this->formatInput($name, $value); 4399 4400 if(isset($this->columns[$name])){ 4401 4402 if(GO::config()->debug){ 4403 if($this->columns[$name]['gotype']!='file' && is_object($value) || is_array($value)) 4404 throw new \Exception($this->className()."::setAttribute : Invalid attribute value for ".$name.". Type was: ".gettype($value)); 4405 } 4406 4407 $relationFieldName = $this->_getAclFk(); 4408 4409 if($name === $relationFieldName){ 4410 $aclWasOverwritten = $this->isAclOverwritten(); 4411 } 4412 4413 //normalize CRLF to prevent issues with exporting to vcard etc. 4414 if(isset($this->columns[$name]['gotype']) && ($this->columns[$name]['gotype']=='textfield' || $this->columns[$name]['gotype']=='textarea')) 4415 $value=\GO\Base\Util\StringHelper::normalizeCrlf($value, "\n"); 4416 4417 if((!isset($this->_attributes[$name]) || (string)$this->_attributes[$name]!==(string)$value) && !$this->isModified($name)){ 4418 $this->_modifiedAttributes[$name]=isset($this->_attributes[$name]) ? $this->_attributes[$name] : false; 4419// GO::debug("Setting modified attribute $name to ".$this->_modifiedAttributes[$name]); 4420// GO::debugCalledFrom(5); 4421 } 4422 4423 $this->_attributes[$name]=$value; 4424 4425 // Set the ACL_ID if the relation acl FK changed and ACL is overwritten 4426 if($name === $relationFieldName && !$aclWasOverwritten && $this->aclOverwrite() && $this->isModified($name)) { 4427 if(!empty($this->{$name})){ 4428 $modelWithAcl = $this->findRelatedAclModel(); 4429 if($modelWithAcl){ 4430 $this->{$this->aclOverwrite()} = $modelWithAcl->findAclId(); 4431 } 4432 } 4433 } 4434 4435 }else{ 4436 4437 4438 if($r = $this->getRelation($name)){ 4439 if($r['type']==self::BELONGS_TO || $r['type']==self::HAS_ONE){ 4440 4441 if($value instanceof ActiveRecord){ 4442 4443 $cacheKey = $this->_getRelatedCacheKey($r); 4444 $this->_relatedCache[$cacheKey]=$value; 4445 }else 4446 { 4447 throw new \Exception("Value for relation '".$name."' must be a ActiveRecord '". gettype($value)."' was given"); 4448 } 4449 }else 4450 { 4451 throw new \Exception("Can't set one to many relation!"); 4452 } 4453 }else 4454 { 4455 parent::__set($name, $value); 4456 } 4457 } 4458 4459 return true; 4460 } 4461 4462 4463 /** 4464 * Pass another model to this function and they will be linked with the 4465 * Group-Office link system. 4466 4467 * @param \go\core\orm\Entity|self|GO\Base\Model\SearchCacheRecord $model 4468 */ 4469 4470 public function link($model, $description='', $this_folder_id=0, $model_folder_id=0){ 4471 4472 $isSearchCacheModel = ($this instanceof \GO\Base\Model\SearchCacheRecord); 4473 4474 $disableLinksFor = GO::config()->disable_links_for ? GO::config()->disable_links_for : array(); 4475 if (!is_array($disableLinksFor)) { 4476 $disableLinksFor = [$disableLinksFor]; 4477 } 4478 4479 $linksDisabled = false; 4480 if (in_array(self::className(), $disableLinksFor, true) || in_array(get_class($model), $disableLinksFor, true)) { 4481 $linksDisabled = true; 4482 } 4483 4484 if((!$this->hasLinks() && !$isSearchCacheModel) || $linksDisabled) 4485 throw new \Exception("Links not supported by ".$this->className ()); 4486 4487 if($this->linkExists($model)) 4488 return true; 4489 4490 if($model instanceof \GO\Base\Model\SearchCacheRecord){ 4491 $to_model_id = $model->entityId; 4492 $to_model_type_id = $model->entityTypeId; 4493 }else 4494 { 4495 $to_model_id = $model->id; 4496 $to_model_type_id = $model->entityType()->getId(); 4497 } 4498 4499 4500 4501 $from_model_type_id = $isSearchCacheModel ? $this->entityTypeId : $this->modelTypeId(); 4502 4503 $from_model_id = $isSearchCacheModel ? $this->model_id : $this->id; 4504 4505 if($to_model_id == $from_model_id && $to_model_type_id == $from_model_type_id) { 4506 //don't link to self 4507 return true; 4508 } 4509 4510 if(!\go\core\App::get()->getDbConnection()->insert('core_link', [ 4511 "toId" => $to_model_id, 4512 "toEntityTypeId" => $to_model_type_id, 4513 "fromId" => $from_model_id, 4514 "fromEntityTypeId" => $from_model_type_id, 4515 "description" => $description, 4516 "createdAt" => new \DateTime('now',new \DateTimeZone('UTC')) 4517 4518 ])->execute()){ 4519 return false; 4520 } 4521 4522 $reverse = []; 4523 $reverse['fromEntityTypeId'] = $to_model_type_id; 4524 $reverse['toEntityTypeId'] = $from_model_type_id; 4525 $reverse['toId'] = $from_model_id; 4526 $reverse['fromId'] = $to_model_id; 4527 $reverse['description'] = $description; 4528 $reverse['createdAt'] = new \DateTime('now',new \DateTimeZone('UTC')); 4529 4530 4531 if(!\go\core\App::get()->getDbConnection()->insert('core_link', $reverse)->execute()) { 4532 return false; 4533 } 4534 4535 $this->fireEvent('link', array($this, $model, $description, $this_folder_id, $model_folder_id)); 4536 return true; 4537 } 4538 4539// /** 4540// * Can be overriden to do something after linking. It's a public method because sometimes 4541// * searchCacheRecord models are used for linking. In that case we can call the afterLink method of the real model instead of the searchCacheRecord model. 4542// * 4543// * @param ActiveRecord $model 4544// * @param boolean $isSearchCacheModel True if the given model is a search cache model. 4545// * In that case you can use the following code to get the real model: $realModel = $isSearchCacheModel ? GO::getModel($this->model_name)->findByPk($this->model_id) : $this; 4546// * @param string $description 4547// * @param int $this_folder_id 4548// * @param int $model_folder_id 4549// * @param boolean $linkBack 4550// * @return boolean 4551// */ 4552// public function afterLink(ActiveRecord $model, $isSearchCacheModel, $description='', $this_folder_id=0, $model_folder_id=0, $linkBack=true){ 4553// return true; 4554// } 4555 4556 /** 4557 * 4558 * @param \go\core\orm\Entity|self|GO\Base\Model\SearchCacheRecord $model 4559 * @return boolean 4560 */ 4561 public function linkExists($model){ 4562 4563 if($model instanceof \GO\Base\Model\SearchCacheRecord){ 4564 $to_model_id = $model->entityId; 4565 $to_model_type_id = $model->entityTypeId; 4566 }else 4567 { 4568 $to_model_id = $model->id; 4569 $to_model_type_id = $model->entityType()->getId(); 4570 } 4571 4572 if(!$to_model_id) 4573 return false; 4574 4575 $from_model_type_id = $this->className()=="GO\Base\Model\SearchCacheRecord" ? $this->entityTypeId : $this->modelTypeId(); 4576 $from_id = $this->className()=="GO\Base\Model\SearchCacheRecord" ? $this->model_id : $this->id; 4577 4578 $sql = "SELECT id FROM `core_link` WHERE ". 4579 "`fromId`=".intval($from_id)." AND fromEntityTypeId=".$from_model_type_id." AND toEntityTypeId=".$to_model_type_id." AND `toId`=".intval($to_model_id); 4580 4581 $stmt = $this->getDbConnection()->query($sql); 4582 return $stmt->fetchColumn(0); 4583 } 4584// 4585// /** 4586// * Update folder_id or description of a link 4587// * 4588// * @param ActiveRecord $model 4589// * @param array $attributes 4590// * @return boolean 4591// */ 4592// public function updateLink(ActiveRecord $model, array $attributes){ 4593// $sql = "UPDATE `go_links_".$this->tableName()."`"; 4594// 4595// $updates=array(); 4596// $bindParams=array(); 4597// foreach($attributes as $field=>$value){ 4598// $updates[] = "`$field`=:".$field; 4599// $bindParams[':'.$field]=$value; 4600// } 4601// 4602// $sql .= "SET ".implode(',',$updates). 4603// " WHERE model_type_id=".$model->modelTypeId()." AND model_id=".$model->id; 4604// 4605// $result = $this->getDbConnection()->prepare($sql); 4606// return $result->execute($bindParams); 4607// } 4608// 4609 /** 4610 * Unlink a model from this model 4611 * 4612 * @param ActiveRecord $model 4613 * @param boolean $unlinkBack For private use only 4614 * @return boolean 4615 */ 4616 public function unlink($model){ 4617 4618 $isSearchCacheModel = ($this instanceof \GO\Base\Model\SearchCacheRecord); 4619 4620 if(!$this->hasLinks() && !$isSearchCacheModel) 4621 throw new \Exception("Links not supported by ".$this->className ()); 4622 4623 4624 if($model instanceof \GO\Base\Model\SearchCacheRecord){ 4625 $to_model_id = $model->entityId; 4626 $to_model_type_id = $model->entityTypeId; 4627 }else 4628 { 4629 $to_model_id = $model->id; 4630 $to_model_type_id = $model->entityType()->getId(); 4631 } 4632 4633 4634 4635 $from_model_type_id = $isSearchCacheModel ? $this->entityTypeId : $this->modelTypeId(); 4636 4637 $from_model_id = $isSearchCacheModel ? $this->model_id : $this->id; 4638 4639 4640 4641 4642 if(!\go\core\App::get()->getDbConnection()->delete('core_link', [ 4643 "toId" => $to_model_id, 4644 "toEntityTypeId" => $to_model_type_id, 4645 "fromId" => $from_model_id, 4646 "fromEntityTypeId" => $from_model_type_id 4647 ])->execute()){ 4648 return false; 4649 } 4650 4651 4652 4653 $reverse = []; 4654 $reverse['fromEntityTypeId'] = $to_model_type_id; 4655 $reverse['toEntityTypeId'] = $from_model_type_id; 4656 $reverse['toId'] = $from_model_id; 4657 $reverse['fromId'] = $to_model_id; 4658 4659 4660 return \go\core\App::get()->getDbConnection()->delete('core_link', $reverse)->execute(); 4661 } 4662// 4663// protected function afterUnlink(ActiveRecord $model){ 4664// 4665// return true; 4666// } 4667// 4668 /** 4669 * Get the number of links this model has to other models. 4670 * 4671 * @param int $model_id 4672 * @return int 4673 */ 4674 public function countLinks($model_id=0){ 4675 if($model_id==0) 4676 $model_id=$this->id; 4677 $sql = "SELECT count(*) FROM `core_link` WHERE fromId=".intval($model_id)." AND fromEntityTypeId = ".$this->modelTypeId(); 4678 $stmt = $this->getDbConnection()->query($sql); 4679 return intval($stmt->fetchColumn(0)); 4680 } 4681 4682 /** 4683 * Find links of this model type to a given model. 4684 * 4685 * eg.: 4686 * 4687 * \GO\Addressbook\Model\Contact::model()->findLinks($noteModel); 4688 * 4689 * selects all contacts linked to the $noteModel 4690 * 4691 * @param ActiveRecord|Entity $model 4692 * @param FindParams $findParams 4693 * @return ActiveStatement 4694 */ 4695 public function findLinks($model, $extraFindParams=false){ 4696 4697 $findParams = FindParams::newInstance (); 4698 4699 $findParams->select('t.*,l.description AS link_description'); 4700 4701 $joinCriteria = FindCriteria::newInstance() 4702 ->addCondition('fromId', $model->id,'=','l') 4703 ->addCondition('fromEntityTypeId', $model->entityType()->getId(),'=','l') 4704 ->addRawCondition("t.id", "l.toId") 4705 ->addCondition('toEntityTypeId', $this->entityType()->getId(),'=','l'); 4706 4707 $findParams->join("core_link", $joinCriteria, 'l'); 4708 4709 if($extraFindParams) 4710 $findParams->mergeWith ($extraFindParams); 4711 4712 return $this->find($findParams); 4713 } 4714 4715 4716 /** 4717 * Copy links from this model to the target model. 4718 * 4719 * @param ActiveRecord $targetModel 4720 */ 4721 public function copyLinks(ActiveRecord $targetModel){ 4722 if(!$this->hasLinks() || !$targetModel->hasLinks()) 4723 return false; 4724 4725 $stmt = \GO\Base\Model\SearchCacheRecord::model()->findLinks($this); 4726 while($searchCacheModel = $stmt->fetch()){ 4727 $targetModel->link($searchCacheModel, $searchCacheModel->link_description); 4728 } 4729 return true; 4730 } 4731 4732 4733 4734 /** 4735 * Get's the Acces Control List for this model if it has one. 4736 * 4737 * @return \GO\Base\Model\Acl 4738 */ 4739 public function getAcl(){ 4740 if($this->_acl){ 4741 return $this->_acl; 4742 }else 4743 { 4744 $aclId = $this->findAclId(); 4745 if($aclId){ 4746 $this->_acl=\GO\Base\Model\Acl::model()->findByPk($aclId); 4747 return $this->_acl; 4748 }else{ 4749 return false; 4750 } 4751 } 4752 } 4753 4754 /** 4755 * Check if it's necessary to run a database check for this model. 4756 * If it has an ACL, Files or an overrided method it returns true. 4757 * @return boolean 4758 */ 4759 public function checkDatabaseSupported(){ 4760 4761 if($this->aclField()) 4762 return true; 4763 4764 if($this->hasFiles() && GO::modules()->isInstalled('files')) 4765 return true; 4766 4767 $class = new \GO\Base\Util\ReflectionClass($this->className()); 4768 return $class->methodIsOverridden('checkDatabase'); 4769 } 4770 4771 /** 4772 * A function that checks the consistency with the database. 4773 * Generally this is called by r=maintenance/checkDabase 4774 */ 4775 public function checkDatabase(){ 4776 //$this->save(); 4777 4778 echo "Checking ".(is_array($this->pk)?implode(',',$this->pk):$this->pk)." ".$this->className()."\n"; 4779 flush(); 4780 4781 if($this->aclField() && (!$this->isJoinedAclField || $this instanceof \GO\Files\Model\Folder)) { 4782 if (!($this instanceof \GO\Files\Model\Folder) || (!$this->readonly && $this->acl_id > 0)) { 4783 $acl = $this->acl; 4784 if (!$acl) 4785 $this->setNewAcl(); 4786 else { 4787 $user_id = empty($this->user_id) ? 1 : $this->user_id; 4788 4789 $acl->ownedBy = $user_id; 4790 $acl->usedIn = $this->tableName() . '.' . $this->aclField(); 4791 $acl->entityTypeId = $this->entityType()->getId(); 4792 $acl->entityId = $this->id; 4793 if($acl->isModified()) 4794 $acl->save(); 4795 } 4796 } 4797 } 4798 4799 if ($this->hasFiles() && GO::modules()->isInstalled('files')) { 4800 //ACL must be generated here. 4801 $fc = new \GO\Files\Controller\FolderController(); 4802 $this->files_folder_id = $fc->checkModelFolder($this); 4803 } 4804 4805 //normalize crlf 4806 foreach($this->columns as $field=>$attr){ 4807 if(($attr['gotype']=='textfield' || $attr['gotype']=='textarea') && !empty($this->_attributes[$field])){ 4808 $this->$field=\GO\Base\Util\StringHelper::normalizeCrlf($this->_attributes[$field], "\n"); 4809 } 4810 } 4811 4812 //fill in empty required attributes that have defaults 4813 $defaults=$this->getDefaultAttributes(); 4814 foreach($this->columns as $field=>$attr){ 4815 if($attr['required'] && empty($this->$field) && isset($defaults[$field])){ 4816 $this->$field=$defaults[$field]; 4817 4818 echo "Setting default value ".$this->className().":".$this->id." $field=".$defaults[$field]."\n"; 4819 4820 } 4821 } 4822 4823 if($this->isModified()) 4824 $this->save(); 4825 } 4826 4827 4828 public function rebuildSearchCache() { 4829 4830 4831 4832 $rc = new \GO\Base\Util\ReflectionClass($this); 4833 $overriddenMethods = $rc->getOverriddenMethods(); 4834 if(in_array("getCacheAttributes", $overriddenMethods)){ 4835 4836 echo "Processing ".static::class ."\n"; 4837 4838 $entityTypeId = static::entityType()->getId(); 4839 4840 $start = 0; 4841 $limit = 100; 4842 4843 $findParams = FindParams::newInstance() 4844 ->ignoreAcl() 4845 ->debugSql() 4846 ->select('t.*') 4847 ->limit($limit) 4848 ->start($start) 4849 ->join('core_search', FindCriteria::newInstance()->addRawCondition('search.entityId', 't.id')->addRawCondition("search.entityTypeId", $entityTypeId), 'search', 'LEFT'); 4850 4851 $findParams->getCriteria()->addCondition('entityId',null, 'IS', 'search'); 4852 4853 //In small batches to keep memory low 4854 $stmt = $this->find($findParams); 4855 while($stmt->rowCount()) { 4856 4857 while ($m = $stmt->fetch()) { 4858 4859 try { 4860 flush(); 4861 4862 if($m->cacheSearchRecord()) { 4863 echo "."; 4864 } else 4865 { 4866 echo "S"; 4867 $start++; 4868 } 4869 4870 } catch (\Exception $e) { 4871 \go\core\ErrorHandler::logException($e); 4872 echo "\nError: " . $e->getMessage() ."\n"; 4873 $start++; 4874 } 4875 } 4876 echo "\n"; 4877 4878 $stmt = $this->find($findParams->start($start)); 4879 } 4880 4881 echo "\nDone\n\n"; 4882 4883 } 4884 } 4885 4886 4887 /** 4888 * Duplicates the current activerecord to a new one. 4889 * 4890 * Instead of cloning it will create a new instance of the called class 4891 * Copy all the attributes from the original and overwrite the one in the $attibutes parameter 4892 * Unset the primary key if it's not multicolumn and assumably auto_increment 4893 * 4894 * @param array $attributes Array of attributes that need to be set in 4895 * the newly created activerecord as KEY => VALUE. 4896 * Like: $params = array('attribute1'=>1,'attribute2'=>'Hello'); 4897 * @param boolean $save if the copy should be save when calling this function 4898 * @param boolean $ignoreAclPermissions 4899 * @return mixed The newly created object or false if before or after duplicate fails 4900 * 4901 */ 4902 public function duplicate($attributes = array(), $save=true, $ignoreAclPermissions=false, $ignoreCustomFields = false) { 4903 4904 $copy = new static(); 4905 $copiedAttrs = $this->getAttributes('raw'); 4906 unset($copiedAttrs['ctime'],$copiedAttrs['files_folder_id']); 4907 $pkField = $this->primaryKey(); 4908 if(!is_array($pkField)) 4909 unset($copiedAttrs[$pkField]); 4910 4911 $copiedAttrs = array_merge($copiedAttrs, $attributes); 4912 4913 $copy->setAttributes($copiedAttrs,false); 4914 4915 if(!$this->beforeDuplicate($copy)){ 4916 return false; 4917 } 4918 4919// foreach($attributes as $key=>$value) { 4920// $copy->$key = $value; 4921// } 4922 $copy->setAttributes($attributes, false); 4923 4924 //Generate new acl for this model 4925 if($this->aclField() && !$this->isJoinedAclField){ 4926 4927 $user_id = isset($this->user_id) ? $this->user_id : GO::user()->id; 4928 $copy->setNewAcl($user_id); 4929 } 4930 4931 if(!$ignoreCustomFields && $this->hasCustomFields()){ 4932 $copy->setCustomFields($this->getCustomFields()); 4933 } 4934 4935 $this->_duplicateFileColumns($copy); 4936 4937 if($save){ 4938 4939 if(!$copy->save($ignoreAclPermissions)){ 4940 throw new \Exception("Could not save duplicate: ".implode("\n",$copy->getValidationErrors())); 4941 4942 } 4943 } 4944 4945 if(!$this->afterDuplicate($copy)){ 4946 $copy->delete(true); 4947 return false; 4948 } 4949 4950 return $copy; 4951 } 4952 4953 protected function beforeDuplicate(&$duplicate){ 4954 return true; 4955 } 4956 protected function afterDuplicate(&$duplicate){ 4957 return true; 4958 } 4959 4960 /** 4961 * Duplicate related items to another model. 4962 * 4963 * @param StringHelper $relationName 4964 * @param ActiveRecord $duplicate 4965 * @return boolean 4966 * @throws Exception 4967 */ 4968 public function duplicateRelation($relationName, $duplicate, array $attributes=array(), $findParams=false){ 4969 4970 $r= $this->relations(); 4971 4972 if(!isset($r[$relationName])) 4973 throw new \Exception("Relation $relationName not found"); 4974 4975 if($r[$relationName]['type']!=self::HAS_MANY){ 4976 throw new \Exception("Only HAS_MANY relations are supported in duplicateRelation"); 4977 } 4978 4979 $field = $r[$relationName]['field']; 4980 4981 if(!$findParams) 4982 $findParams= FindParams::newInstance (); 4983 4984 $findParams->select('t.*'); 4985 4986 $stmt = $this->_getRelated($relationName, $findParams); 4987 while($model = $stmt->fetch()){ 4988 4989 //set new foreign key 4990 $attributes[$field]=$duplicate->pk; 4991 4992// var_dump(array_merge($model->getAttributes('raw'),$attributes)); 4993 4994 $duplicateRelatedModel = $model->duplicate($attributes, true, true); 4995 4996 $this->afterDuplicateRelation($relationName, $model, $duplicateRelatedModel); 4997 } 4998 4999 return true; 5000 } 5001 5002 protected function afterDuplicateRelation($relationName, ActiveRecord $relatedModel, ActiveRecord $duplicatedRelatedModel){ 5003 return true; 5004 } 5005 5006 /** 5007 * Lock the database table 5008 * 5009 * @param StringHelper $mode Modes are: "read", "read local", "write", "low priority write" 5010 * @return boolean 5011 */ 5012 public function lockTable($mode="WRITE"){ 5013 $sql = "LOCK TABLES `".$this->tableName()."` AS t $mode"; 5014 $this->getDbConnection()->query($sql); 5015 5016 if($this->hasFiles() && GO::modules()->isInstalled('files')){ 5017 $sql = "LOCK TABLES `fs_folders` AS t $mode"; 5018 $this->getDbConnection()->query($sql); 5019 } 5020 5021 return true; 5022 } 5023 /** 5024 * Unlock tables 5025 * 5026 * @return bool True on success 5027 */ 5028 5029 public function unlockTable(){ 5030 $sql = "UNLOCK TABLES;"; 5031 return $this->getDbConnection()->query($sql); 5032 } 5033 5034 /** 5035 * Get's all the default attributes. The defaults coming from the database and 5036 * the programmed ones defined in defaultAttributes(). 5037 * 5038 * @return array 5039 */ 5040 public function getDefaultAttributes(){ 5041 $attr=array(); 5042 foreach($this->getColumns() as $field => $colAttr){ 5043 if(isset($colAttr['default'])) 5044 $attr[$field]=$colAttr['default']; 5045 } 5046 5047 if(isset($this->columns['user_id'])) 5048 $attr['user_id']=GO::user() ? GO::user()->id : 1; 5049 if(isset($this->columns['muser_id'])) 5050 $attr['muser_id']=GO::user() ? GO::user()->id : 1; 5051 5052 return array_merge($attr, $this->defaultAttributes()); 5053 } 5054 5055 /** 5056 * 5057 * Get the extra default attibutes not determined from the database. 5058 * 5059 * This function can be overridden in the model. 5060 * Example override: 5061 * $attr = parent::defaultAttributes(); 5062 * $attr['time'] = time(); 5063 * return $attr; 5064 * 5065 * @return Array An empty array. 5066 */ 5067 protected function defaultAttributes() { 5068 return array(); 5069 } 5070 5071 5072 5073 /** 5074 * Delete all reminders linked to this midel. 5075 */ 5076 public function deleteReminders(){ 5077 5078 $stmt = \GO\Base\Model\Reminder::model()->findByModel($this->className(), $this->pk); 5079 $stmt->callOnEach("delete"); 5080 } 5081 5082 /** 5083 * Add a reminder linked to this model 5084 * 5085 * @param StringHelper $name The name of the reminder 5086 * @param int $time This needs to be an unixtimestamp 5087 * @param int $user_id The user where this reminder belongs to. 5088 * @param int $vtime The time that will be displayed in the reminder 5089 * @return \GO\Base\Model\Reminder 5090 */ 5091 public function addReminder($name, $time, $user_id, $vtime=null){ 5092 5093 $userModel = \GO\Base\Model\User::model()->findByPk($user_id, false, true); 5094 if (!empty($userModel) && !$userModel->no_reminders) { 5095 $reminder = \GO\Base\Model\Reminder::newInstance($name, $time, $this->className(), $this->pk, $vtime); 5096 $reminder->setForUser($user_id); 5097 5098 return $reminder; 5099 } else { 5100 return false; 5101 } 5102 5103 } 5104 5105 /** 5106 * Add a record to the given MANY_MANY relation 5107 * 5108 * @param String $relationName 5109 * @param int $foreignPk 5110 * @param array $extraAttributes 5111 * @return boolean Saved 5112 */ 5113 public function addManyMany($relationName, $foreignPk, $extraAttributes=array()){ 5114 5115 if(empty($foreignPk)) 5116 return false; 5117 5118 if(!$this->hasManyMany($relationName, $foreignPk)){ 5119 5120 $r = $this->getRelation($relationName); 5121 5122 if($this->isNew) 5123 throw new \Exception("Can't add manymany relation to a new model. Call save() first."); 5124 5125 if(!$r) 5126 throw new \Exception("Relation '$relationName' not found in ActiveRecord::addManyMany()"); 5127 5128 $linkModel = new $r['linkModel']; 5129 $linkModel->{$r['field']} = $this->pk; 5130 5131 $keys = $linkModel->primaryKey(); 5132 5133 $foreignField = $keys[0]==$r['field'] ? $keys[1] : $keys[0]; 5134 5135 $linkModel->$foreignField = $foreignPk; 5136 5137 $linkModel->setAttributes($extraAttributes); 5138 5139 return $linkModel->save(); 5140 }else 5141 { 5142 return true; 5143 } 5144 } 5145 5146 /** 5147 * Remove a record from the given MANY_MANY relation 5148 * 5149 * @param String $relationName 5150 * @param int $foreignPk 5151 * 5152 * @return ActiveRecord or false 5153 */ 5154 public function removeManyMany($relationName, $foreignPk){ 5155 $linkModel = $this->hasManyMany($relationName, $foreignPk); 5156 5157 if($linkModel) 5158 return $linkModel->delete(); 5159 else 5160 return true; 5161 } 5162 5163 public function removeAllManyMany($relationName){ 5164 $r = $this->getRelation($relationName); 5165 if(!$r) 5166 throw new \Exception("Relation '$relationName' not found in ActiveRecord::hasManyMany()"); 5167 $linkModel = GO::getModel($r['linkModel']); 5168 5169 $linkModel->deleteByAttribute($r['field'],$this->pk); 5170 } 5171 5172 /** 5173 * Check for records in the given MANY_MANY relation 5174 * 5175 * @param String $relationName 5176 * @param int $foreignPk 5177 * 5178 * @return ActiveRecord or false 5179 */ 5180 public function hasManyMany($relationName, $foreignPk){ 5181 $r = $this->getRelation($relationName); 5182 if(!$r) 5183 throw new \Exception("Relation '$relationName' not found in ActiveRecord::hasManyMany()"); 5184 5185 if($this->isNew) 5186 throw new \Exception("You can't call hasManyMany on a new model. Call save() first."); 5187 5188 $linkModel = GO::getModel($r['linkModel']); 5189 $keys = $linkModel->primaryKey(); 5190 if(count($keys)!=2){ 5191 throw new \Exception("Primary key of many many linkModel ".$r['linkModel']." must be an array of two fields"); 5192 } 5193 $foreignField = $keys[0]==$r['field'] ? $keys[1] : $keys[0]; 5194 5195 $primaryKey = array($r['field']=>$this->pk, $foreignField=>$foreignPk); 5196 5197 return $linkModel->findByPk($primaryKey); 5198 } 5199 5200 /** 5201 * Quickly delete all records by attribute. This function does NOT check the ACL. 5202 * 5203 * @param StringHelper $name 5204 * @param mixed $value 5205 */ 5206 public function deleteByAttribute($name, $value){ 5207 $this->deleteByAttributes([$name => $value]); 5208 } 5209 5210 public function deleteByAttributes($attributes){ 5211 $criteria = FindCriteria::newInstance(); 5212 foreach($attributes as $name => $value) { 5213 $criteria->addCondition($name, $value); 5214 } 5215 $stmt = $this->find(FindParams::newInstance()->ignoreAcl()->criteria($criteria)); 5216 $stmt->callOnEach('delete'); 5217 } 5218 5219 /** 5220 * Add a comment to the model. If the comments module is not installed this 5221 * function will return false. 5222 * 5223 * @param StringHelper $text 5224 * @return boolean 5225 */ 5226 public function addComment($text){ 5227 if(!GO::modules()->isInstalled('comments') || !GO::modules()->isInstalled('comments') && !$this->hasLinks()) 5228 return false; 5229 5230 $comment = new \go\modules\community\comments\model\Comment(); 5231 $comment->setEntity($this->entityType()); 5232 $comment->entityId = $this->id; 5233 $comment->text=$text; 5234 if(!$comment->save()) { 5235 throw new \Exception("Failed to save comment"); 5236 } 5237 5238 return $comment; 5239 5240 } 5241 5242 /** 5243 * Merge this model with another one of the same type. 5244 * 5245 * All attributes of the given model will be applied to this model if they are empty. Textarea's will be concatenated. 5246 * All links will be moved to this model. 5247 * Finally the given model will be deleted. 5248 * 5249 * @param ActiveRecord $model 5250 */ 5251 public function mergeWith(ActiveRecord $model, $mergeAttributes=true, $deleteModel=true){ 5252 5253 if($model->id==$this->id && $this->className()==$model->className()) 5254 return false; 5255 5256 //copy attributes if models are of the same type. 5257 if($mergeAttributes){ 5258 $attributes = $model->getAttributes('raw'); 5259 5260 //don't copy primary key 5261 if(is_array($this->primaryKey())){ 5262 foreach($this->primaryKey() as $field) 5263 unset($attributes[$field]); 5264 }else 5265 unset($attributes[$this->primaryKey()]); 5266 5267 unset($attributes['files_folder_id']); 5268 5269 foreach($attributes as $name=>$value){ 5270 $isset = isset($this->columns[$name]); 5271 5272 if($isset && !empty($value)){ 5273 if($this->columns[$name]['gotype']=='textarea'){ 5274 $this->$name .= "\n\n-- merge --\n\n".$value; 5275 }elseif($this->columns[$name]['gotype']='date' && $value == '0000-00-00') 5276 $this->$name=""; //Don't copy old 0000-00-00 that might still be in the database 5277 elseif(empty($this->$name)) 5278 $this->$name=$value; 5279 5280 } 5281 } 5282 5283 if($this->hasCustomFields()) { 5284 $this->setCustomFields($model->getCustomFields()); 5285 } 5286 5287 $this->save(); 5288 } 5289 5290 $model->copyLinks($this); 5291 5292 //move files. 5293 if($deleteModel){ 5294 $this->_moveFiles($model); 5295 5296 $this->_moveComments($model); 5297 }else 5298 { 5299 $this->_copyFiles($model); 5300 5301 $this->_copyComments($model); 5302 } 5303 5304 $this->afterMergeWith($model); 5305 5306 if($deleteModel) 5307 $model->delete(); 5308 } 5309 5310 private function _copyComments(ActiveRecord $sourceModel) { 5311 if (GO::modules()->isInstalled('comments') && $this->hasLinks()) { 5312 $findParams = FindParams::newInstance() 5313 ->ignoreAcl() 5314 ->order('id', 'DESC') 5315 ->select() 5316 ->criteria( 5317 FindCriteria::newInstance() 5318 ->addCondition('model_id', $sourceModel->id) 5319 ->addCondition('model_type_id', $sourceModel->modelTypeId()) 5320 ); 5321 $stmt = \GO\Comments\Model\Comment::model()->find($findParams); 5322 while ($comment = $stmt->fetch()) { 5323 $comment->duplicate( 5324 array( 5325 'model_type_id' => $this->modelTypeId(), 5326 'model_id' => $this->id, 5327 'ctime' => $comment->ctime 5328 ) 5329 ); 5330 } 5331 } 5332 } 5333 5334 private function _copyFiles(ActiveRecord $sourceModel) { 5335 if (!$this->hasFiles()) { 5336 return false; 5337 } 5338 5339 $sourceFolder = \GO\Files\Model\Folder::model()->findByPk($sourceModel->files_folder_id); 5340 if (!$sourceFolder) { 5341 return false; 5342 } 5343 5344 $this->filesFolder->copyContentsFrom($sourceFolder); 5345 } 5346 5347 private function _moveComments(ActiveRecord $sourceModel){ 5348 if(GO::modules()->isInstalled('comments') && $this->hasLinks()){ 5349 $findParams = FindParams::newInstance() 5350 ->ignoreAcl() 5351 ->order('id','DESC') 5352 ->criteria( 5353 FindCriteria::newInstance() 5354 ->addCondition('model_id', $sourceModel->id) 5355 ->addCondition('model_type_id', $sourceModel->modelTypeId()) 5356 ); 5357 5358 $stmt = \GO\Comments\Model\Comment::model()->find($findParams); 5359 while($comment = $stmt->fetch()){ 5360 $comment->model_type_id=$this->modelTypeId(); 5361 $comment->model_id=$this->id; 5362 $comment->save(); 5363 } 5364 } 5365 } 5366 5367 private function _moveFiles(ActiveRecord $sourceModel){ 5368 if(!$this->hasFiles()) 5369 return false; 5370 5371 $sourceFolder = \GO\Files\Model\Folder::model()->findByPk($sourceModel->files_folder_id); 5372 if(!$sourceFolder) 5373 return false; 5374 5375 $this->filesFolder->moveContentsFrom($sourceFolder); 5376 } 5377 5378 /** 5379 * This function forces this activeRecord to save itself. 5380 */ 5381 public function forceSave(){ 5382 5383 $this->_forceSave=true; 5384 } 5385 5386 /** 5387 * Override this if you need to do extra stuff after merging. 5388 * Move relations for example. 5389 * 5390 * @param ActiveRecord $model The model that will be deleted after merging. 5391 */ 5392 protected function afterMergeWith(ActiveRecord $model){} 5393 5394 /** 5395 * This function will unset the invalid properties so they will not be saved. 5396 */ 5397 public function ignoreInvalidProperties(){ 5398 $this->validate(); 5399 5400 foreach($this->_validationErrors as $attrib=>$error){ 5401 GO::debug('Atribute not successfully validated, unsetting '.$attrib); 5402 $this->_unsetAttribute($attrib); 5403 } 5404 } 5405 5406 private function _unsetAttribute($attribute){ 5407 unset($this->$attribute); 5408 5409 if(isset($this->_validationErrors[$attribute])) 5410 unset($this->_validationErrors[$attribute]); 5411 5412 if(isset($this->_modifiedAttributes[$attribute])) 5413 unset($this->_modifiedAttributes[$attribute]); 5414 } 5415 5416 /** 5417 * Find the relation names that are using the given culumnName 5418 * 5419 * You can also provide the types of relations as an array to filter. 5420 * Example array: 5421 * $relationTypes = array( 5422 * \GO\Base\Db\ActiveRecord::BELONGS_TO, 5423 * \GO\Base\Db\ActiveRecord::HAS_MANY, 5424 * \GO\Base\Db\ActiveRecord::HAS_ONE, 5425 * \GO\Base\Db\ActiveRecord::MANY_MANY 5426 * ); 5427 * 5428 * You can also leave the $relationTypes variable empty to search for all types 5429 * 5430 * @param StringHelper $columnName 5431 * @param array $relationTypes 5432 * @return array With names of the relations Eg. array('categories','users'); 5433 */ 5434 public function findRelationsByColumnName($columnName,$relationTypes = false){ 5435 5436 $relationNames = array(); 5437 5438 if(!is_array($relationTypes) && $relationTypes !== false) 5439 Throw new Exception('RelationTypes needs to be false or an array'); 5440 5441 $relations = $this->getRelations(); 5442 5443 foreach($relations as $relationKey=>$relation){ 5444 5445 if($relationTypes !== false){ 5446 5447 if(in_array($relation['type'], $relationTypes) && $relation['field'] === $columnName){ 5448 $relationNames[] = $relationKey; 5449 } 5450 5451 } else { 5452 5453 if($relation['field'] === $columnName){ 5454 $relationNames[] = $relationKey; 5455 } 5456 5457 } 5458 5459 } 5460 5461 return $relationNames; 5462 } 5463 5464 5465 /** 5466 * 5467 * Get's the class name without the namespace 5468 * 5469 * eg. class go\modules\community\notes\model\Note becomes just "note" 5470 * 5471 * @return string 5472 * 5473 * @return string 5474 */ 5475 public static function getClassName() { 5476 $cls = static::class; 5477 return substr($cls, strrpos($cls, '\\') + 1); 5478 } 5479 5480} 5481