comparison vendor/sabre/vobject/lib/ITip/Broker.php @ 7:430dbd5346f7

vendor sabre as distributed
author Charlie Root
date Sat, 13 Jan 2018 09:06:10 -0500
parents
children
comparison
equal deleted inserted replaced
6:cec75ba50afc 7:430dbd5346f7
1 <?php
2
3 namespace Sabre\VObject\ITip;
4
5 use Sabre\VObject\Component\VCalendar;
6 use Sabre\VObject\DateTimeParser;
7 use Sabre\VObject\Reader;
8 use 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) 2011-2015 fruux GmbH (https://fruux.com/).
35 * @author Evert Pot (http://evertpot.com/)
36 * @license http://sabre.io/license/ Modified BSD License
37 */
38 class 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 $org = $event->add('ORGANIZER', $eventInfo['organizer']);
529 if ($eventInfo['organizerName']) $org['CN'] = $eventInfo['organizerName'];
530 $event->add('ATTENDEE', $attendee['href'], array(
531 'CN' => $attendee['name'],
532 ));
533 $message->significantChange = true;
534
535 } else {
536
537 // The attendee gets the updated event body
538 $message->method = 'REQUEST';
539
540 // Creating the new iCalendar body.
541 $icalMsg = new VCalendar();
542 $icalMsg->METHOD = $message->method;
543
544 foreach($calendar->select('VTIMEZONE') as $timezone) {
545 $icalMsg->add(clone $timezone);
546 }
547
548 // We need to find out that this change is significant. If it's
549 // not, systems may opt to not send messages.
550 //
551 // We do this based on the 'significantChangeHash' which is
552 // some value that changes if there's a certain set of
553 // properties changed in the event, or simply if there's a
554 // difference in instances that the attendee is invited to.
555
556 $message->significantChange =
557 $attendee['forceSend'] === 'REQUEST' ||
558 array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) ||
559 $oldEventInfo['significantChangeHash']!==$eventInfo['significantChangeHash'];
560
561 foreach($attendee['newInstances'] as $instanceId => $instanceInfo) {
562
563 $currentEvent = clone $eventInfo['instances'][$instanceId];
564 if ($instanceId === 'master') {
565
566 // We need to find a list of events that the attendee
567 // is not a part of to add to the list of exceptions.
568 $exceptions = array();
569 foreach($eventInfo['instances'] as $instanceId=>$vevent) {
570 if (!isset($attendee['newInstances'][$instanceId])) {
571 $exceptions[] = $instanceId;
572 }
573 }
574
575 // If there were exceptions, we need to add it to an
576 // existing EXDATE property, if it exists.
577 if ($exceptions) {
578 if (isset($currentEvent->EXDATE)) {
579 $currentEvent->EXDATE->setParts(array_merge(
580 $currentEvent->EXDATE->getParts(),
581 $exceptions
582 ));
583 } else {
584 $currentEvent->EXDATE = $exceptions;
585 }
586 }
587
588 // Cleaning up any scheduling information that
589 // shouldn't be sent along.
590 unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
591 unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
592
593 foreach($currentEvent->ATTENDEE as $attendee) {
594 unset($attendee['SCHEDULE-FORCE-SEND']);
595 unset($attendee['SCHEDULE-STATUS']);
596
597 // We're adding PARTSTAT=NEEDS-ACTION to ensure that
598 // iOS shows an "Inbox Item"
599 if (!isset($attendee['PARTSTAT'])) {
600 $attendee['PARTSTAT'] = 'NEEDS-ACTION';
601 }
602
603 }
604
605 }
606
607 $icalMsg->add($currentEvent);
608
609 }
610
611 }
612
613 $message->message = $icalMsg;
614 $messages[] = $message;
615
616 }
617
618 return $messages;
619
620 }
621
622 /**
623 * Parse an event update for an attendee.
624 *
625 * This function figures out if we need to send a reply to an organizer.
626 *
627 * @param VCalendar $calendar
628 * @param array $eventInfo
629 * @param array $oldEventInfo
630 * @param string $attendee
631 * @return Message[]
632 */
633 protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) {
634
635 if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent']==='CLIENT') {
636 return array();
637 }
638
639 // Don't bother generating messages for events that have already been
640 // cancelled.
641 if ($eventInfo['status']==='CANCELLED') {
642 return array();
643 }
644
645 $instances = array();
646 foreach($oldEventInfo['attendees'][$attendee]['instances'] as $instance) {
647
648 $instances[$instance['id']] = array(
649 'id' => $instance['id'],
650 'oldstatus' => $instance['partstat'],
651 'newstatus' => null,
652 );
653
654 }
655 foreach($eventInfo['attendees'][$attendee]['instances'] as $instance) {
656
657 if (isset($instances[$instance['id']])) {
658 $instances[$instance['id']]['newstatus'] = $instance['partstat'];
659 } else {
660 $instances[$instance['id']] = array(
661 'id' => $instance['id'],
662 'oldstatus' => null,
663 'newstatus' => $instance['partstat'],
664 );
665 }
666
667 }
668
669 // We need to also look for differences in EXDATE. If there are new
670 // items in EXDATE, it means that an attendee deleted instances of an
671 // event, which means we need to send DECLINED specifically for those
672 // instances.
673 // We only need to do that though, if the master event is not declined.
674 if ($instances['master']['newstatus'] !== 'DECLINED') {
675 foreach($eventInfo['exdate'] as $exDate) {
676
677 if (!in_array($exDate, $oldEventInfo['exdate'])) {
678 if (isset($instances[$exDate])) {
679 $instances[$exDate]['newstatus'] = 'DECLINED';
680 } else {
681 $instances[$exDate] = array(
682 'id' => $exDate,
683 'oldstatus' => null,
684 'newstatus' => 'DECLINED',
685 );
686 }
687 }
688
689 }
690 }
691
692 // Gathering a few extra properties for each instance.
693 foreach($instances as $recurId=>$instanceInfo) {
694
695 if (isset($eventInfo['instances'][$recurId])) {
696 $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART;
697 } else {
698 $instances[$recurId]['dtstart'] = $recurId;
699 }
700
701 }
702
703 $message = new Message();
704 $message->uid = $eventInfo['uid'];
705 $message->method = 'REPLY';
706 $message->component = 'VEVENT';
707 $message->sequence = $eventInfo['sequence'];
708 $message->sender = $attendee;
709 $message->senderName = $eventInfo['attendees'][$attendee]['name'];
710 $message->recipient = $eventInfo['organizer'];
711 $message->recipientName = $eventInfo['organizerName'];
712
713 $icalMsg = new VCalendar();
714 $icalMsg->METHOD = 'REPLY';
715
716 $hasReply = false;
717
718 foreach($instances as $instance) {
719
720 if ($instance['oldstatus']==$instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') {
721 // Skip
722 continue;
723 }
724
725 $event = $icalMsg->add('VEVENT', array(
726 'UID' => $message->uid,
727 'SEQUENCE' => $message->sequence,
728 ));
729 $summary = isset($calendar->VEVENT->SUMMARY)?$calendar->VEVENT->SUMMARY->getValue():'';
730 // Adding properties from the correct source instance
731 if (isset($eventInfo['instances'][$instance['id']])) {
732 $instanceObj = $eventInfo['instances'][$instance['id']];
733 $event->add(clone $instanceObj->DTSTART);
734 if (isset($instanceObj->SUMMARY)) {
735 $event->add('SUMMARY', $instanceObj->SUMMARY->getValue());
736 } elseif ($summary) {
737 $event->add('SUMMARY', $summary);
738 }
739 } else {
740 // This branch of the code is reached, when a reply is
741 // generated for an instance of a recurring event, through the
742 // fact that the instance has disappeared by showing up in
743 // EXDATE
744 $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
745 // Treat is as a DATE field
746 if (strlen($instance['id']) <= 8) {
747 $recur = $event->add('DTSTART', $dt, array('VALUE' => 'DATE'));
748 } else {
749 $recur = $event->add('DTSTART', $dt);
750 }
751 if ($summary) {
752 $event->add('SUMMARY', $summary);
753 }
754 }
755 if ($instance['id'] !== 'master') {
756 $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
757 // Treat is as a DATE field
758 if (strlen($instance['id']) <= 8) {
759 $recur = $event->add('RECURRENCE-ID', $dt, array('VALUE' => 'DATE'));
760 } else {
761 $recur = $event->add('RECURRENCE-ID', $dt);
762 }
763 }
764 $organizer = $event->add('ORGANIZER', $message->recipient);
765 if ($message->recipientName) {
766 $organizer['CN'] = $message->recipientName;
767 }
768 $attendee = $event->add('ATTENDEE', $message->sender, array(
769 'PARTSTAT' => $instance['newstatus']
770 ));
771 if ($message->senderName) {
772 $attendee['CN'] = $message->senderName;
773 }
774 $hasReply = true;
775
776 }
777
778 if ($hasReply) {
779 $message->message = $icalMsg;
780 return array($message);
781 } else {
782 return array();
783 }
784
785 }
786
787 /**
788 * Returns attendee information and information about instances of an
789 * event.
790 *
791 * Returns an array with the following keys:
792 *
793 * 1. uid
794 * 2. organizer
795 * 3. organizerName
796 * 4. attendees
797 * 5. instances
798 *
799 * @param VCalendar $calendar
800 * @return array
801 */
802 protected function parseEventInfo(VCalendar $calendar = null) {
803
804 $uid = null;
805 $organizer = null;
806 $organizerName = null;
807 $organizerForceSend = null;
808 $sequence = null;
809 $timezone = null;
810 $status = null;
811 $organizerScheduleAgent = 'SERVER';
812
813 $significantChangeHash = '';
814
815 // Now we need to collect a list of attendees, and which instances they
816 // are a part of.
817 $attendees = array();
818
819 $instances = array();
820 $exdate = array();
821
822 foreach($calendar->VEVENT as $vevent) {
823
824 if (is_null($uid)) {
825 $uid = $vevent->UID->getValue();
826 } else {
827 if ($uid !== $vevent->UID->getValue()) {
828 throw new ITipException('If a calendar contained more than one event, they must have the same UID.');
829 }
830 }
831
832 if (!isset($vevent->DTSTART)) {
833 throw new ITipException('An event MUST have a DTSTART property.');
834 }
835
836 if (isset($vevent->ORGANIZER)) {
837 if (is_null($organizer)) {
838 $organizer = $vevent->ORGANIZER->getNormalizedValue();
839 $organizerName = isset($vevent->ORGANIZER['CN'])?$vevent->ORGANIZER['CN']:null;
840 } else {
841 if ($organizer !== $vevent->ORGANIZER->getNormalizedValue()) {
842 throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
843 }
844 }
845 $organizerForceSend =
846 isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ?
847 strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) :
848 null;
849 $organizerScheduleAgent =
850 isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ?
851 strtoupper((string)$vevent->ORGANIZER['SCHEDULE-AGENT']) :
852 'SERVER';
853 }
854 if (is_null($sequence) && isset($vevent->SEQUENCE)) {
855 $sequence = $vevent->SEQUENCE->getValue();
856 }
857 if (isset($vevent->EXDATE)) {
858 $exdate = $vevent->EXDATE->getParts();
859 }
860 if (isset($vevent->STATUS)) {
861 $status = strtoupper($vevent->STATUS->getValue());
862 }
863
864 $recurId = isset($vevent->{'RECURRENCE-ID'})?$vevent->{'RECURRENCE-ID'}->getValue():'master';
865 if ($recurId==='master') {
866 $timezone = $vevent->DTSTART->getDateTime()->getTimeZone();
867 }
868 if(isset($vevent->ATTENDEE)) {
869 foreach($vevent->ATTENDEE as $attendee) {
870
871 if ($this->scheduleAgentServerRules &&
872 isset($attendee['SCHEDULE-AGENT']) &&
873 strtoupper($attendee['SCHEDULE-AGENT']->getValue()) === 'CLIENT'
874 ) {
875 continue;
876 }
877 $partStat =
878 isset($attendee['PARTSTAT']) ?
879 strtoupper($attendee['PARTSTAT']) :
880 'NEEDS-ACTION';
881
882 $forceSend =
883 isset($attendee['SCHEDULE-FORCE-SEND']) ?
884 strtoupper($attendee['SCHEDULE-FORCE-SEND']) :
885 null;
886
887
888 if (isset($attendees[$attendee->getNormalizedValue()])) {
889 $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = array(
890 'id' => $recurId,
891 'partstat' => $partStat,
892 'force-send' => $forceSend,
893 );
894 } else {
895 $attendees[$attendee->getNormalizedValue()] = array(
896 'href' => $attendee->getNormalizedValue(),
897 'instances' => array(
898 $recurId => array(
899 'id' => $recurId,
900 'partstat' => $partStat,
901 ),
902 ),
903 'name' => isset($attendee['CN'])?(string)$attendee['CN']:null,
904 'forceSend' => $forceSend,
905 );
906 }
907
908 }
909 $instances[$recurId] = $vevent;
910
911 }
912
913 foreach($this->significantChangeProperties as $prop) {
914 if (isset($vevent->$prop)) {
915 $significantChangeHash.=$prop.':';
916 foreach($vevent->select($prop) as $val) {
917 $significantChangeHash.= $val->getValue().';';
918 }
919 }
920 }
921
922 }
923 $significantChangeHash = md5($significantChangeHash);
924
925 return compact(
926 'uid',
927 'organizer',
928 'organizerName',
929 'organizerScheduleAgent',
930 'organizerForceSend',
931 'instances',
932 'attendees',
933 'sequence',
934 'exdate',
935 'timezone',
936 'significantChangeHash',
937 'status'
938 );
939
940 }
941
942 }