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