1<?php
2
3/**
4 * Xcal based Kolab format class wrapping libkolabxml bindings
5 *
6 * Base class for xcal-based Kolab groupware objects such as event, todo, journal
7 *
8 * @version @package_version@
9 * @author Thomas Bruederli <bruederli@kolabsys.com>
10 *
11 * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License as
15 * published by the Free Software Foundation, either version 3 of the
16 * License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU Affero General Public License for more details.
22 *
23 * You should have received a copy of the GNU Affero General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27abstract class kolab_format_xcal extends kolab_format
28{
29    public $CTYPE = 'application/calendar+xml';
30
31    public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
32
33    public static $scheduling_properties = array('start', 'end', 'location');
34
35    protected $_scheduling_properties = null;
36
37    protected $sensitivity_map = array(
38        'public'       => kolabformat::ClassPublic,
39        'private'      => kolabformat::ClassPrivate,
40        'confidential' => kolabformat::ClassConfidential,
41    );
42
43    protected $role_map = array(
44        'REQ-PARTICIPANT' => kolabformat::Required,
45        'OPT-PARTICIPANT' => kolabformat::Optional,
46        'NON-PARTICIPANT' => kolabformat::NonParticipant,
47        'CHAIR' => kolabformat::Chair,
48    );
49
50    protected $cutype_map = array(
51        'INDIVIDUAL' => kolabformat::CutypeIndividual,
52        'GROUP'      => kolabformat::CutypeGroup,
53        'ROOM'       => kolabformat::CutypeRoom,
54        'RESOURCE'   => kolabformat::CutypeResource,
55        'UNKNOWN'    => kolabformat::CutypeUnknown,
56    );
57
58    protected $rrule_type_map = array(
59        'MINUTELY' => RecurrenceRule::Minutely,
60        'HOURLY' => RecurrenceRule::Hourly,
61        'DAILY' => RecurrenceRule::Daily,
62        'WEEKLY' => RecurrenceRule::Weekly,
63        'MONTHLY' => RecurrenceRule::Monthly,
64        'YEARLY' => RecurrenceRule::Yearly,
65    );
66
67    protected $weekday_map = array(
68        'MO' => kolabformat::Monday,
69        'TU' => kolabformat::Tuesday,
70        'WE' => kolabformat::Wednesday,
71        'TH' => kolabformat::Thursday,
72        'FR' => kolabformat::Friday,
73        'SA' => kolabformat::Saturday,
74        'SU' => kolabformat::Sunday,
75    );
76
77    protected $alarm_type_map = array(
78        'DISPLAY' => Alarm::DisplayAlarm,
79        'EMAIL' => Alarm::EMailAlarm,
80        'AUDIO' => Alarm::AudioAlarm,
81    );
82
83    protected $status_map = array(
84        'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
85        'IN-PROCESS'   => kolabformat::StatusInProcess,
86        'COMPLETED'    => kolabformat::StatusCompleted,
87        'CANCELLED'    => kolabformat::StatusCancelled,
88        'TENTATIVE'    => kolabformat::StatusTentative,
89        'CONFIRMED'    => kolabformat::StatusConfirmed,
90        'DRAFT'        => kolabformat::StatusDraft,
91        'FINAL'        => kolabformat::StatusFinal,
92    );
93
94    protected $part_status_map = array(
95        'UNKNOWN'      => kolabformat::PartNeedsAction,
96        'NEEDS-ACTION' => kolabformat::PartNeedsAction,
97        'TENTATIVE'    => kolabformat::PartTentative,
98        'ACCEPTED'     => kolabformat::PartAccepted,
99        'DECLINED'     => kolabformat::PartDeclined,
100        'DELEGATED'    => kolabformat::PartDelegated,
101        'IN-PROCESS'   => kolabformat::PartInProcess,
102        'COMPLETED'    => kolabformat::PartCompleted,
103      );
104
105
106    /**
107     * Convert common xcard properties into a hash array data structure
108     *
109     * @param array Additional data for merge
110     *
111     * @return array  Object data as hash array
112     */
113    public function to_array($data = array())
114    {
115        // read common object props
116        $object = parent::to_array($data);
117
118        $status_map = array_flip($this->status_map);
119        $sensitivity_map = array_flip($this->sensitivity_map);
120
121        $object += array(
122            'sequence'    => intval($this->obj->sequence()),
123            'title'       => $this->obj->summary(),
124            'location'    => $this->obj->location(),
125            'description' => $this->obj->description(),
126            'url'         => $this->obj->url(),
127            'status'      => $status_map[$this->obj->status()],
128            'sensitivity' => $sensitivity_map[$this->obj->classification()],
129            'priority'    => $this->obj->priority(),
130            'categories'  => self::vector2array($this->obj->categories()),
131            'start'       => self::php_datetime($this->obj->start()),
132        );
133
134        if (method_exists($this->obj, 'comment')) {
135            $object['comment'] = $this->obj->comment();
136        }
137
138        // read organizer and attendees
139        if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
140            $object['organizer'] = array(
141                'email' => $organizer->email(),
142                'name' => $organizer->name(),
143            );
144        }
145
146        $role_map = array_flip($this->role_map);
147        $cutype_map = array_flip($this->cutype_map);
148        $part_status_map = array_flip($this->part_status_map);
149        $attvec = $this->obj->attendees();
150        for ($i=0; $i < $attvec->size(); $i++) {
151            $attendee = $attvec->get($i);
152            $cr = $attendee->contact();
153            if ($cr->email() != $object['organizer']['email']) {
154                $delegators = $delegatees = array();
155                $vdelegators = $attendee->delegatedFrom();
156                for ($j=0; $j < $vdelegators->size(); $j++) {
157                    $delegators[] = $vdelegators->get($j)->email();
158                }
159                $vdelegatees = $attendee->delegatedTo();
160                for ($j=0; $j < $vdelegatees->size(); $j++) {
161                    $delegatees[] = $vdelegatees->get($j)->email();
162                }
163
164                $object['attendees'][] = array(
165                    'role' => $role_map[$attendee->role()],
166                    'cutype' => $cutype_map[$attendee->cutype()],
167                    'status' => $part_status_map[$attendee->partStat()],
168                    'rsvp' => $attendee->rsvp(),
169                    'email' => $cr->email(),
170                    'name' => $cr->name(),
171                    'delegated-from' => $delegators,
172                    'delegated-to' => $delegatees,
173                );
174            }
175        }
176
177        if ($object['start'] instanceof DateTime) {
178            $start_tz = $object['start']->getTimezone();
179        }
180
181        // read recurrence rule
182        if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
183            $rrule_type_map = array_flip($this->rrule_type_map);
184            $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
185
186            if ($intvl = $rr->interval())
187                $object['recurrence']['INTERVAL'] = $intvl;
188
189            if (($count = $rr->count()) && $count > 0) {
190                $object['recurrence']['COUNT'] = $count;
191            }
192            else if ($until = self::php_datetime($rr->end(), $start_tz)) {
193                $refdate = $this->get_reference_date();
194                if ($refdate && $refdate instanceof DateTime && !$refdate->_dateonly) {
195                    $until->setTime($refdate->format('G'), $refdate->format('i'), 0);
196                }
197                $object['recurrence']['UNTIL'] = $until;
198            }
199
200            if (($byday = $rr->byday()) && $byday->size()) {
201                $weekday_map = array_flip($this->weekday_map);
202                $weekdays = array();
203                for ($i=0; $i < $byday->size(); $i++) {
204                    $daypos = $byday->get($i);
205                    $prefix = $daypos->occurence();
206                    $weekdays[] = ($prefix ?: '') . $weekday_map[$daypos->weekday()];
207                }
208                $object['recurrence']['BYDAY'] = join(',', $weekdays);
209            }
210
211            if (($bymday = $rr->bymonthday()) && $bymday->size()) {
212                $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
213            }
214
215            if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
216                $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
217            }
218
219            if ($exdates = $this->obj->exceptionDates()) {
220                for ($i=0; $i < $exdates->size(); $i++) {
221                    if ($exdate = self::php_datetime($exdates->get($i), $start_tz)) {
222                        $object['recurrence']['EXDATE'][] = $exdate;
223                    }
224                }
225            }
226        }
227
228        if ($rdates = $this->obj->recurrenceDates()) {
229            for ($i=0; $i < $rdates->size(); $i++) {
230                if ($rdate = self::php_datetime($rdates->get($i), $start_tz)) {
231                    $object['recurrence']['RDATE'][] = $rdate;
232                }
233            }
234        }
235
236        // read alarm
237        $valarms = $this->obj->alarms();
238        $alarm_types = array_flip($this->alarm_type_map);
239        $object['valarms'] = array();
240        for ($i=0; $i < $valarms->size(); $i++) {
241            $alarm = $valarms->get($i);
242            $type  = $alarm_types[$alarm->type()];
243
244            if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') {  // only some alarms are supported
245                $valarm = array(
246                    'action'      => $type,
247                    'summary'     => $alarm->summary(),
248                    'description' => $alarm->description(),
249                );
250
251                if ($type == 'EMAIL') {
252                    $valarm['attendees'] = array();
253                    $attvec = $alarm->attendees();
254                    for ($j=0; $j < $attvec->size(); $j++) {
255                        $cr = $attvec->get($j);
256                        $valarm['attendees'][] = $cr->email();
257                    }
258                }
259                else if ($type == 'AUDIO') {
260                    $attach = $alarm->audioFile();
261                    $valarm['uri'] = $attach->uri();
262                }
263
264                if ($start = self::php_datetime($alarm->start())) {
265                    $object['alarms']  = '@' . $start->format('U');
266                    $valarm['trigger'] = $start;
267                }
268                else if ($offset = $alarm->relativeStart()) {
269                    $prefix = $offset->isNegative() ? '-' : '+';
270                    $value  = '';
271                    $time   = '';
272
273                    if      ($w = $offset->weeks())     $value .= $w . 'W';
274                    else if ($d = $offset->days())      $value .= $d . 'D';
275                    else if ($h = $offset->hours())     $time  .= $h . 'H';
276                    else if ($m = $offset->minutes())   $time  .= $m . 'M';
277                    else if ($s = $offset->seconds())   $time  .= $s . 'S';
278
279                    // assume 'at event time'
280                    if (empty($value) && empty($time)) {
281                        $prefix = '';
282                        $time   = '0S';
283                    }
284
285                    $object['alarms']  = $prefix . $value . $time;
286                    $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
287
288                    if ($alarm->relativeTo() == kolabformat::End) {
289                        $valarm['related'] == 'END';
290                    }
291                }
292
293                // read alarm duration and repeat properties
294                if (($duration = $alarm->duration()) && $duration->isValid()) {
295                    $value = $time = '';
296
297                    if      ($w = $duration->weeks())     $value .= $w . 'W';
298                    else if ($d = $duration->days())      $value .= $d . 'D';
299                    else if ($h = $duration->hours())     $time  .= $h . 'H';
300                    else if ($m = $duration->minutes())   $time  .= $m . 'M';
301                    else if ($s = $duration->seconds())   $time  .= $s . 'S';
302
303                    $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
304                    $valarm['repeat']   = $alarm->numrepeat();
305                }
306
307                $object['alarms']  .= ':' . $type;  // legacy property
308                $object['valarms'][] = array_filter($valarm);
309            }
310        }
311
312        $this->get_attachments($object);
313
314        return $object;
315    }
316
317
318    /**
319     * Set common xcal properties to the kolabformat object
320     *
321     * @param array  Event data as hash array
322     */
323    public function set(&$object)
324    {
325        $this->init();
326
327        $is_new = !$this->obj->uid();
328        $old_sequence = $this->obj->sequence();
329        $reschedule = $is_new;
330
331        // set common object properties
332        parent::set($object);
333
334        // set sequence value
335        if (!isset($object['sequence'])) {
336            if ($is_new) {
337                $object['sequence'] = 0;
338            }
339            else {
340                $object['sequence'] = $old_sequence;
341
342                // increment sequence when updating properties relevant for scheduling.
343                // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
344                if ($this->check_rescheduling($object)) {
345                    $object['sequence']++;
346                }
347            }
348        }
349        $this->obj->setSequence(intval($object['sequence']));
350
351        if ($object['sequence'] > $old_sequence) {
352            $reschedule = true;
353        }
354
355        $this->obj->setSummary($object['title']);
356        $this->obj->setLocation($object['location']);
357        $this->obj->setDescription($object['description']);
358        $this->obj->setPriority($object['priority']);
359        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
360        $this->obj->setCategories(self::array2vector($object['categories']));
361        $this->obj->setUrl(strval($object['url']));
362
363        if (method_exists($this->obj, 'setComment')) {
364            $this->obj->setComment($object['comment']);
365        }
366
367        // process event attendees
368        $attendees = new vectorattendee;
369        foreach ((array)$object['attendees'] as $i => $attendee) {
370            if ($attendee['role'] == 'ORGANIZER') {
371                $object['organizer'] = $attendee;
372            }
373            else if ($attendee['email'] != $object['organizer']['email']) {
374                $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
375                $cr->setName($attendee['name']);
376
377                // set attendee RSVP if missing
378                if (!isset($attendee['rsvp'])) {
379                    $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = $reschedule;
380                }
381
382                $att = new Attendee;
383                $att->setContact($cr);
384                $att->setPartStat($this->part_status_map[$attendee['status']]);
385                $att->setRole($this->role_map[$attendee['role']] ?: kolabformat::Required);
386                $att->setCutype($this->cutype_map[$attendee['cutype']] ?: kolabformat::CutypeIndividual);
387                $att->setRSVP((bool)$attendee['rsvp']);
388
389                if (!empty($attendee['delegated-from'])) {
390                    $vdelegators = new vectorcontactref;
391                    foreach ((array)$attendee['delegated-from'] as $delegator) {
392                        $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
393                    }
394                    $att->setDelegatedFrom($vdelegators);
395                }
396                if (!empty($attendee['delegated-to'])) {
397                    $vdelegatees = new vectorcontactref;
398                    foreach ((array)$attendee['delegated-to'] as $delegatee) {
399                        $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
400                    }
401                    $att->setDelegatedTo($vdelegatees);
402                }
403
404                if ($att->isValid()) {
405                    $attendees->push($att);
406                }
407                else {
408                    rcube::raise_error(array(
409                        'code' => 600, 'type' => 'php',
410                        'file' => __FILE__, 'line' => __LINE__,
411                        'message' => "Invalid event attendee: " . json_encode($attendee),
412                    ), true);
413                }
414            }
415        }
416        $this->obj->setAttendees($attendees);
417
418        if ($object['organizer']) {
419            $organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
420            $organizer->setName($object['organizer']['name']);
421            $this->obj->setOrganizer($organizer);
422        }
423
424        if ($object['start'] instanceof DateTime) {
425            $start_tz = $object['start']->getTimezone();
426        }
427
428        // save recurrence rule
429        $rr = new RecurrenceRule;
430        $rr->setFrequency(RecurrenceRule::FreqNone);
431
432        if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
433            $freq     = $object['recurrence']['FREQ'];
434            $bysetpos = explode(',', $object['recurrence']['BYSETPOS']);
435
436            $rr->setFrequency($this->rrule_type_map[$freq]);
437
438            if ($object['recurrence']['INTERVAL'])
439                $rr->setInterval(intval($object['recurrence']['INTERVAL']));
440
441            if ($object['recurrence']['BYDAY']) {
442                $byday = new vectordaypos;
443                foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
444                    $occurrence = 0;
445                    if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
446                        $occurrence = intval($m[1]);
447                        $day = $m[2];
448                    }
449
450                    if (isset($this->weekday_map[$day])) {
451                        // @TODO: libkolabxml does not support BYSETPOS, neither we.
452                        // However, we can convert most common cases to BYDAY
453                        if (!$occurrence && $freq == 'MONTHLY' && !empty($bysetpos)) {
454                            foreach ($bysetpos as $pos) {
455                                $byday->push(new DayPos(intval($pos), $this->weekday_map[$day]));
456                            }
457                        }
458                        else {
459                            $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
460                        }
461                    }
462                }
463                $rr->setByday($byday);
464            }
465
466            if ($object['recurrence']['BYMONTHDAY']) {
467                $bymday = new vectori;
468                foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
469                    $bymday->push(intval($day));
470                $rr->setBymonthday($bymday);
471            }
472
473            if ($object['recurrence']['BYMONTH']) {
474                $bymonth = new vectori;
475                foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
476                    $bymonth->push(intval($month));
477                $rr->setBymonth($bymonth);
478            }
479
480            if ($object['recurrence']['COUNT'])
481                $rr->setCount(intval($object['recurrence']['COUNT']));
482            else if ($object['recurrence']['UNTIL'])
483                $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true, $start_tz));
484
485            if ($rr->isValid()) {
486                // add exception dates (only if recurrence rule is valid)
487                $exdates = new vectordatetime;
488                foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
489                    $exdates->push(self::get_datetime($exdate, null, true, $start_tz));
490                $this->obj->setExceptionDates($exdates);
491            }
492            else {
493                rcube::raise_error(array(
494                    'code' => 600, 'type' => 'php',
495                    'file' => __FILE__, 'line' => __LINE__,
496                    'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
497                ), true);
498            }
499        }
500
501        $this->obj->setRecurrenceRule($rr);
502
503        // save recurrence dates (aka RDATE)
504        if (!empty($object['recurrence']['RDATE'])) {
505            $rdates = new vectordatetime;
506            foreach ((array)$object['recurrence']['RDATE'] as $rdate)
507                $rdates->push(self::get_datetime($rdate, null, true, $start_tz));
508            $this->obj->setRecurrenceDates($rdates);
509        }
510
511        // save alarm(s)
512        $valarms = new vectoralarm;
513        $valarm_hashes = array();
514        if ($object['valarms']) {
515            foreach ($object['valarms'] as $valarm) {
516                if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
517                    continue;  // skip unknown alarm types
518                }
519
520                // Get rid of duplicates, some CalDAV clients can set them
521                $hash = serialize($valarm);
522                if (in_array($hash, $valarm_hashes)) {
523                    continue;
524                }
525                $valarm_hashes[] = $hash;
526
527                if ($valarm['action'] == 'EMAIL') {
528                    $recipients = new vectorcontactref;
529                    foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
530                        $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
531                    }
532                    $alarm = new Alarm(
533                        strval($valarm['summary'] ?: $object['title']),
534                        strval($valarm['description'] ?: $object['description']),
535                        $recipients
536                    );
537                }
538                else if ($valarm['action'] == 'AUDIO') {
539                    $attach = new Attachment;
540                    $attach->setUri($valarm['uri'] ?: 'null', 'unknown');
541                    $alarm = new Alarm($attach);
542                }
543                else {
544                    // action == DISPLAY
545                    $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
546                }
547
548                if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
549                    $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
550                }
551                else if (preg_match('/^@([0-9]+)$/', $valarm['trigger'], $m)) {
552                    $alarm->setStart(self::get_datetime($m[1], new DateTimeZone('UTC')));
553                }
554                else {
555                    // Support also interval in format without PT, e.g. -10M
556                    if (preg_match('/^([-+]*)([0-9]+[DHMS])$/', strtoupper($valarm['trigger']), $m)) {
557                        $valarm['trigger'] = $m[1] . ($m[2][strlen($m[2])-1] == 'D' ? 'P' : 'PT') . $m[2];
558                    }
559
560                    try {
561                        $period   = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
562                        $duration = new Duration($period->d, $period->h, $period->i, $period->s, $valarm['trigger'][0] == '-');
563                    }
564                    catch (Exception $e) {
565                        // skip alarm with invalid trigger values
566                        rcube::raise_error($e, true);
567                        continue;
568                    }
569
570                    $related = strtoupper($valarm['related']) == 'END' ? kolabformat::End : kolabformat::Start;
571                    $alarm->setRelativeStart($duration, $related);
572                }
573
574                if ($valarm['duration']) {
575                    try {
576                        $d = new DateInterval($valarm['duration']);
577                        $duration = new Duration($d->d, $d->h, $d->i, $d->s);
578                        $alarm->setDuration($duration, intval($valarm['repeat']));
579                    }
580                    catch (Exception $e) {
581                        // ignore
582                    }
583                }
584
585                $valarms->push($alarm);
586            }
587        }
588        // legacy support
589        else if ($object['alarms']) {
590            list($offset, $type) = explode(":", $object['alarms']);
591
592            if ($type == 'EMAIL' && !empty($object['_owner'])) {  // email alarms implicitly go to event owner
593                $recipients = new vectorcontactref;
594                $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
595                $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
596            }
597            else {  // default: display alarm
598                $alarm = new Alarm($object['title']);
599            }
600
601            if (preg_match('/^@(\d+)/', $offset, $d)) {
602                $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
603            }
604            else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
605                $days = $hours = $minutes = $seconds = 0;
606                switch ($d[3]) {
607                    case 'W': $days  = 7*intval($d[2]); break;
608                    case 'D': $days    = intval($d[2]); break;
609                    case 'H': $hours   = intval($d[2]); break;
610                    case 'M': $minutes = intval($d[2]); break;
611                    case 'S': $seconds = intval($d[2]); break;
612                }
613                $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
614            }
615
616            $valarms->push($alarm);
617        }
618        $this->obj->setAlarms($valarms);
619
620        $this->set_attachments($object);
621    }
622
623    /**
624     * Return the reference date for recurrence and alarms
625     *
626     * @return mixed DateTime instance of null if no refdate is available
627     */
628    public function get_reference_date()
629    {
630        if ($this->data['start'] && $this->data['start'] instanceof DateTime) {
631            return $this->data['start'];
632        }
633
634        return self::php_datetime($this->obj->start());
635    }
636
637    /**
638     * Callback for kolab_storage_cache to get words to index for fulltext search
639     *
640     * @return array List of words to save in cache
641     */
642    public function get_words($obj = null)
643    {
644        $data = '';
645        $object = $obj ?: $this->data;
646
647        foreach (self::$fulltext_cols as $colname) {
648            list($col, $field) = explode(':', $colname);
649
650            if ($field) {
651                $a = array();
652                foreach ((array)$object[$col] as $attr)
653                    $a[] = $attr[$field];
654                $val = join(' ', $a);
655            }
656            else {
657                $val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
658            }
659
660            if (strlen($val))
661                $data .= $val . ' ';
662        }
663
664        $words = rcube_utils::normalize_string($data, true);
665
666        // collect words from recurrence exceptions
667        if (is_array($object['exceptions'])) {
668            foreach ($object['exceptions'] as $exception) {
669                $words = array_merge($words, $this->get_words($exception));
670            }
671        }
672
673        return array_unique($words);
674    }
675
676    /**
677     * Callback for kolab_storage_cache to get object specific tags to cache
678     *
679     * @return array List of tags to save in cache
680     */
681    public function get_tags($obj = null)
682    {
683        $tags = array();
684        $object = $obj ?: $this->data;
685
686        if (!empty($object['valarms'])) {
687            $tags[] = 'x-has-alarms';
688        }
689
690        // create tags reflecting participant status
691        if (is_array($object['attendees'])) {
692            foreach ($object['attendees'] as $attendee) {
693                if (!empty($attendee['email']) && !empty($attendee['status']))
694                    $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
695            }
696        }
697
698        // collect tags from recurrence exceptions
699        if (is_array($object['exceptions'])) {
700            foreach ($object['exceptions'] as $exception) {
701                $tags = array_merge($tags, $this->get_tags($exception));
702            }
703        }
704
705        if (!empty($object['status'])) {
706          $tags[] = 'x-status:' . strtolower($object['status']);
707        }
708
709        return array_unique($tags);
710    }
711
712    /**
713     * Identify changes considered relevant for scheduling
714     *
715     * @param array Hash array with NEW object properties
716     * @param array Hash array with OLD object properties
717     *
718     * @return boolean True if changes affect scheduling, False otherwise
719     */
720    public function check_rescheduling($object, $old = null)
721    {
722        $reschedule = false;
723
724        if (!is_array($old)) {
725            $old = $this->data['uid'] ? $this->data : $this->to_array();
726        }
727
728        foreach ($this->_scheduling_properties ?: self::$scheduling_properties as $prop) {
729            $a = $old[$prop];
730            $b = $object[$prop];
731            if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
732                $a = $a->format('Y-m-d');
733                $b = $b->format('Y-m-d');
734            }
735            if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
736                unset($a['EXCEPTIONS'], $b['EXCEPTIONS']);
737                $a = array_filter($a);
738                $b = array_filter($b);
739
740                // advanced rrule comparison: no rescheduling if series was shortened
741                if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
742                  unset($a['COUNT'], $b['COUNT']);
743                }
744                else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
745                  unset($a['UNTIL'], $b['UNTIL']);
746                }
747            }
748            if ($a != $b) {
749                $reschedule = true;
750                break;
751            }
752        }
753
754        return $reschedule;
755    }
756
757    /**
758     * Clones into an instance of libcalendaring's extended EventCal class
759     *
760     * @return mixed EventCal object or false on failure
761     */
762    public function to_libcal()
763    {
764        static $error_logged = false;
765
766        if (class_exists('kolabcalendaring')) {
767            return new EventCal($this->obj);
768        }
769        else if (!$error_logged) {
770            $error_logged = true;
771            rcube::raise_error(array(
772                'code'    => 900,
773                'message' => "Required kolabcalendaring module not found"
774            ), true);
775        }
776
777        return false;
778    }
779}
780