Completed
Branch develop (a11c59)
by
unknown
24:05
created
htdocs/includes/sabre/sabre/vobject/lib/Component.php 2 patches
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -237,7 +237,7 @@  discard block
 block discarded – undo
237 237
                 // more.
238 238
                 return array_filter(
239 239
                     $result,
240
-                    function ($child) use ($group) {
240
+                    function($child) use ($group) {
241 241
                         return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
242 242
                     }
243 243
                 );
@@ -282,7 +282,7 @@  discard block
 block discarded – undo
282 282
          *
283 283
          * @return int
284 284
          */
285
-        $sortScore = function ($key, $array) {
285
+        $sortScore = function($key, $array) {
286 286
             if ($array[$key] instanceof Component) {
287 287
                 // We want to encode VTIMEZONE first, this is a personal
288 288
                 // preference.
@@ -317,7 +317,7 @@  discard block
 block discarded – undo
317 317
         $tmp = $children;
318 318
         uksort(
319 319
             $children,
320
-            function ($a, $b) use ($sortScore, $tmp) {
320
+            function($a, $b) use ($sortScore, $tmp) {
321 321
                 $sA = $sortScore($a, $tmp);
322 322
                 $sB = $sortScore($b, $tmp);
323 323
 
Please login to merge, or discard this patch.
Indentation   +653 added lines, -653 removed lines patch added patch discarded remove patch
@@ -16,657 +16,657 @@
 block discarded – undo
16 16
  */
17 17
 class Component extends Node
18 18
 {
19
-    /**
20
-     * Component name.
21
-     *
22
-     * This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.
23
-     *
24
-     * @var string
25
-     */
26
-    public $name;
27
-
28
-    /**
29
-     * A list of properties and/or sub-components.
30
-     *
31
-     * @var array<string, Component|Property>
32
-     */
33
-    protected $children = [];
34
-
35
-    /**
36
-     * Creates a new component.
37
-     *
38
-     * You can specify the children either in key=>value syntax, in which case
39
-     * properties will automatically be created, or you can just pass a list of
40
-     * Component and Property object.
41
-     *
42
-     * By default, a set of sensible values will be added to the component. For
43
-     * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
44
-     * ensure that this does not happen, set $defaults to false.
45
-     *
46
-     * @param string|null $name     such as VCALENDAR, VEVENT
47
-     * @param bool        $defaults
48
-     */
49
-    public function __construct(Document $root, $name, array $children = [], $defaults = true)
50
-    {
51
-        $this->name = isset($name) ? strtoupper($name) : '';
52
-        $this->root = $root;
53
-
54
-        if ($defaults) {
55
-            // This is a terribly convoluted way to do this, but this ensures
56
-            // that the order of properties as they are specified in both
57
-            // defaults and the childrens list, are inserted in the object in a
58
-            // natural way.
59
-            $list = $this->getDefaults();
60
-            $nodes = [];
61
-            foreach ($children as $key => $value) {
62
-                if ($value instanceof Node) {
63
-                    if (isset($list[$value->name])) {
64
-                        unset($list[$value->name]);
65
-                    }
66
-                    $nodes[] = $value;
67
-                } else {
68
-                    $list[$key] = $value;
69
-                }
70
-            }
71
-            foreach ($list as $key => $value) {
72
-                $this->add($key, $value);
73
-            }
74
-            foreach ($nodes as $node) {
75
-                $this->add($node);
76
-            }
77
-        } else {
78
-            foreach ($children as $k => $child) {
79
-                if ($child instanceof Node) {
80
-                    // Component or Property
81
-                    $this->add($child);
82
-                } else {
83
-                    // Property key=>value
84
-                    $this->add($k, $child);
85
-                }
86
-            }
87
-        }
88
-    }
89
-
90
-    /**
91
-     * Adds a new property or component, and returns the new item.
92
-     *
93
-     * This method has 3 possible signatures:
94
-     *
95
-     * add(Component $comp) // Adds a new component
96
-     * add(Property $prop)  // Adds a new property
97
-     * add($name, $value, array $parameters = []) // Adds a new property
98
-     * add($name, array $children = []) // Adds a new component
99
-     * by name.
100
-     *
101
-     * @return Node
102
-     */
103
-    public function add()
104
-    {
105
-        $arguments = func_get_args();
106
-
107
-        if ($arguments[0] instanceof Node) {
108
-            if (isset($arguments[1])) {
109
-                throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
110
-            }
111
-            $arguments[0]->parent = $this;
112
-            $newNode = $arguments[0];
113
-        } elseif (is_string($arguments[0])) {
114
-            $newNode = call_user_func_array([$this->root, 'create'], $arguments);
115
-        } else {
116
-            throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
117
-        }
118
-
119
-        $name = $newNode->name;
120
-        if (isset($this->children[$name])) {
121
-            $this->children[$name][] = $newNode;
122
-        } else {
123
-            $this->children[$name] = [$newNode];
124
-        }
125
-
126
-        return $newNode;
127
-    }
128
-
129
-    /**
130
-     * This method removes a component or property from this component.
131
-     *
132
-     * You can either specify the item by name (like DTSTART), in which case
133
-     * all properties/components with that name will be removed, or you can
134
-     * pass an instance of a property or component, in which case only that
135
-     * exact item will be removed.
136
-     *
137
-     * @param string|Property|Component $item
138
-     */
139
-    public function remove($item)
140
-    {
141
-        if (is_string($item)) {
142
-            // If there's no dot in the name, it's an exact property name and
143
-            // we can just wipe out all those properties.
144
-            //
145
-            if (false === strpos($item, '.')) {
146
-                unset($this->children[strtoupper($item)]);
147
-
148
-                return;
149
-            }
150
-            // If there was a dot, we need to ask select() to help us out and
151
-            // then we just call remove recursively.
152
-            foreach ($this->select($item) as $child) {
153
-                $this->remove($child);
154
-            }
155
-        } else {
156
-            foreach ($this->select($item->name) as $k => $child) {
157
-                if ($child === $item) {
158
-                    unset($this->children[$item->name][$k]);
159
-
160
-                    return;
161
-                }
162
-            }
163
-
164
-            throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
165
-        }
166
-    }
167
-
168
-    /**
169
-     * Returns a flat list of all the properties and components in this
170
-     * component.
171
-     *
172
-     * @return array
173
-     */
174
-    public function children()
175
-    {
176
-        $result = [];
177
-        foreach ($this->children as $childGroup) {
178
-            $result = array_merge($result, $childGroup);
179
-        }
180
-
181
-        return $result;
182
-    }
183
-
184
-    /**
185
-     * This method only returns a list of sub-components. Properties are
186
-     * ignored.
187
-     *
188
-     * @return array
189
-     */
190
-    public function getComponents()
191
-    {
192
-        $result = [];
193
-
194
-        foreach ($this->children as $childGroup) {
195
-            foreach ($childGroup as $child) {
196
-                if ($child instanceof self) {
197
-                    $result[] = $child;
198
-                }
199
-            }
200
-        }
201
-
202
-        return $result;
203
-    }
204
-
205
-    /**
206
-     * Returns an array with elements that match the specified name.
207
-     *
208
-     * This function is also aware of MIME-Directory groups (as they appear in
209
-     * vcards). This means that if a property is grouped as "HOME.EMAIL", it
210
-     * will also be returned when searching for just "EMAIL". If you want to
211
-     * search for a property in a specific group, you can select on the entire
212
-     * string ("HOME.EMAIL"). If you want to search on a specific property that
213
-     * has not been assigned a group, specify ".EMAIL".
214
-     *
215
-     * @param string $name
216
-     *
217
-     * @return array
218
-     */
219
-    public function select($name)
220
-    {
221
-        $group = null;
222
-        $name = strtoupper($name);
223
-        if (false !== strpos($name, '.')) {
224
-            list($group, $name) = explode('.', $name, 2);
225
-        }
226
-        if ('' === $name) {
227
-            $name = null;
228
-        }
229
-
230
-        if (!is_null($name)) {
231
-            $result = isset($this->children[$name]) ? $this->children[$name] : [];
232
-
233
-            if (is_null($group)) {
234
-                return $result;
235
-            } else {
236
-                // If we have a group filter as well, we need to narrow it down
237
-                // more.
238
-                return array_filter(
239
-                    $result,
240
-                    function ($child) use ($group) {
241
-                        return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
242
-                    }
243
-                );
244
-            }
245
-        }
246
-
247
-        // If we got to this point, it means there was no 'name' specified for
248
-        // searching, implying that this is a group-only search.
249
-        $result = [];
250
-        foreach ($this->children as $childGroup) {
251
-            foreach ($childGroup as $child) {
252
-                if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) {
253
-                    $result[] = $child;
254
-                }
255
-            }
256
-        }
257
-
258
-        return $result;
259
-    }
260
-
261
-    /**
262
-     * Turns the object back into a serialized blob.
263
-     *
264
-     * @return string
265
-     */
266
-    public function serialize()
267
-    {
268
-        $str = 'BEGIN:'.$this->name."\r\n";
269
-
270
-        /**
271
-         * Gives a component a 'score' for sorting purposes.
272
-         *
273
-         * This is solely used by the childrenSort method.
274
-         *
275
-         * A higher score means the item will be lower in the list.
276
-         * To avoid score collisions, each "score category" has a reasonable
277
-         * space to accommodate elements. The $key is added to the $score to
278
-         * preserve the original relative order of elements.
279
-         *
280
-         * @param int   $key
281
-         * @param array $array
282
-         *
283
-         * @return int
284
-         */
285
-        $sortScore = function ($key, $array) {
286
-            if ($array[$key] instanceof Component) {
287
-                // We want to encode VTIMEZONE first, this is a personal
288
-                // preference.
289
-                if ('VTIMEZONE' === $array[$key]->name) {
290
-                    $score = 300000000;
291
-
292
-                    return $score + $key;
293
-                } else {
294
-                    $score = 400000000;
295
-
296
-                    return $score + $key;
297
-                }
298
-            } else {
299
-                // Properties get encoded first
300
-                // VCARD version 4.0 wants the VERSION property to appear first
301
-                if ($array[$key] instanceof Property) {
302
-                    if ('VERSION' === $array[$key]->name) {
303
-                        $score = 100000000;
304
-
305
-                        return $score + $key;
306
-                    } else {
307
-                        // All other properties
308
-                        $score = 200000000;
309
-
310
-                        return $score + $key;
311
-                    }
312
-                }
313
-            }
314
-        };
315
-
316
-        $children = $this->children();
317
-        $tmp = $children;
318
-        uksort(
319
-            $children,
320
-            function ($a, $b) use ($sortScore, $tmp) {
321
-                $sA = $sortScore($a, $tmp);
322
-                $sB = $sortScore($b, $tmp);
323
-
324
-                return $sA - $sB;
325
-            }
326
-        );
327
-
328
-        foreach ($children as $child) {
329
-            $str .= $child->serialize();
330
-        }
331
-        $str .= 'END:'.$this->name."\r\n";
332
-
333
-        return $str;
334
-    }
335
-
336
-    /**
337
-     * This method returns an array, with the representation as it should be
338
-     * encoded in JSON. This is used to create jCard or jCal documents.
339
-     *
340
-     * @return array
341
-     */
342
-    #[\ReturnTypeWillChange]
343
-    public function jsonSerialize()
344
-    {
345
-        $components = [];
346
-        $properties = [];
347
-
348
-        foreach ($this->children as $childGroup) {
349
-            foreach ($childGroup as $child) {
350
-                if ($child instanceof self) {
351
-                    $components[] = $child->jsonSerialize();
352
-                } else {
353
-                    $properties[] = $child->jsonSerialize();
354
-                }
355
-            }
356
-        }
357
-
358
-        return [
359
-            strtolower($this->name),
360
-            $properties,
361
-            $components,
362
-        ];
363
-    }
364
-
365
-    /**
366
-     * This method serializes the data into XML. This is used to create xCard or
367
-     * xCal documents.
368
-     *
369
-     * @param Xml\Writer $writer XML writer
370
-     */
371
-    public function xmlSerialize(Xml\Writer $writer): void
372
-    {
373
-        $components = [];
374
-        $properties = [];
375
-
376
-        foreach ($this->children as $childGroup) {
377
-            foreach ($childGroup as $child) {
378
-                if ($child instanceof self) {
379
-                    $components[] = $child;
380
-                } else {
381
-                    $properties[] = $child;
382
-                }
383
-            }
384
-        }
385
-
386
-        $writer->startElement(strtolower($this->name));
387
-
388
-        if (!empty($properties)) {
389
-            $writer->startElement('properties');
390
-
391
-            foreach ($properties as $property) {
392
-                $property->xmlSerialize($writer);
393
-            }
394
-
395
-            $writer->endElement();
396
-        }
397
-
398
-        if (!empty($components)) {
399
-            $writer->startElement('components');
400
-
401
-            foreach ($components as $component) {
402
-                $component->xmlSerialize($writer);
403
-            }
404
-
405
-            $writer->endElement();
406
-        }
407
-
408
-        $writer->endElement();
409
-    }
410
-
411
-    /**
412
-     * This method should return a list of default property values.
413
-     *
414
-     * @return array
415
-     */
416
-    protected function getDefaults()
417
-    {
418
-        return [];
419
-    }
420
-
421
-    /* Magic property accessors {{{ */
422
-
423
-    /**
424
-     * Using 'get' you will either get a property or component.
425
-     *
426
-     * If there were no child-elements found with the specified name,
427
-     * null is returned.
428
-     *
429
-     * To use this, this may look something like this:
430
-     *
431
-     * $event = $calendar->VEVENT;
432
-     *
433
-     * @param string $name
434
-     *
435
-     * @return Property|null
436
-     */
437
-    public function __get($name)
438
-    {
439
-        if ('children' === $name) {
440
-            throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead');
441
-        }
442
-
443
-        $matches = $this->select($name);
444
-        if (0 === count($matches)) {
445
-            return;
446
-        } else {
447
-            $firstMatch = current($matches);
448
-            /* @var $firstMatch Property */
449
-            $firstMatch->setIterator(new ElementList(array_values($matches)));
450
-
451
-            return $firstMatch;
452
-        }
453
-    }
454
-
455
-    /**
456
-     * This method checks if a sub-element with the specified name exists.
457
-     *
458
-     * @param string $name
459
-     *
460
-     * @return bool
461
-     */
462
-    public function __isset($name)
463
-    {
464
-        $matches = $this->select($name);
465
-
466
-        return count($matches) > 0;
467
-    }
468
-
469
-    /**
470
-     * Using the setter method you can add properties or subcomponents.
471
-     *
472
-     * You can either pass a Component, Property
473
-     * object, or a string to automatically create a Property.
474
-     *
475
-     * If the item already exists, it will be removed. If you want to add
476
-     * a new item with the same name, always use the add() method.
477
-     *
478
-     * @param string $name
479
-     * @param mixed  $value
480
-     */
481
-    public function __set($name, $value)
482
-    {
483
-        $name = strtoupper($name);
484
-        $this->remove($name);
485
-        if ($value instanceof self || $value instanceof Property) {
486
-            $this->add($value);
487
-        } else {
488
-            $this->add($name, $value);
489
-        }
490
-    }
491
-
492
-    /**
493
-     * Removes all properties and components within this component with the
494
-     * specified name.
495
-     *
496
-     * @param string $name
497
-     */
498
-    public function __unset($name)
499
-    {
500
-        $this->remove($name);
501
-    }
502
-
503
-    /* }}} */
504
-
505
-    /**
506
-     * This method is automatically called when the object is cloned.
507
-     * Specifically, this will ensure all child elements are also cloned.
508
-     */
509
-    public function __clone()
510
-    {
511
-        foreach ($this->children as $childName => $childGroup) {
512
-            foreach ($childGroup as $key => $child) {
513
-                $clonedChild = clone $child;
514
-                $clonedChild->parent = $this;
515
-                $clonedChild->root = $this->root;
516
-                $this->children[$childName][$key] = $clonedChild;
517
-            }
518
-        }
519
-    }
520
-
521
-    /**
522
-     * A simple list of validation rules.
523
-     *
524
-     * This is simply a list of properties, and how many times they either
525
-     * must or must not appear.
526
-     *
527
-     * Possible values per property:
528
-     *   * 0 - Must not appear.
529
-     *   * 1 - Must appear exactly once.
530
-     *   * + - Must appear at least once.
531
-     *   * * - Can appear any number of times.
532
-     *   * ? - May appear, but not more than once.
533
-     *
534
-     * It is also possible to specify defaults and severity levels for
535
-     * violating the rule.
536
-     *
537
-     * See the VEVENT implementation for getValidationRules for a more complex
538
-     * example.
539
-     *
540
-     * @var array
541
-     */
542
-    public function getValidationRules()
543
-    {
544
-        return [];
545
-    }
546
-
547
-    /**
548
-     * Validates the node for correctness.
549
-     *
550
-     * The following options are supported:
551
-     *   Node::REPAIR - May attempt to automatically repair the problem.
552
-     *   Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
553
-     *   Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
554
-     *
555
-     * This method returns an array with detected problems.
556
-     * Every element has the following properties:
557
-     *
558
-     *  * level - problem level.
559
-     *  * message - A human-readable string describing the issue.
560
-     *  * node - A reference to the problematic node.
561
-     *
562
-     * The level means:
563
-     *   1 - The issue was repaired (only happens if REPAIR was turned on).
564
-     *   2 - A warning.
565
-     *   3 - An error.
566
-     *
567
-     * @param int $options
568
-     *
569
-     * @return array
570
-     */
571
-    public function validate($options = 0)
572
-    {
573
-        $rules = $this->getValidationRules();
574
-        $defaults = $this->getDefaults();
575
-
576
-        $propertyCounters = [];
577
-
578
-        $messages = [];
579
-
580
-        foreach ($this->children() as $child) {
581
-            $name = strtoupper($child->name);
582
-            if (!isset($propertyCounters[$name])) {
583
-                $propertyCounters[$name] = 1;
584
-            } else {
585
-                ++$propertyCounters[$name];
586
-            }
587
-            $messages = array_merge($messages, $child->validate($options));
588
-        }
589
-
590
-        foreach ($rules as $propName => $rule) {
591
-            switch ($rule) {
592
-                case '0':
593
-                    if (isset($propertyCounters[$propName])) {
594
-                        $messages[] = [
595
-                            'level' => 3,
596
-                            'message' => $propName.' MUST NOT appear in a '.$this->name.' component',
597
-                            'node' => $this,
598
-                        ];
599
-                    }
600
-                    break;
601
-                case '1':
602
-                    if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) {
603
-                        $repaired = false;
604
-                        if ($options & self::REPAIR && isset($defaults[$propName])) {
605
-                            $this->add($propName, $defaults[$propName]);
606
-                            $repaired = true;
607
-                        }
608
-                        $messages[] = [
609
-                            'level' => $repaired ? 1 : 3,
610
-                            'message' => $propName.' MUST appear exactly once in a '.$this->name.' component',
611
-                            'node' => $this,
612
-                        ];
613
-                    }
614
-                    break;
615
-                case '+':
616
-                    if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) {
617
-                        $messages[] = [
618
-                            'level' => 3,
619
-                            'message' => $propName.' MUST appear at least once in a '.$this->name.' component',
620
-                            'node' => $this,
621
-                        ];
622
-                    }
623
-                    break;
624
-                case '*':
625
-                    break;
626
-                case '?':
627
-                    if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) {
628
-                        $level = 3;
629
-
630
-                        // We try to repair the same property appearing multiple times with the exact same value
631
-                        // by removing the duplicates and keeping only one property
632
-                        if ($options & self::REPAIR) {
633
-                            $properties = array_unique($this->select($propName), SORT_REGULAR);
634
-
635
-                            if (1 === count($properties)) {
636
-                                $this->remove($propName);
637
-                                $this->add($properties[0]);
638
-
639
-                                $level = 1;
640
-                            }
641
-                        }
642
-
643
-                        $messages[] = [
644
-                            'level' => $level,
645
-                            'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component',
646
-                            'node' => $this,
647
-                        ];
648
-                    }
649
-                    break;
650
-            }
651
-        }
652
-
653
-        return $messages;
654
-    }
655
-
656
-    /**
657
-     * Call this method on a document if you're done using it.
658
-     *
659
-     * It's intended to remove all circular references, so PHP can easily clean
660
-     * it up.
661
-     */
662
-    public function destroy()
663
-    {
664
-        parent::destroy();
665
-        foreach ($this->children as $childGroup) {
666
-            foreach ($childGroup as $child) {
667
-                $child->destroy();
668
-            }
669
-        }
670
-        $this->children = [];
671
-    }
19
+	/**
20
+	 * Component name.
21
+	 *
22
+	 * This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.
23
+	 *
24
+	 * @var string
25
+	 */
26
+	public $name;
27
+
28
+	/**
29
+	 * A list of properties and/or sub-components.
30
+	 *
31
+	 * @var array<string, Component|Property>
32
+	 */
33
+	protected $children = [];
34
+
35
+	/**
36
+	 * Creates a new component.
37
+	 *
38
+	 * You can specify the children either in key=>value syntax, in which case
39
+	 * properties will automatically be created, or you can just pass a list of
40
+	 * Component and Property object.
41
+	 *
42
+	 * By default, a set of sensible values will be added to the component. For
43
+	 * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
44
+	 * ensure that this does not happen, set $defaults to false.
45
+	 *
46
+	 * @param string|null $name     such as VCALENDAR, VEVENT
47
+	 * @param bool        $defaults
48
+	 */
49
+	public function __construct(Document $root, $name, array $children = [], $defaults = true)
50
+	{
51
+		$this->name = isset($name) ? strtoupper($name) : '';
52
+		$this->root = $root;
53
+
54
+		if ($defaults) {
55
+			// This is a terribly convoluted way to do this, but this ensures
56
+			// that the order of properties as they are specified in both
57
+			// defaults and the childrens list, are inserted in the object in a
58
+			// natural way.
59
+			$list = $this->getDefaults();
60
+			$nodes = [];
61
+			foreach ($children as $key => $value) {
62
+				if ($value instanceof Node) {
63
+					if (isset($list[$value->name])) {
64
+						unset($list[$value->name]);
65
+					}
66
+					$nodes[] = $value;
67
+				} else {
68
+					$list[$key] = $value;
69
+				}
70
+			}
71
+			foreach ($list as $key => $value) {
72
+				$this->add($key, $value);
73
+			}
74
+			foreach ($nodes as $node) {
75
+				$this->add($node);
76
+			}
77
+		} else {
78
+			foreach ($children as $k => $child) {
79
+				if ($child instanceof Node) {
80
+					// Component or Property
81
+					$this->add($child);
82
+				} else {
83
+					// Property key=>value
84
+					$this->add($k, $child);
85
+				}
86
+			}
87
+		}
88
+	}
89
+
90
+	/**
91
+	 * Adds a new property or component, and returns the new item.
92
+	 *
93
+	 * This method has 3 possible signatures:
94
+	 *
95
+	 * add(Component $comp) // Adds a new component
96
+	 * add(Property $prop)  // Adds a new property
97
+	 * add($name, $value, array $parameters = []) // Adds a new property
98
+	 * add($name, array $children = []) // Adds a new component
99
+	 * by name.
100
+	 *
101
+	 * @return Node
102
+	 */
103
+	public function add()
104
+	{
105
+		$arguments = func_get_args();
106
+
107
+		if ($arguments[0] instanceof Node) {
108
+			if (isset($arguments[1])) {
109
+				throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
110
+			}
111
+			$arguments[0]->parent = $this;
112
+			$newNode = $arguments[0];
113
+		} elseif (is_string($arguments[0])) {
114
+			$newNode = call_user_func_array([$this->root, 'create'], $arguments);
115
+		} else {
116
+			throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
117
+		}
118
+
119
+		$name = $newNode->name;
120
+		if (isset($this->children[$name])) {
121
+			$this->children[$name][] = $newNode;
122
+		} else {
123
+			$this->children[$name] = [$newNode];
124
+		}
125
+
126
+		return $newNode;
127
+	}
128
+
129
+	/**
130
+	 * This method removes a component or property from this component.
131
+	 *
132
+	 * You can either specify the item by name (like DTSTART), in which case
133
+	 * all properties/components with that name will be removed, or you can
134
+	 * pass an instance of a property or component, in which case only that
135
+	 * exact item will be removed.
136
+	 *
137
+	 * @param string|Property|Component $item
138
+	 */
139
+	public function remove($item)
140
+	{
141
+		if (is_string($item)) {
142
+			// If there's no dot in the name, it's an exact property name and
143
+			// we can just wipe out all those properties.
144
+			//
145
+			if (false === strpos($item, '.')) {
146
+				unset($this->children[strtoupper($item)]);
147
+
148
+				return;
149
+			}
150
+			// If there was a dot, we need to ask select() to help us out and
151
+			// then we just call remove recursively.
152
+			foreach ($this->select($item) as $child) {
153
+				$this->remove($child);
154
+			}
155
+		} else {
156
+			foreach ($this->select($item->name) as $k => $child) {
157
+				if ($child === $item) {
158
+					unset($this->children[$item->name][$k]);
159
+
160
+					return;
161
+				}
162
+			}
163
+
164
+			throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
165
+		}
166
+	}
167
+
168
+	/**
169
+	 * Returns a flat list of all the properties and components in this
170
+	 * component.
171
+	 *
172
+	 * @return array
173
+	 */
174
+	public function children()
175
+	{
176
+		$result = [];
177
+		foreach ($this->children as $childGroup) {
178
+			$result = array_merge($result, $childGroup);
179
+		}
180
+
181
+		return $result;
182
+	}
183
+
184
+	/**
185
+	 * This method only returns a list of sub-components. Properties are
186
+	 * ignored.
187
+	 *
188
+	 * @return array
189
+	 */
190
+	public function getComponents()
191
+	{
192
+		$result = [];
193
+
194
+		foreach ($this->children as $childGroup) {
195
+			foreach ($childGroup as $child) {
196
+				if ($child instanceof self) {
197
+					$result[] = $child;
198
+				}
199
+			}
200
+		}
201
+
202
+		return $result;
203
+	}
204
+
205
+	/**
206
+	 * Returns an array with elements that match the specified name.
207
+	 *
208
+	 * This function is also aware of MIME-Directory groups (as they appear in
209
+	 * vcards). This means that if a property is grouped as "HOME.EMAIL", it
210
+	 * will also be returned when searching for just "EMAIL". If you want to
211
+	 * search for a property in a specific group, you can select on the entire
212
+	 * string ("HOME.EMAIL"). If you want to search on a specific property that
213
+	 * has not been assigned a group, specify ".EMAIL".
214
+	 *
215
+	 * @param string $name
216
+	 *
217
+	 * @return array
218
+	 */
219
+	public function select($name)
220
+	{
221
+		$group = null;
222
+		$name = strtoupper($name);
223
+		if (false !== strpos($name, '.')) {
224
+			list($group, $name) = explode('.', $name, 2);
225
+		}
226
+		if ('' === $name) {
227
+			$name = null;
228
+		}
229
+
230
+		if (!is_null($name)) {
231
+			$result = isset($this->children[$name]) ? $this->children[$name] : [];
232
+
233
+			if (is_null($group)) {
234
+				return $result;
235
+			} else {
236
+				// If we have a group filter as well, we need to narrow it down
237
+				// more.
238
+				return array_filter(
239
+					$result,
240
+					function ($child) use ($group) {
241
+						return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
242
+					}
243
+				);
244
+			}
245
+		}
246
+
247
+		// If we got to this point, it means there was no 'name' specified for
248
+		// searching, implying that this is a group-only search.
249
+		$result = [];
250
+		foreach ($this->children as $childGroup) {
251
+			foreach ($childGroup as $child) {
252
+				if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) {
253
+					$result[] = $child;
254
+				}
255
+			}
256
+		}
257
+
258
+		return $result;
259
+	}
260
+
261
+	/**
262
+	 * Turns the object back into a serialized blob.
263
+	 *
264
+	 * @return string
265
+	 */
266
+	public function serialize()
267
+	{
268
+		$str = 'BEGIN:'.$this->name."\r\n";
269
+
270
+		/**
271
+		 * Gives a component a 'score' for sorting purposes.
272
+		 *
273
+		 * This is solely used by the childrenSort method.
274
+		 *
275
+		 * A higher score means the item will be lower in the list.
276
+		 * To avoid score collisions, each "score category" has a reasonable
277
+		 * space to accommodate elements. The $key is added to the $score to
278
+		 * preserve the original relative order of elements.
279
+		 *
280
+		 * @param int   $key
281
+		 * @param array $array
282
+		 *
283
+		 * @return int
284
+		 */
285
+		$sortScore = function ($key, $array) {
286
+			if ($array[$key] instanceof Component) {
287
+				// We want to encode VTIMEZONE first, this is a personal
288
+				// preference.
289
+				if ('VTIMEZONE' === $array[$key]->name) {
290
+					$score = 300000000;
291
+
292
+					return $score + $key;
293
+				} else {
294
+					$score = 400000000;
295
+
296
+					return $score + $key;
297
+				}
298
+			} else {
299
+				// Properties get encoded first
300
+				// VCARD version 4.0 wants the VERSION property to appear first
301
+				if ($array[$key] instanceof Property) {
302
+					if ('VERSION' === $array[$key]->name) {
303
+						$score = 100000000;
304
+
305
+						return $score + $key;
306
+					} else {
307
+						// All other properties
308
+						$score = 200000000;
309
+
310
+						return $score + $key;
311
+					}
312
+				}
313
+			}
314
+		};
315
+
316
+		$children = $this->children();
317
+		$tmp = $children;
318
+		uksort(
319
+			$children,
320
+			function ($a, $b) use ($sortScore, $tmp) {
321
+				$sA = $sortScore($a, $tmp);
322
+				$sB = $sortScore($b, $tmp);
323
+
324
+				return $sA - $sB;
325
+			}
326
+		);
327
+
328
+		foreach ($children as $child) {
329
+			$str .= $child->serialize();
330
+		}
331
+		$str .= 'END:'.$this->name."\r\n";
332
+
333
+		return $str;
334
+	}
335
+
336
+	/**
337
+	 * This method returns an array, with the representation as it should be
338
+	 * encoded in JSON. This is used to create jCard or jCal documents.
339
+	 *
340
+	 * @return array
341
+	 */
342
+	#[\ReturnTypeWillChange]
343
+	public function jsonSerialize()
344
+	{
345
+		$components = [];
346
+		$properties = [];
347
+
348
+		foreach ($this->children as $childGroup) {
349
+			foreach ($childGroup as $child) {
350
+				if ($child instanceof self) {
351
+					$components[] = $child->jsonSerialize();
352
+				} else {
353
+					$properties[] = $child->jsonSerialize();
354
+				}
355
+			}
356
+		}
357
+
358
+		return [
359
+			strtolower($this->name),
360
+			$properties,
361
+			$components,
362
+		];
363
+	}
364
+
365
+	/**
366
+	 * This method serializes the data into XML. This is used to create xCard or
367
+	 * xCal documents.
368
+	 *
369
+	 * @param Xml\Writer $writer XML writer
370
+	 */
371
+	public function xmlSerialize(Xml\Writer $writer): void
372
+	{
373
+		$components = [];
374
+		$properties = [];
375
+
376
+		foreach ($this->children as $childGroup) {
377
+			foreach ($childGroup as $child) {
378
+				if ($child instanceof self) {
379
+					$components[] = $child;
380
+				} else {
381
+					$properties[] = $child;
382
+				}
383
+			}
384
+		}
385
+
386
+		$writer->startElement(strtolower($this->name));
387
+
388
+		if (!empty($properties)) {
389
+			$writer->startElement('properties');
390
+
391
+			foreach ($properties as $property) {
392
+				$property->xmlSerialize($writer);
393
+			}
394
+
395
+			$writer->endElement();
396
+		}
397
+
398
+		if (!empty($components)) {
399
+			$writer->startElement('components');
400
+
401
+			foreach ($components as $component) {
402
+				$component->xmlSerialize($writer);
403
+			}
404
+
405
+			$writer->endElement();
406
+		}
407
+
408
+		$writer->endElement();
409
+	}
410
+
411
+	/**
412
+	 * This method should return a list of default property values.
413
+	 *
414
+	 * @return array
415
+	 */
416
+	protected function getDefaults()
417
+	{
418
+		return [];
419
+	}
420
+
421
+	/* Magic property accessors {{{ */
422
+
423
+	/**
424
+	 * Using 'get' you will either get a property or component.
425
+	 *
426
+	 * If there were no child-elements found with the specified name,
427
+	 * null is returned.
428
+	 *
429
+	 * To use this, this may look something like this:
430
+	 *
431
+	 * $event = $calendar->VEVENT;
432
+	 *
433
+	 * @param string $name
434
+	 *
435
+	 * @return Property|null
436
+	 */
437
+	public function __get($name)
438
+	{
439
+		if ('children' === $name) {
440
+			throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead');
441
+		}
442
+
443
+		$matches = $this->select($name);
444
+		if (0 === count($matches)) {
445
+			return;
446
+		} else {
447
+			$firstMatch = current($matches);
448
+			/* @var $firstMatch Property */
449
+			$firstMatch->setIterator(new ElementList(array_values($matches)));
450
+
451
+			return $firstMatch;
452
+		}
453
+	}
454
+
455
+	/**
456
+	 * This method checks if a sub-element with the specified name exists.
457
+	 *
458
+	 * @param string $name
459
+	 *
460
+	 * @return bool
461
+	 */
462
+	public function __isset($name)
463
+	{
464
+		$matches = $this->select($name);
465
+
466
+		return count($matches) > 0;
467
+	}
468
+
469
+	/**
470
+	 * Using the setter method you can add properties or subcomponents.
471
+	 *
472
+	 * You can either pass a Component, Property
473
+	 * object, or a string to automatically create a Property.
474
+	 *
475
+	 * If the item already exists, it will be removed. If you want to add
476
+	 * a new item with the same name, always use the add() method.
477
+	 *
478
+	 * @param string $name
479
+	 * @param mixed  $value
480
+	 */
481
+	public function __set($name, $value)
482
+	{
483
+		$name = strtoupper($name);
484
+		$this->remove($name);
485
+		if ($value instanceof self || $value instanceof Property) {
486
+			$this->add($value);
487
+		} else {
488
+			$this->add($name, $value);
489
+		}
490
+	}
491
+
492
+	/**
493
+	 * Removes all properties and components within this component with the
494
+	 * specified name.
495
+	 *
496
+	 * @param string $name
497
+	 */
498
+	public function __unset($name)
499
+	{
500
+		$this->remove($name);
501
+	}
502
+
503
+	/* }}} */
504
+
505
+	/**
506
+	 * This method is automatically called when the object is cloned.
507
+	 * Specifically, this will ensure all child elements are also cloned.
508
+	 */
509
+	public function __clone()
510
+	{
511
+		foreach ($this->children as $childName => $childGroup) {
512
+			foreach ($childGroup as $key => $child) {
513
+				$clonedChild = clone $child;
514
+				$clonedChild->parent = $this;
515
+				$clonedChild->root = $this->root;
516
+				$this->children[$childName][$key] = $clonedChild;
517
+			}
518
+		}
519
+	}
520
+
521
+	/**
522
+	 * A simple list of validation rules.
523
+	 *
524
+	 * This is simply a list of properties, and how many times they either
525
+	 * must or must not appear.
526
+	 *
527
+	 * Possible values per property:
528
+	 *   * 0 - Must not appear.
529
+	 *   * 1 - Must appear exactly once.
530
+	 *   * + - Must appear at least once.
531
+	 *   * * - Can appear any number of times.
532
+	 *   * ? - May appear, but not more than once.
533
+	 *
534
+	 * It is also possible to specify defaults and severity levels for
535
+	 * violating the rule.
536
+	 *
537
+	 * See the VEVENT implementation for getValidationRules for a more complex
538
+	 * example.
539
+	 *
540
+	 * @var array
541
+	 */
542
+	public function getValidationRules()
543
+	{
544
+		return [];
545
+	}
546
+
547
+	/**
548
+	 * Validates the node for correctness.
549
+	 *
550
+	 * The following options are supported:
551
+	 *   Node::REPAIR - May attempt to automatically repair the problem.
552
+	 *   Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
553
+	 *   Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
554
+	 *
555
+	 * This method returns an array with detected problems.
556
+	 * Every element has the following properties:
557
+	 *
558
+	 *  * level - problem level.
559
+	 *  * message - A human-readable string describing the issue.
560
+	 *  * node - A reference to the problematic node.
561
+	 *
562
+	 * The level means:
563
+	 *   1 - The issue was repaired (only happens if REPAIR was turned on).
564
+	 *   2 - A warning.
565
+	 *   3 - An error.
566
+	 *
567
+	 * @param int $options
568
+	 *
569
+	 * @return array
570
+	 */
571
+	public function validate($options = 0)
572
+	{
573
+		$rules = $this->getValidationRules();
574
+		$defaults = $this->getDefaults();
575
+
576
+		$propertyCounters = [];
577
+
578
+		$messages = [];
579
+
580
+		foreach ($this->children() as $child) {
581
+			$name = strtoupper($child->name);
582
+			if (!isset($propertyCounters[$name])) {
583
+				$propertyCounters[$name] = 1;
584
+			} else {
585
+				++$propertyCounters[$name];
586
+			}
587
+			$messages = array_merge($messages, $child->validate($options));
588
+		}
589
+
590
+		foreach ($rules as $propName => $rule) {
591
+			switch ($rule) {
592
+				case '0':
593
+					if (isset($propertyCounters[$propName])) {
594
+						$messages[] = [
595
+							'level' => 3,
596
+							'message' => $propName.' MUST NOT appear in a '.$this->name.' component',
597
+							'node' => $this,
598
+						];
599
+					}
600
+					break;
601
+				case '1':
602
+					if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) {
603
+						$repaired = false;
604
+						if ($options & self::REPAIR && isset($defaults[$propName])) {
605
+							$this->add($propName, $defaults[$propName]);
606
+							$repaired = true;
607
+						}
608
+						$messages[] = [
609
+							'level' => $repaired ? 1 : 3,
610
+							'message' => $propName.' MUST appear exactly once in a '.$this->name.' component',
611
+							'node' => $this,
612
+						];
613
+					}
614
+					break;
615
+				case '+':
616
+					if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) {
617
+						$messages[] = [
618
+							'level' => 3,
619
+							'message' => $propName.' MUST appear at least once in a '.$this->name.' component',
620
+							'node' => $this,
621
+						];
622
+					}
623
+					break;
624
+				case '*':
625
+					break;
626
+				case '?':
627
+					if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) {
628
+						$level = 3;
629
+
630
+						// We try to repair the same property appearing multiple times with the exact same value
631
+						// by removing the duplicates and keeping only one property
632
+						if ($options & self::REPAIR) {
633
+							$properties = array_unique($this->select($propName), SORT_REGULAR);
634
+
635
+							if (1 === count($properties)) {
636
+								$this->remove($propName);
637
+								$this->add($properties[0]);
638
+
639
+								$level = 1;
640
+							}
641
+						}
642
+
643
+						$messages[] = [
644
+							'level' => $level,
645
+							'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component',
646
+							'node' => $this,
647
+						];
648
+					}
649
+					break;
650
+			}
651
+		}
652
+
653
+		return $messages;
654
+	}
655
+
656
+	/**
657
+	 * Call this method on a document if you're done using it.
658
+	 *
659
+	 * It's intended to remove all circular references, so PHP can easily clean
660
+	 * it up.
661
+	 */
662
+	public function destroy()
663
+	{
664
+		parent::destroy();
665
+		foreach ($this->children as $childGroup) {
666
+			foreach ($childGroup as $child) {
667
+				$child->destroy();
668
+			}
669
+		}
670
+		$this->children = [];
671
+	}
672 672
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php 1 patch
Indentation   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -6,5 +6,5 @@
 block discarded – undo
