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.'&lt;br&gt;';
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