Merge branch '2016.11-develop' into 2017.02
[tine20] / tine20 / Tinebase / Record / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Record
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2017 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  */
11
12 /**
13  * Abstract implemetation of Tinebase_Record_Interface
14  * 
15  * @package     Tinebase
16  * @subpackage  Record
17  */
18 abstract class Tinebase_Record_Abstract implements Tinebase_Record_Interface
19 {
20     /**
21      * ISO8601LONG datetime representation
22      */
23     const ISO8601LONG = 'Y-m-d H:i:s';
24     
25     /**
26      * holds the configuration object (must be declared in the concrete class)
27      *
28      * @var Tinebase_ModelConfiguration
29      */
30     protected static $_configurationObject = NULL;
31
32     /**
33      * Holds the model configuration (must be assigned in the concrete class)
34      * 
35      * @var array
36      */
37     protected static $_modelConfiguration = NULL;
38
39     /**
40      * should datas be validated on the fly(false) or only on demand(true)
41      *
42      * @var bool
43      */
44     public $bypassFilters;
45     
46     /**
47      * should datetimeFields be converted from iso8601 (or optionally others {@see $this->dateConversionFormat}) strings to DateTime objects and back 
48      *
49      * @var bool
50      */
51     public $convertDates;
52     
53     /**
54      * differnet format than iso8601 to use for conversions 
55      *
56      * @var string
57      */
58     public $dateConversionFormat = NULL;
59     
60     /**
61      * key in $_validators/$_properties array for the field which 
62      * represents the identifier
63      * NOTE: _Must_ be set by the derived classes!
64      * 
65      * @var string
66      */
67     protected $_identifier = NULL;
68     
69     /**
70      * application the record belongs to
71      * NOTE: _Must_ be set by the derived classes!
72      *
73      * @var string
74      */
75     protected $_application = NULL;
76     
77     /**
78      * stores if values got modified after loaded via constructor
79      * 
80      * @var bool
81      */
82     protected $_isDirty = false;
83     
84     /**
85      * holds properties of record
86      * 
87      * @var array 
88      */
89     protected $_properties = array();
90     
91     /**
92      * this filter get used when validating user generated content with Zend_Input_Filter
93      *
94      * @var array list of zend inputfilter
95      */
96     protected $_filters = array();
97     
98     /**
99      * Defintion of properties. All properties of record _must_ be declared here!
100      * This validators get used when validating user generated content with Zend_Input_Filter
101      * NOTE: _Must_ be set by the derived classes!
102      * 
103      * @var array list of zend validator
104      */
105     protected $_validators = array();
106     
107     /**
108      * the validators place there validation errors in this variable
109      * 
110      * @var array list of validation errors
111      */
112     protected $_validationErrors = array();
113     
114     /**
115      * name of fields containing datetime or an array of datetime
116      * information
117      *
118      * @var array list of datetime fields
119      */
120     protected $_datetimeFields = array();
121     
122     /**
123      * date fields
124      * 
125      * @var array
126      */
127     protected $_dateFields = array();
128     
129     /**
130      * alarm datetime field
131      *
132      * @var string
133      */
134     protected $_alarmDateTimeField = '';
135     
136     /**
137      * name of fields containing time information
138      *
139      * @var array list of time fields
140      */
141     protected $_timeFields = array();
142
143     /**
144      * name of fields that should be omitted from modlog
145      *
146      * @var array list of modlog omit fields
147      */
148     protected $_modlogOmitFields = array();
149     
150     /**
151      * name of fields that should not be persisted during create/update in backend
152      *
153      * @var array
154      * 
155      * @todo think about checking the setting of readonly field and not allow it
156      */
157     protected $_readOnlyFields = array();
158     
159     /**
160      * save state if data are validated
161      *
162      * @var bool
163      */
164     protected $_isValidated = false;
165     
166     /**
167      * fields to translate when translate() function is called
168      *
169      * @var array
170      */
171     protected $_toTranslate = array();
172     
173     /**
174      * holds instance of Zend_Filters
175      *
176      * @var array
177      */
178     protected static $_inputFilters = array();
179     
180     /**
181      * If model is relatable and a special config should be applied, this is configured here
182      * @var array
183      */
184     protected static $_relatableConfig = NULL;
185
186     /**
187      * if foreign Id fields should be resolved on search and get from json
188      * should have this format: 
189      *     array('Calendar_Model_Contact' => 'contact_id', ...)
190      * or for more fields:
191      *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
192      * (e.g. resolves contact_id with the corresponding Model)
193      * 
194      * @var array
195      */
196     protected static $_resolveForeignIdFields = NULL;
197     
198     /**
199      * this property holds all field information for autoboot strapping
200      * if this is not null, these properties will be overridden in the abstract constructor:
201      *     - _filters
202      *     - _validators
203      *     - _dateTimeFields
204      *     - _alarmDateTimeField
205      *     - _timeFields
206      *     - _modlogOmitFields
207      *     - _readOnlyFields
208      *     - _resolveForeignIdFields
209      * @var array
210      */
211     protected static $_fields = NULL;
212     
213     /**
214      * right, user must have to see the module for this model
215      */
216     protected static $_requiredRight = NULL;
217
218
219     /******************************** functions ****************************************/
220     
221     /**
222      * Default constructor
223      * Constructs an object and sets its record related properties.
224      * 
225      * @todo what happens if not all properties in the datas are set?
226      * The default values must also be set, even if no filtering is done!
227      * 
228      * @param mixed $_data
229      * @param bool $_bypassFilters sets {@see this->bypassFilters}
230      * @param mixed $_convertDates sets {@see $this->convertDates} and optionaly {@see $this->$dateConversionFormat}
231      * @throws Tinebase_Exception_Record_DefinitionFailure
232      */
233     public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
234     {
235         // apply configuration
236         $this->_setFromConfigurationObject();
237         
238         if ($this->_identifier === NULL) {
239             throw new Tinebase_Exception_Record_DefinitionFailure('$_identifier is not declared');
240         }
241         
242         $this->bypassFilters = (bool)$_bypassFilters;
243         $this->convertDates = (bool)$_convertDates;
244         if (is_string($_convertDates)) {
245             $this->dateConversionFormat = $_convertDates;
246         }
247
248         if ($this->has('description') && (! (isset($this->_filters['description']) || array_key_exists('description', $this->_filters)))) {
249             $this->_filters['description'] = new Tinebase_Model_InputFilter_CrlfConvert();
250         }
251
252         if (is_array($_data)) {
253             $this->setFromArray($_data);
254         }
255         
256         $this->_isDirty = false;
257     }
258     
259     /**
260      * returns the configuration object
261      *
262      * @return Tinebase_ModelConfiguration|NULL
263      */
264     public static function getConfiguration()
265     {
266         if (! isset (static::$_modelConfiguration)) {
267             return NULL;
268         }
269         
270         if (! static::$_configurationObject) {
271             static::$_configurationObject = new Tinebase_ModelConfiguration(static::$_modelConfiguration);
272         }
273     
274         return static::$_configurationObject;
275     }
276
277     /**
278      * returns the relation config
279      * 
280      * @deprecated
281      * @return array
282      */
283     public static function getRelatableConfig()
284     {
285         return static::$_relatableConfig;
286     }
287     
288     /**
289      * recursivly clone properties
290      */
291     public function __clone()
292     {
293         foreach ($this->_properties as $name => $value)
294         {
295             if (is_object($value)) {
296                 $this->_properties[$name] = clone $value;
297             } else if (is_array($value)) {
298                 foreach ($value as $arrKey => $arrValue) {
299                     if (is_object($arrValue)) {
300                         $value[$arrKey] = clone $arrValue;
301                     }
302                 }
303             }
304         }
305     }
306     
307     /**
308      * sets identifier of record
309      * 
310      * @param int $_id
311      * @return void
312      */
313     public function setId($_id)
314     {
315         // set internal state to "not validated"
316         $this->_isValidated = false;
317         
318         if ($this->bypassFilters === true) {
319             $this->_properties[$this->_identifier] = $_id;
320         } else {
321             $this->__set($this->_identifier, $_id);
322         }
323     }
324     
325     /**
326      * gets identifier of record
327      * 
328      * @return int identifier
329      */
330     public function getId()
331     {
332         if (! isset($this->_properties[$this->_identifier])) {
333             $this->setId(NULL);
334         }
335         return $this->_properties[$this->_identifier];
336     }
337     
338     /**
339      * gets application the records belongs to
340      * 
341      * @return string application
342      */
343     public function getApplication()
344     {
345         return $this->_application;
346     }
347     
348     /**
349      * returns id property of this model
350      *
351      * @return string
352      */
353     public function getIdProperty()
354     {
355         return $this->_identifier;
356     }
357     
358     /**
359      * sets the record related properties from user generated input.
360      * 
361      * Input-filtering and validation by Zend_Filter_Input can enabled and disabled
362      *
363      * @param array $_data            the new data to set
364      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
365      * 
366      * @todo remove custom fields handling (use Tinebase_Record_RecordSet for them)
367      */
368     public function setFromArray(array $_data)
369     {
370         if ($this->convertDates === true) {
371             $this->_convertISO8601ToDateTime($_data);
372         }
373         
374         // set internal state to "not validated"
375         $this->_isValidated = false;
376
377         // get custom fields
378         if ($this->has('customfields')) {
379             $application = Tinebase_Application::getInstance()->getApplicationByName($this->_application);
380             $customFields = Tinebase_CustomField::getInstance()->getCustomFieldsForApplication($application, get_class($this))->name;
381             $recordCustomFields = array();
382         } else {
383             $customFields = array();
384         }
385         
386         // make sure we run through the setters
387         $bypassFilter = $this->bypassFilters;
388         $this->bypassFilters = true;
389         foreach ($_data as $key => $value) {
390             if ((isset($this->_validators[$key]) || array_key_exists ($key, $this->_validators))) {
391                 $this->$key = $value;
392             } else if (in_array($key, $customFields)) {
393                 $recordCustomFields[$key] = $value;
394             }
395         }
396         if (!empty($recordCustomFields)) {
397             $this->customfields = $recordCustomFields;
398         }
399         
400         $this->bypassFilters = $bypassFilter;
401         if ($this->bypassFilters !== true) {
402             $this->isValid(true);
403         }
404     }
405     
406     /**
407      * wrapper for setFromJason which expects datetimes in array to be in
408      * users timezone and converts them to UTC
409      *
410      * @todo move this to a generic __call interceptor setFrom<API>InUsersTimezone
411      * 
412      * @param  string $_data json encoded data
413      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
414      */
415     public function setFromJsonInUsersTimezone($_data)
416     {
417         // change timezone of current php process to usertimezone to let new dates be in the users timezone
418         // NOTE: this is neccessary as creating the dates in UTC and just adding/substracting the timeshift would
419         //       lead to incorrect results on DST transistions 
420         date_default_timezone_set(Tinebase_Core::getUserTimezone());
421
422         // NOTE: setFromArray creates new Tinebase_DateTimes of $this->datetimeFields
423         $this->setFromJson($_data);
424         
425         // convert $this->_datetimeFields into the configured server's timezone (UTC)
426         $this->setTimezone('UTC');
427         
428         // finally reset timzone of current php process to the configured server timezone (UTC)
429         date_default_timezone_set('UTC');
430     }
431     
432     /**
433      * Sets timezone of $this->_datetimeFields
434      * 
435      * @see Tinebase_DateTime::setTimezone()
436      * @param  string $_timezone
437      * @param  bool   $_recursive
438      * @return  void
439      * @throws Tinebase_Exception_Record_Validation
440      */
441     public function setTimezone($_timezone, $_recursive = TRUE)
442     {
443         foreach ($this->_datetimeFields as $field) {
444             if (!isset($this->_properties[$field])) continue;
445             
446             if (!is_array($this->_properties[$field])) {
447                 $toConvert = array($this->_properties[$field]);
448             } else {
449                 $toConvert = $this->_properties[$field];
450             }
451
452             foreach ($toConvert as $convertField => &$value) {
453                 if (! method_exists($value, 'setTimezone')) {
454                     throw new Tinebase_Exception_Record_Validation($convertField . ' must be a method setTimezone');
455                 } 
456                 $value->setTimezone($_timezone);
457             } 
458         }
459         
460         if ($_recursive) {
461             foreach ($this->_properties as $property => $propValue) {
462                 if ($propValue && is_object($propValue) &&
463                         (in_array('Tinebase_Record_Interface', class_implements($propValue)) ||
464                             $propValue instanceof Tinebase_Record_RecordSet) ) {
465
466                     $propValue->setTimezone($_timezone, TRUE);
467                 }
468             }
469         }
470     }
471     
472     /**
473      * returns array of fields with validation errors 
474      *
475      * @return array
476      */
477     public function getValidationErrors()
478     {
479         return $this->_validationErrors;
480     }
481     
482     /**
483      * returns array with record related properties 
484      *
485      * @param boolean $_recursive
486      * @return array
487      */
488     public function toArray($_recursive = TRUE)
489     {
490         $recordArray = $this->_properties;
491         if ($this->convertDates === true) {
492             if (! is_string($this->dateConversionFormat)) {
493                 $this->_convertDateTimeToString($recordArray, Tinebase_Record_Abstract::ISO8601LONG);
494             } else {
495                 $this->_convertDateTimeToString($recordArray, $this->dateConversionFormat);
496             }
497         }
498         
499         if ($_recursive) {
500             /** @var Tinebase_Record_Interface  $value */
501             foreach ($recordArray as $property => $value) {
502                 if ($this->_hasToArray($value)) {
503                     $recordArray[$property] = $value->toArray();
504                 }
505             }
506         }
507         
508         return $recordArray;
509     }
510     
511     /**
512      * checks if variable has toArray()
513      * 
514      * @param mixed $mixed
515      * @return boolean
516      */
517     protected function _hasToArray($mixed)
518     {
519         return is_object($mixed) && method_exists($mixed, 'toArray');
520     }
521     
522     /**
523      * validate and filter the the internal data
524      *
525      * @param $_throwExceptionOnInvalidData
526      * @return bool
527      * @throws Tinebase_Exception_Record_Validation
528      */
529     public function isValid($_throwExceptionOnInvalidData = false)
530     {
531         if ($this->_isValidated === true) {
532             return true;
533         }
534         
535         $inputFilter = $this->_getFilter()
536             ->setData($this->_properties);
537         
538         if ($inputFilter->isValid()) {
539             // set $this->_properties with the filtered values
540             $this->_properties  = $inputFilter->getUnescaped();
541             $this->_isValidated = true;
542             
543             return true;
544         }
545         
546         $this->_validationErrors = array();
547         
548         foreach ($inputFilter->getMessages() as $fieldName => $errorMessage) {
549             $this->_validationErrors[] = array(
550                 'id'  => $fieldName,
551                 'msg' => $errorMessage
552             );
553         }
554         
555         if ($_throwExceptionOnInvalidData) {
556             $e = new Tinebase_Exception_Record_Validation('Some fields ' . implode(',', array_keys($inputFilter->getMessages()))
557                 . ' have invalid content');
558             
559             if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " "
560                 . $e->getMessage()
561                 . print_r($this->_validationErrors, true));
562             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
563                 . ' Record: ' . print_r($this->toArray(), true));
564             
565             throw $e;
566         }
567         
568         return false;
569     }
570     
571     /**
572      * apply filter
573      *
574      * @todo implement
575      */
576     public function applyFilter()
577     {
578         $this->isValid(true);
579     }
580     
581     /**
582      * sets record related properties
583      * 
584      * @param string $_name of property
585      * @param mixed $_value of property
586      * @throws Tinebase_Exception_UnexpectedValue
587      * @return void
588      */
589     public function __set($_name, $_value)
590     {
591         if (! (isset($this->_validators[$_name]) || array_key_exists ($_name, $this->_validators))) {
592             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
593         }
594         
595         if ($this->bypassFilters !== true) {
596             $this->_properties[$_name] = $this->_validateField($_name, $_value);
597         } else {
598             $this->_properties[$_name] = $_value;
599             
600             $this->_isValidated = false;
601         }
602         
603         $this->_isDirty = true;
604     }
605     
606     protected function _validateField($name, $value)
607     {
608         $inputFilter = $this->_getFilter($name);
609         $inputFilter->setData(array(
610             $name => $value
611         ));
612         
613         if ($inputFilter->isValid()) {
614             return $inputFilter->getUnescaped($name);
615         }
616         
617         $this->_validationErrors = array();
618         
619         foreach($inputFilter->getMessages() as $fieldName => $errorMessage) {
620             $this->_validationErrors[] = array(
621                 'id'  => $fieldName,
622                 'msg' => $errorMessage
623             );
624         }
625         
626         $e = new Tinebase_Exception_Record_Validation('the field ' . implode(',', array_keys($inputFilter->getMessages())) . ' has invalid content');
627         Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ":\n" .
628             print_r($this->_validationErrors,true). $e);
629         throw $e;
630     }
631     
632     /**
633      * unsets record related properties
634      * 
635      * @param string $_name of property
636      * @throws Tinebase_Exception_UnexpectedValue
637      * @return void
638      */
639     public function __unset($_name)
640     {
641         if (!(isset($this->_validators[$_name]) || array_key_exists ($_name, $this->_validators))) {
642             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
643         }
644
645         unset($this->_properties[$_name]);
646         
647         $this->_isValidated = false;
648         
649         if ($this->bypassFilters !== true) {
650             $this->isValid(true);
651         }
652     }
653     
654     /**
655      * checkes if an propertiy is set
656      * 
657      * @param string $_name name of property
658      * @return bool property is set or not
659      */
660     public function __isset($_name)
661     {
662         return isset($this->_properties[$_name]);
663     }
664     
665     /**
666      * gets record related properties
667      * 
668      * @param  string  $name  name of property
669      * @return mixed value of property
670      */
671     public function __get($name)
672     {
673         return (isset($this->_properties[$name]) || array_key_exists($name, $this->_properties))
674             ? $this->_properties[$name]
675             : NULL;
676     }
677     
678    /** convert this to string
679     *
680     * @return string
681     */
682     public function __toString()
683     {
684        return (string) print_r($this->toArray(), true);
685     }
686     
687     /**
688      * returns a Zend_Filter for the $_filters and $_validators of this record class.
689      * we just create an instance of Filter if we really need it.
690      *
691      * @param string $field
692      * @return Zend_Filter_Input
693      */
694     protected function _getFilter($field = null)
695     {
696         $keyName = get_class($this) . $field;
697         
698         if (! (isset(self::$_inputFilters[$keyName]) || array_key_exists($keyName, self::$_inputFilters))) {
699             if ($field !== null) {
700                 $filters    = (isset($this->_filters[$field]) || array_key_exists($field, $this->_filters)) ? array($field => $this->_filters[$field]) : array();
701                 $validators = array($field => $this->_validators[$field]); 
702                 
703                 self::$_inputFilters[$keyName] = new Zend_Filter_Input($filters, $validators);
704             } else {
705                 self::$_inputFilters[$keyName] = new Zend_Filter_Input($this->_filters, $this->_validators);
706             }
707         }
708         
709         return self::$_inputFilters[$keyName];
710     }
711     
712     /**
713      * Converts Tinebase_DateTimes into custom representation
714      *
715      * @param array &$_toConvert
716      * @param string $_format
717      * @return void
718      */
719     protected function _convertDateTimeToString(&$_toConvert, $_format)
720     {
721         //$_format = "Y-m-d H:i:s";
722         foreach ($_toConvert as $field => &$value) {
723             if (! $value) {
724                 if (in_array($field, $this->_datetimeFields)) {
725                     $_toConvert[$field] = NULL;
726                 }
727             } elseif ($value instanceof DateTime) {
728                 $_toConvert[$field] = $value->format($_format);
729             } elseif (is_array($value)) {
730                 $this->_convertDateTimeToString($value, $_format);
731             }
732         }
733     }
734     
735     /**
736      * Converts iso8601 formated dates into Tinebase_DateTime representation
737      * 
738      * @param array &$_data
739      * @return void
740      */
741     public function _convertISO8601ToDateTime(array &$_data)
742     {
743         foreach (array($this->_datetimeFields, $this->_dateFields) as $dtFields) {
744             foreach ($dtFields as $field) {
745                 if (!isset($_data[$field])) {
746                     continue;
747                 }
748                 
749                 $value = $_data[$field];
750                 
751                 if ($value instanceof DateTime) {
752                     continue;
753                 }
754                 
755                 if (! is_array($value) && strpos($value, ',') !== false) {
756                     $value = explode(',', $value);
757                 }
758                 
759                 try {
760                     if (is_array($value)) {
761                         foreach($value as $dataKey => $dataValue) {
762                             if ($dataValue instanceof DateTime) {
763                                 continue;
764                             }
765                             
766                             $value[$dataKey] =  (int)$dataValue == 0 ? NULL : new Tinebase_DateTime($dataValue);
767                         }
768                     } else {
769                         $value = (int)$value == 0 ? NULL : new Tinebase_DateTime($value);
770                         
771                     }
772                 } catch (Tinebase_Exception_Date $zde) {
773                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Error while converting date field "' . $field . '": ' . $zde->getMessage());
774                     $value = NULL;
775                 }
776                 
777                 $_data[$field] = $value;
778             }
779         }
780         
781     }
782     
783     /**
784      * cut the timezone-offset from the iso representation in order to force 
785      * Tinebase_DateTime to create dates in the user timezone. otherwise they will be 
786      * created with Etc/GMT+<offset> as timezone which would lead to incorrect 
787      * results in datetime computations!
788      * 
789      * @param  string Tinebase_DateTime::ISO8601 representation of a datetime filed
790      * @return string ISO8601LONG representation ('Y-m-d H:i:s')
791      */
792     protected function _convertISO8601ToISO8601LONG($_ISO)
793     {
794         $cutedISO = preg_replace('/[+\-]{1}\d{2}:\d{2}/', '', $_ISO);
795         $cutedISO = str_replace('T', ' ', $cutedISO);
796         
797         return $cutedISO;
798     }
799     
800     /**
801      * returns the default filter group for this model
802      * @return string
803      */
804     protected static function _getDefaultFilterGroup()
805     {
806         return get_called_class() . 'Filter';
807     }
808     
809     /**
810      * required by ArrayAccess interface
811      *
812      * @param mixed $_offset
813      * @return boolean
814      */
815     public function offsetExists($_offset)
816     {
817         return isset($this->_properties[$_offset]);
818     }
819     
820     /**
821      * required by ArrayAccess interface
822      *
823      * @param mixed $_offset
824      * @return mixed
825      */
826     public function offsetGet($_offset)
827     {
828         return $this->__get($_offset);
829     }
830     
831     /**
832      * required by ArrayAccess interface
833      *
834      * @param mixed $_offset
835      * @param mixed $_value
836      */
837     public function offsetSet($_offset, $_value)
838     {
839         $this->__set($_offset, $_value);
840     }
841     
842     /**
843      * required by ArrayAccess interface
844      *
845      * @param mixed $_offset
846      * @throws Tinebase_Exception_Record_NotAllowed
847      */
848     public function offsetUnset($_offset)
849     {
850         throw new Tinebase_Exception_Record_NotAllowed('Unsetting of properties is not allowed');
851     }
852     
853     /**
854      * required by IteratorAggregate interface
855      */
856     public function getIterator()
857     {
858         return new ArrayIterator($this->_properties);
859     }
860     
861     /**
862      * returns a random 40-character hexadecimal number to be used as 
863      * universal identifier (UID)
864      * 
865      * @param int|null $_length the length of the uid, defaults to 40
866      * @return string 40-character hexadecimal number
867      */
868     public static function generateUID($_length = null)
869     {
870         $uid = sha1(mt_rand() . microtime());
871         
872         if ($_length && $_length > 0) {
873             $uid = substr($uid, 0, $_length);
874         }
875         
876         return $uid;
877     }
878
879     /**
880      * converts a int, string or Tinebase_Record_Interface to a id
881      *
882      * @param int|string|Tinebase_Record_Abstract $_id the id to convert
883      * @param string $_modelName
884      * @return int|string
885      * @throws Tinebase_Exception_InvalidArgument
886      */
887     public static function convertId($_id, $_modelName = 'Tinebase_Record_Abstract')
888     {
889         if ($_id instanceof $_modelName) {
890             /** @var Tinebase_Record_Interface $_id */
891             if (! $_id->getId()) {
892                 throw new Tinebase_Exception_InvalidArgument('No id set!');
893             }
894             $id = $_id->getId();
895         } elseif (is_array($_id)) {
896             throw new Tinebase_Exception_InvalidArgument('Id can not be an array!');
897         } else {
898             $id = $_id;
899         }
900     
901         if ($id === 0) {
902             throw new Tinebase_Exception_InvalidArgument($_modelName . '.id can not be 0!');
903         }
904     
905         return $id;
906     }
907     
908     /**
909      * returns a Tinebase_Record_Diff record with differences to the given record
910      * 
911      * @param Tinebase_Record_Interface $_record record for comparison
912      * @param array $omitFields omit fields (for example modlog fields)
913      * @return Tinebase_Record_Diff|NULL
914      */
915     public function diff($_record, $omitFields = array())
916     {
917         /** TODO remove this! why is it here? */
918         if (! $_record instanceof Tinebase_Record_Abstract) {
919             /** @var Tinebase_Record_Diff $_record  ... really?!? */
920             return $_record;
921         }
922         
923         $result = new Tinebase_Record_Diff(array(
924             'id'     => $this->getId(),
925             'model'  => get_class($_record),
926         ));
927         $diff = array();
928         $oldData = array();
929         foreach (array_keys($this->_validators) as $fieldName) {
930             if (in_array($fieldName, $omitFields)) {
931                 continue;
932             }
933             
934             $ownField = $this->__get($fieldName);
935             $recordField = $_record->$fieldName;
936
937             if ($fieldName == 'customfields' && is_array($ownField) && is_array($recordField)) {
938                 // special handling for customfields, remove empty customfields from array
939                 foreach (array_keys($recordField, '', true) as $key) {
940                     unset($recordField[$key]);
941                 }
942                 foreach (array_keys($ownField, '', true) as $key) {
943                     unset($ownField[$key]);
944                 }
945             }
946
947             if (in_array($fieldName, $this->_datetimeFields)) {
948                 if ($ownField instanceof DateTime
949                     && $recordField instanceof DateTime) {
950
951                     /** @var Tinebase_DateTime $recordField */
952                     
953                     if (! $ownField instanceof Tinebase_DateTime) {
954                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . 
955                             ' Convert ' . $fieldName .' to Tinebase_DateTime to make sure we have the compare() method');
956                         $ownField = new Tinebase_DateTime($ownField);
957                     }
958                         
959                     if ($ownField->compare($recordField) === 0) {
960                         continue;
961                     } else {
962                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
963                             ' datetime for field ' . $fieldName . ' is not equal: '
964                             . $ownField->getIso() . ' != '
965                             . $recordField->getIso()
966                         );
967                     } 
968                 } else if (! $recordField instanceof DateTime && $ownField == $recordField) {
969                     continue;
970                 } 
971             } else if ($fieldName == $this->_identifier && $this->getId() == $_record->getId()) {
972                 continue;
973             } else if ($ownField instanceof Tinebase_Record_Abstract || $ownField instanceof Tinebase_Record_RecordSet) {
974                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
975                     ' Doing subdiff for field ' . $fieldName);
976                 $subdiff = $ownField->diff($recordField);
977                 if (is_object($subdiff) && ! $subdiff->isEmpty()) {
978                     $diff[$fieldName] = $subdiff;
979                     $oldData[$fieldName] = $ownField;
980                 }
981                 continue;
982             } else if ($recordField instanceof Tinebase_Record_Abstract && is_scalar($ownField)) {
983                 // maybe we have the id of the record -> just compare the id
984                 if ($recordField->getId() == $ownField) {
985                     continue;
986                 } else {
987                     $recordField = $recordField->getId();
988                 }
989             } else if ($ownField == $recordField) {
990                 continue;
991             } else if (empty($ownField) && empty($recordField)) {
992                 continue;
993             } else if ((empty($ownField)    && $recordField instanceof Tinebase_Record_RecordSet && count($recordField) == 0)
994                 ||     (empty($recordField) && $ownField    instanceof Tinebase_Record_RecordSet && count($ownField) == 0) )
995             {
996                 continue;
997             }
998
999             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1000                 ' Found diff for ' . $fieldName .'(this/other):' . print_r($ownField, true) . '/' . print_r($recordField, true) );
1001             
1002             $diff[$fieldName] = $recordField;
1003             $oldData[$fieldName] = $ownField;
1004         }
1005         
1006         $result->diff = $diff;
1007         $result->oldData = $oldData;
1008         return $result;
1009     }
1010     
1011     /**
1012      * merge given record into $this
1013      * 
1014      * @param Tinebase_Record_Interface $record
1015      * @param Tinebase_Record_Diff $diff
1016      * @return Tinebase_Record_Interface
1017      */
1018     public function merge($record, $diff = null)
1019     {
1020         if (! $this->getId()) {
1021             $this->setId($record->getId());
1022         }
1023         
1024         if ($diff === null) {
1025             $diff = $this->diff($record);
1026         }
1027         
1028         if ($diff === null || empty($diff->diff)) {
1029             return $this;
1030         }
1031         
1032         foreach ($diff->diff as $field => $value) {
1033             if (empty($this->{$field})) {
1034                 $this->{$field} = $value;
1035             }
1036         }
1037         
1038         return $this;
1039     }
1040     
1041     /**
1042      * check if data got modified
1043      * 
1044      * @return boolean
1045      */
1046     public function isDirty()
1047     {
1048         return $this->_isDirty;
1049     }
1050
1051     /**
1052      * returns TRUE if given record obsoletes this one
1053      *
1054      * @param  Tinebase_Record_Interface $_record
1055      * @return bool
1056      * @throws Tinebase_Exception_InvalidArgument
1057      */
1058     public function isObsoletedBy(Tinebase_Record_Interface $_record)
1059     {
1060         if (get_class($_record) !== get_class($this)) {
1061             throw new Tinebase_Exception_InvalidArgument('Records could not be compared');
1062         } else if ($this->getId() && $_record->getId() !== $this->getId()) {
1063             throw new Tinebase_Exception_InvalidArgument('Record id mismatch');
1064         }
1065         
1066         if ($this->has('seq') && $_record->seq != $this->seq) {
1067             return $_record->seq > $this->seq;
1068         }
1069         
1070         return ($this->has('last_modified_time')) ? $_record->last_modified_time > $this->last_modified_time : TRUE;
1071     }
1072     
1073     /**
1074      * check if two records are equal
1075      * 
1076      * @param  Tinebase_Record_Interface $_record record for comparism
1077      * @param  array                     $_toOmit fields to omit
1078      * @return bool
1079      */
1080     public function isEqual($_record, array $_toOmit = array())
1081     {
1082         $diff = $this->diff($_record);
1083         return ($diff) ? $diff->isEmpty($_toOmit) : FALSE;
1084     }
1085     
1086     /**
1087      * translate this records' fields
1088      *
1089      */
1090     public function translate()
1091     {
1092         // get translation object
1093         if (!empty($this->_toTranslate)) {
1094             $translate = Tinebase_Translation::getTranslation($this->_application);
1095             
1096             foreach ($this->_toTranslate as $field) {
1097                 $this->$field = $translate->_($this->$field);
1098             }
1099         }
1100     }
1101
1102     /**
1103      * check if the model has a specific field (container_id for example)
1104      *
1105      * @param string $_field
1106      * @return boolean
1107      */
1108     public function has($_field) 
1109     {
1110         return ((isset($this->_validators[$_field]) || array_key_exists ($_field, $this->_validators)));
1111     }   
1112
1113     /**
1114      * get fields
1115      * 
1116      * @return array
1117      */
1118     public function getFields()
1119     {
1120         return array_keys($this->_validators);
1121     }
1122     
1123     /**
1124      * fills a record from json data
1125      *
1126      * @param string $_data json encoded data
1127      * @return void
1128      * 
1129      * @todo replace this (and setFromJsonInUsersTimezone) with Tinebase_Convert_Json::toTine20Model
1130      * @todo move custom _setFromJson to (custom) converter
1131      */
1132     public function setFromJson($_data)
1133     {
1134         if (is_array($_data)) {
1135             $recordData = $_data;
1136         } else {
1137             $recordData = Zend_Json::decode($_data);
1138         }
1139
1140         if ($this->has('image') && !empty($_data['image']) && preg_match('/location=tempFile&id=([a-z0-9]*)/', $_data['image'], $matches)) {
1141             // add image to attachments
1142             if (! isset($recordData['attachments'])) {
1143                 $recordData['attachments'] = array();
1144             }
1145             $recordData['attachments'][] = array('tempFile' => array('id' => $matches[1]));
1146         }
1147
1148         // sanitize container id if it is an array
1149         if ($this->has('container_id') && isset($recordData['container_id']) && is_array($recordData['container_id']) && isset($recordData['container_id']['id']) ) {
1150             $recordData['container_id'] = $recordData['container_id']['id'];
1151         }
1152
1153         $this->_setFromJson($recordData);
1154         $this->setFromArray($recordData);
1155     }
1156     
1157     /**
1158      * can be reimplemented by subclasses to modify values during setFromJson
1159      * @param array $_data the json decoded values
1160      * @return void
1161      */
1162     protected function _setFromJson(array &$_data)
1163     {
1164         
1165     }
1166
1167     /**
1168      * returns modlog omit fields
1169      *
1170      * @return array
1171      */
1172     public function getModlogOmitFields()
1173     {
1174         return $this->_modlogOmitFields;
1175     }
1176
1177     /**
1178      * returns read only fields
1179      *
1180      * @return array
1181      */
1182     public function getReadOnlyFields()
1183     {
1184         return $this->_readOnlyFields;
1185     }
1186
1187     /**
1188      * sets the non static properties by the created configuration object on instantiation
1189      */
1190     protected function _setFromConfigurationObject()
1191     {
1192         // set protected, non static properties
1193         $co = static::getConfiguration();
1194         if ($co && $mc = $co->toArray()) {
1195             foreach ($mc as $property => $value) {
1196                 $this->{$property} = $value;
1197             }
1198         }
1199     }
1200
1201     /**
1202      * returns the title of the record
1203      * 
1204      * @return string
1205      */
1206     public function getTitle()
1207     {
1208         $c = static::getConfiguration();
1209         
1210         // TODO: fallback, remove if all models use modelconfiguration
1211         if (! $c) {
1212             return $this->has('title') ? $this->title :
1213                 ($this->has('name') ? $this->name : $this->{$this->_identifier});
1214         }
1215         
1216         // use vsprintf formatting if it is an array
1217         if (is_array($c->titleProperty)) {
1218             if (! is_array($c->titleProperty[1])) {
1219                 $propertyValues = array($this->{$c->titleProperty[1]});
1220             } else {
1221                 $propertyValues = array();
1222                 foreach($c->titleProperty[1] as $property) {
1223                     $propertyValues[] = $this->{$property};
1224                 }
1225             }
1226             return vsprintf($c->titleProperty[0], $propertyValues);
1227         } else {
1228             return $this->{$c->titleProperty};
1229         }
1230     }
1231     
1232     /**
1233      * returns the foreignId fields (used in Tinebase_Convert_Json)
1234      * @return array
1235      */
1236     public static function getResolveForeignIdFields()
1237     {
1238         return static::$_resolveForeignIdFields;
1239     }
1240     
1241     /**
1242      * returns all textfields having labels for the autocomplete field function
1243      * 
1244      * @return array
1245      */
1246     public static function getAutocompleteFields()
1247     {
1248         $keys = array();
1249         
1250         foreach (self::getConfiguration()->getFields() as $key => $fieldDef) {
1251             if ($fieldDef['type'] == 'string' || $fieldDef['type'] == 'text') {
1252                 $keys[] = $key;
1253             }
1254         }
1255         
1256         return $keys;
1257     }
1258
1259     public function runConvertToRecord()
1260     {
1261         $conf = self::getConfiguration();
1262         if (null === $conf) {
1263             return;
1264         }
1265         foreach ($conf->getConverters() as $key => $converters) {
1266             if (isset($this->_properties[$key])) {
1267                 /** @var Tinebase_Model_Converter_Interface $converter */
1268                 foreach ($converters as $converter) {
1269                     $this->_properties[$key] = $converter::convertToRecord($this->_properties[$key]);
1270                 }
1271             }
1272         }
1273     }
1274
1275     public function runConvertToData()
1276     {
1277         $conf = self::getConfiguration();
1278         if (null === $conf) {
1279             return;
1280         }
1281         foreach ($conf->getConverters() as $key => $converters) {
1282             if (isset($this->_properties[$key])) {
1283                 foreach ($converters as $converter) {
1284                     /** @var Tinebase_Model_Converter_Interface $converter */
1285                     $this->_properties[$key] = $converter::convertToData($this->_properties[$key]);
1286                 }
1287             }
1288         }
1289     }
1290
1291     public static function getSimpleModelName($application, $model)
1292     {
1293         $appName = is_string($application) ? $application : $application->name;
1294         return str_replace($appName . '_Model_', '', $model);
1295     }
1296
1297     /**
1298      * undoes the change stored in the diff
1299      *
1300      * @param Tinebase_Record_Diff $diff
1301      * @return void
1302      */
1303     public function undo(Tinebase_Record_Diff $diff)
1304     {
1305         /* TODO special treatment? for what? how?
1306          * oldData does not contain RecordSetDiffs. It plainly contains the old data present in the property before it was changed.
1307          */
1308
1309         if ($this->has('is_deleted')) {
1310             $this->is_deleted = 0;
1311         }
1312
1313         foreach((array)($diff->oldData) as $property => $oldValue)
1314         {
1315             if ('customfields' === $property) {
1316                 if (!is_array($oldValue)) {
1317                     $oldValue = array();
1318                 }
1319                 if (isset($diff->diff['customfields']) && is_array($diff->diff['customfields'])) {
1320                     foreach (array_keys($diff->diff['customfields']) as $unSetProperty) {
1321                         if (!isset($oldValue[$unSetProperty])) {
1322                             $oldValue[$unSetProperty] = null;
1323                         }
1324                     }
1325                 }
1326             } elseif (in_array($property, $this->_datetimeFields) && ! is_object($oldValue)) {
1327                 $oldValue = new Tinebase_DateTime($oldValue);
1328             }
1329             $this->$property = $oldValue;
1330         }
1331     }
1332
1333     /**
1334      * applies the change stored in the diff
1335      *
1336      * @param Tinebase_Record_Diff $diff
1337      * @return void
1338      */
1339     public function applyDiff(Tinebase_Record_Diff $diff)
1340     {
1341         /* TODO special treatment? for what? how? */
1342
1343         if ($this->has('is_deleted')) {
1344             $this->is_deleted = 0;
1345         }
1346
1347         foreach((array)($diff->diff) as $property => $oldValue)
1348         {
1349             if (is_array($oldValue) && count($oldValue) === 4 &&
1350                     isset($oldValue['model']) && isset($oldValue['added']) &&
1351                     isset($oldValue['removed']) && isset($oldValue['modified'])) {
1352                 // RecordSetDiff
1353                 $recordSetDiff = new Tinebase_Record_RecordSetDiff($oldValue);
1354
1355                 if (! $this->$property instanceof Tinebase_Record_RecordSet) {
1356                     $this->$property = new Tinebase_Record_RecordSet($oldValue['model'],
1357                         is_array($this->$property)?$this->$property:array());
1358                 }
1359
1360                 /** @var Tinebase_Record_Abstract $model */
1361                 $model = $recordSetDiff->model;
1362                 if (true !== $model::applyRecordSetDiff($this->$property, $recordSetDiff)) {
1363                     $this->$property->applyRecordSetDiff($recordSetDiff);
1364                 }
1365             } else {
1366                 if (in_array($property, $this->_datetimeFields) && ! is_object($oldValue)) {
1367                     $oldValue = new Tinebase_DateTime($oldValue);
1368                 }
1369                 $this->$property = $oldValue;
1370             }
1371         }
1372     }
1373
1374     /**
1375      * @param Tinebase_Record_RecordSet $_recordSet
1376      * @param Tinebase_Record_RecordSetDiff $_recordSetDiff
1377      * @return bool
1378      */
1379     public static function applyRecordSetDiff(Tinebase_Record_RecordSet $_recordSet, Tinebase_Record_RecordSetDiff $_recordSetDiff)
1380     {
1381         return false;
1382     }
1383
1384     /**
1385      * returns true if this record should be replicated
1386      *
1387      * @return boolean
1388      */
1389     public function isReplicable()
1390     {
1391         return false;
1392     }
1393
1394     /**
1395      * @param Tinebase_Record_Interface|null $_parent
1396      * @param Tinebase_Record_Interface|null $_child
1397      * @return string
1398      */
1399     public function getPathPart(Tinebase_Record_Interface $_parent = null, Tinebase_Record_Interface $_child = null)
1400     {
1401         /** @var Tinebase_Record_Abstract_GetPathPartDelegatorInterface $delegate */
1402         $delegate = Tinebase_Core::getDelegate($this->_application, 'getPathPartDelegate_' . get_called_class() ,
1403                                                 'Tinebase_Record_Abstract_GetPathPartDelegatorInterface');
1404         if (false !== $delegate) {
1405             return $delegate->getPathPart($this, $_parent, $_child);
1406         }
1407
1408         $parentType = null !== $_parent ? $_parent->getTypeForPathPart() : '';
1409         $childType = null !== $_child ? $_child->getTypeForPathPart() : '';
1410
1411         return $parentType . '/' . mb_substr(str_replace(array('/', '{', '}'), '', trim($this->getTitle())), 0, 1024) . $childType;
1412     }
1413
1414     /**
1415      * @return string
1416      */
1417     public function getTypeForPathPart()
1418     {
1419         return '';
1420     }
1421
1422     /**
1423      * @param Tinebase_Record_Interface|null $_parent
1424      * @param Tinebase_Record_Interface|null $_child
1425      * @return string
1426      *
1427      * TODO use decorators ? or overwrite
1428      */
1429     public function getShadowPathPart(Tinebase_Record_Interface $_parent = null, Tinebase_Record_Interface $_child = null)
1430     {
1431         $parentType = null !== $_parent ? $_parent->getTypeForPathPart() : '';
1432         $childType = null !== $_child ? $_child->getTypeForPathPart() : '';
1433
1434         return $parentType . '/{' . get_class($this) . '}' . $this->getId() . $childType;
1435     }
1436
1437     /**
1438      * returns an array containing the parent neighbours relation objects or record(s) (ids) in the key 'parents'
1439      * and containing the children neighbours in the key 'children'
1440      *
1441      * @return array
1442      */
1443     public function getPathNeighbours()
1444     {
1445         $oldRelations = $this->relations;
1446         $this->relations = null;
1447
1448         $relations = Tinebase_Relations::getInstance();
1449         $result = array(
1450             'parents'  => $relations->getRelationsOfRecordByDegree($this, Tinebase_Model_Relation::DEGREE_PARENT, true)->asArray(),
1451             'children' => $relations->getRelationsOfRecordByDegree($this, Tinebase_Model_Relation::DEGREE_CHILD, true)->asArray()
1452         );
1453
1454         $this->relations = $oldRelations;
1455         return $result;
1456     }
1457
1458     /**
1459      * extended properties getter
1460      *
1461      * @param string $_property
1462      * @return array
1463      */
1464     public function &xprops($_property = 'xprops')
1465     {
1466         if (!isset($this->_properties[$_property])) {
1467             $this->_properties[$_property] = array();
1468         } else if (is_string($this->_properties[$_property])) {
1469             $this->_properties[$_property] = json_decode($this->_properties[$_property], true);
1470         }
1471
1472         return $this->_properties[$_property];
1473     }
1474
1475     /**
1476      * @param Tinebase_Record_RecordSet $_recordSetOne
1477      * @param Tinebase_Record_RecordSet $_recordSetTwo
1478      * @return null|Tinebase_Record_RecordSetDiff
1479      */
1480     public static function recordSetDiff(Tinebase_Record_RecordSet $_recordSetOne, Tinebase_Record_RecordSet $_recordSetTwo)
1481     {
1482         return null;
1483     }
1484 }