Completed
Branch develop (37f7b7)
by
unknown
24:41
created
htdocs/includes/sabre/sabre/vobject/lib/ITip/Broker.php 2 patches
Spacing   +5 added lines, -10 removed lines patch added patch discarded remove patch
@@ -632,8 +632,7 @@  discard block
 block discarded – undo
632 632
         }
633 633
 
634 634
         $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ?
635
-            $oldEventInfo['attendees'][$attendee]['instances'] :
636
-            [];
635
+            $oldEventInfo['attendees'][$attendee]['instances'] : [];
637 636
 
638 637
         $instances = [];
639 638
         foreach ($oldInstances as $instance) {
@@ -849,12 +848,10 @@  discard block
 block discarded – undo
849 848
                 }
850 849
                 $organizerForceSend =
851 850
                     isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ?
852
-                    strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) :
853
-                    null;
851
+                    strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : null;
854 852
                 $organizerScheduleAgent =
855 853
                     isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ?
856
-                    strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) :
857
-                    'SERVER';
854
+                    strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) : 'SERVER';
858 855
             }
859 856
             if (is_null($sequence) && isset($vevent->SEQUENCE)) {
860 857
                 $sequence = $vevent->SEQUENCE->getValue();
@@ -902,13 +899,11 @@  discard block
 block discarded – undo
902 899
                     }
903 900
                     $partStat =
904 901
                         isset($attendee['PARTSTAT']) ?
905
-                        strtoupper($attendee['PARTSTAT']) :
906
-                        'NEEDS-ACTION';
902
+                        strtoupper($attendee['PARTSTAT']) : 'NEEDS-ACTION';
907 903
 
908 904
                     $forceSend =
909 905
                         isset($attendee['SCHEDULE-FORCE-SEND']) ?
910
-                        strtoupper($attendee['SCHEDULE-FORCE-SEND']) :
911
-                        null;
906
+                        strtoupper($attendee['SCHEDULE-FORCE-SEND']) : null;
912 907
 
