1<?php 2 3namespace Sabre\VObject\ITip; 4 5use Sabre\VObject\Component\VCalendar; 6use Sabre\VObject\DateTimeParser; 7use Sabre\VObject\Reader; 8use Sabre\VObject\Recur\EventIterator; 9 10/** 11 * The ITip\Broker class is a utility class that helps with processing 12 * so-called iTip messages. 13 * 14 * iTip is defined in rfc5546, stands for iCalendar Transport-Independent 15 * Interoperability Protocol, and describes the underlying mechanism for 16 * using iCalendar for scheduling for for example through email (also known as 17 * IMip) and CalDAV Scheduling. 18 * 19 * This class helps by: 20 * 21 * 1. Creating individual invites based on an iCalendar event for each 22 * attendee. 23 * 2. Generating invite updates based on an iCalendar update. This may result 24 * in new invites, updates and cancellations for attendees, if that list 25 * changed. 26 * 3. On the receiving end, it can create a local iCalendar event based on 27 * a received invite. 28 * 4. It can also process an invite update on a local event, ensuring that any 29 * overridden properties from attendees are retained. 30 * 5. It can create a accepted or declined iTip reply based on an invite. 31 * 6. It can process a reply from an invite and update an events attendee 32 * status based on a reply. 33 * 34 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 35 * @author Evert Pot (http://evertpot.com/) 36 * @license http://sabre.io/license/ Modified BSD License 37 */ 38class Broker { 39 40 /** 41 * This setting determines whether the rules for the SCHEDULE-AGENT 42 * parameter should be followed. 43 * 44 * This is a parameter defined on ATTENDEE properties, introduced by RFC 45 * 6638. This parameter allows a caldav client to tell the server 'Don't do 46 * any scheduling operations'. 47 * 48 * If this setting is turned on, any attendees with SCHEDULE-AGENT set to 49 * CLIENT will be ignored. This is the desired behavior for a CalDAV 50 * server, but if you're writing an iTip application that doesn't deal with 51 * CalDAV, you may want to ignore this parameter. 52 * 53 * @var bool 54 */ 55 public $scheduleAgentServerRules = true; 56 57 /** 58 * The broker will try during 'parseEvent' figure out whether the change 59 * was significant. 60 * 61 * It uses a few different ways to do this. One of these ways is seeing if 62 * certain properties changed values. This list of specified here. 63 * 64 * This list is taken from: 65 * * http://tools.ietf.org/html/rfc5546#section-2.1.4 66 * 67 * @var string[] 68 */ 69 public $significantChangeProperties = array( 70 'DTSTART', 71 'DTEND', 72 'DURATION', 73 'DUE', 74 'RRULE', 75 'RDATE', 76 'EXDATE', 77 'STATUS', 78 ); 79 80 /** 81 * This method is used to process an incoming itip message. 82 * 83 * Examples: 84 * 85 * 1. A user is an attendee to an event. The organizer sends an updated 86 * meeting using a new iTip message with METHOD:REQUEST. This function 87 * will process the message and update the attendee's event accordingly. 88 * 89 * 2. The organizer cancelled the event using METHOD:CANCEL. We will update 90 * the users event to state STATUS:CANCELLED. 91 * 92 * 3. An attendee sent a reply to an invite using METHOD:REPLY. We can 93 * update the organizers event to update the ATTENDEE with its correct 94 * PARTSTAT. 95 * 96 * The $existingObject is updated in-place. If there is no existing object 97 * (because it's a new invite for example) a new object will be created. 98 * 99 * If an existing object does not exist, and the method was CANCEL or 100 * REPLY, the message effectively gets ignored, and no 'existingObject' 101 * will be created. 102 * 103 * The updated $existingObject is also returned from this function. 104 * 105 * If the iTip message was not supported, we will always return false. 106 * 107 * @param Message $itipMessage 108 * @param VCalendar $existingObject 109 * @return VCalendar|null 110 */ 111 public function processMessage(Message $itipMessage, VCalendar $existingObject = null) { 112 113 // We only support events at the moment. 114 if ($itipMessage->component !== 'VEVENT') { 115 return false; 116 } 117 118 switch($itipMessage->method) { 119 120 case 'REQUEST' : 121 return $this->processMessageRequest($itipMessage, $existingObject); 122 123 case 'CANCEL' : 124 return $this->processMessageCancel($itipMessage, $existingObject); 125 126 case 'REPLY' : 127 return $this->processMessageReply($itipMessage, $existingObject); 128 129 default : 130 // Unsupported iTip message 131 return null; 132 133 } 134 135 return $existingObject; 136 137 } 138 139 /** 140 * This function parses a VCALENDAR object and figure out if any messages 141 * need to be sent. 142 * 143 * A VCALENDAR object will be created from the perspective of either an 144 * attendee, or an organizer. You must pass a string identifying the 145 * current user, so we can figure out who in the list of attendees or the 146 * organizer we are sending this message on behalf of. 147 * 148 * It's possible to specify the current user as an array, in case the user 149 * has more than one identifying href (such as multiple emails). 150 * 151 * It $oldCalendar is specified, it is assumed that the operation is 152 * updating an existing event, which means that we need to look at the 153 * differences between events, and potentially send old attendees 154 * cancellations, and current attendees updates. 155 * 156 * If $calendar is null, but $oldCalendar is specified, we treat the 157 * operation as if the user has deleted an event. If the user was an 158 * organizer, this means that we need to send cancellation notices to 159 * people. If the user was an attendee, we need to make sure that the 160 * organizer gets the 'declined' message. 161 * 162 * @param VCalendar|string $calendar 163 * @param string|array $userHref 164 * @param VCalendar|string $oldCalendar 165 * @return array 166 */ 167 public function parseEvent($calendar = null, $userHref, $oldCalendar = null) { 168 169 if ($oldCalendar) { 170 if (is_string($oldCalendar)) { 171 $oldCalendar = Reader::read($oldCalendar); 172 } 173 if (!isset($oldCalendar->VEVENT)) { 174 // We only support events at the moment 175 return array(); 176 } 177 178 $oldEventInfo = $this->parseEventInfo($oldCalendar); 179 } else { 180 $oldEventInfo = array( 181 'organizer' => null, 182 'significantChangeHash' => '', 183 'attendees' => array(), 184 ); 185 } 186 187 $userHref = (array)$userHref; 188 189 if (!is_null($calendar)) { 190 191 if (is_string($calendar)) { 192 $calendar = Reader::read($calendar); 193 } 194 if (!isset($calendar->VEVENT)) { 195 // We only support events at the moment 196 return array(); 197 } 198 $eventInfo = $this->parseEventInfo($calendar); 199 if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) { 200 // If there were no attendees on either side of the equation, 201 // we don't need to do anything. 202 return array(); 203 } 204 if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) { 205 // There was no organizer before or after the change. 206 return array(); 207 } 208 209 $baseCalendar = $calendar; 210 211 // If the new object didn't have an organizer, the organizer 212 // changed the object from a scheduling object to a non-scheduling 213 // object. We just copy the info from the old object. 214 if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) { 215 $eventInfo['organizer'] = $oldEventInfo['organizer']; 216 $eventInfo['organizerName'] = $oldEventInfo['organizerName']; 217 } 218 219 } else { 220 // The calendar object got deleted, we need to process this as a 221 // cancellation / decline. 222 if (!$oldCalendar) { 223 // No old and no new calendar, there's no thing to do. 224 return array(); 225 } 226 227 $eventInfo = $oldEventInfo; 228 229 if (in_array($eventInfo['organizer'], $userHref)) { 230 // This is an organizer deleting the event. 231 $eventInfo['attendees'] = array(); 232 // Increasing the sequence, but only if the organizer deleted 233 // the event. 234 $eventInfo['sequence']++; 235 } else { 236 // This is an attendee deleting the event. 237 foreach($eventInfo['attendees'] as $key=>$attendee) { 238 if (in_array($attendee['href'], $userHref)) { 239 $eventInfo['attendees'][$key]['instances'] = array('master' => 240 array('id'=>'master', 'partstat' => 'DECLINED') 241 ); 242 } 243 } 244 } 245 $baseCalendar = $oldCalendar; 246 247 } 248 249 if (in_array($eventInfo['organizer'], $userHref)) { 250 return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); 251 } elseif ($oldCalendar) { 252 // We need to figure out if the user is an attendee, but we're only 253 // doing so if there's an oldCalendar, because we only want to 254 // process updates, not creation of new events. 255 foreach($eventInfo['attendees'] as $attendee) { 256 if (in_array($attendee['href'], $userHref)) { 257 return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); 258 } 259 } 260 } 261 return array(); 262 263 } 264 265 /** 266 * Processes incoming REQUEST messages. 267 * 268 * This is message from an organizer, and is either a new event 269 * invite, or an update to an existing one. 270 * 271 * 272 * @param Message $itipMessage 273 * @param VCalendar $existingObject 274 * @return VCalendar|null 275 */ 276 protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null) { 277 278 if (!$existingObject) { 279 // This is a new invite, and we're just going to copy over 280 // all the components from the invite. 281 $existingObject = new VCalendar(); 282 foreach($itipMessage->message->getComponents() as $component) { 283 $existingObject->add(clone $component); 284 } 285 } else { 286 // We need to update an existing object with all the new 287 // information. We can just remove all existing components 288 // and create new ones. 289 foreach($existingObject->getComponents() as $component) { 290 $existingObject->remove($component); 291 } 292 foreach($itipMessage->message->getComponents() as $component) { 293 $existingObject->add(clone $component); 294 } 295 } 296 return $existingObject; 297 298 } 299 300 /** 301 * Processes incoming CANCEL messages. 302 * 303 * This is a message from an organizer, and means that either an 304 * attendee got removed from an event, or an event got cancelled 305 * altogether. 306 * 307 * @param Message $itipMessage 308 * @param VCalendar $existingObject 309 * @return VCalendar|null 310 */ 311 protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null) { 312 313 if (!$existingObject) { 314 // The event didn't exist in the first place, so we're just 315 // ignoring this message. 316 } else { 317 foreach($existingObject->VEVENT as $vevent) { 318 $vevent->STATUS = 'CANCELLED'; 319 $vevent->SEQUENCE = $itipMessage->sequence; 320 } 321 } 322 return $existingObject; 323 324 } 325 326 /** 327 * Processes incoming REPLY messages. 328 * 329 * The message is a reply. This is for example an attendee telling 330 * an organizer he accepted the invite, or declined it. 331 * 332 * @param Message $itipMessage 333 * @param VCalendar $existingObject 334 * @return VCalendar|null 335 */ 336 protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null) { 337 338 // A reply can only be processed based on an existing object. 339 // If the object is not available, the reply is ignored. 340 if (!$existingObject) { 341 return null; 342 } 343 $instances = array(); 344 $requestStatus = '2.0'; 345 346 // Finding all the instances the attendee replied to. 347 foreach($itipMessage->message->VEVENT as $vevent) { 348 $recurId = isset($vevent->{'RECURRENCE-ID'})?$vevent->{'RECURRENCE-ID'}->getValue():'master'; 349 $attendee = $vevent->ATTENDEE; 350 $instances[$recurId] = $attendee['PARTSTAT']->getValue(); 351 if (isset($vevent->{'REQUEST-STATUS'})) { 352 $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); 353 list($requestStatus) = explode(';', $requestStatus); 354 } 355 } 356 357 // Now we need to loop through the original organizer event, to find 358 // all the instances where we have a reply for. 359 $masterObject = null; 360 foreach($existingObject->VEVENT as $vevent) { 361 $recurId = isset($vevent->{'RECURRENCE-ID'})?$vevent->{'RECURRENCE-ID'}->getValue():'master'; 362 if ($recurId==='master') { 363 $masterObject = $vevent; 364 } 365 if (isset($instances[$recurId])) { 366 $attendeeFound = false; 367 if (isset($vevent->ATTENDEE)) { 368 foreach($vevent->ATTENDEE as $attendee) { 369 if ($attendee->getValue() === $itipMessage->sender) { 370 $attendeeFound = true; 371 $attendee['PARTSTAT'] = $instances[$recurId]; 372 $attendee['SCHEDULE-STATUS'] = $requestStatus; 373 // Un-setting the RSVP status, because we now know 374 // that the attende already replied. 375 unset($attendee['RSVP']); 376 break; 377 } 378 } 379 } 380 if (!$attendeeFound) { 381 // Adding a new attendee. The iTip documentation calls this 382 // a party crasher. 383 $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, array( 384 'PARTSTAT' => $instances[$recurId] 385 )); 386 if ($itipMessage->senderName) $attendee['CN'] = $itipMessage->senderName; 387 } 388 unset($instances[$recurId]); 389 } 390 } 391 392 if(!$masterObject) { 393 // No master object, we can't add new instances. 394 return null; 395 } 396 // If we got replies to instances that did not exist in the 397 // original list, it means that new exceptions must be created. 398 foreach($instances as $recurId=>$partstat) { 399 400 $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); 401 $found = false; 402 $iterations = 1000; 403 do { 404 405 $newObject = $recurrenceIterator->getEventObject(); 406 $recurrenceIterator->next(); 407 408 if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue()===$recurId) { 409 $found = true; 410 } 411 $iterations--; 412 413 } while($recurrenceIterator->valid() && !$found && $iterations); 414 415 // Invalid recurrence id. Skipping this object. 416 if (!$found) continue; 417 418 unset( 419 $newObject->RRULE, 420 $newObject->EXDATE, 421 $newObject->RDATE 422 ); 423 $attendeeFound = false; 424 if (isset($newObject->ATTENDEE)) { 425 foreach($newObject->ATTENDEE as $attendee) { 426 if ($attendee->getValue() === $itipMessage->sender) { 427 $attendeeFound = true; 428 $attendee['PARTSTAT'] = $partstat; 429 break; 430 } 431 } 432 } 433 if (!$attendeeFound) { 434 // Adding a new attendee 435 $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, array( 436 'PARTSTAT' => $partstat 437 )); 438 if ($itipMessage->senderName) { 439 $attendee['CN'] = $itipMessage->senderName; 440 } 441 } 442 $existingObject->add($newObject); 443 444 } 445 return $existingObject; 446 447 } 448 449 /** 450 * This method is used in cases where an event got updated, and we 451 * potentially need to send emails to attendees to let them know of updates 452 * in the events. 453 * 454 * We will detect which attendees got added, which got removed and create 455 * specific messages for these situations. 456 * 457 * @param VCalendar $calendar 458 * @param array $eventInfo 459 * @param array $oldEventInfo 460 * @return array 461 */ 462 protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) { 463 464 // Merging attendee lists. 465 $attendees = array(); 466 foreach($oldEventInfo['attendees'] as $attendee) { 467 $attendees[$attendee['href']] = array( 468 'href' => $attendee['href'], 469 'oldInstances' => $attendee['instances'], 470 'newInstances' => array(), 471 'name' => $attendee['name'], 472 'forceSend' => null, 473 ); 474 } 475 foreach($eventInfo['attendees'] as $attendee) { 476 if (isset($attendees[$attendee['href']])) { 477 $attendees[$attendee['href']]['name'] = $attendee['name']; 478 $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; 479 $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; 480 } else { 481 $attendees[$attendee['href']] = array( 482 'href' => $attendee['href'], 483 'oldInstances' => array(), 484 'newInstances' => $attendee['instances'], 485 'name' => $attendee['name'], 486 'forceSend' => $attendee['forceSend'], 487 ); 488 } 489 } 490 491 $messages = array(); 492 493 foreach($attendees as $attendee) { 494 495 // An organizer can also be an attendee. We should not generate any 496 // messages for those. 497 if ($attendee['href']===$eventInfo['organizer']) { 498 continue; 499 } 500 501 $message = new Message(); 502 $message->uid = $eventInfo['uid']; 503 $message->component = 'VEVENT'; 504 $message->sequence = $eventInfo['sequence']; 505 $message->sender = $eventInfo['organizer']; 506 $message->senderName = $eventInfo['organizerName']; 507 $message->recipient = $attendee['href']; 508 $message->recipientName = $attendee['name']; 509 510 if (!$attendee['newInstances']) { 511 512 // If there are no instances the attendee is a part of, it 513 // means the attendee was removed and we need to send him a 514 // CANCEL. 515 $message->method = 'CANCEL'; 516 517 // Creating the new iCalendar body. 518 $icalMsg = new VCalendar(); 519 $icalMsg->METHOD = $message->method; 520 $event = $icalMsg->add('VEVENT', array( 521 'UID' => $message->uid, 522 'SEQUENCE' => $message->sequence, 523 )); 524 if (isset($calendar->VEVENT->SUMMARY)) { 525 $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); 526 } 527 $event->add(clone $calendar->VEVENT->DTSTART); 528 if (isset($calendar->VEVENT->DTEND)) { 529 $event->add(clone $calendar->VEVENT->DTEND); 530 } elseif (isset($calendar->VEVENT->DURATION)) { 531 $event->add(clone $calendar->VEVENT->DURATION); 532 } 533 $org = $event->add('ORGANIZER', $eventInfo['organizer']); 534 if ($eventInfo['organizerName']) $org['CN'] = $eventInfo['organizerName']; 535 $event->add('ATTENDEE', $attendee['href'], array( 536 'CN' => $attendee['name'], 537 )); 538 $message->significantChange = true; 539 540 } else { 541 542 // The attendee gets the updated event body 543 $message->method = 'REQUEST'; 544 545 // Creating the new iCalendar body. 546 $icalMsg = new VCalendar(); 547 $icalMsg->METHOD = $message->method; 548 549 foreach($calendar->select('VTIMEZONE') as $timezone) { 550 $icalMsg->add(clone $timezone); 551 } 552 553 // We need to find out that this change is significant. If it's 554 // not, systems may opt to not send messages. 555 // 556 // We do this based on the 'significantChangeHash' which is 557 // some value that changes if there's a certain set of 558 // properties changed in the event, or simply if there's a 559 // difference in instances that the attendee is invited to. 560 561 $message->significantChange = 562 $attendee['forceSend'] === 'REQUEST' || 563 array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) || 564 $oldEventInfo['significantChangeHash']!==$eventInfo['significantChangeHash']; 565 566 foreach($attendee['newInstances'] as $instanceId => $instanceInfo) { 567 568 $currentEvent = clone $eventInfo['instances'][$instanceId]; 569 if ($instanceId === 'master') { 570 571 // We need to find a list of events that the attendee 572 // is not a part of to add to the list of exceptions. 573 $exceptions = array(); 574 foreach($eventInfo['instances'] as $instanceId=>$vevent) { 575 if (!isset($attendee['newInstances'][$instanceId])) { 576 $exceptions[] = $instanceId; 577 } 578 } 579 580 // If there were exceptions, we need to add it to an 581 // existing EXDATE property, if it exists. 582 if ($exceptions) { 583 if (isset($currentEvent->EXDATE)) { 584 $currentEvent->EXDATE->setParts(array_merge( 585 $currentEvent->EXDATE->getParts(), 586 $exceptions 587 )); 588 } else { 589 $currentEvent->EXDATE = $exceptions; 590 } 591 } 592 593 // Cleaning up any scheduling information that 594 // shouldn't be sent along. 595 unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); 596 unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); 597 598 foreach($currentEvent->ATTENDEE as $attendee) { 599 unset($attendee['SCHEDULE-FORCE-SEND']); 600 unset($attendee['SCHEDULE-STATUS']); 601 602 // We're adding PARTSTAT=NEEDS-ACTION to ensure that 603 // iOS shows an "Inbox Item" 604 if (!isset($attendee['PARTSTAT'])) { 605 $attendee['PARTSTAT'] = 'NEEDS-ACTION'; 606 } 607 608 } 609 610 } 611 612 $icalMsg->add($currentEvent); 613 614 } 615 616 } 617 618 $message->message = $icalMsg; 619 $messages[] = $message; 620 621 } 622 623 return $messages; 624 625 } 626 627 /** 628 * Parse an event update for an attendee. 629 * 630 * This function figures out if we need to send a reply to an organizer. 631 * 632 * @param VCalendar $calendar 633 * @param array $eventInfo 634 * @param array $oldEventInfo 635 * @param string $attendee 636 * @return Message[] 637 */ 638 protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) { 639 640 if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent']==='CLIENT') { 641 return array(); 642 } 643 644 // Don't bother generating messages for events that have already been 645 // cancelled. 646 if ($eventInfo['status']==='CANCELLED') { 647 return array(); 648 } 649 650 $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? 651 $oldEventInfo['attendees'][$attendee]['instances'] : 652 array(); 653 654 $instances = array(); 655 foreach($oldInstances as $instance) { 656 657 $instances[$instance['id']] = array( 658 'id' => $instance['id'], 659 'oldstatus' => $instance['partstat'], 660 'newstatus' => null, 661 ); 662 663 } 664 foreach($eventInfo['attendees'][$attendee]['instances'] as $instance) { 665 666 if (isset($instances[$instance['id']])) { 667 $instances[$instance['id']]['newstatus'] = $instance['partstat']; 668 } else { 669 $instances[$instance['id']] = array( 670 'id' => $instance['id'], 671 'oldstatus' => null, 672 'newstatus' => $instance['partstat'], 673 ); 674 } 675 676 } 677 678 // We need to also look for differences in EXDATE. If there are new 679 // items in EXDATE, it means that an attendee deleted instances of an 680 // event, which means we need to send DECLINED specifically for those 681 // instances. 682 // We only need to do that though, if the master event is not declined. 683 if (isset($instances['master']) && $instances['master']['newstatus'] !== 'DECLINED') { 684 foreach($eventInfo['exdate'] as $exDate) { 685 686 if (!in_array($exDate, $oldEventInfo['exdate'])) { 687 if (isset($instances[$exDate])) { 688 $instances[$exDate]['newstatus'] = 'DECLINED'; 689 } else { 690 $instances[$exDate] = array( 691 'id' => $exDate, 692 'oldstatus' => null, 693 'newstatus' => 'DECLINED', 694 ); 695 } 696 } 697 698 } 699 } 700 701 // Gathering a few extra properties for each instance. 702 foreach($instances as $recurId=>$instanceInfo) { 703 704 if (isset($eventInfo['instances'][$recurId])) { 705 $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; 706 } else { 707 $instances[$recurId]['dtstart'] = $recurId; 708 } 709 710 } 711 712 $message = new Message(); 713 $message->uid = $eventInfo['uid']; 714 $message->method = 'REPLY'; 715 $message->component = 'VEVENT'; 716 $message->sequence = $eventInfo['sequence']; 717 $message->sender = $attendee; 718 $message->senderName = $eventInfo['attendees'][$attendee]['name']; 719 $message->recipient = $eventInfo['organizer']; 720 $message->recipientName = $eventInfo['organizerName']; 721 722 $icalMsg = new VCalendar(); 723 $icalMsg->METHOD = 'REPLY'; 724 725 $hasReply = false; 726 727 foreach($instances as $instance) { 728 729 if ($instance['oldstatus']==$instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') { 730 // Skip 731 continue; 732 } 733 734 $event = $icalMsg->add('VEVENT', array( 735 'UID' => $message->uid, 736 'SEQUENCE' => $message->sequence, 737 )); 738 $summary = isset($calendar->VEVENT->SUMMARY)?$calendar->VEVENT->SUMMARY->getValue():''; 739 // Adding properties from the correct source instance 740 if (isset($eventInfo['instances'][$instance['id']])) { 741 $instanceObj = $eventInfo['instances'][$instance['id']]; 742 $event->add(clone $instanceObj->DTSTART); 743 if (isset($instanceObj->DTEND)) { 744 $event->add(clone $instanceObj->DTEND); 745 } elseif (isset($instanceObj->DURATION)) { 746 $event->add(clone $instanceObj->DURATION); 747 } 748 if (isset($instanceObj->SUMMARY)) { 749 $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); 750 } elseif ($summary) { 751 $event->add('SUMMARY', $summary); 752 } 753 } else { 754 // This branch of the code is reached, when a reply is 755 // generated for an instance of a recurring event, through the 756 // fact that the instance has disappeared by showing up in 757 // EXDATE 758 $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); 759 // Treat is as a DATE field 760 if (strlen($instance['id']) <= 8) { 761 $recur = $event->add('DTSTART', $dt, array('VALUE' => 'DATE')); 762 } else { 763 $recur = $event->add('DTSTART', $dt); 764 } 765 if ($summary) { 766 $event->add('SUMMARY', $summary); 767 } 768 } 769 if ($instance['id'] !== 'master') { 770 $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); 771 // Treat is as a DATE field 772 if (strlen($instance['id']) <= 8) { 773 $recur = $event->add('RECURRENCE-ID', $dt, array('VALUE' => 'DATE')); 774 } else { 775 $recur = $event->add('RECURRENCE-ID', $dt); 776 } 777 } 778 $organizer = $event->add('ORGANIZER', $message->recipient); 779 if ($message->recipientName) { 780 $organizer['CN'] = $message->recipientName; 781 } 782 $attendee = $event->add('ATTENDEE', $message->sender, array( 783 'PARTSTAT' => $instance['newstatus'] 784 )); 785 if ($message->senderName) { 786 $attendee['CN'] = $message->senderName; 787 } 788 $hasReply = true; 789 790 } 791 792 if ($hasReply) { 793 $message->message = $icalMsg; 794 return array($message); 795 } else { 796 return array(); 797 } 798 799 } 800 801 /** 802 * Returns attendee information and information about instances of an 803 * event. 804 * 805 * Returns an array with the following keys: 806 * 807 * 1. uid 808 * 2. organizer 809 * 3. organizerName 810 * 4. organizerScheduleAgent 811 * 5. organizerForceSend 812 * 6. instances 813 * 7. attendees 814 * 8. sequence 815 * 9. exdate 816 * 10. timezone - strictly the timezone on which the recurrence rule is 817 * based on. 818 * 11. significantChangeHash 819 * 12. status 820 * @param VCalendar $calendar 821 * @return array 822 */ 823 protected function parseEventInfo(VCalendar $calendar = null) { 824 825 $uid = null; 826 $organizer = null; 827 $organizerName = null; 828 $organizerForceSend = null; 829 $sequence = null; 830 $timezone = null; 831 $status = null; 832 $organizerScheduleAgent = 'SERVER'; 833 834 $significantChangeHash = ''; 835 836 // Now we need to collect a list of attendees, and which instances they 837 // are a part of. 838 $attendees = array(); 839 840 $instances = array(); 841 $exdate = array(); 842 843 foreach($calendar->VEVENT as $vevent) { 844 845 if (is_null($uid)) { 846 $uid = $vevent->UID->getValue(); 847 } else { 848 if ($uid !== $vevent->UID->getValue()) { 849 throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); 850 } 851 } 852 853 if (!isset($vevent->DTSTART)) { 854 throw new ITipException('An event MUST have a DTSTART property.'); 855 } 856 857 if (isset($vevent->ORGANIZER)) { 858 if (is_null($organizer)) { 859 $organizer = $vevent->ORGANIZER->getNormalizedValue(); 860 $organizerName = isset($vevent->ORGANIZER['CN'])?$vevent->ORGANIZER['CN']:null; 861 } else { 862 if ($organizer !== $vevent->ORGANIZER->getNormalizedValue()) { 863 throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); 864 } 865 } 866 $organizerForceSend = 867 isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? 868 strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : 869 null; 870 $organizerScheduleAgent = 871 isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? 872 strtoupper((string)$vevent->ORGANIZER['SCHEDULE-AGENT']) : 873 'SERVER'; 874 } 875 if (is_null($sequence) && isset($vevent->SEQUENCE)) { 876 $sequence = $vevent->SEQUENCE->getValue(); 877 } 878 if (isset($vevent->EXDATE)) { 879 foreach ($vevent->select('EXDATE') as $val) { 880 $exdate = array_merge($exdate, $val->getParts()); 881 } 882 sort($exdate); 883 } 884 if (isset($vevent->STATUS)) { 885 $status = strtoupper($vevent->STATUS->getValue()); 886 } 887 888 $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; 889 if (is_null($timezone)) { 890 if ($recurId === 'master') { 891 $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); 892 } else { 893 $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); 894 } 895 } 896 if(isset($vevent->ATTENDEE)) { 897 foreach($vevent->ATTENDEE as $attendee) { 898 899 if ($this->scheduleAgentServerRules && 900 isset($attendee['SCHEDULE-AGENT']) && 901 strtoupper($attendee['SCHEDULE-AGENT']->getValue()) === 'CLIENT' 902 ) { 903 continue; 904 } 905 $partStat = 906 isset($attendee['PARTSTAT']) ? 907 strtoupper($attendee['PARTSTAT']) : 908 'NEEDS-ACTION'; 909 910 $forceSend = 911 isset($attendee['SCHEDULE-FORCE-SEND']) ? 912 strtoupper($attendee['SCHEDULE-FORCE-SEND']) : 913 null; 914 915 916 if (isset($attendees[$attendee->getNormalizedValue()])) { 917 $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = array( 918 'id' => $recurId, 919 'partstat' => $partStat, 920 'force-send' => $forceSend, 921 ); 922 } else { 923 $attendees[$attendee->getNormalizedValue()] = array( 924 'href' => $attendee->getNormalizedValue(), 925 'instances' => array( 926 $recurId => array( 927 'id' => $recurId, 928 'partstat' => $partStat, 929 ), 930 ), 931 'name' => isset($attendee['CN'])?(string)$attendee['CN']:null, 932 'forceSend' => $forceSend, 933 ); 934 } 935 936 } 937 $instances[$recurId] = $vevent; 938 939 } 940 941 foreach($this->significantChangeProperties as $prop) { 942 if (isset($vevent->$prop)) { 943 $propertyValues = $vevent->select($prop); 944 945 $significantChangeHash.=$prop.':'; 946 947 if ($prop === 'EXDATE') { 948 949 $significantChangeHash.= implode(',', $exdate).';'; 950 951 } else { 952 953 foreach($propertyValues as $val) { 954 $significantChangeHash.= $val->getValue().';'; 955 } 956 957 } 958 } 959 } 960 961 } 962 $significantChangeHash = md5($significantChangeHash); 963 964 return compact( 965 'uid', 966 'organizer', 967 'organizerName', 968 'organizerScheduleAgent', 969 'organizerForceSend', 970 'instances', 971 'attendees', 972 'sequence', 973 'exdate', 974 'timezone', 975 'significantChangeHash', 976 'status' 977 ); 978 979 } 980 981} 982