6 6
 
7 7
 interface TimezoneFinder
8 8
 {
9
-    public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone;
9
+	public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone;
10 10
 }
Please login to merge, or discard this patch.
sabre/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php 1 patch
Indentation   +53 added lines, -53 removed lines patch added patch discarded remove patch
@@ -12,60 +12,60 @@
 block discarded – undo
12 12
  */
13 13
 class FindFromTimezoneIdentifier implements TimezoneFinder
14 14
 {
15
-    public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
16
-    {
17
-        // First we will just see if the tzid is a support timezone identifier.
18
-        //
19
-        // The only exception is if the timezone starts with (. This is to
20
-        // handle cases where certain microsoft products generate timezone
21
-        // identifiers that for instance look like:
22
-        //
23
-        // (GMT+01.00) Sarajevo/Warsaw/Zagreb
24
-        //
25
-        // Since PHP 5.5.10, the first bit will be used as the timezone and
26
-        // this method will return just GMT+01:00. This is wrong, because it
27
-        // doesn't take DST into account
28
-        if (!isset($tzid[0])) {
29
-            return null;
30
-        }
31
-        if ('(' === $tzid[0]) {
32
-            return null;
33
-        }
34
-        // PHP has a bug that logs PHP warnings even it shouldn't:
35
-        // https://bugs.php.net/bug.php?id=67881
36
-        //
37
-        // That's why we're checking if we'll be able to successfully instantiate
38
-        // \DateTimeZone() before doing so. Otherwise we could simply instantiate
39
-        // and catch the exception.
40
-        $tzIdentifiers = DateTimeZone::listIdentifiers();
15
+	public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
16
+	{
17
+		// First we will just see if the tzid is a support timezone identifier.
18
+		//
19
+		// The only exception is if the timezone starts with (. This is to
20
+		// handle cases where certain microsoft products generate timezone
21
+		// identifiers that for instance look like:
22
+		//
23
+		// (GMT+01.00) Sarajevo/Warsaw/Zagreb
24
+		//
25
+		// Since PHP 5.5.10, the first bit will be used as the timezone and
26
+		// this method will return just GMT+01:00. This is wrong, because it
27
+		// doesn't take DST into account
28
+		if (!isset($tzid[0])) {
29
+			return null;
30
+		}
31
+		if ('(' === $tzid[0]) {
32
+			return null;
33
+		}
34
+		// PHP has a bug that logs PHP warnings even it shouldn't:
35
+		// https://bugs.php.net/bug.php?id=67881
36
+		//
37
+		// That's why we're checking if we'll be able to successfully instantiate
38
+		// \DateTimeZone() before doing so. Otherwise we could simply instantiate
39
+		// and catch the exception.
40
+		$tzIdentifiers = DateTimeZone::listIdentifiers();
41 41
 
42
-        try {
43
-            if (
44
-                (in_array($tzid, $tzIdentifiers)) ||
45
-                (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) ||
46
-                (in_array($tzid, $this->getIdentifiersBC()))
47
-            ) {
48
-                return new DateTimeZone($tzid);
49
-            }
50
-        } catch (Exception $e) {
51
-        }
42
+		try {
43
+			if (
44
+				(in_array($tzid, $tzIdentifiers)) ||
45
+				(preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) ||
46
+				(in_array($tzid, $this->getIdentifiersBC()))
47
+			) {
48
+				return new DateTimeZone($tzid);
49
+			}
50
+		} catch (Exception $e) {
51
+		}
52 52
 
53
-        return null;
54
-    }
53
+		return null;
54
+	}
55 55
 
56
-    /**
57
-     * This method returns an array of timezone identifiers, that are supported
58
-     * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
59
-     *
60
-     * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
61
-     * - It's not supported by some PHP versions as well as HHVM.
62
-     * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
63
-     * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
64
-     *
65
-     * @return array
66
-     */
67
-    private function getIdentifiersBC()
68
-    {
69
-        return include __DIR__.'/../timezonedata/php-bc.php';
70
-    }
56
+	/**
57
+	 * This method returns an array of timezone identifiers, that are supported
58
+	 * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
59
+	 *
60
+	 * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
61
+	 * - It's not supported by some PHP versions as well as HHVM.
62
+	 * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
63
+	 * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
64
+	 *
65
+	 * @return array
66
+	 */
67
+	private function getIdentifiersBC()
68
+	{
69
+		return include __DIR__.'/../timezonedata/php-bc.php';
70
+	}
71 71
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php 1 patch
Indentation   +102 added lines, -102 removed lines patch added patch discarded remove patch
@@ -9,111 +9,111 @@
 block discarded – undo
9 9
 
10 10
 class GuessFromMsTzId implements TimezoneGuesser
11 11
 {
12
-    /**
13
-     * List of microsoft exchange timezone ids.
14
-     *
15
-     * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
16
-     */
17
-    public static $microsoftExchangeMap = [
18
-        0 => 'UTC',
19
-        31 => 'Africa/Casablanca',
12
+	/**
13
+	 * List of microsoft exchange timezone ids.
14
+	 *
15
+	 * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
16
+	 */
17
+	public static $microsoftExchangeMap = [
18
+		0 => 'UTC',
19
+		31 => 'Africa/Casablanca',
20 20
 
21
-        // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
22
-        // I'm not even kidding.. We handle this special case in the
23
-        // getTimeZone method.
24
-        2 => 'Europe/Lisbon',
25
-        1 => 'Europe/London',
26
-        4 => 'Europe/Berlin',
27
-        6 => 'Europe/Prague',
28
-        3 => 'Europe/Paris',
29
-        69 => 'Africa/Luanda', // This was a best guess
30
-        7 => 'Europe/Athens',
31
-        5 => 'Europe/Bucharest',
32
-        49 => 'Africa/Cairo',
33
-        50 => 'Africa/Harare',
34
-        59 => 'Europe/Helsinki',
35
-        27 => 'Asia/Jerusalem',
36
-        26 => 'Asia/Baghdad',
37
-        74 => 'Asia/Kuwait',
38
-        51 => 'Europe/Moscow',
39
-        56 => 'Africa/Nairobi',
40
-        25 => 'Asia/Tehran',
41
-        24 => 'Asia/Muscat', // Best guess
42
-        54 => 'Asia/Baku',
43
-        48 => 'Asia/Kabul',
44
-        58 => 'Asia/Yekaterinburg',
45
-        47 => 'Asia/Karachi',
46
-        23 => 'Asia/Calcutta',
47
-        62 => 'Asia/Kathmandu',
48
-        46 => 'Asia/Almaty',
49
-        71 => 'Asia/Dhaka',
50
-        66 => 'Asia/Colombo',
51
-        61 => 'Asia/Rangoon',
52
-        22 => 'Asia/Bangkok',
53
-        64 => 'Asia/Krasnoyarsk',
54
-        45 => 'Asia/Shanghai',
55
-        63 => 'Asia/Irkutsk',
56
-        21 => 'Asia/Singapore',
57
-        73 => 'Australia/Perth',
58
-        75 => 'Asia/Taipei',
59
-        20 => 'Asia/Tokyo',
60
-        72 => 'Asia/Seoul',
61
-        70 => 'Asia/Yakutsk',
62
-        19 => 'Australia/Adelaide',
63
-        44 => 'Australia/Darwin',
64
-        18 => 'Australia/Brisbane',
65
-        76 => 'Australia/Sydney',
66
-        43 => 'Pacific/Guam',
67
-        42 => 'Australia/Hobart',
68
-        68 => 'Asia/Vladivostok',
69
-        41 => 'Asia/Magadan',
70
-        17 => 'Pacific/Auckland',
71
-        40 => 'Pacific/Fiji',
72
-        67 => 'Pacific/Tongatapu',
73
-        29 => 'Atlantic/Azores',
74
-        53 => 'Atlantic/Cape_Verde',
75
-        30 => 'America/Noronha',
76
-        8 => 'America/Sao_Paulo', // Best guess
77
-        32 => 'America/Argentina/Buenos_Aires',
78
-        60 => 'America/Godthab',
79
-        28 => 'America/St_Johns',
80
-        9 => 'America/Halifax',
81
-        33 => 'America/Caracas',
82
-        65 => 'America/Santiago',
83
-        35 => 'America/Bogota',
84
-        10 => 'America/New_York',
85
-        34 => 'America/Indiana/Indianapolis',
86
-        55 => 'America/Guatemala',
87
-        11 => 'America/Chicago',
88
-        37 => 'America/Mexico_City',
89
-        36 => 'America/Edmonton',
90
-        38 => 'America/Phoenix',
91
-        12 => 'America/Denver', // Best guess
92
-        13 => 'America/Los_Angeles', // Best guess
93
-        14 => 'America/Anchorage',
94
-        15 => 'Pacific/Honolulu',
95
-        16 => 'Pacific/Midway',
96
-        39 => 'Pacific/Kwajalein',
97
-    ];
21
+		// Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
22
+		// I'm not even kidding.. We handle this special case in the
23
+		// getTimeZone method.
24
+		2 => 'Europe/Lisbon',
25
+		1 => 'Europe/London',
26
+		4 => 'Europe/Berlin',
27
+		6 => 'Europe/Prague',
28
+		3 => 'Europe/Paris',
29
+		69 => 'Africa/Luanda', // This was a best guess
30
+		7 => 'Europe/Athens',
31
+		5 => 'Europe/Bucharest',
32
+		49 => 'Africa/Cairo',
33
+		50 => 'Africa/Harare',
34
+		59 => 'Europe/Helsinki',
35
+		27 => 'Asia/Jerusalem',
36
+		26 => 'Asia/Baghdad',
37
+		74 => 'Asia/Kuwait',
38
+		51 => 'Europe/Moscow',
39
+		56 => 'Africa/Nairobi',
40
+		25 => 'Asia/Tehran',
41
+		24 => 'Asia/Muscat', // Best guess
42
+		54 => 'Asia/Baku',
43
+		48 => 'Asia/Kabul',
44
+		58 => 'Asia/Yekaterinburg',
45
+		47 => 'Asia/Karachi',
46
+		23 => 'Asia/Calcutta',
47
+		62 => 'Asia/Kathmandu',
48
+		46 => 'Asia/Almaty',
49
+		71 => 'Asia/Dhaka',
50
+		66 => 'Asia/Colombo',
51
+		61 => 'Asia/Rangoon',
52
+		22 => 'Asia/Bangkok',
53
+		64 => 'Asia/Krasnoyarsk',
54
+		45 => 'Asia/Shanghai',
55
+		63 => 'Asia/Irkutsk',
56
+		21 => 'Asia/Singapore',
57
+		73 => 'Australia/Perth',
58
+		75 => 'Asia/Taipei',
59
+		20 => 'Asia/Tokyo',
60
+		72 => 'Asia/Seoul',
61
+		70 => 'Asia/Yakutsk',
62
+		19 => 'Australia/Adelaide',
63
+		44 => 'Australia/Darwin',
64
+		18 => 'Australia/Brisbane',
65
+		76 => 'Australia/Sydney',
66
+		43 => 'Pacific/Guam',
67
+		42 => 'Australia/Hobart',
68
+		68 => 'Asia/Vladivostok',
69
+		41 => 'Asia/Magadan',
70
+		17 => 'Pacific/Auckland',
71
+		40 => 'Pacific/Fiji',
72
+		67 => 'Pacific/Tongatapu',
73
+		29 => 'Atlantic/Azores',
74
+		53 => 'Atlantic/Cape_Verde',
75
+		30 => 'America/Noronha',
76
+		8 => 'America/Sao_Paulo', // Best guess
77
+		32 => 'America/Argentina/Buenos_Aires',
78
+		60 => 'America/Godthab',
79
+		28 => 'America/St_Johns',
80
+		9 => 'America/Halifax',
81
+		33 => 'America/Caracas',
82
+		65 => 'America/Santiago',
83
+		35 => 'America/Bogota',
84
+		10 => 'America/New_York',
85
+		34 => 'America/Indiana/Indianapolis',
86
+		55 => 'America/Guatemala',
87
+		11 => 'America/Chicago',
88
+		37 => 'America/Mexico_City',
89
+		36 => 'America/Edmonton',
90
+		38 => 'America/Phoenix',
91
+		12 => 'America/Denver', // Best guess
92
+		13 => 'America/Los_Angeles', // Best guess
93
+		14 => 'America/Anchorage',
94
+		15 => 'Pacific/Honolulu',
95
+		16 => 'Pacific/Midway',
96
+		39 => 'Pacific/Kwajalein',
97
+	];
98 98
 
99
-    public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone
100
-    {
101
-        // Microsoft may add a magic number, which we also have an
102
-        // answer for.
103
-        if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
104
-            return null;
105
-        }
106
-        $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
99
+	public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone
100
+	{
101
+		// Microsoft may add a magic number, which we also have an
102
+		// answer for.
103
+		if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
104
+			return null;
105
+		}
106
+		$cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
107 107
 
108
-        // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
109
-        if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) {
110
-            return new DateTimeZone('Europe/Sarajevo');
111
-        }
108
+		// 2 can mean both Europe/Lisbon and Europe/Sarajevo.
109
+		if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) {
110
+			return new DateTimeZone('Europe/Sarajevo');
111
+		}
112 112
 
