Mercurial > hg > rc1
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 } |