913 908
                     if (isset($attendees[$attendee->getNormalizedValue()])) {
914 909
                         $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [
Please login to merge, or discard this patch.
Indentation   +938 added lines, -938 removed lines patch added patch discarded remove patch
@@ -37,942 +37,942 @@
 block discarded – undo
37 37
  */
38 38
 class Broker
39 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 = [
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 VCalendar $existingObject
108
-     *
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 ('VEVENT' !== $itipMessage->component) {
115
-            return false;
116
-        }
117
-
118
-        switch ($itipMessage->method) {
119
-            case 'REQUEST':
120
-                return $this->processMessageRequest($itipMessage, $existingObject);
121
-
122
-            case 'CANCEL':
123
-                return $this->processMessageCancel($itipMessage, $existingObject);
124
-
125
-            case 'REPLY':
126
-                return $this->processMessageReply($itipMessage, $existingObject);
127
-
128
-            default:
129
-                // Unsupported iTip message
130
-                return;
131
-        }
132
-
133
-        return $existingObject;
134
-    }
135
-
136
-    /**
137
-     * This function parses a VCALENDAR object and figure out if any messages
138
-     * need to be sent.
139
-     *
140
-     * A VCALENDAR object will be created from the perspective of either an
141
-     * attendee, or an organizer. You must pass a string identifying the
142
-     * current user, so we can figure out who in the list of attendees or the
143
-     * organizer we are sending this message on behalf of.
144
-     *
145
-     * It's possible to specify the current user as an array, in case the user
146
-     * has more than one identifying href (such as multiple emails).
147
-     *
148
-     * It $oldCalendar is specified, it is assumed that the operation is
149
-     * updating an existing event, which means that we need to look at the
150
-     * differences between events, and potentially send old attendees
151
-     * cancellations, and current attendees updates.
152
-     *
153
-     * If $calendar is null, but $oldCalendar is specified, we treat the
154
-     * operation as if the user has deleted an event. If the user was an
155
-     * organizer, this means that we need to send cancellation notices to
156
-     * people. If the user was an attendee, we need to make sure that the
157
-     * organizer gets the 'declined' message.
158
-     *
159
-     * @param VCalendar|string      $calendar
160
-     * @param string|array          $userHref
161
-     * @param VCalendar|string|null $oldCalendar
162
-     *
163
-     * @return array
164
-     */
165
-    public function parseEvent($calendar, $userHref, $oldCalendar = null)
166
-    {
167
-        if ($oldCalendar) {
168
-            if (is_string($oldCalendar)) {
169
-                $oldCalendar = Reader::read($oldCalendar);
170
-            }
171
-            if (!isset($oldCalendar->VEVENT)) {
172
-                // We only support events at the moment
173
-                return [];
174
-            }
175
-
176
-            $oldEventInfo = $this->parseEventInfo($oldCalendar);
177
-        } else {
178
-            $oldEventInfo = [
179
-                'organizer' => null,
180
-                'significantChangeHash' => '',
181
-                'attendees' => [],
182
-            ];
183
-        }
184
-
185
-        $userHref = (array) $userHref;
186
-
187
-        if (!is_null($calendar)) {
188
-            if (is_string($calendar)) {
189
-                $calendar = Reader::read($calendar);
190
-            }
191
-            if (!isset($calendar->VEVENT)) {
192
-                // We only support events at the moment
193
-                return [];
194
-            }
195
-            $eventInfo = $this->parseEventInfo($calendar);
196
-            if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) {
197
-                // If there were no attendees on either side of the equation,
198
-                // we don't need to do anything.
199
-                return [];
200
-            }
201
-            if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) {
202
-                // There was no organizer before or after the change.
203
-                return [];
204
-            }
205
-
206
-            $baseCalendar = $calendar;
207
-
208
-            // If the new object didn't have an organizer, the organizer
209
-            // changed the object from a scheduling object to a non-scheduling
210
-            // object. We just copy the info from the old object.
211
-            if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) {
212
-                $eventInfo['organizer'] = $oldEventInfo['organizer'];
213
-                $eventInfo['organizerName'] = $oldEventInfo['organizerName'];
214
-            }
215
-        } else {
216
-            // The calendar object got deleted, we need to process this as a
217
-            // cancellation / decline.
218
-            if (!$oldCalendar) {
219
-                // No old and no new calendar, there's no thing to do.
220
-                return [];
221
-            }
222
-
223
-            $eventInfo = $oldEventInfo;
224
-
225
-            if (in_array($eventInfo['organizer'], $userHref)) {
226
-                // This is an organizer deleting the event.
227
-                $eventInfo['attendees'] = [];
228
-                // Increasing the sequence, but only if the organizer deleted
229
-                // the event.
230
-                ++$eventInfo['sequence'];
231
-            } else {
232
-                // This is an attendee deleting the event.
233
-                foreach ($eventInfo['attendees'] as $key => $attendee) {
234
-                    if (in_array($attendee['href'], $userHref)) {
235
-                        $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'],
236
-                        ];
237
-                    }
238
-                }
239
-            }
240
-            $baseCalendar = $oldCalendar;
241
-        }
242
-
243
-        if (in_array($eventInfo['organizer'], $userHref)) {
244
-            return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo);
245
-        } elseif ($oldCalendar) {
246
-            // We need to figure out if the user is an attendee, but we're only
247
-            // doing so if there's an oldCalendar, because we only want to
248
-            // process updates, not creation of new events.
249
-            foreach ($eventInfo['attendees'] as $attendee) {
250
-                if (in_array($attendee['href'], $userHref)) {
251
-                    return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
252
-                }
253
-            }
254
-        }
255
-
256
-        return [];
257
-    }
258
-
259
-    /**
260
-     * Processes incoming REQUEST messages.
261
-     *
262
-     * This is message from an organizer, and is either a new event
263
-     * invite, or an update to an existing one.
264
-     *
265
-     * @param VCalendar $existingObject
266
-     *
267
-     * @return VCalendar|null
268
-     */
269
-    protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null)
270
-    {
271
-        if (!$existingObject) {
272
-            // This is a new invite, and we're just going to copy over
273
-            // all the components from the invite.
274
-            $existingObject = new VCalendar();
275
-            foreach ($itipMessage->message->getComponents() as $component) {
276
-                $existingObject->add(clone $component);
277
-            }
278
-        } else {
279
-            // We need to update an existing object with all the new
280
-            // information. We can just remove all existing components
281
-            // and create new ones.
282
-            foreach ($existingObject->getComponents() as $component) {
283
-                $existingObject->remove($component);
284
-            }
285
-            foreach ($itipMessage->message->getComponents() as $component) {
286
-                $existingObject->add(clone $component);
287
-            }
288
-        }
289
-
290
-        return $existingObject;
291
-    }
292
-
293
-    /**
294
-     * Processes incoming CANCEL messages.
295
-     *
296
-     * This is a message from an organizer, and means that either an
297
-     * attendee got removed from an event, or an event got cancelled
298
-     * altogether.
299
-     *
300
-     * @param VCalendar $existingObject
301
-     *
302
-     * @return VCalendar|null
303
-     */
304
-    protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null)
305
-    {
306
-        if (!$existingObject) {
307
-            // The event didn't exist in the first place, so we're just
308
-            // ignoring this message.
309
-        } else {
310
-            foreach ($existingObject->VEVENT as $vevent) {
311
-                $vevent->STATUS = 'CANCELLED';
312
-                $vevent->SEQUENCE = $itipMessage->sequence;
313
-            }
314
-        }
315
-
316
-        return $existingObject;
317
-    }
318
-
319
-    /**
320
-     * Processes incoming REPLY messages.
321
-     *
322
-     * The message is a reply. This is for example an attendee telling
323
-     * an organizer he accepted the invite, or declined it.
324
-     *
325
-     * @param VCalendar $existingObject
326
-     *
327
-     * @return VCalendar|null
328
-     */
329
-    protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null)
330
-    {
331
-        // A reply can only be processed based on an existing object.
332
-        // If the object is not available, the reply is ignored.
333
-        if (!$existingObject) {
334
-            return;
335
-        }
336
-        $instances = [];
337
-        $requestStatus = '2.0';
338
-
339
-        // Finding all the instances the attendee replied to.
340
-        foreach ($itipMessage->message->VEVENT as $vevent) {
341
-            $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
342
-            $attendee = $vevent->ATTENDEE;
343
-            $instances[$recurId] = $attendee['PARTSTAT']->getValue();
344
-            if (isset($vevent->{'REQUEST-STATUS'})) {
345
-                $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
346
-                list($requestStatus) = explode(';', $requestStatus);
347
-            }
348
-        }
349
-
350
-        // Now we need to loop through the original organizer event, to find
351
-        // all the instances where we have a reply for.
352
-        $masterObject = null;
353
-        foreach ($existingObject->VEVENT as $vevent) {
354
-            $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
355
-            if ('master' === $recurId) {
356
-                $masterObject = $vevent;
357
-            }
358
-            if (isset($instances[$recurId])) {
359
-                $attendeeFound = false;
360
-                if (isset($vevent->ATTENDEE)) {
361
-                    foreach ($vevent->ATTENDEE as $attendee) {
362
-                        if ($attendee->getValue() === $itipMessage->sender) {
363
-                            $attendeeFound = true;
364
-                            $attendee['PARTSTAT'] = $instances[$recurId];
365
-                            $attendee['SCHEDULE-STATUS'] = $requestStatus;
366
-                            // Un-setting the RSVP status, because we now know
367
-                            // that the attendee already replied.
368
-                            unset($attendee['RSVP']);
369
-                            break;
370
-                        }
371
-                    }
372
-                }
373
-                if (!$attendeeFound) {
374
-                    // Adding a new attendee. The iTip documentation calls this
375
-                    // a party crasher.
376
-                    $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [
377
-                        'PARTSTAT' => $instances[$recurId],
378
-                    ]);
379
-                    if ($itipMessage->senderName) {
380
-                        $attendee['CN'] = $itipMessage->senderName;
381
-                    }
382
-                }
383
-                unset($instances[$recurId]);
384
-            }
385
-        }
386
-
387
-        if (!$masterObject) {
388
-            // No master object, we can't add new instances.
389
-            return;
390
-        }
391
-        // If we got replies to instances that did not exist in the
392
-        // original list, it means that new exceptions must be created.
393
-        foreach ($instances as $recurId => $partstat) {
394
-            $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
395
-            $found = false;
396
-            $iterations = 1000;
397
-            do {
398
-                $newObject = $recurrenceIterator->getEventObject();
399
-                $recurrenceIterator->next();
400
-
401
-                if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
402
-                    $found = true;
403
-                }
404
-                --$iterations;
405
-            } while ($recurrenceIterator->valid() && !$found && $iterations);
406
-
407
-            // Invalid recurrence id. Skipping this object.
408
-            if (!$found) {
409
-                continue;
410
-            }
411
-
412
-            unset(
413
-                $newObject->RRULE,
414
-                $newObject->EXDATE,
415
-                $newObject->RDATE
416
-            );
417
-            $attendeeFound = false;
418
-            if (isset($newObject->ATTENDEE)) {
419
-                foreach ($newObject->ATTENDEE as $attendee) {
420
-                    if ($attendee->getValue() === $itipMessage->sender) {
421
-                        $attendeeFound = true;
422
-                        $attendee['PARTSTAT'] = $partstat;
423
-                        break;
424
-                    }
425
-                }
426
-            }
427
-            if (!$attendeeFound) {
428
-                // Adding a new attendee
429
-                $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [
430
-                    'PARTSTAT' => $partstat,
431
-                ]);
432
-                if ($itipMessage->senderName) {
433
-                    $attendee['CN'] = $itipMessage->senderName;
434
-                }
435
-            }
436
-            $existingObject->add($newObject);
437
-        }
438
-
439
-        return $existingObject;
440
-    }
441
-
442
-    /**
443
-     * This method is used in cases where an event got updated, and we
444
-     * potentially need to send emails to attendees to let them know of updates
445
-     * in the events.
446
-     *
447
-     * We will detect which attendees got added, which got removed and create
448
-     * specific messages for these situations.
449
-     *
450
-     * @return array
451
-     */
452
-    protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo)
453
-    {
454
-        // Merging attendee lists.
455
-        $attendees = [];
456
-        foreach ($oldEventInfo['attendees'] as $attendee) {
457
-            $attendees[$attendee['href']] = [
458
-                'href' => $attendee['href'],
459
-                'oldInstances' => $attendee['instances'],
460
-                'newInstances' => [],
461
-                'name' => $attendee['name'],
462
-                'forceSend' => null,
463
-            ];
464
-        }
465
-        foreach ($eventInfo['attendees'] as $attendee) {
466
-            if (isset($attendees[$attendee['href']])) {
467
-                $attendees[$attendee['href']]['name'] = $attendee['name'];
468
-                $attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
469
-                $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
470
-            } else {
471
-                $attendees[$attendee['href']] = [
472
-                    'href' => $attendee['href'],
473
-                    'oldInstances' => [],
474
-                    'newInstances' => $attendee['instances'],
475
-                    'name' => $attendee['name'],
476
-                    'forceSend' => $attendee['forceSend'],
477
-                ];
478
-            }
479
-        }
480
-
481
-        $messages = [];
482
-
483
-        foreach ($attendees as $attendee) {
484
-            // An organizer can also be an attendee. We should not generate any
485
-            // messages for those.
486
-            if ($attendee['href'] === $eventInfo['organizer']) {
487
-                continue;
488
-            }
489
-
490
-            $message = new Message();
491
-            $message->uid = $eventInfo['uid'];
492
-            $message->component = 'VEVENT';
493
-            $message->sequence = $eventInfo['sequence'];
494
-            $message->sender = $eventInfo['organizer'];
495
-            $message->senderName = $eventInfo['organizerName'];
496
-            $message->recipient = $attendee['href'];
497
-            $message->recipientName = $attendee['name'];
498
-
499
-            // Creating the new iCalendar body.
500
-            $icalMsg = new VCalendar();
501
-
502
-            foreach ($calendar->select('VTIMEZONE') as $timezone) {
503
-                $icalMsg->add(clone $timezone);
504
-            }
505
-
506
-            if (!$attendee['newInstances']) {
507
-                // If there are no instances the attendee is a part of, it
508
-                // means the attendee was removed and we need to send him a
509
-                // CANCEL.
510
-                $message->method = 'CANCEL';
511
-
512
-                $icalMsg->METHOD = $message->method;
513
-
514
-                $event = $icalMsg->add('VEVENT', [
515
-                    'UID' => $message->uid,
516
-                    'SEQUENCE' => $message->sequence,
517
-                    'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
518
-                ]);
519
-                if (isset($calendar->VEVENT->SUMMARY)) {
520
-                    $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue());
521
-                }
522
-                $event->add(clone $calendar->VEVENT->DTSTART);
523
-                if (isset($calendar->VEVENT->DTEND)) {
524
-                    $event->add(clone $calendar->VEVENT->DTEND);
525
-                } elseif (isset($calendar->VEVENT->DURATION)) {
526
-                    $event->add(clone $calendar->VEVENT->DURATION);
527
-                }
528
-                $org = $event->add('ORGANIZER', $eventInfo['organizer']);
529
-                if ($eventInfo['organizerName']) {
530
-                    $org['CN'] = $eventInfo['organizerName'];
531
-                }
532
-                $event->add('ATTENDEE', $attendee['href'], [
533
-                    'CN' => $attendee['name'],
534
-                ]);
535
-                $message->significantChange = true;
536
-            } else {
537
-                // The attendee gets the updated event body
538
-                $message->method = 'REQUEST';
539
-
540
-                $icalMsg->METHOD = $message->method;
541
-
542
-                // We need to find out that this change is significant. If it's
543
-                // not, systems may opt to not send messages.
544
-                //
545
-                // We do this based on the 'significantChangeHash' which is
546
-                // some value that changes if there's a certain set of
547
-                // properties changed in the event, or simply if there's a
548
-                // difference in instances that the attendee is invited to.
549
-
550
-                $oldAttendeeInstances = array_keys($attendee['oldInstances']);
551
-                $newAttendeeInstances = array_keys($attendee['newInstances']);
552
-
553
-                $message->significantChange =
554
-                    'REQUEST' === $attendee['forceSend'] ||
555
-                    count($oldAttendeeInstances) != count($newAttendeeInstances) ||
556
-                    count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 ||
557
-                    $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
558
-
559
-                foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
560
-                    $currentEvent = clone $eventInfo['instances'][$instanceId];
561
-                    if ('master' === $instanceId) {
562
-                        // We need to find a list of events that the attendee
563
-                        // is not a part of to add to the list of exceptions.
564
-                        $exceptions = [];
565
-                        foreach ($eventInfo['instances'] as $instanceId => $vevent) {
566
-                            if (!isset($attendee['newInstances'][$instanceId])) {
567
-                                $exceptions[] = $instanceId;
568
-                            }
569
-                        }
570
-
571
-                        // If there were exceptions, we need to add it to an
572
-                        // existing EXDATE property, if it exists.
573
-                        if ($exceptions) {
574
-                            if (isset($currentEvent->EXDATE)) {
575
-                                $currentEvent->EXDATE->setParts(array_merge(
576
-                                    $currentEvent->EXDATE->getParts(),
577
-                                    $exceptions
578
-                                ));
579
-                            } else {
580
-                                $currentEvent->EXDATE = $exceptions;
581
-                            }
582
-                        }
583
-
584
-                        // Cleaning up any scheduling information that
585
-                        // shouldn't be sent along.
586
-                        unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
587
-                        unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
588
-
589
-                        foreach ($currentEvent->ATTENDEE as $attendee) {
590
-                            unset($attendee['SCHEDULE-FORCE-SEND']);
591
-                            unset($attendee['SCHEDULE-STATUS']);
592
-
593
-                            // We're adding PARTSTAT=NEEDS-ACTION to ensure that
594
-                            // iOS shows an "Inbox Item"
595
-                            if (!isset($attendee['PARTSTAT'])) {
596
-                                $attendee['PARTSTAT'] = 'NEEDS-ACTION';
597
-                            }
598
-                        }
599
-                    }
600
-
601
-                    $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
602
-                    $icalMsg->add($currentEvent);
603
-                }
604
-            }
605
-
606
-            $message->message = $icalMsg;
607
-            $messages[] = $message;
608
-        }
609
-
610
-        return $messages;
611
-    }
612
-
613
-    /**
614
-     * Parse an event update for an attendee.
615
-     *
616
-     * This function figures out if we need to send a reply to an organizer.
617
-     *
618
-     * @param string $attendee
619
-     *
620
-     * @return Message[]
621
-     */
622
-    protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee)
623
-    {
624
-        if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) {
625
-            return [];
626
-        }
627
-
628
-        // Don't bother generating messages for events that have already been
629
-        // cancelled.
630
-        if ('CANCELLED' === $eventInfo['status']) {
631
-            return [];
632
-        }
633
-
634
-        $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ?
635
-            $oldEventInfo['attendees'][$attendee]['instances'] :
636
-            [];
637
-
638
-        $instances = [];
639
-        foreach ($oldInstances as $instance) {
640
-            $instances[$instance['id']] = [
641
-                'id' => $instance['id'],
642
-                'oldstatus' => $instance['partstat'],
643
-                'newstatus' => null,
644
-            ];
645
-        }
646
-        foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) {
647
-            if (isset($instances[$instance['id']])) {
648
-                $instances[$instance['id']]['newstatus'] = $instance['partstat'];
649
-            } else {
650
-                $instances[$instance['id']] = [
651
-                    'id' => $instance['id'],
652
-                    'oldstatus' => null,
653
-                    'newstatus' => $instance['partstat'],
654
-                ];
655
-            }
656
-        }
657
-
658
-        // We need to also look for differences in EXDATE. If there are new
659
-        // items in EXDATE, it means that an attendee deleted instances of an
660
-        // event, which means we need to send DECLINED specifically for those
661
-        // instances.
662
-        // We only need to do that though, if the master event is not declined.
663
-        if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
664
-            foreach ($eventInfo['exdate'] as $exDate) {
665
-                if (!in_array($exDate, $oldEventInfo['exdate'])) {
666
-                    if (isset($instances[$exDate])) {
667
-                        $instances[$exDate]['newstatus'] = 'DECLINED';
668
-                    } else {
669
-                        $instances[$exDate] = [
670
-                            'id' => $exDate,
671
-                            'oldstatus' => null,
672
-                            'newstatus' => 'DECLINED',
673
-                        ];
674
-                    }
675
-                }
676
-            }
677
-        }
678
-
679
-        // Gathering a few extra properties for each instance.
680
-        foreach ($instances as $recurId => $instanceInfo) {
681
-            if (isset($eventInfo['instances'][$recurId])) {
682
-                $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART;
683
-            } else {
684
-                $instances[$recurId]['dtstart'] = $recurId;
685
-            }
686
-        }
687
-
688
-        $message = new Message();
689
-        $message->uid = $eventInfo['uid'];
690
-        $message->method = 'REPLY';
691
-        $message->component = 'VEVENT';
692
-        $message->sequence = $eventInfo['sequence'];
693
-        $message->sender = $attendee;
694
-        $message->senderName = $eventInfo['attendees'][$attendee]['name'];
695
-        $message->recipient = $eventInfo['organizer'];
696
-        $message->recipientName = $eventInfo['organizerName'];
697
-
698
-        $icalMsg = new VCalendar();
699
-        $icalMsg->METHOD = 'REPLY';
700
-
701
-        foreach ($calendar->select('VTIMEZONE') as $timezone) {
702
-            $icalMsg->add(clone $timezone);
703
-        }
704
-
705
-        $hasReply = false;
706
-
707
-        foreach ($instances as $instance) {
708
-            if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) {
709
-                // Skip
710
-                continue;
711
-            }
712
-
713
-            $event = $icalMsg->add('VEVENT', [
714
-                'UID' => $message->uid,
715
-                'SEQUENCE' => $message->sequence,
716
-            ]);
717
-            $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : '';
718
-            // Adding properties from the correct source instance
719
-            if (isset($eventInfo['instances'][$instance['id']])) {
720
-                $instanceObj = $eventInfo['instances'][$instance['id']];
721
-                $event->add(clone $instanceObj->DTSTART);
722
-                if (isset($instanceObj->DTEND)) {
723
-                    $event->add(clone $instanceObj->DTEND);
724
-                } elseif (isset($instanceObj->DURATION)) {
725
-                    $event->add(clone $instanceObj->DURATION);
726
-                }
727
-                if (isset($instanceObj->SUMMARY)) {
728
-                    $event->add('SUMMARY', $instanceObj->SUMMARY->getValue());
729
-                } elseif ($summary) {
730
-                    $event->add('SUMMARY', $summary);
731
-                }
732
-            } else {
733
-                // This branch of the code is reached, when a reply is
734
-                // generated for an instance of a recurring event, through the
735
-                // fact that the instance has disappeared by showing up in
736
-                // EXDATE
737
-                $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
738
-                // Treat is as a DATE field
739
-                if (strlen($instance['id']) <= 8) {
740
-                    $event->add('DTSTART', $dt, ['VALUE' => 'DATE']);
741
-                } else {
742
-                    $event->add('DTSTART', $dt);
743
-                }
744
-                if ($summary) {
745
-                    $event->add('SUMMARY', $summary);
746
-                }
747
-            }
748
-            if ('master' !== $instance['id']) {
749
-                $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
750
-                // Treat is as a DATE field
751
-                if (strlen($instance['id']) <= 8) {
752
-                    $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']);
753
-                } else {
754
-                    $event->add('RECURRENCE-ID', $dt);
755
-                }
756
-            }
757
-            $organizer = $event->add('ORGANIZER', $message->recipient);
758
-            if ($message->recipientName) {
759
-                $organizer['CN'] = $message->recipientName;
760
-            }
761
-            $attendee = $event->add('ATTENDEE', $message->sender, [
762
-                'PARTSTAT' => $instance['newstatus'],
763
-            ]);
764
-            if ($message->senderName) {
765
-                $attendee['CN'] = $message->senderName;
766
-            }
767
-            $hasReply = true;
768
-        }
769
-
770
-        if ($hasReply) {
771
-            $message->message = $icalMsg;
772
-
773
-            return [$message];
774
-        } else {
775
-            return [];
776
-        }
777
-    }
778
-
779
-    /**
780
-     * Returns attendee information and information about instances of an
781
-     * event.
782
-     *
783
-     * Returns an array with the following keys:
784
-     *
785
-     * 1. uid
786
-     * 2. organizer
787
-     * 3. organizerName
788
-     * 4. organizerScheduleAgent
789
-     * 5. organizerForceSend
790
-     * 6. instances
791
-     * 7. attendees
792
-     * 8. sequence
793
-     * 9. exdate
794
-     * 10. timezone - strictly the timezone on which the recurrence rule is
795
-     *                based on.
796
-     * 11. significantChangeHash
797
-     * 12. status
798
-     *
799
-     * @param VCalendar $calendar
800
-     *
801
-     * @return array
802
-     */
803
-    protected function parseEventInfo(VCalendar $calendar = null)
804
-    {
805
-        $uid = null;
806
-        $organizer = null;
807
-        $organizerName = null;
808
-        $organizerForceSend = null;
809
-        $sequence = null;
810
-        $timezone = null;
811
-        $status = null;
812
-        $organizerScheduleAgent = 'SERVER';
813
-
814
-        $significantChangeHash = '';
815
-
816
-        // Now we need to collect a list of attendees, and which instances they
817
-        // are a part of.
818
-        $attendees = [];
819
-
820
-        $instances = [];
821
-        $exdate = [];
822
-
823
-        $significantChangeEventProperties = [];
824
-
825
-        foreach ($calendar->VEVENT as $vevent) {
826
-            $eventSignificantChangeHash = '';
827
-            $rrule = [];
828
-
829
-            if (is_null($uid)) {
830
-                $uid = $vevent->UID->getValue();
831
-            } else {
832
-                if ($uid !== $vevent->UID->getValue()) {
833
-                    throw new ITipException('If a calendar contained more than one event, they must have the same UID.');
834
-                }
835
-            }
836
-
837
-            if (!isset($vevent->DTSTART)) {
838
-                throw new ITipException('An event MUST have a DTSTART property.');
839
-            }
840
-
841
-            if (isset($vevent->ORGANIZER)) {
842
-                if (is_null($organizer)) {
843
-                    $organizer = $vevent->ORGANIZER->getNormalizedValue();
844
-                    $organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null;
845
-                } else {
846
-                    if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) {
847
-                        throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
848
-                    }
849
-                }
850
-                $organizerForceSend =
851
-                    isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ?
852
-                    strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) :
853
-                    null;
854
-                $organizerScheduleAgent =
855
-                    isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ?
856
-                    strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) :
857
-                    'SERVER';
858
-            }
859
-            if (is_null($sequence) && isset($vevent->SEQUENCE)) {
860
-                $sequence = $vevent->SEQUENCE->getValue();
861
-            }
862
-            if (isset($vevent->EXDATE)) {
863
-                foreach ($vevent->select('EXDATE') as $val) {
864
-                    $exdate = array_merge($exdate, $val->getParts());
865
-                }
866
-                sort($exdate);
867
-            }
868
-            if (isset($vevent->RRULE)) {
869
-                foreach ($vevent->select('RRULE') as $rr) {
870
-                    foreach ($rr->getParts() as $key => $val) {
871
-                        // ignore default values (https://github.com/sabre-io/vobject/issues/126)
872
-                        if ('INTERVAL' === $key && 1 == $val) {
873
-                            continue;
874
-                        }
875
-                        if (is_array($val)) {
876
-                            $val = implode(',', $val);
877
-                        }
878
-                        $rrule[] = "$key=$val";
879
-                    }
880
-                }
881
-                sort($rrule);
882
-            }
883
-            if (isset($vevent->STATUS)) {
884
-                $status = strtoupper($vevent->STATUS->getValue());
885
-            }
886
-
887
-            $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
888
-            if (is_null($timezone)) {
889
-                if ('master' === $recurId) {
890
-                    $timezone = $vevent->DTSTART->getDateTime()->getTimeZone();
891
-                } else {
892
-                    $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
893
-                }
894
-            }
895
-            if (isset($vevent->ATTENDEE)) {
896
-                foreach ($vevent->ATTENDEE as $attendee) {
897
-                    if ($this->scheduleAgentServerRules &&
898
-                        isset($attendee['SCHEDULE-AGENT']) &&
899
-                        'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue())
900
-                    ) {
901
-                        continue;
902
-                    }
903
-                    $partStat =
904
-                        isset($attendee['PARTSTAT']) ?
905
-                        strtoupper($attendee['PARTSTAT']) :
906
-                        'NEEDS-ACTION';
907
-
908
-                    $forceSend =
909
-                        isset($attendee['SCHEDULE-FORCE-SEND']) ?
910
-                        strtoupper($attendee['SCHEDULE-FORCE-SEND']) :
911
-                        null;
912
-
913
-                    if (isset($attendees[$attendee->getNormalizedValue()])) {
914
-                        $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [
915
-                            'id' => $recurId,
916
-                            'partstat' => $partStat,
917
-                            'forceSend' => $forceSend,
918
-                        ];
919
-                    } else {
920
-                        $attendees[$attendee->getNormalizedValue()] = [
921
-                            'href' => $attendee->getNormalizedValue(),
922
-                            'instances' => [
923
-                                $recurId => [
924
-                                    'id' => $recurId,
925
-                                    'partstat' => $partStat,
926
-                                ],
927
-                            ],
928
-                            'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null,
929
-                            'forceSend' => $forceSend,
930
-                        ];
931
-                    }
932
-                }
933
-                $instances[$recurId] = $vevent;
934
-            }
935
-
936
-            foreach ($this->significantChangeProperties as $prop) {
937
-                if (isset($vevent->$prop)) {
938
-                    $propertyValues = $vevent->select($prop);
939
-
940
-                    $eventSignificantChangeHash .= $prop.':';
941
-
942
-                    if ('EXDATE' === $prop) {
943
-                        $eventSignificantChangeHash .= implode(',', $exdate).';';
944
-                    } elseif ('RRULE' === $prop) {
945
-                        $eventSignificantChangeHash .= implode(',', $rrule).';';
946
-                    } else {
947
-                        foreach ($propertyValues as $val) {
948
-                            $eventSignificantChangeHash .= $val->getValue().';';
949
-                        }
950
-                    }
951
-                }
952
-            }
953
-            $significantChangeEventProperties[] = $eventSignificantChangeHash;
954
-        }
955
-
956
-        asort($significantChangeEventProperties);
957
-
958
-        foreach ($significantChangeEventProperties as $eventSignificantChangeHash) {
959
-            $significantChangeHash .= $eventSignificantChangeHash;
960
-        }
961
-        $significantChangeHash = md5($significantChangeHash);
962
-
963
-        return compact(
964
-            'uid',
965
-            'organizer',
966
-            'organizerName',
967
-            'organizerScheduleAgent',
968
-            'organizerForceSend',
969
-            'instances',
970
-            'attendees',
971
-            'sequence',
972
-            'exdate',
973
-            'timezone',
974
-            'significantChangeHash',
975
-            'status'
976
-        );
977
-    }
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 = [
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 VCalendar $existingObject
108
+	 *
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 ('VEVENT' !== $itipMessage->component) {
115
+			return false;
116
+		}
117
+
118
+		switch ($itipMessage->method) {
119
+			case 'REQUEST':
120
+				return $this->processMessageRequest($itipMessage, $existingObject);
121
+
122
+			case 'CANCEL':
123
+				return $this->processMessageCancel($itipMessage, $existingObject);
124
+
125
+			case 'REPLY':
126
+				return $this->processMessageReply($itipMessage, $existingObject);
127
+
128
+			default:
129
+				// Unsupported iTip message
130
+				return;
131
+		}
132
+
133
+		return $existingObject;
134
+	}
135
+
136
+	/**
137
+	 * This function parses a VCALENDAR object and figure out if any messages
138
+	 * need to be sent.
139
+	 *
140
+	 * A VCALENDAR object will be created from the perspective of either an
141
+	 * attendee, or an organizer. You must pass a string identifying the
142
+	 * current user, so we can figure out who in the list of attendees or the
143
+	 * organizer we are sending this message on behalf of.
144
+	 *
145
+	 * It's possible to specify the current user as an array, in case the user
146
+	 * has more than one identifying href (such as multiple emails).
147
+	 *
148
+	 * It $oldCalendar is specified, it is assumed that the operation is
149
+	 * updating an existing event, which means that we need to look at the
150
+	 * differences between events, and potentially send old attendees
151
+	 * cancellations, and current attendees updates.
152
+	 *
153
+	 * If $calendar is null, but $oldCalendar is specified, we treat the
154
+	 * operation as if the user has deleted an event. If the user was an
155
+	 * organizer, this means that we need to send cancellation notices to
156
+	 * people. If the user was an attendee, we need to make sure that the
157
+	 * organizer gets the 'declined' message.
158
+	 *
159
+	 * @param VCalendar|string      $calendar
160
+	 * @param string|array          $userHref
161
+	 * @param VCalendar|string|null $oldCalendar
162
+	 *
163
+	 * @return array
164
+	 */
165
+	public function parseEvent($calendar, $userHref, $oldCalendar = null)
166
+	{
167
+		if ($oldCalendar) {
168
+			if (is_string($oldCalendar)) {
169
+				$oldCalendar = Reader::read($oldCalendar);
170
+			}
171
+			if (!isset($oldCalendar->VEVENT)) {
172
+				// We only support events at the moment
173
+				return [];
174
+			}
175
+
176
+			$oldEventInfo = $this->parseEventInfo($oldCalendar);
177
+		} else {
178
+			$oldEventInfo = [
179
+				'organizer' => null,
180
+				'significantChangeHash' => '',
181
+				'attendees' => [],
182
+			];
183
+		}
184
+
185
+		$userHref = (array) $userHref;
186
+
187
+		if (!is_null($calendar)) {
188
+			if (is_string($calendar)) {
189
+				$calendar = Reader::read($calendar);
190
+			}
191
+			if (!isset($calendar->VEVENT)) {
192
+				// We only support events at the moment
193
+				return [];
194
+			}
195
+			$eventInfo = $this->parseEventInfo($calendar);
196
+			if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) {
197
+				// If there were no attendees on either side of the equation,
198
+				// we don't need to do anything.
199
+				return [];
200
+			}
201
+			if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) {
202
+				// There was no organizer before or after the change.
203
+				return [];
204
+			}
205
+
206
+			$baseCalendar = $calendar;
207
+
208
+			// If the new object didn't have an organizer, the organizer
209
+			// changed the object from a scheduling object to a non-scheduling
210
+			// object. We just copy the info from the old object.
211
+			if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) {
212
+				$eventInfo['organizer'] = $oldEventInfo['organizer'];
213
+				$eventInfo['organizerName'] = $oldEventInfo['organizerName'];
214
+			}
215
+		} else {
216
+			// The calendar object got deleted, we need to process this as a
217
+			// cancellation / decline.
218
+			if (!$oldCalendar) {
219
+				// No old and no new calendar, there's no thing to do.
220
+				return [];
221
+			}
222
+
223
+			$eventInfo = $oldEventInfo;
224
+
225
+			if (in_array($eventInfo['organizer'], $userHref)) {
226
+				// This is an organizer deleting the event.
227
+				$eventInfo['attendees'] = [];
228
+				// Increasing the sequence, but only if the organizer deleted
229
+				// the event.
230
+				++$eventInfo['sequence'];
231
+			} else {
232
+				// This is an attendee deleting the event.
233
+				foreach ($eventInfo['attendees'] as $key => $attendee) {
234
+					if (in_array($attendee['href'], $userHref)) {
235
+						$eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'],
236
+						];
237
+					}
238
+				}
239
+			}
240
+			$baseCalendar = $oldCalendar;
241
+		}
242
+
243
+		if (in_array($eventInfo['organizer'], $userHref)) {
244
+			return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo);
245
+		} elseif ($oldCalendar) {
246
+			// We need to figure out if the user is an attendee, but we're only
247
+			// doing so if there's an oldCalendar, because we only want to
248
+			// process updates, not creation of new events.
249
+			foreach ($eventInfo['attendees'] as $attendee) {
250
+				if (in_array($attendee['href'], $userHref)) {
251
+					return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
252
+				}
253
+			}
254
+		}
255
+
256
+		return [];
257
+	}
258
+
259
+	/**
260
+	 * Processes incoming REQUEST messages.
261
+	 *
262
+	 * This is message from an organizer, and is either a new event
263
+	 * invite, or an update to an existing one.
264
+	 *
265
+	 * @param VCalendar $existingObject
266
+	 *
267
+	 * @return VCalendar|null
268
+	 */
269
+	protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null)
270
+	{
271
+		if (!$existingObject) {
272
+			// This is a new invite, and we're just going to copy over
273
+			// all the components from the invite.
274
+			$existingObject = new VCalendar();
275
+			foreach ($itipMessage->message->getComponents() as $component) {
276
+				$existingObject->add(clone $component);
277
+			}
278
+		} else {
279
+			// We need to update an existing object with all the new
280
+			// information. We can just remove all existing components
281
+			// and create new ones.
282
+			foreach ($existingObject->getComponents() as $component) {
283
+				$existingObject->remove($component);
284
+			}
285
+			foreach ($itipMessage->message->getComponents() as $component) {
286
+				$existingObject->add(clone $component);
287
+			}
288
+		}
289
+
290
+		return $existingObject;
291
+	}
292
+
293
+	/**
294
+	 * Processes incoming CANCEL messages.
295
+	 *
296
+	 * This is a message from an organizer, and means that either an
297
+	 * attendee got removed from an event, or an event got cancelled
298
+	 * altogether.
299
+	 *
300
+	 * @param VCalendar $existingObject
301
+	 *
302
+	 * @return VCalendar|null
303
+	 */
304
+	protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null)
305
+	{
306
+		if (!$existingObject) {
307
+			// The event didn't exist in the first place, so we're just
308
+			// ignoring this message.
309
+		} else {
310
+			foreach ($existingObject->VEVENT as $vevent) {
311
+				$vevent->STATUS = 'CANCELLED';
312
+				$vevent->SEQUENCE = $itipMessage->sequence;
313
+			}
314
+		}
315
+
316
+		return $existingObject;
317
+	}
318
+
319
+	/**
320
+	 * Processes incoming REPLY messages.
321
+	 *
322
+	 * The message is a reply. This is for example an attendee telling
323
+	 * an organizer he accepted the invite, or declined it.
324
+	 *
325
+	 * @param VCalendar $existingObject
326
+	 *
327
+	 * @return VCalendar|null
328
+	 */
329
+	protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null)
330
+	{
331
+		// A reply can only be processed based on an existing object.
332
+		// If the object is not available, the reply is ignored.
333
+		if (!$existingObject) {
334
+			return;
335
+		}
336
+		$instances = [];
337
+		$requestStatus = '2.0';
338
+
339
+		// Finding all the instances the attendee replied to.
340
+		foreach ($itipMessage->message->VEVENT as $vevent) {
341
+			$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
342
+			$attendee = $vevent->ATTENDEE;
343
+			$instances[$recurId] = $attendee['PARTSTAT']->getValue();
344
+			if (isset($vevent->{'REQUEST-STATUS'})) {
345
+				$requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
346
+				list($requestStatus) = explode(';', $requestStatus);
347
+			}
348
+		}
349
+
350
+		// Now we need to loop through the original organizer event, to find
351
+		// all the instances where we have a reply for.
352
+		$masterObject = null;
353
+		foreach ($existingObject->VEVENT as $vevent) {
354
+			$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
355
+			if ('master' === $recurId) {
356
+				$masterObject = $vevent;
357
+			}
358
+			if (isset($instances[$recurId])) {
359
+				$attendeeFound = false;
360
+				if (isset($vevent->ATTENDEE)) {
361
+					foreach ($vevent->ATTENDEE as $attendee) {
362
+						if ($attendee->getValue() === $itipMessage->sender) {
363
+							$attendeeFound = true;
364
+							$attendee['PARTSTAT'] = $instances[$recurId];
365
+							$attendee['SCHEDULE-STATUS'] = $requestStatus;
366
+							// Un-setting the RSVP status, because we now know
367
+							// that the attendee already replied.
368
+							unset($attendee['RSVP']);
369
+							break;
370
+						}
371
+					}
372
+				}
373
+				if (!$attendeeFound) {
374
+					// Adding a new attendee. The iTip documentation calls this
375
+					// a party crasher.
376
+					$attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [
377
+						'PARTSTAT' => $instances[$recurId],
378
+					]);
379
+					if ($itipMessage->senderName) {
380
+						$attendee['CN'] = $itipMessage->senderName;
381
+					}
382
+				}
383
+				unset($instances[$recurId]);
384
+			}
385
+		}
386
+
387
+		if (!$masterObject) {
388
+			// No master object, we can't add new instances.
389
+			return;
390
+		}
391
+		// If we got replies to instances that did not exist in the
392
+		// original list, it means that new exceptions must be created.
393
+		foreach ($instances as $recurId => $partstat) {
394
+			$recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
395
+			$found = false;
396
+			$iterations = 1000;
397
+			do {
398
+				$newObject = $recurrenceIterator->getEventObject();
399
+				$recurrenceIterator->next();
400
+
401
+				if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
402
+					$found = true;
403
+				}
404
+				--$iterations;
405
+			} while ($recurrenceIterator->valid() && !$found && $iterations);
406
+
407
+			// Invalid recurrence id. Skipping this object.
408
+			if (!$found) {
409
+				continue;
410
+			}
411
+
412
+			unset(
413
+				$newObject->RRULE,
414
+				$newObject->EXDATE,
415
+				$newObject->RDATE
416
+			);
417
+			$attendeeFound = false;
418
+			if (isset($newObject->ATTENDEE)) {
419
+				foreach ($newObject->ATTENDEE as $attendee) {
420
+					if ($attendee->getValue() === $itipMessage->sender) {
421
+						$attendeeFound = true;
422
+						$attendee['PARTSTAT'] = $partstat;
423
+						break;
424
+					}
425
+				}
426
+			}
427
+			if (!$attendeeFound) {
428
+				// Adding a new attendee
429
+				$attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [
430
+					'PARTSTAT' => $partstat,
431
+				]);
432
+				if ($itipMessage->senderName) {
433
+					$attendee['CN'] = $itipMessage->senderName;
434
+				}
435
+			}
436
+			$existingObject->add($newObject);
437
+		}
438
+
439
+		return $existingObject;
440
+	}
441
+
442
+	/**
443
+	 * This method is used in cases where an event got updated, and we
444
+	 * potentially need to send emails to attendees to let them know of updates
445
+	 * in the events.
446
+	 *
447
+	 * We will detect which attendees got added, which got removed and create
448
+	 * specific messages for these situations.
449
+	 *
450
+	 * @return array
451
+	 */
452
+	protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo)
453
+	{
454
+		// Merging attendee lists.
455
+		$attendees = [];
456
+		foreach ($oldEventInfo['attendees'] as $attendee) {
457
+			$attendees[$attendee['href']] = [
458
+				'href' => $attendee['href'],
459
+				'oldInstances' => $attendee['instances'],
460
+				'newInstances' => [],
461
+				'name' => $attendee['name'],
462
+				'forceSend' => null,
463
+			];
464
+		}
465
+		foreach ($eventInfo['attendees'] as $attendee) {
466
+			if (isset($attendees[$attendee['href']])) {
467
+				$attendees[$attendee['href']]['name'] = $attendee['name'];
468
+				$attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
469
+				$attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
470
+			} else {
471
+				$attendees[$attendee['href']] = [
472
+					'href' => $attendee['href'],
473
+					'oldInstances' => [],
474
+					'newInstances' => $attendee['instances'],
475
+					'name' => $attendee['name'],
476
+					'forceSend' => $attendee['forceSend'],
477
+				];
478
+			}
479
+		}
480
+
481
+		$messages = [];
482
+
483
+		foreach ($attendees as $attendee) {
484
+			// An organizer can also be an attendee. We should not generate any
485
+			// messages for those.
486
+			if ($attendee['href'] === $eventInfo['organizer']) {
487
+				continue;
488
+			}
489
+
490
+			$message = new Message();
491
+			$message->uid = $eventInfo['uid'];
492
+			$message->component = 'VEVENT';
493
+			$message->sequence = $eventInfo['sequence'];
494
+			$message->sender = $eventInfo['organizer'];
495
+			$message->senderName = $eventInfo['organizerName'];
496
+			$message->recipient = $attendee['href'];
497
+			$message->recipientName = $attendee['name'];
498
+
499
+			// Creating the new iCalendar body.
500
+			$icalMsg = new VCalendar();
501
+
502
+			foreach ($calendar->select('VTIMEZONE') as $timezone) {
503
+				$icalMsg->add(clone $timezone);
504
+			}
505
+
506
+			if (!$attendee['newInstances']) {
507
+				// If there are no instances the attendee is a part of, it
508
+				// means the attendee was removed and we need to send him a
509
+				// CANCEL.
510
+				$message->method = 'CANCEL';
511
+
512
+				$icalMsg->METHOD = $message->method;
513
+
514
+				$event = $icalMsg->add('VEVENT', [
515
+					'UID' => $message->uid,
516
+					'SEQUENCE' => $message->sequence,
517
+					'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
518
+				]);
519
+				if (isset($calendar->VEVENT->SUMMARY)) {
520
+					$event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue());
521
+				}
522
+				$event->add(clone $calendar->VEVENT->DTSTART);
523
+				if (isset($calendar->VEVENT->DTEND)) {
524
+					$event->add(clone $calendar->VEVENT->DTEND);
525
+				} elseif (isset($calendar->VEVENT->DURATION)) {
526
+					$event->add(clone $calendar->VEVENT->DURATION);
527
+				}
528
+				$org = $event->add('ORGANIZER', $eventInfo['organizer']);
529
+				if ($eventInfo['organizerName']) {
530
+					$org['CN'] = $eventInfo['organizerName'];
531
+				}
532
+				$event->add('ATTENDEE', $attendee['href'], [
533
+					'CN' => $attendee['name'],
534
+				]);
535
+				$message->significantChange = true;
536
+			} else {
537
+				// The attendee gets the updated event body
538
+				$message->method = 'REQUEST';
539
+
540
+				$icalMsg->METHOD = $message->method;
541
+
542
+				// We need to find out that this change is significant. If it's
543
+				// not, systems may opt to not send messages.
544
+				//
545
+				// We do this based on the 'significantChangeHash' which is
546
+				// some value that changes if there's a certain set of
547
+				// properties changed in the event, or simply if there's a
548
+				// difference in instances that the attendee is invited to.
549
+
550
+				$oldAttendeeInstances = array_keys($attendee['oldInstances']);
551
+				$newAttendeeInstances = array_keys($attendee['newInstances']);
552
+
553
+				$message->significantChange =
554
+					'REQUEST' === $attendee['forceSend'] ||
555
+					count($oldAttendeeInstances) != count($newAttendeeInstances) ||
556
+					count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 ||
557
+					$oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
558
+
559
+				foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
560
+					$currentEvent = clone $eventInfo['instances'][$instanceId];
561
+					if ('master' === $instanceId) {
562
+						// We need to find a list of events that the attendee
563
+						// is not a part of to add to the list of exceptions.
564
+						$exceptions = [];
565
+						foreach ($eventInfo['instances'] as $instanceId => $vevent) {
566
+							if (!isset($attendee['newInstances'][$instanceId])) {
567
+								$exceptions[] = $instanceId;
568
+							}
569
+						}
570
+
571
+						// If there were exceptions, we need to add it to an
572
+						// existing EXDATE property, if it exists.
573
+						if ($exceptions) {
574
+							if (isset($currentEvent->EXDATE)) {
575
+								$currentEvent->EXDATE->setParts(array_merge(
576
+									$currentEvent->EXDATE->getParts(),
577
+									$exceptions
578
+								));
579
+							} else {
580
+								$currentEvent->EXDATE = $exceptions;
581
+							}
582
+						}
583
+
584
+						// Cleaning up any scheduling information that
585
+						// shouldn't be sent along.
586
+						unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
587
+						unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
588
+
589
+						foreach ($currentEvent->ATTENDEE as $attendee) {
590
+							unset($attendee['SCHEDULE-FORCE-SEND']);
591
+							unset($attendee['SCHEDULE-STATUS']);
592
+
593
+							// We're adding PARTSTAT=NEEDS-ACTION to ensure that
594
+							// iOS shows an "Inbox Item"
595
+							if (!isset($attendee['PARTSTAT'])) {
596
+								$attendee['PARTSTAT'] = 'NEEDS-ACTION';
597
+							}
598
+						}
599
+					}
600
+
601
+					$currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
602
+					$icalMsg->add($currentEvent);
603
+				}
604
+			}
605
+
606
+			$message->message = $icalMsg;
607
+			$messages[] = $message;
608
+		}
609
+
610
+		return $messages;
611
+	}
612
+
613
+	/**
614
+	 * Parse an event update for an attendee.
615
+	 *
616
+	 * This function figures out if we need to send a reply to an organizer.
617
+	 *
618
+	 * @param string $attendee
619
+	 *
620
+	 * @return Message[]
621
+	 */
622
+	protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee)
623
+	{
624
+		if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) {
625
+			return [];
626
+		}
627
+
628
+		// Don't bother generating messages for events that have already been
629
+		// cancelled.
630
+		if ('CANCELLED' === $eventInfo['status']) {
631
+			return [];
632
+		}
633
+
634
+		$oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ?
635
+			$oldEventInfo['attendees'][$attendee]['instances'] :
636
+			[];
637
+
638
+		$instances = [];
639
+		foreach ($oldInstances as $instance) {
640
+			$instances[$instance['id']] = [
641
+				'id' => $instance['id'],
642
+				'oldstatus' => $instance['partstat'],
643
+				'newstatus' => null,
644
+			];
645
+		}
646
+		foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) {
647
+			if (isset($instances[$instance['id']])) {
648
+				$instances[$instance['id']]['newstatus'] = $instance['partstat'];
649
+			} else {
650
+				$instances[$instance['id']] = [
651
+					'id' => $instance['id'],
652
+					'oldstatus' => null,
653
+					'newstatus' => $instance['partstat'],
654
+				];
655
+			}
656
+		}
657
+
658
+		// We need to also look for differences in EXDATE. If there are new
659
+		// items in EXDATE, it means that an attendee deleted instances of an
660
+		// event, which means we need to send DECLINED specifically for those
661
+		// instances.
662
+		// We only need to do that though, if the master event is not declined.
663
+		if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
664
+			foreach ($eventInfo['exdate'] as $exDate) {
665
+				if (!in_array($exDate, $oldEventInfo['exdate'])) {
666
+					if (isset($instances[$exDate])) {
667
+						$instances[$exDate]['newstatus'] = 'DECLINED';
668
+					} else {
669
+						$instances[$exDate] = [
670
+							'id' => $exDate,
671
+							'oldstatus' => null,
672
+							'newstatus' => 'DECLINED',
673
+						];
674
+					}
675
+				}
676
+			}
677
+		}
678
+
679
+		// Gathering a few extra properties for each instance.
680
+		foreach ($instances as $recurId => $instanceInfo) {
681
+			if (isset($eventInfo['instances'][$recurId])) {
682
+				$instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART;
683
+			} else {
684
+				$instances[$recurId]['dtstart'] = $recurId;
685
+			}
686
+		}
687
+
688
+		$message = new Message();
689
+		$message->uid = $eventInfo['uid'];
690
+		$message->method = 'REPLY';
691
+		$message->component = 'VEVENT';
692
+		$message->sequence = $eventInfo['sequence'];
693
+		$message->sender = $attendee;
694
+		$message->senderName = $eventInfo['attendees'][$attendee]['name'];
695
+		$message->recipient = $eventInfo['organizer'];
696
+		$message->recipientName = $eventInfo['organizerName'];
697
+
698
+		$icalMsg = new VCalendar();
699
+		$icalMsg->METHOD = 'REPLY';
700
+
701
+		foreach ($calendar->select('VTIMEZONE') as $timezone) {
702
+			$icalMsg->add(clone $timezone);
703
+		}
704
+
705
+		$hasReply = false;
706
+
707
+		foreach ($instances as $instance) {
708
+			if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) {
709
+				// Skip
710
+				continue;
711
+			}
712
+
713
+			$event = $icalMsg->add('VEVENT', [
714
+				'UID' => $message->uid,
715
+				'SEQUENCE' => $message->sequence,
716
+			]);
717
+			$summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : '';
718
+			// Adding properties from the correct source instance
719
+			if (isset($eventInfo['instances'][$instance['id']])) {
720
+				$instanceObj = $eventInfo['instances'][$instance['id']];
721
+				$event->add(clone $instanceObj->DTSTART);
722
+				if (isset($instanceObj->DTEND)) {
723
+					$event->add(clone $instanceObj->DTEND);
724
+				} elseif (isset($instanceObj->DURATION)) {
725
+					$event->add(clone $instanceObj->DURATION);
726
+				}
727
+				if (isset($instanceObj->SUMMARY)) {
728
+					$event->add('SUMMARY', $instanceObj->SUMMARY->getValue());
729
+				} elseif ($summary) {
730
+					$event->add('SUMMARY', $summary);
731
+				}
732
+			} else {
733
+				// This branch of the code is reached, when a reply is
734
+				// generated for an instance of a recurring event, through the
735
+				// fact that the instance has disappeared by showing up in
736
+				// EXDATE
737
+				$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
738
+				// Treat is as a DATE field
739
+				if (strlen($instance['id']) <= 8) {
740
+					$event->add('DTSTART', $dt, ['VALUE' => 'DATE']);
741
+				} else {
742
+					$event->add('DTSTART', $dt);
743
+				}
744
+				if ($summary) {
745
+					$event->add('SUMMARY', $summary);
746
+				}
747
+			}
748
+			if ('master' !== $instance['id']) {
749
+				$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
750
+				// Treat is as a DATE field
751
+				if (strlen($instance['id']) <= 8) {
752
+					$event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']);
753
+				} else {
754
+					$event->add('RECURRENCE-ID', $dt);
755
+				}
756
+			}
757
+			$organizer = $event->add('ORGANIZER', $message->recipient);
758
+			if ($message->recipientName) {
759
+				$organizer['CN'] = $message->recipientName;
760
+			}
761
+			$attendee = $event->add('ATTENDEE', $message->sender, [
762
+				'PARTSTAT' => $instance['newstatus'],
763
+			]);
764
+			if ($message->senderName) {
765
+				$attendee['CN'] = $message->senderName;
766
+			}
767
+			$hasReply = true;
768
+		}
769
+
770
+		if ($hasReply) {
771
+			$message->message = $icalMsg;
772
+
773
+			return [$message];
774
+		} else {
775
+			return [];
776
+		}
777
+	}
778
+
779
+	/**
780
+	 * Returns attendee information and information about instances of an
781
+	 * event.
782
+	 *
783
+	 * Returns an array with the following keys:
784
+	 *
785
+	 * 1. uid
786
+	 * 2. organizer
787
+	 * 3. organizerName
788
+	 * 4. organizerScheduleAgent
789
+	 * 5. organizerForceSend
790
+	 * 6. instances
791
+	 * 7. attendees
792
+	 * 8. sequence
793
+	 * 9. exdate
794
+	 * 10. timezone - strictly the timezone on which the recurrence rule is
795
+	 *                based on.
796
+	 * 11. significantChangeHash
797
+	 * 12. status
798
+	 *
799
+	 * @param VCalendar $calendar
800
+	 *
801
+	 * @return array
802
+	 */
803
+	protected function parseEventInfo(VCalendar $calendar = null)
804
+	{
805
+		$uid = null;
806
+		$organizer = null;
807
+		$organizerName = null;
808
+		$organizerForceSend = null;
809
+		$sequence = null;
810
+		$timezone = null;
811
+		$status = null;
812
+		$organizerScheduleAgent = 'SERVER';
813
+
814
+		$significantChangeHash = '';
815
+
816
+		// Now we need to collect a list of attendees, and which instances they
817
+		// are a part of.
818
+		$attendees = [];
819
+
820
+		$instances = [];
821
+		$exdate = [];
822
+
823
+		$significantChangeEventProperties = [];
824
+
825
+		foreach ($calendar->VEVENT as $vevent) {
826
+			$eventSignificantChangeHash = '';
827
+			$rrule = [];
828
+
829
+			if (is_null($uid)) {
830
+				$uid = $vevent->UID->getValue();
831
+			} else {
832
+				if ($uid !== $vevent->UID->getValue()) {
833
+					throw new ITipException('If a calendar contained more than one event, they must have the same UID.');
834
+				}
835
+			}
836
+
837
+			if (!isset($vevent->DTSTART)) {
838
+				throw new ITipException('An event MUST have a DTSTART property.');
839
+			}
840
+
841
+			if (isset($vevent->ORGANIZER)) {
842
+				if (is_null($organizer)) {
843
+					$organizer = $vevent->ORGANIZER->getNormalizedValue();
844
+					$organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null;
845
+				} else {
846
+					if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) {
847
+						throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
848
+					}
849
+				}
850
+				$organizerForceSend =
851
+					isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ?
852
+					strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) :
853
+					null;
854
+				$organizerScheduleAgent =
855
+					isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ?
856
+					strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) :
857
+					'SERVER';
858
+			}
859
+			if (is_null($sequence) && isset($vevent->SEQUENCE)) {
860
+				$sequence = $vevent->SEQUENCE->getValue();
861
+			}
862
+			if (isset($vevent->EXDATE)) {
863
+				foreach ($vevent->select('EXDATE') as $val) {
864
+					$exdate = array_merge($exdate, $val->getParts());
865
+				}
866
+				sort($exdate);
867
+			}
868
+			if (isset($vevent->RRULE)) {
869
+				foreach ($vevent->select('RRULE') as $rr) {
870
+					foreach ($rr->getParts() as $key => $val) {
871
+						// ignore default values (https://github.com/sabre-io/vobject/issues/126)
872
+						if ('INTERVAL' === $key && 1 == $val) {
873
+							continue;
874
+						}
875
+						if (is_array($val)) {
876
+							$val = implode(',', $val);
877
+						}
878
+						$rrule[] = "$key=$val";
879
+					}
880
+				}
881
+				sort($rrule);
882
+			}
883
+			if (isset($vevent->STATUS)) {
884
+				$status = strtoupper($vevent->STATUS->getValue());
885
+			}
886
+
887
+			$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
888
+			if (is_null($timezone)) {
889
+				if ('master' === $recurId) {
890
+					$timezone = $vevent->DTSTART->getDateTime()->getTimeZone();
891
+				} else {
892
+					$timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
893
+				}
894
+			}
895
+			if (isset($vevent->ATTENDEE)) {
896
+				foreach ($vevent->ATTENDEE as $attendee) {
897
+					if ($this->scheduleAgentServerRules &&
898
+						isset($attendee['SCHEDULE-AGENT']) &&
899
+						'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue())
900
+					) {
901
+						continue;
902
+					}
903
+					$partStat =
904
+						isset($attendee['PARTSTAT']) ?
905
+						strtoupper($attendee['PARTSTAT']) :
906
+						'NEEDS-ACTION';
907
+
908
+					$forceSend =
909
+						isset($attendee['SCHEDULE-FORCE-SEND']) ?
910
+						strtoupper($attendee['SCHEDULE-FORCE-SEND']) :
911
+						null;
912
+
913
+					if (isset($attendees[$attendee->getNormalizedValue()])) {
914
+						$attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [
915
+							'id' => $recurId,
916
+							'partstat' => $partStat,
917
+							'forceSend' => $forceSend,
918
+						];
919
+					} else {
920
+						$attendees[$attendee->getNormalizedValue()] = [
921
+							'href' => $attendee->getNormalizedValue(),
922
+							'instances' => [
923
+								$recurId => [
924
+									'id' => $recurId,
925
+									'partstat' => $partStat,
926
+								],
927
+							],
928
+							'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null,
929
+							'forceSend' => $forceSend,
930
+						];
931
+					}
932
+				}
933
+				$instances[$recurId] = $vevent;
934
+			}
935
+
936
+			foreach ($this->significantChangeProperties as $prop) {
937
+				if (isset($vevent->$prop)) {
938
+					$propertyValues = $vevent->select($prop);
939
+
940
+					$eventSignificantChangeHash .= $prop.':';
941
+
942
+					if ('EXDATE' === $prop) {
943
+						$eventSignificantChangeHash .= implode(',', $exdate).';';
944
+					} elseif ('RRULE' === $prop) {
945
+						$eventSignificantChangeHash .= implode(',', $rrule).';';
946
+					} else {
947
+						foreach ($propertyValues as $val) {
948
+							$eventSignificantChangeHash .= $val->getValue().';';
949
+						}
950
+					}
951
+				}
952
+			}
953
+			$significantChangeEventProperties[] = $eventSignificantChangeHash;
954
+		}
955
+
956
+		asort($significantChangeEventProperties);
957
+
958
+		foreach ($significantChangeEventProperties as $eventSignificantChangeHash) {
959
+			$significantChangeHash .= $eventSignificantChangeHash;
960
+		}
961
+		$significantChangeHash = md5($significantChangeHash);
962
+
963
+		return compact(
964
+			'uid',
965
+			'organizer',
966
+			'organizerName',
967
+			'organizerScheduleAgent',
968
+			'organizerForceSend',
969
+			'instances',
970
+			'attendees',
971
+			'sequence',
972
+			'exdate',
973
+			'timezone',
974
+			'significantChangeHash',
975
+			'status'
976
+		);
977
+	}
978 978
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/ITip/Message.php 1 patch
Indentation   +105 added lines, -105 removed lines patch added patch discarded remove patch
@@ -16,121 +16,121 @@
 block discarded – undo