113
-        if (isset(self::$microsoftExchangeMap[$cdoId])) {
114
-            return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
115
-        }
113
+		if (isset(self::$microsoftExchangeMap[$cdoId])) {
114
+			return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
115
+		}
116 116
 
117
-        return null;
118
-    }
117
+		return null;
118
+	}
119 119
 }
Please login to merge, or discard this patch.
includes/sabre/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php 1 patch
Indentation   +56 added lines, -56 removed lines patch added patch discarded remove patch
@@ -11,68 +11,68 @@
 block discarded – undo
11 11
  */
12 12
 class FindFromTimezoneMap implements TimezoneFinder
13 13
 {
14
-    private $map = [];
14
+	private $map = [];
15 15
 
16
-    private $patterns = [
17
-        '/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/',
18
-        '/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/',
19
-    ];
16
+	private $patterns = [
17
+		'/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/',
18
+		'/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/',
19
+	];
20 20
 
21
-    public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
22
-    {
23
-        // Next, we check if the tzid is somewhere in our tzid map.
24
-        if ($this->hasTzInMap($tzid)) {
25
-            return new DateTimeZone($this->getTzFromMap($tzid));
26
-        }
21
+	public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
22
+	{
23
+		// Next, we check if the tzid is somewhere in our tzid map.
24
+		if ($this->hasTzInMap($tzid)) {
25
+			return new DateTimeZone($this->getTzFromMap($tzid));
26
+		}
27 27
 
28
-        // Some Microsoft products prefix the offset first, so let's strip that off
29
-        // and see if it is our tzid map.  We don't want to check for this first just
30
-        // in case there are overrides in our tzid map.
31
-        foreach ($this->patterns as $pattern) {
32
-            if (!preg_match($pattern, $tzid, $matches)) {
33
-                continue;
34
-            }
35
-            $tzidAlternate = $matches[3];
36
-            if ($this->hasTzInMap($tzidAlternate)) {
37
-                return new DateTimeZone($this->getTzFromMap($tzidAlternate));
38
-            }
39
-        }
28
+		// Some Microsoft products prefix the offset first, so let's strip that off
29
+		// and see if it is our tzid map.  We don't want to check for this first just
30
+		// in case there are overrides in our tzid map.
31
+		foreach ($this->patterns as $pattern) {
32
+			if (!preg_match($pattern, $tzid, $matches)) {
33
+				continue;
34
+			}
35
+			$tzidAlternate = $matches[3];
36
+			if ($this->hasTzInMap($tzidAlternate)) {
37
+				return new DateTimeZone($this->getTzFromMap($tzidAlternate));
38
+			}
39
+		}
40 40
 
41
-        return null;
42
-    }
41
+		return null;
42
+	}
43 43
 
44
-    /**
45
-     * This method returns an array of timezone identifiers, that are supported
46
-     * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
47
-     *
48
-     * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
49
-     * - It's not supported by some PHP versions as well as HHVM.
50
-     * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
51
-     * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
52
-     *
53
-     * @return array
54
-     */
55
-    private function getTzMaps()
56
-    {
57
-        if ([] === $this->map) {
58
-            $this->map = array_merge(
59
-                include __DIR__.'/../timezonedata/windowszones.php',
60
-                include __DIR__.'/../timezonedata/lotuszones.php',
61
-                include __DIR__.'/../timezonedata/exchangezones.php',
62
-                include __DIR__.'/../timezonedata/php-workaround.php'
63
-            );
64
-        }
44
+	/**
45
+	 * This method returns an array of timezone identifiers, that are supported
46
+	 * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
47
+	 *
48
+	 * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
49
+	 * - It's not supported by some PHP versions as well as HHVM.
50
+	 * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
51
+	 * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
52
+	 *
53
+	 * @return array
54
+	 */
55
+	private function getTzMaps()
56
+	{
57
+		if ([] === $this->map) {
58
+			$this->map = array_merge(
59
+				include __DIR__.'/../timezonedata/windowszones.php',
60
+				include __DIR__.'/../timezonedata/lotuszones.php',
61
+				include __DIR__.'/../timezonedata/exchangezones.php',
62
+				include __DIR__.'/../timezonedata/php-workaround.php'
63
+			);
64
+		}
65 65
 
66
-        return $this->map;
67
-    }
66
+		return $this->map;
67
+	}
68 68
 
69
-    private function getTzFromMap(string $tzid): string
70
-    {
71
-        return $this->getTzMaps()[$tzid];
72
-    }
69
+	private function getTzFromMap(string $tzid): string
70
+	{
71
+		return $this->getTzMaps()[$tzid];
72
+	}
73 73
 
74
-    private function hasTzInMap(string $tzid): bool
75
-    {
76
-        return isset($this->getTzMaps()[$tzid]);
77
-    }
74
+	private function hasTzInMap(string $tzid): bool
75
+	{
76
+		return isset($this->getTzMaps()[$tzid]);
77
+	}
78 78
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php 1 patch
Indentation   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -11,21 +11,21 @@
 block discarded – undo
11 11
  */
12 12
 class FindFromOffset implements TimezoneFinder
13 13
 {
14
-    public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
15
-    {
16
-        // Maybe the author was hyper-lazy and just included an offset. We
17
-        // support it, but we aren't happy about it.
18
-        if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
19
-            // Note that the path in the source will never be taken from PHP 5.5.10
20
-            // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
21
-            // already gets returned early in this function. Once we drop support
22
-            // for versions under PHP 5.5.10, this bit can be taken out of the
23
-            // source.
24
-            // @codeCoverageIgnoreStart
25
-            return new DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0'));
26
-            // @codeCoverageIgnoreEnd
27
-        }
14
+	public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
15
+	{
16
+		// Maybe the author was hyper-lazy and just included an offset. We
17
+		// support it, but we aren't happy about it.
18
+		if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
19
+			// Note that the path in the source will never be taken from PHP 5.5.10
20
+			// onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
21
+			// already gets returned early in this function. Once we drop support
22
+			// for versions under PHP 5.5.10, this bit can be taken out of the
23
+			// source.
24
+			// @codeCoverageIgnoreStart
25
+			return new DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0'));
26
+			// @codeCoverageIgnoreEnd
27
+		}
28 28
 
29
-        return null;
30
-    }
29
+		return null;
30
+	}
31 31
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php 1 patch
Indentation   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -7,5 +7,5 @@
 block discarded – undo
