VEVENT converter: rrule until on the same day as event
[tine20] / tine20 / Calendar / Model / Event.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Calendar
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Cornelius Weiss <c.weiss@metaways.de>
8  * @copyright   Copyright (c) 2009-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  *
10  */
11
12 /**
13  * Model of an event
14  * 
15  * Recuring Notes: 
16  *  - deleted recurring exceptions are stored in exdate (array of datetimes)
17  *  - modified recurring exceptions have their own event with recurid set the uid-dtstart
18  *    of the originators event (@see RFC2445)
19  *  - as id is unique, each modified recurring event has its own id
20  *  - rrule is stored in RCF2445 format
21  *  - the rrule_until is redundat to the rrule until property for fast queries
22  *  - we don't use rrule count, they are converted to an until
23  *  - like always in tine, we save all dates in UTC, but to correctly compute
24  *    recurring events, we also save the timezone of the organizer
25  *  - despite RFC2445 we have an expicit isAllDayEvent property
26  * 
27  * @package Calendar
28  * @property Tinebase_Record_RecordSet alarms
29  * @property Tinebase_DateTime creation_time
30  * @property string is_all_day_event
31  * @property string originator_tz
32  * @property string seq
33  * @property string uid
34  * @property string etag
35  * @property int container_id
36  */
37 class Calendar_Model_Event extends Tinebase_Record_Abstract
38 {
39     const TRANSP_TRANSP        = 'TRANSPARENT';
40     const TRANSP_OPAQUE        = 'OPAQUE';
41     
42     const CLASS_PUBLIC         = 'PUBLIC';
43     const CLASS_PRIVATE        = 'PRIVATE';
44     //const CLASS_CONFIDENTIAL   = 'CONFIDENTIAL';
45     
46     const STATUS_CONFIRMED     = 'CONFIRMED';
47     const STATUS_TENTATIVE     = 'TENTATIVE';
48     const STATUS_CANCELED      = 'CANCELED';
49     
50     const RANGE_ALL           = 'ALL';
51     const RANGE_THIS          = 'THIS';
52     const RANGE_THISANDFUTURE = 'THISANDFUTURE';
53     
54     /**
55      * key in $_validators/$_properties array for the filed which 
56      * represents the identifier
57      * 
58      * @var string
59      */
60     protected $_identifier = 'id';
61     
62     /**
63      * application the record belongs to
64      *
65      * @var string
66      */
67     protected $_application = 'Calendar';
68     
69     /**
70      * validators
71      *
72      * @var array
73      */
74     protected $_validators = array(
75         // tine record fields
76         'id'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true,  /*'Alnum'*/),
77         'container_id'         => array(Zend_Filter_Input::ALLOW_EMPTY => true,  'Int'  ),
78         'created_by'           => array(Zend_Filter_Input::ALLOW_EMPTY => true,         ),
79         'creation_time'        => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
80         'last_modified_by'     => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
81         'last_modified_time'   => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
82         'is_deleted'           => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
83         'deleted_time'         => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
84         'deleted_by'           => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
85         'seq'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true,  'Int'  ),
86         // calendar only fields
87         'dtend'                => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
88         'transp'               => array(
89             Zend_Filter_Input::ALLOW_EMPTY => true,
90             array('InArray', array(self::TRANSP_OPAQUE, self::TRANSP_TRANSP))
91         ),
92         // ical common fields
93         'class'                => array(
94             Zend_Filter_Input::ALLOW_EMPTY => true,
95             array('InArray', array(self::CLASS_PUBLIC, self::CLASS_PRIVATE, /*self::CLASS_CONFIDENTIAL*/))
96         ),
97         'description'          => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
98         'geo'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
99         'location'             => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
100         'organizer'            => array(Zend_Filter_Input::ALLOW_EMPTY => false,        ),
101         'priority'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, 'Int'   ),
102         'status'            => array(
103             Zend_Filter_Input::ALLOW_EMPTY => true,
104             array('InArray', array(self::STATUS_CONFIRMED, self::STATUS_TENTATIVE, self::STATUS_CANCELED))
105         ),
106         'summary'              => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
107         'url'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
108         'uid'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
109         'etag'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
110         // ical common fields with multiple appearance
111         //'attach'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
112         'attendee'              => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // RecordSet of Calendar_Model_Attender
113         'alarms'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // RecordSet of Tinebase_Model_Alarm
114         'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // originally categories handled by Tinebase_Tags
115         'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // originally comment handled by Tinebase_Notes
116         'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
117         'attachments'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
118         
119         //'contact'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
120         //'related'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
121         //'resources'             => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
122         //'rstatus'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
123         // ical scheduleable interface fields
124         'dtstart'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
125         'recurid'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
126         // ical scheduleable interface fields with multiple appearance
127         'exdate'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), //  array of Tinebase_DateTimeTinebase_DateTime's
128         //'exrule'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
129         //'rdate'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
130         'rrule'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
131         // calendar helper fields
132
133         'is_all_day_event'      => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
134         'rrule_until'           => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
135         'originator_tz'         => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
136
137         // relations
138         'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
139         'customfields'          => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
140         
141         // grant helper fields
142         Tinebase_Model_Grants::GRANT_FREEBUSY => array(Zend_Filter_Input::ALLOW_EMPTY => true),
143         Tinebase_Model_Grants::GRANT_READ     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
144         Tinebase_Model_Grants::GRANT_SYNC     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
145         Tinebase_Model_Grants::GRANT_EXPORT   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
146         Tinebase_Model_Grants::GRANT_EDIT     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
147         Tinebase_Model_Grants::GRANT_DELETE   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
148         Tinebase_Model_Grants::GRANT_PRIVATE  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
149     );
150     
151     /**
152      * datetime fields
153      *
154      * @var array
155      */
156     protected $_datetimeFields = array(
157         'creation_time', 
158         'last_modified_time', 
159         'deleted_time', 
160         'completed', 
161         'dtstart', 
162         'dtend', 
163         'exdate',
164         //'rdate',
165         'rrule_until',
166     );
167     
168     /**
169      * name of fields that should be omited from modlog
170      *
171      * @var array list of modlog omit fields
172      */
173     protected $_modlogOmitFields = array(
174         Tinebase_Model_Grants::GRANT_READ,
175         Tinebase_Model_Grants::GRANT_SYNC,
176         Tinebase_Model_Grants::GRANT_EXPORT,
177         Tinebase_Model_Grants::GRANT_EDIT,
178         Tinebase_Model_Grants::GRANT_DELETE,
179         Tinebase_Model_Grants::GRANT_PRIVATE,
180     );
181     
182     /**
183      * sets record related properties
184      * 
185      * @param string _name of property
186      * @param mixed _value of property
187      * @throws Tinebase_Exception_UnexpectedValue
188      * @return void
189      */
190     public function __set($_name, $_value)
191     {
192         // ensure exdate as array
193         if ($_name == 'exdate' && ! empty($_value) && ! is_array($_value) && ! $_value instanceof Tinebase_Record_RecordSet ) {
194             $_value = array($_value);
195         }
196         
197         if ($_name == 'attendee' && is_array($_value)) {
198             $_value = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $_value);
199         }
200         
201         if ($_name == 'rrule' && is_string($_value) && ! empty($_value)) {
202             // normalize rrule
203             $_value = new Calendar_Model_Rrule($_value);
204             $_value = (string) $_value;
205         }
206         parent::__set($_name, $_value);
207     }
208     
209     /**
210      * the constructor
211      * it is needed because we have more validation fields in Calendars
212      * 
213      * @param mixed $_data
214      * @param bool $bypassFilters sets {@see this->bypassFilters}
215      * @param bool $convertDates sets {@see $this->convertDates}
216      */
217     public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
218     {
219         $this->_filters['organizer'] = new Zend_Filter_Empty(NULL);
220         
221         parent::__construct($_data, $_bypassFilters, $_convertDates);
222     }
223     
224     /**
225      * (non-PHPdoc)
226      * @see Tinebase_Record_Abstract::diff()
227      */
228     public function diff($record, $omitFields = array())
229     {
230         $checkRrule = false;
231         if (! in_array('rrule', $omitFields)) {
232             $omitFields[] = 'rrule';
233             $checkRrule = true;
234         }
235         
236         $diff = parent::diff($record, $omitFields);
237         
238         if ($checkRrule) {
239             $ownRrule    = ! $this->rrule instanceof Calendar_Model_Rrule ? Calendar_Model_Rrule::getRruleFromString((string) $this->rrule) : $this->rrule;
240             $recordRrule = ! $record->rrule instanceof Calendar_Model_Rrule ? Calendar_Model_Rrule::getRruleFromString($record->rrule) : $record->rrule;
241             
242             $rruleDiff = $ownRrule->diff($recordRrule);
243             
244             // don't take small ( < one day) rrule_until changes as diff
245             if (
246                     $ownRrule->until instanceof Tinebase_DateTime 
247                     && (isset($rruleDiff->diff['until']) || array_key_exists('until', $rruleDiff->diff)) && $rruleDiff->diff['until'] instanceof Tinebase_DateTime
248                     && abs($rruleDiff->diff['until']->getTimestamp() - $ownRrule->until->getTimestamp()) < 86400
249             ){
250                 $rruleDiffArray = $rruleDiff->diff;
251                 unset($rruleDiffArray['until']);
252                 $rruleDiff->diff = $rruleDiffArray;
253             }
254             
255             if (! empty($rruleDiff->diff)) {
256                 $diffArray = $diff->diff;
257                 $diffArray['rrule'] = $rruleDiff;
258                 
259                 $diff->diff = $diffArray;
260             }
261         }
262         
263         return $diff;
264     }
265     /**
266      * add current user to attendee if he's organizer
267      * 
268      * @param bool $ifOrganizer      only add current user if he's organizer
269      * @param bool $ifNoOtherAttendee  only add current user if no other attendee are present
270      */
271     public function assertCurrentUserAsAttendee($ifOrganizer = TRUE, $ifNoOtherAttendee = FALSE)
272     {
273         if ($ifNoOtherAttendee && $this->attendee instanceof Tinebase_Record_RecordSet && $this->attendee->count() > 0) {
274             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
275                     __METHOD__ . '::' . __LINE__ . " not adding current user as attendee as other attendee are present.");
276             return;
277         }
278         
279         $ownAttender = Calendar_Model_Attender::getOwnAttender($this->attendee);
280         
281         if (! $ownAttender) {
282             if ($ifOrganizer && $this->organizer && $this->organizer != Tinebase_Core::getUser()->contact_id) {
283                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
284                     __METHOD__ . '::' . __LINE__ . " not adding current user as attendee as current user is not organizer.");
285             }
286             
287             else {
288                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
289                     __METHOD__ . '::' . __LINE__ . " adding current user as attendee.");
290                 
291                 $newAttender = new Calendar_Model_Attender(array(
292                     'user_id'   => Tinebase_Core::getUser()->contact_id,
293                     'user_type' => Calendar_Model_Attender::USERTYPE_USER,
294                     'status'    => Calendar_Model_Attender::STATUS_ACCEPTED,
295                     'role'      => Calendar_Model_Attender::ROLE_REQUIRED
296                 ));
297                 
298                 if (! $this->attendee instanceof Tinebase_Record_RecordSet) {
299                     $this->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
300                 }
301                 $this->attendee->addRecord($newAttender);
302             }
303         }
304     }
305     
306     /**
307      * returns the original dtstart of a recur series exception event 
308      *  -> when the event should have started with no exception
309      * 
310      * @return Tinebase_DateTime
311      */
312     public function getOriginalDtStart()
313     {
314         $origianlDtStart = $this->dtstart instanceof stdClass ? clone $this->dtstart : $this->dtstart;
315         
316         if ($this->isRecurException()) {
317             if ($this->recurid instanceof DateTime) {
318                 $origianlDtStart = clone $this->recurid;
319             } else if (is_string($this->recurid)) {
320                 $origianlDtStartString = substr($this->recurid, -19);
321                 if (! Tinebase_DateTime::isDate($origianlDtStartString)) {
322                     throw new Tinebase_Exception_InvalidArgument('recurid does not contain a valid original start date');
323                 }
324                 
325                 $origianlDtStart = new Tinebase_DateTime($origianlDtStartString, 'UTC');
326             }
327         }
328         
329         return $origianlDtStart;
330     }
331     
332     /**
333      * gets translated field name
334      * 
335      * NOTE: this has to be done explicitly as our field names are technically 
336      *       and have no translations
337      *       
338      * @param string         $_field
339      * @param Zend_Translate $_translation
340      * @return string
341      */
342     public static function getTranslatedFieldName($_field, $_translation)
343     {
344         $t = $_translation;
345         switch ($_field) {
346             case 'dtstart':           return $t->_('Start');
347             case 'dtend':             return $t->_('End');
348             case 'transp':            return $t->_('Blocking');
349             case 'class':             return $t->_('Classification');
350             case 'description':       return $t->_('Description');
351             case 'location':          return $t->_('Location');
352             case 'organizer':         return $t->_('Organizer');
353             case 'priority':          return $t->_('Priority');
354             case 'status':            return $t->_('Status');
355             case 'summary':           return $t->_('Summary');
356             case 'url':               return $t->_('Url');
357             case 'rrule':             return $t->_('Recurrance rule');
358             case 'is_all_day_event':  return $t->_('Is all day event');
359             case 'originator_tz':     return $t->_('Organizer timezone');
360             default:                  return $_field;
361         }
362     }
363     
364     /**
365      * gets translated value
366      * 
367      * NOTE: This is needed for values like Yes/No, Datetimes, etc.
368      * 
369      * @param  string           $_field
370      * @param  mixed            $_value
371      * @param  Zend_Translate   $_translation
372      * @param  string           $_timezone
373      * @return string
374      */
375     public static function getTranslatedValue($_field, $_value, $_translation, $_timezone)
376     {
377         if ($_value instanceof Tinebase_DateTime) {
378             $locale = new Zend_Locale($_translation->getAdapter()->getLocale());
379             return Tinebase_Translation::dateToStringInTzAndLocaleFormat($_value, $_timezone, $locale, 'datetime', true);
380         }
381         
382         switch ($_field) {
383             case 'transp':
384                 return $_value && $_value == Calendar_Model_Event::TRANSP_TRANSP ? $_translation->_('No') : $_translation->_('Yes');
385             case 'organizer':
386                 if (! $_value instanceof Addressbook_Model_Contact) {
387                     $organizer = Addressbook_Controller_Contact::getInstance()->getMultiple($_value, TRUE)->getFirstRecord();
388                 }
389                 return $organizer instanceof Addressbook_Model_Contact ? $organizer->n_fileas : '';
390             case 'rrule':
391                 if ($_value) {
392                     $rrule = $_value instanceof Calendar_Model_Rrule ? $_value : new Calendar_Model_Rrule($_value);
393                     return $rrule->getTranslatedRule($_translation);
394                 }
395             default:
396                 return $_value;
397         }
398     }
399     
400     /**
401      * checks event for given grant
402      * 
403      * @param  string $_grant
404      * @return bool
405      */
406     public function hasGrant($_grant)
407     {
408         $hasGrant = (isset($this->_properties[$_grant]) || array_key_exists($_grant, $this->_properties)) && (bool)$this->{$_grant};
409         
410         if ($this->class !== Calendar_Model_Event::CLASS_PUBLIC) {
411             $hasGrant &= (
412                 // private grant
413                 $this->{Tinebase_Model_Grants::GRANT_PRIVATE} ||
414                 // I'm organizer
415                 Tinebase_Core::getUser()->contact_id == ($this->organizer instanceof Addressbook_Model_Contact ? $this->organizer->getId() : $this->organizer) ||
416                 // I'm attendee
417                 Calendar_Model_Attender::getOwnAttender($this->attendee)
418             );
419         }
420         
421         return $hasGrant;
422     }
423     
424     /**
425      * event is an exception of a recur event series
426      * 
427      * @return boolean
428      */
429     public function isRecurException()
430     {
431         return !!$this->recurid;
432     }
433     
434     /**
435      * sets recurId of this model
436      * 
437      * @return string recurid which was set
438      */
439     public function setRecurId()
440     {
441         if (! ($this->uid && $this->dtstart)) {
442             throw new Exception ('uid _and_ dtstart must be set to generate recurid');
443         }
444         
445         // make sure we store recurid in utc
446         $dtstart = $this->getOriginalDtStart();
447         $dtstart->setTimezone('UTC');
448         
449         $this->recurid = $this->uid . '-' . $dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
450         
451         return $this->recurid;
452     }
453     
454     /**
455      * sets rrule until helper field
456      *
457      * @return void
458      */
459     public function setRruleUntil()
460     {
461         if (empty($this->rrule)) {
462             $this->rrule_until = NULL;
463         } else {
464             $rrule = $this->rrule;
465             if (! $this->rrule instanceof Calendar_Model_Rrule) {
466                 $rrule = new Calendar_Model_Rrule(array());
467                 $rrule->setFromString($this->rrule);
468                 $this->rrule = $rrule;
469             }
470             
471             if (isset($rrule->count)) {
472                 $this->rrule_until = NULL;
473                 $exdates = $this->exdate;
474                 $this->exdate = NULL;
475                 
476                 $lastOccurrence = Calendar_Model_Rrule::computeNextOccurrence($this, new Tinebase_Record_RecordSet('Calendar_Model_Event'), $this->dtend, $rrule->count -1);
477                 $this->rrule_until = $lastOccurrence->dtend;
478                 $this->exdate = $exdates;
479             } else {
480                 // set until to end of day in organizers timezone.
481                 // NOTE: this is in contrast to the iCal spec which says until should be the
482                 //       dtstart of the last occurence. But as the client with the name of the
483                 //       spec sets it to the end of the day, we do it also.
484                 if ($rrule->until instanceof Tinebase_DateTime) {
485                     $rrule->until->setTimezone($this->originator_tz);
486                     // NOTE: subSecond cause some clients send 00:00:00 for midnight
487                     $rrule->until->subSecond(1)->setTime(23, 59, 59);
488                     $rrule->until->setTimezone('UTC');
489                 }
490                 
491                 $this->rrule_until = $rrule->until;
492             }
493         }
494         
495         if ($this->rrule_until && $this->rrule_until->getTimeStamp() - $this->dtstart->getTimeStamp() < -1) {
496             throw new Tinebase_Exception_Record_Validation('rrule until must not be before dtstart');
497         }
498     }
499     
500     /**
501      * cleans up data to only contain freebusy infos
502      * removes all fields except dtstart/dtend/id/modlog fields
503      * 
504      * @return boolean TRUE if cleanup took place
505      */
506     public function doFreeBusyCleanup()
507     {
508         if ($this->hasGrant(Tinebase_Model_Grants::GRANT_READ)) {
509            return FALSE;
510         }
511         
512         $this->_properties = array_intersect_key($this->_properties, array_flip(array(
513             'id', 
514             'dtstart', 
515             'dtend',
516             'transp',
517             'seq',
518             'uid',
519             'is_all_day_event',
520             'rrule',
521             'rrule_until',
522             'recurid',
523             'exdate',
524             'originator_tz',
525             'attendee', // if we remove this, we need to adopt attendee resolveing
526             'container_id',
527             'created_by',
528             'creation_time',
529             'last_modified_by',
530             'last_modified_time',
531             'is_deleted',
532             'deleted_time',
533             'deleted_by',
534         )));
535         
536         return TRUE;
537     }
538     
539     /**
540      * sets the record related properties from user generated input.
541      * 
542      * Input-filtering and validation by Zend_Filter_Input can enabled and disabled
543      *
544      * @param array $_data            the new data to set
545      * @throws Tinebase_Exception_Record_Validation when content contains invalid or missing data
546      */
547     public function setFromArray(array $_data)
548     {
549         if (empty($_data['geo'])) {
550             $_data['geo'] = NULL;
551         }
552         
553         if (empty($_data['class'])) {
554             $_data['class'] = self::CLASS_PUBLIC;
555         }
556         
557         if (empty($_data['priority'])) {
558             $_data['priority'] = NULL;
559         }
560         
561         if (empty($_data['status'])) {
562             $_data['status'] = self::STATUS_CONFIRMED;
563         }
564         
565         if (isset($_data['container_id']) && is_array($_data['container_id'])) {
566             $_data['container_id'] = $_data['container_id']['id'];
567         }
568         
569         if (isset($_data['organizer']) && is_array($_data['organizer'])) {
570             $_data['organizer'] = $_data['organizer']['id'];
571         }
572         
573         if (isset($_data['attendee']) && is_array($_data['attendee'])) {
574             $_data['attendee'] = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $_data['attendee'], $this->bypassFilters, $this->convertDates);
575         }
576         
577         if (isset($_data['rrule']) && ! empty($_data['rrule']) && ! $_data['rrule'] instanceof Calendar_Model_Rrule) {
578             // rrule can be array or string
579             $_data['rrule'] = new Calendar_Model_Rrule($_data['rrule'], $this->bypassFilters, $this->convertDates);
580         }
581         
582         if (isset($_data['alarms']) && is_array($_data['alarms'])) {
583             $_data['alarms'] = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', $_data['alarms'], TRUE, $this->convertDates);
584         }
585         
586         parent::setFromArray($_data);
587     }
588     
589     /**
590      * checks if event matches period filter
591      * 
592      * @param Calendar_Model_PeriodFilter $_period
593      * @return boolean
594      */
595     public function isInPeriod(Calendar_Model_PeriodFilter $_period)
596     {
597         $result = TRUE;
598         
599         if ($this->dtend->compare($_period->getFrom()) == -1 || $this->dtstart->compare($_period->getUntil()) == 1) {
600             $result = FALSE;
601         }
602         
603         return $result;
604     }
605     
606     /**
607      * returns TRUE if comparison detects a resechedule / significant change
608      * 
609      * @param  Calendar_Model_Event $_event
610      * @return bool
611      */
612     public function isRescheduled($_event)
613     {
614         $diff = $this->diff($_event)->diff;
615         
616         return (isset($diff['dtstart']) || array_key_exists('dtstart', $diff))
617             || (! $this->is_all_day_event && (isset($diff['dtend']) || array_key_exists('dtend', $diff)))
618             || (isset($diff['rrule']) || array_key_exists('rrule', $diff));
619     }
620     
621     /**
622      * sets and returns the addressbook entry of the organizer
623      * 
624      * @return Addressbook_Model_Contact
625      */
626     public function resolveOrganizer()
627     {
628         if (! empty($this->organizer) && ! $this->organizer instanceof Addressbook_Model_Contact) {
629             $contacts = Addressbook_Controller_Contact::getInstance()->getMultiple($this->organizer, TRUE);
630             if (count($contacts)) {
631                 $this->organizer = $contacts->getFirstRecord();
632             }
633         }
634         
635         return $this->organizer;
636     }
637     
638     /**
639      * checks if given attendee is organizer of this event
640      * 
641      * @param Calendar_Model_Attender $_attendee
642      */
643     public function isOrganizer($_attendee=NULL)
644     {
645         $organizerContactId = NULL;
646         if ($_attendee && in_array($_attendee->user_type, array(Calendar_Model_Attender::USERTYPE_USER, Calendar_Model_Attender::USERTYPE_GROUPMEMBER))) {
647             $organizerContactId = $_attendee->user_id instanceof Tinebase_Record_Abstract ? $_attendee->user_id->getId() : $_attendee->user_id;
648         } else {
649             $organizerContactId = Tinebase_Core::getUser()->contact_id;
650         }
651         
652         return $organizerContactId == ($this->organizer instanceof Tinebase_Record_Abstract ? $this->organizer->getId() : $this->organizer);
653     }
654 }