16 16
  */
17 17
 class Message
18 18
 {
19
-    /**
20
-     * The object's UID.
21
-     *
22
-     * @var string
23
-     */
24
-    public $uid;
19
+	/**
20
+	 * The object's UID.
21
+	 *
22
+	 * @var string
23
+	 */
24
+	public $uid;
25 25
 
26
-    /**
27
-     * The component type, such as VEVENT.
28
-     *
29
-     * @var string
30
-     */
31
-    public $component;
26
+	/**
27
+	 * The component type, such as VEVENT.
28
+	 *
29
+	 * @var string
30
+	 */
31
+	public $component;
32 32
 
33
-    /**
34
-     * Contains the ITip method, which is something like REQUEST, REPLY or
35
-     * CANCEL.
36
-     *
37
-     * @var string
38
-     */
39
-    public $method;
33
+	/**
34
+	 * Contains the ITip method, which is something like REQUEST, REPLY or
35
+	 * CANCEL.
36
+	 *
37
+	 * @var string
38
+	 */
39
+	public $method;
40 40
 
41
-    /**
42
-     * The current sequence number for the event.
43
-     *
44
-     * @var int
45
-     */
46
-    public $sequence;
41
+	/**
42
+	 * The current sequence number for the event.
43
+	 *
44
+	 * @var int
45
+	 */
46
+	public $sequence;
47 47
 
48
-    /**
49
-     * The senders' email address.
50
-     *
51
-     * Note that this does not imply that this has to be used in a From: field
52
-     * if the message is sent by email. It may also be populated in Reply-To:
53
-     * or not at all.
54
-     *
55
-     * @var string
56
-     */
57
-    public $sender;
48
+	/**
49
+	 * The senders' email address.
50
+	 *
51
+	 * Note that this does not imply that this has to be used in a From: field
52
+	 * if the message is sent by email. It may also be populated in Reply-To:
53
+	 * or not at all.
54
+	 *
55
+	 * @var string
56
+	 */
57
+	public $sender;
58 58
 
59
-    /**
60
-     * The name of the sender. This is often populated from a CN parameter from
61
-     * either the ORGANIZER or ATTENDEE, depending on the message.
62
-     *
63
-     * @var string|null
64
-     */
65
-    public $senderName;
59
+	/**
60
+	 * The name of the sender. This is often populated from a CN parameter from
61
+	 * either the ORGANIZER or ATTENDEE, depending on the message.
62
+	 *
63
+	 * @var string|null
64
+	 */
65
+	public $senderName;
66 66
 
67
-    /**
68
-     * The recipient's email address.
69
-     *
70
-     * @var string
71
-     */
72
-    public $recipient;
67
+	/**
68
+	 * The recipient's email address.
69
+	 *
70
+	 * @var string
71
+	 */
72
+	public $recipient;
73 73
 
74
-    /**
75
-     * The name of the recipient. This is usually populated with the CN
76
-     * parameter from the ATTENDEE or ORGANIZER property, if it's available.
77
-     *
78
-     * @var string|null
79
-     */
80
-    public $recipientName;
74
+	/**
75
+	 * The name of the recipient. This is usually populated with the CN
76
+	 * parameter from the ATTENDEE or ORGANIZER property, if it's available.
77
+	 *
78
+	 * @var string|null
79
+	 */
80
+	public $recipientName;
81 81
 
82
-    /**
83
-     * After the message has been delivered, this should contain a string such
84
-     * as : 1.1;Sent or 1.2;Delivered.
85
-     *
86
-     * In case of a failure, this will hold the error status code.
87
-     *
88
-     * See:
89
-     * http://tools.ietf.org/html/rfc6638#section-7.3
90
-     *
91
-     * @var string
92
-     */
93
-    public $scheduleStatus;
82
+	/**
83
+	 * After the message has been delivered, this should contain a string such
84
+	 * as : 1.1;Sent or 1.2;Delivered.
85
+	 *
86
+	 * In case of a failure, this will hold the error status code.
87
+	 *
88
+	 * See:
89
+	 * http://tools.ietf.org/html/rfc6638#section-7.3
90
+	 *
91
+	 * @var string
92
+	 */
93
+	public $scheduleStatus;
94 94
 
95
-    /**
96
-     * The iCalendar / iTip body.
97
-     *
98
-     * @var \Sabre\VObject\Component\VCalendar
99
-     */
100
-    public $message;
95
+	/**
96
+	 * The iCalendar / iTip body.
97
+	 *
98
+	 * @var \Sabre\VObject\Component\VCalendar
99
+	 */
100
+	public $message;
101 101
 
102
-    /**
103
-     * This will be set to true, if the iTip broker considers the change
104
-     * 'significant'.
105
-     *
106
-     * In practice, this means that we'll only mark it true, if for instance
107
-     * DTSTART changed. This allows systems to only send iTip messages when
108
-     * significant changes happened. This is especially useful for iMip, as
109
-     * normally a ton of messages may be generated for normal calendar use.
110
-     *
111
-     * To see the list of properties that are considered 'significant', check
112
-     * out Sabre\VObject\ITip\Broker::$significantChangeProperties.
113
-     *
114
-     * @var bool
115
-     */
116
-    public $significantChange = true;
102
+	/**
103
+	 * This will be set to true, if the iTip broker considers the change
104
+	 * 'significant'.
105
+	 *
106
+	 * In practice, this means that we'll only mark it true, if for instance
107
+	 * DTSTART changed. This allows systems to only send iTip messages when
108
+	 * significant changes happened. This is especially useful for iMip, as
109
+	 * normally a ton of messages may be generated for normal calendar use.
110
+	 *
111
+	 * To see the list of properties that are considered 'significant', check
112
+	 * out Sabre\VObject\ITip\Broker::$significantChangeProperties.
113
+	 *
114
+	 * @var bool
115
+	 */
116
+	public $significantChange = true;
117 117
 
118
-    /**
119
-     * Returns the schedule status as a string.
120
-     *
121
-     * For example:
122
-     * 1.2
123
-     *
124
-     * @return mixed bool|string
125
-     */
126
-    public function getScheduleStatus()
127
-    {
128
-        if (!$this->scheduleStatus) {
129
-            return false;
130
-        } else {
131
-            list($scheduleStatus) = explode(';', $this->scheduleStatus);
118
+	/**
119
+	 * Returns the schedule status as a string.
120
+	 *
121
+	 * For example:
122
+	 * 1.2
123
+	 *
124
+	 * @return mixed bool|string
125
+	 */
126
+	public function getScheduleStatus()
127
+	{
128
+		if (!$this->scheduleStatus) {
129
+			return false;
130
+		} else {
131
+			list($scheduleStatus) = explode(';', $this->scheduleStatus);
132 132
 
133
-            return $scheduleStatus;
134
-        }
135
-    }
133
+			return $scheduleStatus;
134
+		}
135
+	}
136 136
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/Writer.php 1 patch
Indentation   +42 added lines, -42 removed lines patch added patch discarded remove patch
@@ -16,53 +16,53 @@
 block discarded – undo
16 16
  */
17 17
 class Writer
18 18
 {
19
-    /**
20
-     * Serializes a vCard or iCalendar object.
21
-     *
22
-     * @return string
23
-     */
24
-    public static function write(Component $component)
25
-    {
26
-        return $component->serialize();
27
-    }
19
+	/**
20
+	 * Serializes a vCard or iCalendar object.
21
+	 *
22
+	 * @return string
23
+	 */
24
+	public static function write(Component $component)
25
+	{
26
+		return $component->serialize();
27
+	}
28 28
 
29
-    /**
30
-     * Serializes a jCal or jCard object.
31
-     *
32
-     * @param int $options
33
-     *
34
-     * @return string
35
-     */
36
-    public static function writeJson(Component $component, $options = 0)
37
-    {
38
-        return json_encode($component, $options);
39
-    }
29
+	/**
30
+	 * Serializes a jCal or jCard object.
31
+	 *
32
+	 * @param int $options
33
+	 *
34
+	 * @return string
35
+	 */
36
+	public static function writeJson(Component $component, $options = 0)
37
+	{
38
+		return json_encode($component, $options);
39
+	}
40 40
 
41
-    /**
42
-     * Serializes a xCal or xCard object.
43
-     *
44
-     * @return string
45
-     */
46
-    public static function writeXml(Component $component)
47
-    {
48
-        $writer = new Xml\Writer();
49
-        $writer->openMemory();
50
-        $writer->setIndent(true);
41
+	/**
42
+	 * Serializes a xCal or xCard object.
43
+	 *
44
+	 * @return string
45
+	 */
46
+	public static function writeXml(Component $component)
47
+	{
48
+		$writer = new Xml\Writer();
49
+		$writer->openMemory();
50
+		$writer->setIndent(true);
51 51
 
52
-        $writer->startDocument('1.0', 'utf-8');
52
+		$writer->startDocument('1.0', 'utf-8');
53 53
 
54
-        if ($component instanceof Component\VCalendar) {
55
-            $writer->startElement('icalendar');
56
-            $writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE);
57
-        } else {
58
-            $writer->startElement('vcards');
59
-            $writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE);
60
-        }
54
+		if ($component instanceof Component\VCalendar) {
55
+			$writer->startElement('icalendar');
56
+			$writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE);
57
+		} else {
58
+			$writer->startElement('vcards');
59
+			$writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE);
60
+		}
61 61
 