7 7
 
8 8
 interface TimezoneGuesser
9 9
 {
10
-    public function guess(VTimeZone $vtimezone, bool $failIfUncertain = false): ?DateTimeZone;
10
+	public function guess(VTimeZone $vtimezone, bool $failIfUncertain = false): ?DateTimeZone;
11 11
 }
Please login to merge, or discard this patch.
includes/sabre/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php 1 patch
Indentation   +14 added lines, -14 removed lines patch added patch discarded remove patch
@@ -13,21 +13,21 @@
 block discarded – undo
13 13
  */
14 14
 class GuessFromLicEntry implements TimezoneGuesser
15 15
 {
16
-    public function guess(VTimeZone $vtimezone, bool $failIfUncertain = false): ?DateTimeZone
17
-    {
18
-        if (!isset($vtimezone->{'X-LIC-LOCATION'})) {
19
-            return null;
20
-        }
16
+	public function guess(VTimeZone $vtimezone, bool $failIfUncertain = false): ?DateTimeZone
17
+	{
18
+		if (!isset($vtimezone->{'X-LIC-LOCATION'})) {
19
+			return null;
20
+		}
21 21
 
22
-        $lic = (string) $vtimezone->{'X-LIC-LOCATION'};
22
+		$lic = (string) $vtimezone->{'X-LIC-LOCATION'};
23 23
 
24
-        // Libical generators may specify strings like
25
-        // "SystemV/EST5EDT". For those we must remove the
26
-        // SystemV part.
27
-        if ('SystemV/' === substr($lic, 0, 8)) {
28
-            $lic = substr($lic, 8);
29
-        }
24
+		// Libical generators may specify strings like
25
+		// "SystemV/EST5EDT". For those we must remove the
26
+		// SystemV part.
27
+		if ('SystemV/' === substr($lic, 0, 8)) {
28
+			$lic = substr($lic, 8);
29
+		}
30 30
 
31
-        return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain);
32
-    }
31
+		return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain);
32
+	}
33 33
 }
Please login to merge, or discard this patch.
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.