11762 use doctrine for schema creation and update
[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-2016 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     /******************************** functions ****************************************/
219     
220     /**
221      * Default constructor
222      * Constructs an object and sets its record related properties.
223      * 
224      * @todo what happens if not all properties in the datas are set?
225      * The default values must also be set, even if no filtering is done!
226      * 
227      * @param mixed $_data
228      * @param bool $bypassFilters sets {@see this->bypassFilters}
229      * @param mixed $convertDates sets {@see $this->convertDates} and optionaly {@see $this->$dateConversionFormat}
230      * @throws Tinebase_Exception_Record_DefinitionFailure
231      */
232     public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
233     {
234         // apply configuration
235         $this->_setFromConfigurationObject();
236         
237         if ($this->_identifier === NULL) {
238             throw new Tinebase_Exception_Record_DefinitionFailure('$_identifier is not declared');
239         }
240         
241         $this->bypassFilters = (bool)$_bypassFilters;
242         $this->convertDates = (bool)$_convertDates;
243         if (is_string($_convertDates)) {
244             $this->dateConversionFormat = $_convertDates;
245         }
246
247         if ($this->has('description') && (! (isset($this->_filters['description']) || array_key_exists('description', $this->_filters)))) {
248             $this->_filters['description'] = new Tinebase_Model_InputFilter_CrlfConvert();
249         }
250
251         if (is_array($_data)) {
252             $this->setFromArray($_data);
253         }
254         
255         $this->_isDirty = false;
256     }
257     
258     /**
259      * returns the configuration object
260      *
261      * @return Tinebase_ModelConfiguration|NULL
262      */
263     public static function getConfiguration()
264     {
265         if (! isset (static::$_modelConfiguration)) {
266             return NULL;
267         }
268         
269         if (! static::$_configurationObject) {
270             static::$_configurationObject = new Tinebase_ModelConfiguration(static::$_modelConfiguration);
271         }
272     
273         return static::$_configurationObject;
274     }
275
276     /**
277      * returns the relation config
278      * 
279      * @deprecated
280      * @return array
281      */
282     public static function getRelatableConfig()
283     {
284         return static::$_relatableConfig;
285     }
286     
287     /**
288      * recursivly clone properties
289      */
290     public function __clone()
291     {
292         foreach ($this->_properties as $name => $value)
293         {
294             if (is_object($value)) {
295                 $this->_properties[$name] = clone $value;
296             } else if (is_array($value)) {
297                 foreach ($value as $arrKey => $arrValue) {
298                     if (is_object($arrValue)) {
299                         $value[$arrKey] = clone $arrValue;
300                     }
301                 }
302             }
303         }
304     }
305     
306     /**
307      * sets identifier of record
308      * 
309      * @param int identifier
310      * @return void
311      */
312     public function setId($_id)
313     {
314         // set internal state to "not validated"
315         $this->_isValidated = false;
316         
317         if ($this->bypassFilters === true) {
318             $this->_properties[$this->_identifier] = $_id;
319         } else {
320             $this->__set($this->_identifier, $_id);
321         }
322     }
323     
324     /**
325      * gets identifier of record
326      * 
327      * @return int identifier
328      */
329     public function getId()
330     {
331         if (! isset($this->_properties[$this->_identifier])) {
332             $this->setId(NULL);
333         }
334         return $this->_properties[$this->_identifier];
335     }
336     
337     /**
338      * gets application the records belongs to
339      * 
340      * @return string application
341      */
342     public function getApplication()
343     {
344         return $this->_application;
345     }
346     
347     /**
348      * returns id property of this model
349      *
350      * @return string
351      */
352     public function getIdProperty()
353     {
354         return $this->_identifier;
355     }
356     
357     /**
358      * sets the record related properties from user generated input.
359      * 
360      * Input-filtering and validation by Zend_Filter_Input can enabled and disabled
361      *
362      * @param array $_data            the new data to set
363      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
364      * 
365      * @todo remove custom fields handling (use Tinebase_Record_RecordSet for them)
366      */
367     public function setFromArray(array $_data)
368     {
369         if ($this->convertDates === true) {
370             $this->_convertISO8601ToDateTime($_data);
371         }
372         
373         // set internal state to "not validated"
374         $this->_isValidated = false;
375
376         // get custom fields
377         if ($this->has('customfields')) {
378             $application = Tinebase_Application::getInstance()->getApplicationByName($this->_application);
379             $customFields = Tinebase_CustomField::getInstance()->getCustomFieldsForApplication($application, get_class($this))->name;
380             $recordCustomFields = array();
381         } else {
382             $customFields = array();
383         }
384         
385         // make sure we run through the setters
386         $bypassFilter = $this->bypassFilters;
387         $this->bypassFilters = true;
388         foreach ($_data as $key => $value) {
389             if ((isset($this->_validators[$key]) || array_key_exists ($key, $this->_validators))) {
390                 $this->$key = $value;
391             } else if (in_array($key, $customFields)) {
392                 $recordCustomFields[$key] = $value;
393             }
394         }
395         if (!empty($recordCustomFields)) {
396             $this->customfields = $recordCustomFields;
397         }
398         
399         $this->bypassFilters = $bypassFilter;
400         if ($this->bypassFilters !== true) {
401             $this->isValid(true);
402         }
403     }
404     
405     /**
406      * wrapper for setFromJason which expects datetimes in array to be in
407      * users timezone and converts them to UTC
408      *
409      * @todo move this to a generic __call interceptor setFrom<API>InUsersTimezone
410      * 
411      * @param  string $_data json encoded data
412      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
413      */
414     public function setFromJsonInUsersTimezone($_data)
415     {
416         // change timezone of current php process to usertimezone to let new dates be in the users timezone
417         // NOTE: this is neccessary as creating the dates in UTC and just adding/substracting the timeshift would
418         //       lead to incorrect results on DST transistions 
419         date_default_timezone_set(Tinebase_Core::getUserTimezone());
420
421         // NOTE: setFromArray creates new Tinebase_DateTimes of $this->datetimeFields
422         $this->setFromJson($_data);
423         
424         // convert $this->_datetimeFields into the configured server's timezone (UTC)
425         $this->setTimezone('UTC');
426         
427         // finally reset timzone of current php process to the configured server timezone (UTC)
428         date_default_timezone_set('UTC');
429     }
430     
431     /**
432      * Sets timezone of $this->_datetimeFields
433      * 
434      * @see Tinebase_DateTime::setTimezone()
435      * @param  string $_timezone
436      * @param  bool   $_recursive
437      * @return  void
438      * @throws Tinebase_Exception_Record_Validation
439      */
440     public function setTimezone($_timezone, $_recursive = TRUE)
441     {
442         foreach ($this->_datetimeFields as $field) {
443             if (!isset($this->_properties[$field])) continue;
444             
445             if (!is_array($this->_properties[$field])) {
446                 $toConvert = array($this->_properties[$field]);
447             } else {
448                 $toConvert = $this->_properties[$field];
449             }
450
451             foreach ($toConvert as $field => &$value) {
452                 if (! method_exists($value, 'setTimezone')) {
453                     throw new Tinebase_Exception_Record_Validation($field . 'must be a method setTimezone');
454                 } 
455                 $value->setTimezone($_timezone);
456             } 
457         }
458         
459         if ($_recursive) {
460             foreach ($this->_properties as $property => $value) {
461                 if ($value && is_object($value) && 
462                         (in_array('Tinebase_Record_Interface', class_implements($value)) || 
463                         $value instanceof Tinebase_Record_Recordset) ) {
464                        
465                     $value->setTimezone($_timezone, TRUE);
466                 }
467             }
468         }
469     }
470     
471     /**
472      * returns array of fields with validation errors 
473      *
474      * @return array
475      */
476     public function getValidationErrors()
477     {
478         return $this->_validationErrors;
479     }
480     
481     /**
482      * returns array with record related properties 
483      *
484      * @param boolean $_recursive
485      * @return array
486      */
487     public function toArray($_recursive = TRUE)
488     {
489         $recordArray = $this->_properties;
490         if ($this->convertDates === true) {
491             if (! is_string($this->dateConversionFormat)) {
492                 $this->_convertDateTimeToString($recordArray, Tinebase_Record_Abstract::ISO8601LONG);
493             } else {
494                 $this->_convertDateTimeToString($recordArray, $this->dateConversionFormat);
495             }
496         }
497         
498         if ($_recursive) {
499             foreach ($recordArray as $property => $value) {
500                 if ($this->_hasToArray($value)) {
501                     $recordArray[$property] = $value->toArray();
502                 }
503             }
504         }
505         
506         return $recordArray;
507     }
508     
509     /**
510      * checks if variable has toArray()
511      * 
512      * @param mixed $mixed
513      * @return boolean
514      */
515     protected function _hasToArray($mixed)
516     {
517         return is_object($mixed) && method_exists($mixed, 'toArray');
518     }
519     
520     /**
521      * validate and filter the the internal data
522      *
523      * @param $_throwExceptionOnInvalidData
524      * @return bool
525      * @throws Tinebase_Exception_Record_Validation
526      */
527     public function isValid($_throwExceptionOnInvalidData = false)
528     {
529         if ($this->_isValidated === true) {
530             return true;
531         }
532         
533         $inputFilter = $this->_getFilter()
534             ->setData($this->_properties);
535         
536         if ($inputFilter->isValid()) {
537             // set $this->_properties with the filtered values
538             $this->_properties  = $inputFilter->getUnescaped();
539             $this->_isValidated = true;
540             
541             return true;
542         }
543         
544         $this->_validationErrors = array();
545         
546         foreach ($inputFilter->getMessages() as $fieldName => $errorMessage) {
547             $this->_validationErrors[] = array(
548                 'id'  => $fieldName,
549                 'msg' => $errorMessage
550             );
551         }
552         
553         if ($_throwExceptionOnInvalidData) {
554             $e = new Tinebase_Exception_Record_Validation('Some fields ' . implode(',', array_keys($inputFilter->getMessages()))
555                 . ' have invalid content');
556             
557             if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " "
558                 . $e->getMessage()
559                 . print_r($this->_validationErrors, true));
560             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
561                 . ' Record: ' . print_r($this->toArray(), true));
562             
563             throw $e;
564         }
565         
566         return false;
567     }
568     
569     /**
570      * apply filter
571      *
572      * @todo implement
573      */
574     public function applyFilter()
575     {
576         $this->isValid(true);
577     }
578     
579     /**
580      * sets record related properties
581      * 
582      * @param string _name of property
583      * @param mixed _value of property
584      * @throws Tinebase_Exception_UnexpectedValue
585      * @return void
586      */
587     public function __set($_name, $_value)
588     {
589         if (! (isset($this->_validators[$_name]) || array_key_exists ($_name, $this->_validators))) {
590             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
591         }
592         
593         if ($this->bypassFilters !== true) {
594             $this->_properties[$_name] = $this->_validateField($_name, $_value);
595         } else {
596             $this->_properties[$_name] = $_value;
597             
598             $this->_isValidated = false;
599         }
600         
601         $this->_isDirty = true;
602     }
603     
604     protected function _validateField($name, $value)
605     {
606         $inputFilter = $this->_getFilter($name);
607         $inputFilter->setData(array(
608             $name => $value
609         ));
610         
611         if ($inputFilter->isValid()) {
612             return $inputFilter->getUnescaped($name);
613         }
614         
615         $this->_validationErrors = array();
616         
617         foreach($inputFilter->getMessages() as $fieldName => $errorMessage) {
618             $this->_validationErrors[] = array(
619                 'id'  => $fieldName,
620                 'msg' => $errorMessage
621             );
622         }
623         
624         $e = new Tinebase_Exception_Record_Validation('the field ' . implode(',', array_keys($inputFilter->getMessages())) . ' has invalid content');
625         Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ":\n" .
626             print_r($this->_validationErrors,true). $e);
627         throw $e;
628     }
629     
630     /**
631      * unsets record related properties
632      * 
633      * @param string _name of property
634      * @throws Tinebase_Exception_UnexpectedValue
635      * @return void
636      */
637     public function __unset($_name)
638     {
639         if (!(isset($this->_validators[$_name]) || array_key_exists ($_name, $this->_validators))) {
640             throw new Tinebase_Exception_UnexpectedValue($_name . ' is no property of $this->_properties');
641         }
642
643         unset($this->_properties[$_name]);
644         
645         $this->_isValidated = false;
646         
647         if ($this->bypassFilters !== true) {
648             $this->isValid(true);
649         }
650     }
651     
652     /**
653      * checkes if an propertiy is set
654      * 
655      * @param string _name name of property
656      * @return bool property is set or not
657      */
658     public function __isset($_name)
659     {
660         return isset($this->_properties[$_name]);
661     }
662     
663     /**
664      * gets record related properties
665      * 
666      * @param  string  $name  name of property
667      * @return mixed value of property
668      */
669     public function __get($name)
670     {
671         return (isset($this->_properties[$name]) || array_key_exists($name, $this->_properties))
672             ? $this->_properties[$name] 
673             : NULL;
674     }
675     
676    /** convert this to string
677     *
678     * @return string
679     */
680     public function __toString()
681     {
682        return print_r($this->toArray(), TRUE);
683     }
684     
685     /**
686      * returns a Zend_Filter for the $_filters and $_validators of this record class.
687      * we just create an instance of Filter if we really need it.
688      * 
689      * @return Zend_Filter_Input
690      */
691     protected function _getFilter($field = null)
692     {
693         $keyName = get_class($this) . $field;
694         
695         if (! (isset(self::$_inputFilters[$keyName]) || array_key_exists($keyName, self::$_inputFilters))) {
696             if ($field !== null) {
697                 $filters    = (isset($this->_filters[$field]) || array_key_exists($field, $this->_filters)) ? array($field => $this->_filters[$field]) : array();
698                 $validators = array($field => $this->_validators[$field]); 
699                 
700                 self::$_inputFilters[$keyName] = new Zend_Filter_Input($filters, $validators);
701             } else {
702                 self::$_inputFilters[$keyName] = new Zend_Filter_Input($this->_filters, $this->_validators);
703             }
704         }
705         
706         return self::$_inputFilters[$keyName];
707     }
708     
709     /**
710      * Converts Tinebase_DateTimes into custom representation
711      *
712      * @param array &$_toConvert
713      * @param string $_format
714      * @return void
715      */
716     protected function _convertDateTimeToString(&$_toConvert, $_format)
717     {
718         //$_format = "Y-m-d H:i:s";
719         foreach ($_toConvert as $field => &$value) {
720             if (! $value) {
721                 if (in_array($field, $this->_datetimeFields)) {
722                     $_toConvert[$field] = NULL;
723                 }
724             } elseif ($value instanceof DateTime) {
725                 $_toConvert[$field] = $value->format($_format);
726             } elseif (is_array($value)) {
727                 $this->_convertDateTimeToString($value, $_format);
728             }
729         }
730     }
731     
732     /**
733      * Converts iso8601 formated dates into Tinebase_DateTime representation
734      * 
735      * @param array &$_data
736      * @return void
737      */
738     protected function _convertISO8601ToDateTime(array &$_data)
739     {
740         foreach (array($this->_datetimeFields, $this->_dateFields) as $dtFields) {
741             foreach ($dtFields as $field) {
742                 if (!isset($_data[$field])) {
743                     continue;
744                 }
745                 
746                 $value = $_data[$field];
747                 
748                 if ($value instanceof DateTime) {
749                     continue;
750                 }
751                 
752                 if (! is_array($value) && strpos($value, ',') !== false) {
753                     $value = explode(',', $value);
754                 }
755                 
756                 try {
757                     if (is_array($value)) {
758                         foreach($value as $dataKey => $dataValue) {
759                             if ($dataValue instanceof DateTime) {
760                                 continue;
761                             }
762                             
763                             $value[$dataKey] =  (int)$dataValue == 0 ? NULL : new Tinebase_DateTime($dataValue);
764                         }
765                     } else {
766                         $value = (int)$value == 0 ? NULL : new Tinebase_DateTime($value);
767                         
768                     }
769                 } catch (Tinebase_DateTime_Exception $zde) {
770                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Error while converting date field "' . $field . '": ' . $zde->getMessage());
771                     $value = NULL;
772                 }
773                 
774                 $_data[$field] = $value;
775             }
776         }
777         
778     }
779     
780     /**
781      * cut the timezone-offset from the iso representation in order to force 
782      * Tinebase_DateTime to create dates in the user timezone. otherwise they will be 
783      * created with Etc/GMT+<offset> as timezone which would lead to incorrect 
784      * results in datetime computations!
785      * 
786      * @param  string Tinebase_DateTime::ISO8601 representation of a datetime filed
787      * @return string ISO8601LONG representation ('Y-m-d H:i:s')
788      */
789     protected function _convertISO8601ToISO8601LONG($_ISO)
790     {
791         $cutedISO = preg_replace('/[+\-]{1}\d{2}:\d{2}/', '', $_ISO);
792         $cutedISO = str_replace('T', ' ', $cutedISO);
793         
794         return $cutedISO;
795     }
796     
797     /**
798      * Converts time into iso representation (hh:mm:ss)
799      *
800      * @param array &$_data
801      * @return void
802      * 
803      * @todo    add support for hh:mm:ss AM|PM
804      */
805     protected function _convertTime(&$_data)
806     {
807         foreach ($this->_timeFields as $field) {
808             if (!isset($_data[$field]) || empty($_data[$field])) {
809                 continue;
810             }
811             
812             $hhmmss = explode(":", $_data[$field]);
813             if (count($hhmmss) === 2) {
814                 // seconds missing
815                 $hhmmss[] = '00';
816             }
817             list($hours, $minutes, $seconds) = $hhmmss;
818             if (preg_match('/AM|PM/', $minutes)) {
819                 list($minutes, $notation) = explode(" ", $minutes);
820                 switch($notation) {
821                     case 'AM':
822                         $hours = ($hours == '12') ? 0 : $hours;
823                         break;
824                     case 'PM':
825                         $hours = $hours + 12;
826                         break;
827                 }
828                 $_data[$field] = implode(':', $hhmmss);
829             }
830         }
831     }
832     
833     /**
834      * returns the default filter group for this model
835      * @return string
836      */
837     protected static function _getDefaultFilterGroup()
838     {
839         return get_called_class() . 'Filter';
840     }
841     
842     /**
843      * required by ArrayAccess interface
844      */
845     public function offsetExists($_offset)
846     {
847         return isset($this->_properties[$_offset]);
848     }
849     
850     /**
851      * required by ArrayAccess interface
852      */
853     public function offsetGet($_offset)
854     {
855         return $this->__get($_offset);
856     }
857     
858     /**
859      * required by ArrayAccess interface
860      */
861     public function offsetSet($_offset, $_value)
862     {
863         return $this->__set($_offset, $_value);
864     }
865     
866     /**
867      * required by ArrayAccess interface
868      * @throws Tinebase_Exception_Record_NotAllowed
869      */
870     public function offsetUnset($_offset)
871     {
872         throw new Tinebase_Exception_Record_NotAllowed('Unsetting of properties is not allowed');
873     }
874     
875     /**
876      * required by IteratorAggregate interface
877      */
878     public function getIterator()
879     {
880         return new ArrayIterator($this->_properties);
881     }
882     
883     /**
884      * returns a random 40-character hexadecimal number to be used as 
885      * universal identifier (UID)
886      * 
887      * @param int|optional $_length the length of the uid, defaults to 40
888      * @return string 40-character hexadecimal number
889      */
890     public static function generateUID($_length = false)
891     {
892         $uid = sha1(mt_rand(). microtime());
893         
894         if ($_length !== false) {
895             $uid = substr($uid, 0, $_length);
896         }
897         
898         return $uid;
899     }
900     
901     /**
902     * converts a int, string or Tinebase_Record_Interface to a id
903     *
904     * @param int|string|Tinebase_Record_Abstract $_id the id to convert
905     * @param string $_modelName
906     * @return int|string
907     */
908     public static function convertId($_id, $_modelName = 'Tinebase_Record_Abstract')
909     {
910         if ($_id instanceof $_modelName) {
911             if (! $_id->getId()) {
912                 throw new Tinebase_Exception_InvalidArgument('No id set!');
913             }
914             $id = $_id->getId();
915         } elseif (is_array($_id)) {
916             throw new Tinebase_Exception_InvalidArgument('Id can not be an array!');
917         } else {
918             $id = $_id;
919         }
920     
921         if ($id === 0) {
922             throw new Tinebase_Exception_InvalidArgument($_modelName . '.id can not be 0!');
923         }
924     
925         return $id;
926     }
927     
928     /**
929      * returns a Tinebase_Record_Diff record with differences to the given record
930      * 
931      * @param Tinebase_Record_Interface $_record record for comparison
932      * @param array $omitFields omit fields (for example modlog fields)
933      * @return Tinebase_Record_Diff|NULL
934      */
935     public function diff($_record, $omitFields = array())
936     {
937         if (! $_record instanceof Tinebase_Record_Abstract) {
938             return $_record;
939         }
940         
941         $result = new Tinebase_Record_Diff(array(
942             'id'     => $this->getId(),
943             'model'  => get_class($_record),
944         ));
945         $diff = array();
946         foreach (array_keys($this->_validators) as $fieldName) {
947             if (in_array($fieldName, $omitFields)) {
948                 continue;
949             }
950             
951             $ownField = $this->__get($fieldName);
952             $recordField = $_record->$fieldName;
953             
954             if (in_array($fieldName, $this->_datetimeFields)) {
955                 if ($ownField instanceof DateTime
956                     && $recordField instanceof DateTime) {
957                     
958                     if (! $ownField instanceof Tinebase_DateTime) {
959                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . 
960                             ' Convert ' . $fieldName .' to Tinebase_DateTime to make sure we have the compare() method');
961                         $ownField = new Tinebase_DateTime($ownField);
962                     }
963                         
964                     if ($ownField->compare($recordField) === 0) {
965                         continue;
966                     } else {
967                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
968                             ' datetime for field ' . $fieldName . ' is not equal: '
969                             . $ownField->getIso() . ' != '
970                             . $recordField->getIso()
971                         );
972                     } 
973                 } else if (! $recordField instanceof DateTime && $ownField == $recordField) {
974                     continue;
975                 } 
976             } else if ($fieldName == $this->_identifier && $this->getId() == $_record->getId()) {
977                 continue;
978             } else if ($ownField instanceof Tinebase_Record_Abstract || $ownField instanceof Tinebase_Record_RecordSet) {
979                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
980                     ' Doing subdiff for field ' . $fieldName);
981                 $subdiff = $ownField->diff($recordField);
982                 if (is_object($subdiff) && ! $subdiff->isEmpty()) {
983                     $diff[$fieldName] = $subdiff;
984                 }
985                 continue;
986             } else if ($recordField instanceof Tinebase_Record_Abstract && is_scalar($ownField)) {
987                 // maybe we have the id of the record -> just compare the id
988                 if ($recordField->getId() == $ownField) {
989                     continue;
990                 } else {
991                     $recordField = $recordField->getId();
992                 }
993             } else if ($ownField == $recordField) {
994                 continue;
995             } else if (empty($ownField) && empty($recordField)) {
996                 continue;
997             }
998             
999             $diff[$fieldName] = $recordField;
1000         }
1001         
1002         $result->diff = $diff;
1003         return $result;
1004     }
1005     
1006     /**
1007      * merge given record into $this
1008      * 
1009      * @param Tinebase_Record_Interface $record
1010      * @param Tinebase_Record_Diff $diff
1011      * @return Tinebase_Record_Interface
1012      */
1013     public function merge($record, $diff = null)
1014     {
1015         if (! $this->getId()) {
1016             $this->setId($record->getId());
1017         }
1018         
1019         if ($diff === null) {
1020             $diff = $this->diff($record);
1021         }
1022         
1023         if ($diff === null || empty($diff->diff)) {
1024             return $this;
1025         }
1026         
1027         foreach ($diff->diff as $field => $value) {
1028             if (empty($this->{$field})) {
1029                 $this->{$field} = $value;
1030             }
1031         }
1032         
1033         return $this;
1034     }
1035     
1036     /**
1037      * check if data got modified
1038      * 
1039      * @return boolean
1040      */
1041     public function isDirty()
1042     {
1043         return $this->_isDirty;
1044     }
1045     
1046     /**
1047      * returns TRUE if given record obsoletes this one
1048      * 
1049      * @param  Tinebase_Record_Interface $_record
1050      * @return bool
1051      */
1052     public function isObsoletedBy($_record)
1053     {
1054         if (get_class($_record) !== get_class($this)) {
1055             throw new Tinebase_Exception_InvalidArgument('Records could not be compared');
1056         } else if ($this->getId() && $_record->getId() !== $this->getId()) {
1057             throw new Tinebase_Exception_InvalidArgument('Record id mismatch');
1058         }
1059         
1060         if ($this->has('seq') && $_record->seq != $this->seq) {
1061             return $_record->seq > $this->seq;
1062         }
1063         
1064         return ($this->has('last_modified_time')) ? $_record->last_modified_time > $this->last_modified_time : TRUE;
1065     }
1066     
1067     /**
1068      * check if two records are equal
1069      * 
1070      * @param  Tinebase_Record_Interface $_record record for comparism
1071      * @param  array                     $_toOmit fields to omit
1072      * @return bool
1073      */
1074     public function isEqual($_record, array $_toOmit = array())
1075     {
1076         $diff = $this->diff($_record);
1077         return ($diff) ? $diff->isEmpty($_toOmit) : FALSE;
1078     }
1079     
1080     /**
1081      * translate this records' fields
1082      *
1083      */
1084     public function translate()
1085     {
1086         // get translation object
1087         if (!empty($this->_toTranslate)) {
1088             $translate = Tinebase_Translation::getTranslation($this->_application);
1089             
1090             foreach ($this->_toTranslate as $field) {
1091                 $this->$field = $translate->_($this->$field);
1092             }
1093         }
1094     }
1095
1096     /**
1097      * check if the model has a specific field (container_id for example)
1098      *
1099      * @param string $_field
1100      * @return boolean
1101      */
1102     public function has($_field) 
1103     {
1104         return ((isset($this->_validators[$_field]) || array_key_exists ($_field, $this->_validators)));
1105     }   
1106
1107     /**
1108      * get fields
1109      * 
1110      * @return array
1111      */
1112     public function getFields()
1113     {
1114         return array_keys($this->_validators);
1115     }
1116     
1117     /**
1118      * fills a record from json data
1119      *
1120      * @param string $_data json encoded data
1121      * @return void
1122      * 
1123      * @todo replace this (and setFromJsonInUsersTimezone) with Tinebase_Convert_Json::toTine20Model
1124      * @todo move custom _setFromJson to (custom) converter
1125      */
1126     public function setFromJson($_data)
1127     {
1128         if (is_array($_data)) {
1129             $recordData = $_data;
1130         } else {
1131             $recordData = Zend_Json::decode($_data);
1132         }
1133
1134         if ($this->has('image') && !empty($_data['image']) && preg_match('/location=tempFile&id=([a-z0-9]*)/', $_data['image'], $matches)) {
1135             // add image to attachments
1136             if (! isset($recordData['attachments'])) {
1137                 $recordData['attachments'] = array();
1138             }
1139             $recordData['attachments'][] = array('tempFile' => array('id' => $matches[1]));
1140         }
1141
1142         // sanitize container id if it is an array
1143         if ($this->has('container_id') && isset($recordData['container_id']) && is_array($recordData['container_id']) && isset($recordData['container_id']['id']) ) {
1144             $recordData['container_id'] = $recordData['container_id']['id'];
1145         }
1146
1147         $this->_setFromJson($recordData);
1148         $this->setFromArray($recordData);
1149     }
1150     
1151     /**
1152      * can be reimplemented by subclasses to modify values during setFromJson
1153      * @param array $_data the json decoded values
1154      * @return void
1155      */
1156     protected function _setFromJson(array &$_data)
1157     {
1158         
1159     }
1160
1161     /**
1162      * returns modlog omit fields
1163      *
1164      * @return array
1165      */
1166     public function getModlogOmitFields()
1167     {
1168         return $this->_modlogOmitFields;
1169     }
1170
1171     /**
1172      * returns read only fields
1173      *
1174      * @return array
1175      */
1176     public function getReadOnlyFields()
1177     {
1178         return $this->_readOnlyFields;
1179     }
1180
1181     /**
1182      * sets the non static properties by the created configuration object on instantiation
1183      */
1184     protected function _setFromConfigurationObject()
1185     {
1186         // set protected, non static properties
1187         $co = static::getConfiguration();
1188         if ($co && $mc = $co->toArray()) {
1189             foreach ($mc as $property => $value) {
1190                 $this->{$property} = $value;
1191             }
1192         }
1193     }
1194
1195     /**
1196      * returns the title of the record
1197      * 
1198      * @return string
1199      */
1200     public function getTitle()
1201     {
1202         $c = static::getConfiguration();
1203         
1204         // TODO: fallback, remove if all models use modelconfiguration
1205         if (! $c) {
1206             return $this->has('title') ? $this->title :
1207                 ($this->has('name') ? $this->name : $this->{$this->_identifier});
1208         }
1209         
1210         // use vsprintf formatting if it is an array
1211         if (is_array($c->titleProperty)) {
1212             if (! is_array($c->titleProperty[1])) {
1213                 $propertyValues = array($this->{$c->titleProperty[1]});
1214             } else {
1215                 $propertyValues = array();
1216                 foreach($c->titleProperty[1] as $property) {
1217                     $propertyValues[] = $this->{$property};
1218                 }
1219             }
1220             return vsprintf($c->titleProperty[0], $propertyValues);
1221         } else {
1222             return $this->{$c->titleProperty};
1223         }
1224     }
1225     
1226     /**
1227      * returns the foreignId fields (used in Tinebase_Convert_Json)
1228      * @return array
1229      */
1230     public static function getResolveForeignIdFields()
1231     {
1232         return static::$_resolveForeignIdFields;
1233     }
1234     
1235     /**
1236      * returns all textfields having labels for the autocomplete field function
1237      * 
1238      * @return array
1239      */
1240     public static function getAutocompleteFields()
1241     {
1242         $keys = array();
1243         
1244         foreach (self::getConfiguration()->getFields() as $key => $fieldDef) {
1245             if ($fieldDef['type'] == 'string' || $fieldDef['type'] == 'text') {
1246                 $keys[] = $key;
1247             }
1248         }
1249         
1250         return $keys;
1251     }
1252
1253     public function runConvertToRecord()
1254     {
1255         $conf = self::getConfiguration();
1256         if (null === $conf)
1257             return;
1258         foreach ($conf->getConverters() as $key => $converters) {
1259             if (isset($this->_properties[$key])) {
1260                 foreach ($converters as $converter) {
1261                     $this->_properties[$key] = $converter::convertToRecord($this->_properties[$key]);
1262                 }
1263             }
1264         }
1265     }
1266
1267     public function runConvertToData()
1268     {
1269         $conf = self::getConfiguration();
1270         if (null === $conf)
1271             return;
1272         foreach ($conf->getConverters() as $key => $converters) {
1273             if (isset($this->_properties[$key])) {
1274                 foreach ($converters as $converter) {
1275                     $this->_properties[$key] = $converter::convertToData($this->_properties[$key]);
1276                 }
1277             }
1278         }
1279     }
1280 }