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