62
-        $component->xmlSerialize($writer);
62
+		$component->xmlSerialize($writer);
63 63
 
64
-        $writer->endElement();
64
+		$writer->endElement();
65 65
 
66
-        return $writer->outputMemory();
67
-    }
66
+		return $writer->outputMemory();
67
+	}
68 68
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/ElementList.php 1 patch
Indentation   +25 added lines, -25 removed lines patch added patch discarded remove patch
@@ -17,32 +17,32 @@
 block discarded – undo
17 17
  */
18 18
 class ElementList extends ArrayIterator
19 19
 {
20
-    /* {{{ ArrayAccess Interface */
20
+	/* {{{ ArrayAccess Interface */
21 21
 
22
-    /**
23
-     * Sets an item through ArrayAccess.
24
-     *
25
-     * @param int   $offset
26
-     * @param mixed $value
27
-     */
28
-    #[\ReturnTypeWillChange]
29
-    public function offsetSet($offset, $value)
30
-    {
31
-        throw new LogicException('You can not add new objects to an ElementList');
32
-    }
22
+	/**
23
+	 * Sets an item through ArrayAccess.
24
+	 *
25
+	 * @param int   $offset
26
+	 * @param mixed $value
27
+	 */
28
+	#[\ReturnTypeWillChange]
29
+	public function offsetSet($offset, $value)
30
+	{
31
+		throw new LogicException('You can not add new objects to an ElementList');
32
+	}
33 33
 
34
-    /**
35
-     * Sets an item through ArrayAccess.
36
-     *
37
-     * This method just forwards the request to the inner iterator
38
-     *
39
-     * @param int $offset
40
-     */
41
-    #[\ReturnTypeWillChange]
42
-    public function offsetUnset($offset)
43
-    {
44
-        throw new LogicException('You can not remove objects from an ElementList');
45
-    }
34
+	/**
35
+	 * Sets an item through ArrayAccess.
36
+	 *
37
+	 * This method just forwards the request to the inner iterator
38
+	 *
39
+	 * @param int $offset
40
+	 */
41
+	#[\ReturnTypeWillChange]
42
+	public function offsetUnset($offset)
43
+	{
44
+		throw new LogicException('You can not remove objects from an ElementList');
45
+	}
46 46
 
47
-    /* }}} */
47
+	/* }}} */
48 48
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/Settings.php 1 patch
Indentation   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -17,39 +17,39 @@
 block discarded – undo
17 17
  */
18 18
 class Settings
19 19
 {
20
-    /**
21
-     * The minimum date we accept for various calculations with dates, such as
22
-     * recurrences.
23
-     *
24
-     * The choice of 1900 is pretty arbitrary, but it covers most common
25
-     * use-cases. In particular, it covers birthdates for virtually everyone
26
-     * alive on earth, which is less than 5 people at the time of writing.
27
-     */
28
-    public static $minDate = '1900-01-01';
20
+	/**
21
+	 * The minimum date we accept for various calculations with dates, such as
22
+	 * recurrences.
23
+	 *
24
+	 * The choice of 1900 is pretty arbitrary, but it covers most common
25
+	 * use-cases. In particular, it covers birthdates for virtually everyone
26
+	 * alive on earth, which is less than 5 people at the time of writing.
27
+	 */
28
+	public static $minDate = '1900-01-01';
29 29
 
30
-    /**
31
-     * The maximum date we accept for various calculations with dates, such as
32
-     * recurrences.
33
-     *
34
-     * The choice of 2100 is pretty arbitrary, but should cover most
35
-     * appointments made for many years to come.
36
-     */
37
-    public static $maxDate = '2100-01-01';
30
+	/**
31
+	 * The maximum date we accept for various calculations with dates, such as
32
+	 * recurrences.
33
+	 *
34
+	 * The choice of 2100 is pretty arbitrary, but should cover most
35
+	 * appointments made for many years to come.
36
+	 */
37
+	public static $maxDate = '2100-01-01';
38 38
 
39
-    /**
40
-     * The maximum number of recurrences that will be generated.
41
-     *
42
-     * This setting limits the maximum of recurring events that this library
43
-     * generates in its recurrence iterators.
44
-     *
45
-     * This is a security measure. Without this, it would be possible to craft
46
-     * specific events that recur many, many times, potentially DDOSing the
47
-     * server.
48
-     *
49
-     * The default (3500) allows creation of a daily event that goes on for 10
50
-     * years, which is hopefully long enough for most.
51
-     *
52
-     * Set this value to -1 to disable this control altogether.
53
-     */
54
-    public static $maxRecurrences = 3500;
39
+	/**
40
+	 * The maximum number of recurrences that will be generated.
41
+	 *
42
+	 * This setting limits the maximum of recurring events that this library
43
+	 * generates in its recurrence iterators.
44
+	 *
45
+	 * This is a security measure. Without this, it would be possible to craft
46
+	 * specific events that recur many, many times, potentially DDOSing the
47
+	 * server.
48
+	 *
49
+	 * The default (3500) allows creation of a daily event that goes on for 10
50
+	 * years, which is hopefully long enough for most.
51
+	 *
52
+	 * Set this value to -1 to disable this control altogether.
53
+	 */
54
+	public static $maxRecurrences = 3500;
55 55
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/Reader.php 1 patch
Indentation   +71 added lines, -71 removed lines patch added patch discarded remove patch
@@ -14,82 +14,82 @@
 block discarded – undo
14 14
  */
15 15
 class Reader
16 16
 {
17
-    /**
18
-     * If this option is passed to the reader, it will be less strict about the
19
-     * validity of the lines.
20
-     */
21
-    const OPTION_FORGIVING = 1;
17
+	/**
18
+	 * If this option is passed to the reader, it will be less strict about the
19
+	 * validity of the lines.
20
+	 */
21
+	const OPTION_FORGIVING = 1;
22 22
 
23
-    /**
24
-     * If this option is turned on, any lines we cannot parse will be ignored
25
-     * by the reader.
26
-     */
27
-    const OPTION_IGNORE_INVALID_LINES = 2;
23
+	/**
24
+	 * If this option is turned on, any lines we cannot parse will be ignored
25
+	 * by the reader.
26
+	 */
27
+	const OPTION_IGNORE_INVALID_LINES = 2;
28 28
 
29
-    /**
30
-     * Parses a vCard or iCalendar object, and returns the top component.
31
-     *
32
-     * The options argument is a bitfield. Pass any of the OPTIONS constant to
33
-     * alter the parsers' behaviour.
34
-     *
35
-     * You can either supply a string, or a readable stream for input.
36
-     *
37
-     * @param string|resource $data
38
-     * @param int             $options
39
-     * @param string          $charset
40
-     *
41
-     * @return Document
42
-     */
43
-    public static function read($data, $options = 0, $charset = 'UTF-8')
44
-    {
45
-        $parser = new Parser\MimeDir();
46
-        $parser->setCharset($charset);
47
-        $result = $parser->parse($data, $options);
29
+	/**
30
+	 * Parses a vCard or iCalendar object, and returns the top component.
31
+	 *
32
+	 * The options argument is a bitfield. Pass any of the OPTIONS constant to
33
+	 * alter the parsers' behaviour.
34
+	 *
35
+	 * You can either supply a string, or a readable stream for input.
36
+	 *
37
+	 * @param string|resource $data
38
+	 * @param int             $options
39
+	 * @param string          $charset
40
+	 *
41
+	 * @return Document
42
+	 */
43
+	public static function read($data, $options = 0, $charset = 'UTF-8')
44
+	{
45
+		$parser = new Parser\MimeDir();
46
+		$parser->setCharset($charset);
47
+		$result = $parser->parse($data, $options);
48 48
 
49
-        return $result;
50
-    }
49
+		return $result;
50
+	}
51 51
 
52
-    /**
53
-     * Parses a jCard or jCal object, and returns the top component.
54
-     *
55
-     * The options argument is a bitfield. Pass any of the OPTIONS constant to
56
-     * alter the parsers' behaviour.
57
-     *
58
-     * You can either a string, a readable stream, or an array for its input.
59
-     * Specifying the array is useful if json_decode was already called on the
60
-     * input.
61
-     *
62
-     * @param string|resource|array $data
63
-     * @param int                   $options
64
-     *
65
-     * @return Document
66
-     */
67
-    public static function readJson($data, $options = 0)
68
-    {
69
-        $parser = new Parser\Json();
70
-        $result = $parser->parse($data, $options);
52
+	/**
53
+	 * Parses a jCard or jCal object, and returns the top component.
54
+	 *
55
+	 * The options argument is a bitfield. Pass any of the OPTIONS constant to
56
+	 * alter the parsers' behaviour.
57
+	 *
58
+	 * You can either a string, a readable stream, or an array for its input.
59
+	 * Specifying the array is useful if json_decode was already called on the
60
+	 * input.
61
+	 *
62
+	 * @param string|resource|array $data
63
+	 * @param int                   $options
64
+	 *
65
+	 * @return Document
66
+	 */
67
+	public static function readJson($data, $options = 0)
68
+	{
69
+		$parser = new Parser\Json();
70
+		$result = $parser->parse($data, $options);
71 71
 
72
-        return $result;
73
-    }
72
+		return $result;
73
+	}
74 74
 
75
-    /**
76
-     * Parses a xCard or xCal object, and returns the top component.
77
-     *
78
-     * The options argument is a bitfield. Pass any of the OPTIONS constant to
79
-     * alter the parsers' behaviour.
80
-     *
81
-     * You can either supply a string, or a readable stream for input.
82
-     *
83
-     * @param string|resource $data
84
-     * @param int             $options
85
-     *
86
-     * @return Document
87
-     */
88
-    public static function readXML($data, $options = 0)
89
-    {
90
-        $parser = new Parser\XML();
91
-        $result = $parser->parse($data, $options);
75
+	/**
76
+	 * Parses a xCard or xCal object, and returns the top component.
77
+	 *
78
+	 * The options argument is a bitfield. Pass any of the OPTIONS constant to
79
+	 * alter the parsers' behaviour.
80
+	 *
81
+	 * You can either supply a string, or a readable stream for input.
82
+	 *
83
+	 * @param string|resource $data
84
+	 * @param int             $options
85
+	 *
86
+	 * @return Document
87
+	 */
88
+	public static function readXML($data, $options = 0)
89
+	{
90
+		$parser = new Parser\XML();
91
+		$result = $parser->parse($data, $options);
92 92
 
93
-        return $result;
94
-    }
93
+		return $result;
94
+	}
95 95
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/StringUtil.php 1 patch
Indentation   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -11,40 +11,40 @@
 block discarded – undo
11 11
  */
12 12
 class StringUtil
13 13
 {
14
-    /**
15
-     * Returns true or false depending on if a string is valid UTF-8.
16
-     *
17
-     * @param string $str
18
-     *
19
-     * @return bool
20
-     */
21
-    public static function isUTF8($str)
22
-    {
23
-        // Control characters
24
-        if (preg_match('%[\x00-\x08\x0B-\x0C\x0E\x0F]%', $str)) {
25
-            return false;
26
-        }
14
+	/**
15
+	 * Returns true or false depending on if a string is valid UTF-8.
16
+	 *
17
+	 * @param string $str
18
+	 *
19
+	 * @return bool
20
+	 */
21
+	public static function isUTF8($str)
22
+	{
23
+		// Control characters
24
+		if (preg_match('%[\x00-\x08\x0B-\x0C\x0E\x0F]%', $str)) {
25
+			return false;
26
+		}
27 27
 
28
-        return (bool) preg_match('%%u', $str);
29
-    }
28
+		return (bool) preg_match('%%u', $str);
29
+	}
30 30
 
31
-    /**
32
-     * This method tries its best to convert the input string to UTF-8.
33
-     *
34
-     * Currently only ISO-5991-1 input and UTF-8 input is supported, but this
35
-     * may be expanded upon if we receive other examples.
36
-     *
37
-     * @param string $str
38
-     *
39
-     * @return string
40
-     */
41
-    public static function convertToUTF8($str)
42
-    {
43
-        if (!mb_check_encoding($str, 'UTF-8') && mb_check_encoding($str, 'ISO-8859-1')) {
44
-            $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
45
-        }
31
+	/**
32
+	 * This method tries its best to convert the input string to UTF-8.
33
+	 *
34
+	 * Currently only ISO-5991-1 input and UTF-8 input is supported, but this
35
+	 * may be expanded upon if we receive other examples.
36
+	 *
37
+	 * @param string $str
38
+	 *
39
+	 * @return string
40
+	 */
41
+	public static function convertToUTF8($str)
42
+	{
43
+		if (!mb_check_encoding($str, 'UTF-8') && mb_check_encoding($str, 'ISO-8859-1')) {
44
+			$str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
45
+		}
46 46
 
47
-        // Removing any control characters
48
-        return preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $str);
49
-    }
47
+		// Removing any control characters
48
+		return preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $str);
49
+	}
50 50
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/BirthdayCalendarGenerator.php 1 patch
Indentation   +156 added lines, -156 removed lines patch added patch discarded remove patch
@@ -13,160 +13,160 @@
 block discarded – undo
13 13
  */
14 14
 class BirthdayCalendarGenerator
15 15
 {
16
-    /**
17
-     * Input objects.
18
-     *
19
-     * @var array
20
-     */
21
-    protected $objects = [];
22
-
23
-    /**
24
-     * Default year.
25
-     * Used for dates without a year.
26
-     */
27
-    const DEFAULT_YEAR = 2000;
28
-
29
-    /**
30
-     * Output format for the SUMMARY.
31
-     *
32
-     * @var string
33
-     */
34
-    protected $format = '%1$s\'s Birthday';
35
-
36
-    /**
37
-     * Creates the generator.
38
-     *
39
-     * Check the setTimeRange and setObjects methods for details about the
40
-     * arguments.
41
-     *
42
-     * @param mixed $objects
43
-     */
44
-    public function __construct($objects = null)
45
-    {
46
-        if ($objects) {
47
-            $this->setObjects($objects);
48
-        }
49
-    }
50
-
51
-    /**
52
-     * Sets the input objects.
53
-     *
54
-     * You must either supply a vCard as a string or as a Component/VCard object.
55
-     * It's also possible to supply an array of strings or objects.
56
-     *
57
-     * @param mixed $objects
58
-     */
59
-    public function setObjects($objects)
60
-    {
61
-        if (!is_array($objects)) {
62
-            $objects = [$objects];
63
-        }
64
-
65
-        $this->objects = [];
66
-        foreach ($objects as $object) {
67
-            if (is_string($object)) {
68
-                $vObj = Reader::read($object);
69
-                if (!$vObj instanceof Component\VCard) {
70
-                    throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects');
71
-                }
72
-
73
-                $this->objects[] = $vObj;
74
-            } elseif ($object instanceof Component\VCard) {
75
-                $this->objects[] = $object;
76
-            } else {
77
-                throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects');
78
-            }
79
-        }
80
-    }
81
-
82
-    /**
83
-     * Sets the output format for the SUMMARY.
84
-     *
85
-     * @param string $format
86
-     */
87
-    public function setFormat($format)
88
-    {
89
-        $this->format = $format;
90
-    }
91
-
92
-    /**
93
-     * Parses the input data and returns a VCALENDAR.
94
-     *
95
-     * @return Component/VCalendar
96
-     */
97
-    public function getResult()
98
-    {
99
-        $calendar = new VCalendar();
100
-
101
-        foreach ($this->objects as $object) {
102
-            // Skip if there is no BDAY property.
103
-            if (!$object->select('BDAY')) {
104
-                continue;
105
-            }
106
-
107
-            // We've seen clients (ez-vcard) putting "BDAY:" properties
108
-            // without a value into vCards. If we come across those, we'll
109
-            // skip them.
110
-            if (empty($object->BDAY->getValue())) {
111
-                continue;
112
-            }
113
-
114
-            // We're always converting to vCard 4.0 so we can rely on the
115
-            // VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
116
-            $object = $object->convert(Document::VCARD40);
117
-
118
-            // Skip if the card has no FN property.
119
-            if (!isset($object->FN)) {
120
-                continue;
121
-            }
122
-
123
-            // Skip if the BDAY property is not of the right type.
124
-            if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) {
125
-                continue;
126
-            }
127
-
128
-            // Skip if we can't parse the BDAY value.
129
-            try {
130
-                $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
131
-            } catch (InvalidDataException $e) {
132
-                continue;
133
-            }
134
-
135
-            // Set a year if it's not set.
136
-            $unknownYear = false;
137
-
138
-            if (!$dateParts['year']) {
139
-                $object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date'];
140
-
141
-                $unknownYear = true;
142
-            }
143
-
144
-            // Create event.
145
-            $event = $calendar->add('VEVENT', [
146
-                'SUMMARY' => sprintf($this->format, $object->FN->getValue()),
147
-                'DTSTART' => new \DateTime($object->BDAY->getValue()),
148
-                'RRULE' => 'FREQ=YEARLY',
149
-                'TRANSP' => 'TRANSPARENT',
150
-            ]);
151
-
152
-            // add VALUE=date
153
-            $event->DTSTART['VALUE'] = 'DATE';
154
-
155
-            // Add X-SABRE-BDAY property.
156
-            if ($unknownYear) {
157
-                $event->add('X-SABRE-BDAY', 'BDAY', [
158
-                    'X-SABRE-VCARD-UID' => $object->UID->getValue(),
159
-                    'X-SABRE-VCARD-FN' => $object->FN->getValue(),
160
-                    'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR,
161
-                ]);
162
-            } else {
163
-                $event->add('X-SABRE-BDAY', 'BDAY', [
164
-                    'X-SABRE-VCARD-UID' => $object->UID->getValue(),
165
-                    'X-SABRE-VCARD-FN' => $object->FN->getValue(),
166
-                ]);
167
-            }
168
-        }
169
-
170
-        return $calendar;
171
-    }
16
+	/**
17
+	 * Input objects.
18
+	 *
19
+	 * @var array
20
+	 */
21
+	protected $objects = [];
22
+
23
+	/**
24
+	 * Default year.
25
+	 * Used for dates without a year.
26
+	 */
27
+	const DEFAULT_YEAR = 2000;
28
+
29
+	/**
30
+	 * Output format for the SUMMARY.
31
+	 *
32
+	 * @var string
33
+	 */
34
+	protected $format = '%1$s\'s Birthday';
35
+
36
+	/**
37
+	 * Creates the generator.
38
+	 *
39
+	 * Check the setTimeRange and setObjects methods for details about the
40
+	 * arguments.
41
+	 *
42
+	 * @param mixed $objects
43
+	 */
44
+	public function __construct($objects = null)
45
+	{
46
+		if ($objects) {
47
+			$this->setObjects($objects);
48
+		}
49
+	}
50
+
51
+	/**
52
+	 * Sets the input objects.
53
+	 *
54
+	 * You must either supply a vCard as a string or as a Component/VCard object.
55
+	 * It's also possible to supply an array of strings or objects.
56
+	 *
57
+	 * @param mixed $objects
58
+	 */
59
+	public function setObjects($objects)
60
+	{
61
+		if (!is_array($objects)) {
62
+			$objects = [$objects];
63
+		}
64
+
65
+		$this->objects = [];
66
+		foreach ($objects as $object) {
67
+			if (is_string($object)) {
68
+				$vObj = Reader::read($object);
69
+				if (!$vObj instanceof Component\VCard) {
70
+					throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects');
71
+				}
72
+
73
+				$this->objects[] = $vObj;
74
+			} elseif ($object instanceof Component\VCard) {
75
+				$this->objects[] = $object;
76
+			} else {
77
+				throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects');
78
+			}
79
+		}
80
+	}
81
+
82
+	/**
83
+	 * Sets the output format for the SUMMARY.
84
+	 *
85
+	 * @param string $format
86
+	 */
87
+	public function setFormat($format)
88
+	{
89
+		$this->format = $format;
90
+	}
91
+
92
+	/**
93
+	 * Parses the input data and returns a VCALENDAR.
94
+	 *
95
+	 * @return Component/VCalendar
96
+	 */
97
+	public function getResult()
98
+	{
99
+		$calendar = new VCalendar();
100
+
101
+		foreach ($this->objects as $object) {
102
+			// Skip if there is no BDAY property.
103
+			if (!$object->select('BDAY')) {
104
+				continue;
105
+			}
106
+
107
+			// We've seen clients (ez-vcard) putting "BDAY:" properties
108
+			// without a value into vCards. If we come across those, we'll
109
+			// skip them.
110
+			if (empty($object->BDAY->getValue())) {
111
+				continue;
112
+			}
113
+
114
+			// We're always converting to vCard 4.0 so we can rely on the
115
+			// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
116
+			$object = $object->convert(Document::VCARD40);
117
+
118
+			// Skip if the card has no FN property.
119
+			if (!isset($object->FN)) {
120
+				continue;
121
+			}
122
+
123
+			// Skip if the BDAY property is not of the right type.
124
+			if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) {
125
+				continue;
126
+			}
127
+
128
+			// Skip if we can't parse the BDAY value.
129
+			try {
130
+				$dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
131
+			} catch (InvalidDataException $e) {
132
+				continue;
133
+			}
134
+
135
+			// Set a year if it's not set.
136
+			$unknownYear = false;
137
+
138
+			if (!$dateParts['year']) {
139
+				$object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date'];
140
+
141
+				$unknownYear = true;
142
+			}
143
+
144
+			// Create event.
145
+			$event = $calendar->add('VEVENT', [
146
+				'SUMMARY' => sprintf($this->format, $object->FN->getValue()),
147
+				'DTSTART' => new \DateTime($object->BDAY->getValue()),
148
+				'RRULE' => 'FREQ=YEARLY',
149
+				'TRANSP' => 'TRANSPARENT',
150
+			]);
151
+
152
+			// add VALUE=date
153
+			$event->DTSTART['VALUE'] = 'DATE';
154
+
155
+			// Add X-SABRE-BDAY property.
156
+			if ($unknownYear) {
157
+				$event->add('X-SABRE-BDAY', 'BDAY', [
158
+					'X-SABRE-VCARD-UID' => $object->UID->getValue(),
159
+					'X-SABRE-VCARD-FN' => $object->FN->getValue(),
160
+					'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR,
161
+				]);
162
+			} else {
163
+				$event->add('X-SABRE-BDAY', 'BDAY', [
164
+					'X-SABRE-VCARD-UID' => $object->UID->getValue(),
165
+					'X-SABRE-VCARD-FN' => $object->FN->getValue(),
166
+				]);
167
+			}
168
+		}
169
+
170
+		return $calendar;
171
+	}
172 172
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/Recur/EventIterator.php 1 patch
Indentation   +434 added lines, -434 removed lines patch added patch discarded remove patch
@@ -60,438 +60,438 @@
 block discarded – undo
60 60
  */
61 61
 class EventIterator implements \Iterator
62 62
 {
63
-    /**
64
-     * Reference timeZone for floating dates and times.
65
-     *
66
-     * @var DateTimeZone
67
-     */
68
-    protected $timeZone;
69
-
70
-    /**
71
-     * True if we're iterating an all-day event.
72
-     *
73
-     * @var bool
74
-     */
75
-    protected $allDay = false;
76
-
77
-    /**
78
-     * Creates the iterator.
79
-     *
80
-     * There's three ways to set up the iterator.
81
-     *
82
-     * 1. You can pass a VCALENDAR component and a UID.
83
-     * 2. You can pass an array of VEVENTs (all UIDS should match).
84
-     * 3. You can pass a single VEVENT component.
85
-     *
86
-     * Only the second method is recommended. The other 1 and 3 will be removed
87
-     * at some point in the future.
88
-     *
89
-     * The $uid parameter is only required for the first method.
90
-     *
91
-     * @param Component|array $input
92
-     * @param string|null     $uid
93
-     * @param DateTimeZone    $timeZone reference timezone for floating dates and
94
-     *                                  times
95
-     */
96
-    public function __construct($input, $uid = null, DateTimeZone $timeZone = null)
97
-    {
98
-        if (is_null($timeZone)) {
99
-            $timeZone = new DateTimeZone('UTC');
100
-        }
101
-        $this->timeZone = $timeZone;
102
-
103
-        if (is_array($input)) {
104
-            $events = $input;
105
-        } elseif ($input instanceof VEvent) {
106
-            // Single instance mode.
107
-            $events = [$input];
108
-        } else {
109
-            // Calendar + UID mode.
110
-            $uid = (string) $uid;
111
-            if (!$uid) {
112
-                throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
113
-            }
114
-            if (!isset($input->VEVENT)) {
115
-                throw new InvalidArgumentException('No events found in this calendar');
116
-            }
117
-            $events = $input->getByUID($uid);
118
-        }
119
-
120
-        foreach ($events as $vevent) {
121
-            if (!isset($vevent->{'RECURRENCE-ID'})) {
122
-                $this->masterEvent = $vevent;
123
-            } else {
124
-                $this->exceptions[
125
-                    $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
126
-                ] = true;
127
-                $this->overriddenEvents[] = $vevent;
128
-            }
129
-        }
130
-
131
-        if (!$this->masterEvent) {
132
-            // No base event was found. CalDAV does allow cases where only
133
-            // overridden instances are stored.
134
-            //
135
-            // In this particular case, we're just going to grab the first
136
-            // event and use that instead. This may not always give the
137
-            // desired result.
138
-            if (!count($this->overriddenEvents)) {
139
-                throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
140
-            }
141
-            $this->masterEvent = array_shift($this->overriddenEvents);
142
-        }
143
-
144
-        $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone);
145
-        $this->allDay = !$this->masterEvent->DTSTART->hasTime();
146
-
147
-        if (isset($this->masterEvent->EXDATE)) {
148
-            foreach ($this->masterEvent->EXDATE as $exDate) {
149
-                foreach ($exDate->getDateTimes($this->timeZone) as $dt) {
150
-                    $this->exceptions[$dt->getTimeStamp()] = true;
151
-                }
152
-            }
153
-        }
154
-
155
-        if (isset($this->masterEvent->DTEND)) {
156
-            $this->eventDuration =
157
-                $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() -
158
-                $this->startDate->getTimeStamp();
159
-        } elseif (isset($this->masterEvent->DURATION)) {
160
-            $duration = $this->masterEvent->DURATION->getDateInterval();
161
-            $end = clone $this->startDate;
162
-            $end = $end->add($duration);
163
-            $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp();
164
-        } elseif ($this->allDay) {
165
-            $this->eventDuration = 3600 * 24;
166
-        } else {
167
-            $this->eventDuration = 0;
168
-        }
169
-
170
-        if (isset($this->masterEvent->RDATE)) {
171
-            $this->recurIterator = new RDateIterator(
172
-                $this->masterEvent->RDATE->getParts(),
173
-                $this->startDate
174
-            );
175
-        } elseif (isset($this->masterEvent->RRULE)) {
176
-            $this->recurIterator = new RRuleIterator(
177
-                $this->masterEvent->RRULE->getParts(),
178
-                $this->startDate
179
-            );
180
-        } else {
181
-            $this->recurIterator = new RRuleIterator(
182
-                [
183
-                    'FREQ' => 'DAILY',
184
-                    'COUNT' => 1,
185
-                ],
186
-                $this->startDate
187
-            );
188
-        }
189
-
190
-        $this->rewind();
191
-        if (!$this->valid()) {
192
-            throw new NoInstancesException('This recurrence rule does not generate any valid instances');
193
-        }
194
-    }
195
-
196
-    /**
197
-     * Returns the date for the current position of the iterator.
198
-     *
199
-     * @return DateTimeImmutable
200
-     */
201
-    #[\ReturnTypeWillChange]
202
-    public function current()
203
-    {
204
-        if ($this->currentDate) {
205
-            return clone $this->currentDate;
206
-        }
207
-    }
208
-
209
-    /**
210
-     * This method returns the start date for the current iteration of the
211
-     * event.
212
-     *
213
-     * @return DateTimeImmutable
214
-     */
215
-    public function getDtStart()
216
-    {
217
-        if ($this->currentDate) {
218
-            return clone $this->currentDate;
219
-        }
220
-    }
221
-
222
-    /**
223
-     * This method returns the end date for the current iteration of the
224
-     * event.
225
-     *
226
-     * @return DateTimeImmutable
227
-     */
228
-    public function getDtEnd()
229
-    {
230
-        if (!$this->valid()) {
231
-            return;
232
-        }
233
-        if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) {
234
-            return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone);
235
-        } else {
236
-            $end = clone $this->currentDate;
237
-
238
-            return $end->modify('+'.$this->eventDuration.' seconds');
239
-        }
240
-    }
241
-
242
-    /**
243
-     * Returns a VEVENT for the current iterations of the event.
244
-     *
245
-     * This VEVENT will have a recurrence id, and its DTSTART and DTEND
246
-     * altered.
247
-     *
248
-     * @return VEvent
249
-     */
250
-    public function getEventObject()
251
-    {
252
-        if ($this->currentOverriddenEvent) {
253
-            return $this->currentOverriddenEvent;
254
-        }
255
-
256
-        $event = clone $this->masterEvent;
257
-
258
-        // Ignoring the following block, because PHPUnit's code coverage
259
-        // ignores most of these lines, and this messes with our stats.
260
-        //
261
-        // @codeCoverageIgnoreStart
262
-        unset(
263
-            $event->RRULE,
264
-            $event->EXDATE,
265
-            $event->RDATE,
266
-            $event->EXRULE,
267
-            $event->{'RECURRENCE-ID'}
268
-        );
269
-        // @codeCoverageIgnoreEnd
270
-
271
-        $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating());
272
-        if (isset($event->DTEND)) {
273
-            $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating());
274
-        }
275
-        $recurid = clone $event->DTSTART;
276
-        $recurid->name = 'RECURRENCE-ID';
277
-        $event->add($recurid);
278
-
279
-        return $event;
280
-    }
281
-
282
-    /**
283
-     * Returns the current position of the iterator.
284
-     *
285
-     * This is for us simply a 0-based index.
286
-     *
287
-     * @return int
288
-     */
289
-    #[\ReturnTypeWillChange]
290
-    public function key()
291
-    {
292
-        // The counter is always 1 ahead.
293
-        return $this->counter - 1;
294
-    }
295
-
296
-    /**
297
-     * This is called after next, to see if the iterator is still at a valid
298
-     * position, or if it's at the end.
299
-     *
300
-     * @return bool
301
-     */
302
-    #[\ReturnTypeWillChange]
303
-    public function valid()
304
-    {
305
-        if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) {
306
-            throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences);
307
-        }
308
-
309
-        return (bool) $this->currentDate;
310
-    }
311
-
312
-    /**
313
-     * Sets the iterator back to the starting point.
314
-     *
315
-     * @return void
316
-     */
317
-    #[\ReturnTypeWillChange]
318
-    public function rewind()
319
-    {
320
-        $this->recurIterator->rewind();
321
-        // re-creating overridden event index.
322
-        $index = [];
323
-        foreach ($this->overriddenEvents as $key => $event) {
324
-            $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
325
-            $index[$stamp][] = $key;
326
-        }
327
-        krsort($index);
328
-        $this->counter = 0;
329
-        $this->overriddenEventsIndex = $index;
330
-        $this->currentOverriddenEvent = null;
331
-
332
-        $this->nextDate = null;
333
-        $this->currentDate = clone $this->startDate;
334
-
335
-        $this->next();
336
-    }
337
-
338
-    /**
339
-     * Advances the iterator with one step.
340
-     *
341
-     * @return void
342
-     */
343
-    #[\ReturnTypeWillChange]
344
-    public function next()
345
-    {
346
-        $this->currentOverriddenEvent = null;
347
-        ++$this->counter;
348
-        if ($this->nextDate) {
349
-            // We had a stored value.
350
-            $nextDate = $this->nextDate;
351
-            $this->nextDate = null;
352
-        } else {
353
-            // We need to ask rruleparser for the next date.
354
-            // We need to do this until we find a date that's not in the
355
-            // exception list.
356
-            do {
357
-                if (!$this->recurIterator->valid()) {
358
-                    $nextDate = null;
359
-                    break;
360
-                }
361
-                $nextDate = $this->recurIterator->current();
362
-                $this->recurIterator->next();
363
-            } while (isset($this->exceptions[$nextDate->getTimeStamp()]));
364
-        }
365
-
366
-        // $nextDate now contains what rrule thinks is the next one, but an
367
-        // overridden event may cut ahead.
368
-        if ($this->overriddenEventsIndex) {
369
-            $offsets = end($this->overriddenEventsIndex);
370
-            $timestamp = key($this->overriddenEventsIndex);
371
-            $offset = end($offsets);
372
-            if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
373
-                // Overridden event comes first.
374
-                $this->currentOverriddenEvent = $this->overriddenEvents[$offset];
375
-
376
-                // Putting the rrule next date aside.
377
-                $this->nextDate = $nextDate;
378
-                $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
379
-
380
-                // Ensuring that this item will only be used once.
381
-                array_pop($this->overriddenEventsIndex[$timestamp]);
382
-                if (!$this->overriddenEventsIndex[$timestamp]) {
383
-                    array_pop($this->overriddenEventsIndex);
384
-                }
385
-
386
-                // Exit point!
387
-                return;
388
-            }
389
-        }
390
-
391
-        $this->currentDate = $nextDate;
392
-    }
393
-
394
-    /**
395
-     * Quickly jump to a date in the future.
396
-     */
397
-    public function fastForward(DateTimeInterface $dateTime)
398
-    {
399
-        while ($this->valid() && $this->getDtEnd() <= $dateTime) {
400
-            $this->next();
401
-        }
402
-    }
403
-
404
-    /**
405
-     * Returns true if this recurring event never ends.
406
-     *
407
-     * @return bool
408
-     */
409
-    public function isInfinite()
410
-    {
411
-        return $this->recurIterator->isInfinite();
412
-    }
413
-
414
-    /**
415
-     * RRULE parser.
416
-     *
417
-     * @var RRuleIterator
418
-     */
419
-    protected $recurIterator;
420
-
421
-    /**
422
-     * The duration, in seconds, of the master event.
423
-     *
424
-     * We use this to calculate the DTEND for subsequent events.
425
-     */
426
-    protected $eventDuration;
427
-
428
-    /**
429
-     * A reference to the main (master) event.
430
-     *
431
-     * @var VEVENT
432
-     */
433
-    protected $masterEvent;
434
-
435
-    /**
436
-     * List of overridden events.
437
-     *
438
-     * @var array
439
-     */
440
-    protected $overriddenEvents = [];
441
-
442
-    /**
443
-     * Overridden event index.
444
-     *
445
-     * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent
446
-     * property.
447
-     *
448
-     * @var array
449
-     */
450
-    protected $overriddenEventsIndex;
451
-
452
-    /**
453
-     * A list of recurrence-id's that are either part of EXDATE, or are
454
-     * overridden.
455
-     *
456
-     * @var array
457
-     */
458
-    protected $exceptions = [];
459
-
460
-    /**
461
-     * Internal event counter.
462
-     *
463
-     * @var int
464
-     */
465
-    protected $counter;
466
-
467
-    /**
468
-     * The very start of the iteration process.
469
-     *
470
-     * @var DateTimeImmutable
471
-     */
472
-    protected $startDate;
473
-
474
-    /**
475
-     * Where we are currently in the iteration process.
476
-     *
477
-     * @var DateTimeImmutable
478
-     */
479
-    protected $currentDate;
480
-
481
-    /**
482
-     * The next date from the rrule parser.
483
-     *
484
-     * Sometimes we need to temporary store the next date, because an
485
-     * overridden event came before.
486
-     *
487
-     * @var DateTimeImmutable
488
-     */
489
-    protected $nextDate;
490
-
491
-    /**
492
-     * The event that overwrites the current iteration.
493
-     *
494
-     * @var VEVENT
495
-     */
496
-    protected $currentOverriddenEvent;
63
+	/**
64
+	 * Reference timeZone for floating dates and times.
65
+	 *
66
+	 * @var DateTimeZone
67
+	 */
68
+	protected $timeZone;
69
+
70
+	/**
71
+	 * True if we're iterating an all-day event.
72
+	 *
73
+	 * @var bool
74
+	 */
75
+	protected $allDay = false;
76
+
77
+	/**
78
+	 * Creates the iterator.
79
+	 *
80
+	 * There's three ways to set up the iterator.
81
+	 *
82
+	 * 1. You can pass a VCALENDAR component and a UID.
83
+	 * 2. You can pass an array of VEVENTs (all UIDS should match).
84
+	 * 3. You can pass a single VEVENT component.
85
+	 *
86
+	 * Only the second method is recommended. The other 1 and 3 will be removed
87
+	 * at some point in the future.
88
+	 *
89
+	 * The $uid parameter is only required for the first method.
90
+	 *
91
+	 * @param Component|array $input
92
+	 * @param string|null     $uid
93
+	 * @param DateTimeZone    $timeZone reference timezone for floating dates and
94
+	 *                                  times
95
+	 */
96
+	public function __construct($input, $uid = null, DateTimeZone $timeZone = null)
97
+	{
98
+		if (is_null($timeZone)) {
99
+			$timeZone = new DateTimeZone('UTC');
100
+		}
101
+		$this->timeZone = $timeZone;
102
+
103
+		if (is_array($input)) {
104
+			$events = $input;
105
+		} elseif ($input instanceof VEvent) {
106
+			// Single instance mode.
107
+			$events = [$input];
108
+		} else {
109
+			// Calendar + UID mode.
110
+			$uid = (string) $uid;
111
+			if (!$uid) {
112
+				throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
113
+			}
114
+			if (!isset($input->VEVENT)) {
115
+				throw new InvalidArgumentException('No events found in this calendar');
116
+			}
117
+			$events = $input->getByUID($uid);
118
+		}
119
+
120
+		foreach ($events as $vevent) {
121
+			if (!isset($vevent->{'RECURRENCE-ID'})) {
122
+				$this->masterEvent = $vevent;
123
+			} else {
124
+				$this->exceptions[
125
+					$vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
126
+				] = true;
127
+				$this->overriddenEvents[] = $vevent;
128
+			}
129
+		}
130
+
131
+		if (!$this->masterEvent) {
132
+			// No base event was found. CalDAV does allow cases where only
133
+			// overridden instances are stored.
134
+			//
135
+			// In this particular case, we're just going to grab the first
136
+			// event and use that instead. This may not always give the
137
+			// desired result.
138
+			if (!count($this->overriddenEvents)) {
139
+				throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
140
+			}
141
+			$this->masterEvent = array_shift($this->overriddenEvents);
142
+		}
143
+
144
+		$this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone);
145
+		$this->allDay = !$this->masterEvent->DTSTART->hasTime();
146
+
147
+		if (isset($this->masterEvent->EXDATE)) {
148
+			foreach ($this->masterEvent->EXDATE as $exDate) {
149
+				foreach ($exDate->getDateTimes($this->timeZone) as $dt) {
150
+					$this->exceptions[$dt->getTimeStamp()] = true;
151
+				}
152
+			}
153
+		}
154
+
155
+		if (isset($this->masterEvent->DTEND)) {
156
+			$this->eventDuration =
157
+				$this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() -
158
+				$this->startDate->getTimeStamp();
159
+		} elseif (isset($this->masterEvent->DURATION)) {
160
+			$duration = $this->masterEvent->DURATION->getDateInterval();
161
+			$end = clone $this->startDate;
162
+			$end = $end->add($duration);
163
+			$this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp();
164
+		} elseif ($this->allDay) {
165
+			$this->eventDuration = 3600 * 24;
166
+		} else {
167
+			$this->eventDuration = 0;
168
+		}
169
+
170
+		if (isset($this->masterEvent->RDATE)) {
171
+			$this->recurIterator = new RDateIterator(
172
+				$this->masterEvent->RDATE->getParts(),
173
+				$this->startDate
174
+			);
175
+		} elseif (isset($this->masterEvent->RRULE)) {
176
+			$this->recurIterator = new RRuleIterator(
177
+				$this->masterEvent->RRULE->getParts(),
178
+				$this->startDate
179
+			);
180
+		} else {
181
+			$this->recurIterator = new RRuleIterator(
182
+				[
183
+					'FREQ' => 'DAILY',
184
+					'COUNT' => 1,
185
+				],
186
+				$this->startDate
187
+			);
188
+		}
189
+
190
+		$this->rewind();
191
+		if (!$this->valid()) {
192
+			throw new NoInstancesException('This recurrence rule does not generate any valid instances');
193
+		}
194
+	}
195
+
196
+	/**
197
+	 * Returns the date for the current position of the iterator.
198
+	 *
199
+	 * @return DateTimeImmutable
200
+	 */
201
+	#[\ReturnTypeWillChange]
202
+	public function current()
203
+	{
204
+		if ($this->currentDate) {
205
+			return clone $this->currentDate;
206
+		}
207
+	}
208
+
209
+	/**
210
+	 * This method returns the start date for the current iteration of the
211
+	 * event.
212
+	 *
213
+	 * @return DateTimeImmutable
214
+	 */
215
+	public function getDtStart()
216
+	{
217
+		if ($this->currentDate) {
218
+			return clone $this->currentDate;
219
+		}
220
+	}
221
+
222
+	/**
223
+	 * This method returns the end date for the current iteration of the
224
+	 * event.
225
+	 *
226
+	 * @return DateTimeImmutable
227
+	 */
228
+	public function getDtEnd()
229
+	{
230
+		if (!$this->valid()) {
231
+			return;
232
+		}
233
+		if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) {
234
+			return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone);
235
+		} else {
236
+			$end = clone $this->currentDate;
237
+
238
+			return $end->modify('+'.$this->eventDuration.' seconds');
239
+		}
240
+	}
241
+
242
+	/**
243
+	 * Returns a VEVENT for the current iterations of the event.
244
+	 *
245
+	 * This VEVENT will have a recurrence id, and its DTSTART and DTEND
246
+	 * altered.
247
+	 *
248
+	 * @return VEvent
249
+	 */
250
+	public function getEventObject()
251
+	{
252
+		if ($this->currentOverriddenEvent) {
253
+			return $this->currentOverriddenEvent;
254
+		}
255
+
256
+		$event = clone $this->masterEvent;
257
+
258
+		// Ignoring the following block, because PHPUnit's code coverage
259
+		// ignores most of these lines, and this messes with our stats.
260
+		//
261
+		// @codeCoverageIgnoreStart
262
+		unset(
263
+			$event->RRULE,
264
+			$event->EXDATE,
265
+			$event->RDATE,
266
+			$event->EXRULE,
267
+			$event->{'RECURRENCE-ID'}
268
+		);
269
+		// @codeCoverageIgnoreEnd
270
+
271
+		$event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating());
272
+		if (isset($event->DTEND)) {
273
+			$event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating());
274
+		}
275
+		$recurid = clone $event->DTSTART;
276
+		$recurid->name = 'RECURRENCE-ID';
277
+		$event->add($recurid);
278
+
279
+		return $event;
280
+	}
281
+
282
+	/**
283
+	 * Returns the current position of the iterator.
284
+	 *
285
+	 * This is for us simply a 0-based index.
286
+	 *
287
+	 * @return int
288
+	 */
289
+	#[\ReturnTypeWillChange]
290
+	public function key()
291
+	{
292
+		// The counter is always 1 ahead.
293
+		return $this->counter - 1;
294
+	}
295
+
296
+	/**
297
+	 * This is called after next, to see if the iterator is still at a valid
298
+	 * position, or if it's at the end.
299
+	 *
300
+	 * @return bool
301
+	 */
302
+	#[\ReturnTypeWillChange]
303
+	public function valid()
304
+	{
305
+		if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) {
306
+			throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences);
307
+		}
308
+
309
+		return (bool) $this->currentDate;
310
+	}
311
+
312
+	/**
313
+	 * Sets the iterator back to the starting point.
314
+	 *
315
+	 * @return void
316
+	 */
317
+	#[\ReturnTypeWillChange]
318
+	public function rewind()
319
+	{
320
+		$this->recurIterator->rewind();
321
+		// re-creating overridden event index.
322
+		$index = [];
323
+		foreach ($this->overriddenEvents as $key => $event) {
324
+			$stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
325
+			$index[$stamp][] = $key;
326
+		}
327
+		krsort($index);
328
+		$this->counter = 0;
329
+		$this->overriddenEventsIndex = $index;
330
+		$this->currentOverriddenEvent = null;
331
+
332
+		$this->nextDate = null;
333
+		$this->currentDate = clone $this->startDate;
334
+
335
+		$this->next();
336
+	}
337
+
338
+	/**
339
+	 * Advances the iterator with one step.
340
+	 *
341
+	 * @return void
342
+	 */
343
+	#[\ReturnTypeWillChange]
344
+	public function next()
345
+	{
346
+		$this->currentOverriddenEvent = null;
347
+		++$this->counter;
348
+		if ($this->nextDate) {
349
+			// We had a stored value.
350
+			$nextDate = $this->nextDate;
351
+			$this->nextDate = null;
352
+		} else {
353
+			// We need to ask rruleparser for the next date.
354
+			// We need to do this until we find a date that's not in the
355
+			// exception list.
356
+			do {
357
+				if (!$this->recurIterator->valid()) {
358
+					$nextDate = null;
359
+					break;
360
+				}
361
+				$nextDate = $this->recurIterator->current();
362
+				$this->recurIterator->next();
363
+			} while (isset($this->exceptions[$nextDate->getTimeStamp()]));
364
+		}
365
+
366
+		// $nextDate now contains what rrule thinks is the next one, but an
367
+		// overridden event may cut ahead.
368
+		if ($this->overriddenEventsIndex) {
369
+			$offsets = end($this->overriddenEventsIndex);
370
+			$timestamp = key($this->overriddenEventsIndex);
371
+			$offset = end($offsets);
372
+			if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
373
+				// Overridden event comes first.
374
+				$this->currentOverriddenEvent = $this->overriddenEvents[$offset];
375
+
376
+				// Putting the rrule next date aside.
377
+				$this->nextDate = $nextDate;
378
+				$this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
379
+
380
+				// Ensuring that this item will only be used once.
381
+				array_pop($this->overriddenEventsIndex[$timestamp]);
382
+				if (!$this->overriddenEventsIndex[$timestamp]) {
383
+					array_pop($this->overriddenEventsIndex);
384
+				}
385
+
386
+				// Exit point!
387
+				return;
388
+			}
389
+		}
390
+
391
+		$this->currentDate = $nextDate;
392
+	}
393
+
394
+	/**
395
+	 * Quickly jump to a date in the future.
396
+	 */
397
+	public function fastForward(DateTimeInterface $dateTime)
398
+	{
399
+		while ($this->valid() && $this->getDtEnd() <= $dateTime) {
400
+			$this->next();
401
+		}
402
+	}
403
+
404
+	/**
405
+	 * Returns true if this recurring event never ends.
406
+	 *
407
+	 * @return bool
408
+	 */
409
+	public function isInfinite()
410
+	{
411
+		return $this->recurIterator->isInfinite();
412
+	}
413
+
414
+	/**
415
+	 * RRULE parser.
416
+	 *
417
+	 * @var RRuleIterator
418
+	 */
419
+	protected $recurIterator;
420
+
421
+	/**
422
+	 * The duration, in seconds, of the master event.
423
+	 *
424
+	 * We use this to calculate the DTEND for subsequent events.
425
+	 */
426
+	protected $eventDuration;
427
+
428
+	/**
429
+	 * A reference to the main (master) event.
430
+	 *
431
+	 * @var VEVENT
432
+	 */
433
+	protected $masterEvent;
434
+
435
+	/**
436
+	 * List of overridden events.
437
+	 *
438
+	 * @var array
439
+	 */
440
+	protected $overriddenEvents = [];
441
+
442
+	/**
443
+	 * Overridden event index.
444
+	 *
445
+	 * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent
446
+	 * property.
447
+	 *
448
+	 * @var array
449
+	 */
450
+	protected $overriddenEventsIndex;
451
+
452
+	/**
453
+	 * A list of recurrence-id's that are either part of EXDATE, or are
454
+	 * overridden.
455
+	 *
456
+	 * @var array
457
+	 */
458
+	protected $exceptions = [];
459
+
460
+	/**
461
+	 * Internal event counter.
462
+	 *
463
+	 * @var int
464
+	 */
465
+	protected $counter;
466
+
467
+	/**
468
+	 * The very start of the iteration process.
469
+	 *
470
+	 * @var DateTimeImmutable
471
+	 */
472
+	protected $startDate;
473
+
474
+	/**
475
+	 * Where we are currently in the iteration process.
476
+	 *
477
+	 * @var DateTimeImmutable
478
+	 */
479
+	protected $currentDate;
480
+
481
+	/**
482
+	 * The next date from the rrule parser.
483
+	 *
484
+	 * Sometimes we need to temporary store the next date, because an
485
+	 * overridden event came before.
486
+	 *
487
+	 * @var DateTimeImmutable
488
+	 */
489
+	protected $nextDate;
490
+
491
+	/**
492
+	 * The event that overwrites the current iteration.
493
+	 *
494
+	 * @var VEVENT
495
+	 */
496
+	protected $currentOverriddenEvent;
497 497
 }
Please login to merge, or discard this patch.