7
|
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 }
|