1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Sabre\VObject\ITip; |
4
|
|
|
|
5
|
|
|
use Sabre\VObject\Component\VCalendar; |
6
|
|
|
use Sabre\VObject\DateTimeParser; |
7
|
|
|
use Sabre\VObject\Reader; |
8
|
|
|
use Sabre\VObject\Recur\EventIterator; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* The ITip\Broker class is a utility class that helps with processing |
12
|
|
|
* so-called iTip messages. |
13
|
|
|
* |
14
|
|
|
* iTip is defined in rfc5546, stands for iCalendar Transport-Independent |
15
|
|
|
* Interoperability Protocol, and describes the underlying mechanism for |
16
|
|
|
* using iCalendar for scheduling for for example through email (also known as |
17
|
|
|
* IMip) and CalDAV Scheduling. |
18
|
|
|
* |
19
|
|
|
* This class helps by: |
20
|
|
|
* |
21
|
|
|
* 1. Creating individual invites based on an iCalendar event for each |
22
|
|
|
* attendee. |
23
|
|
|
* 2. Generating invite updates based on an iCalendar update. This may result |
24
|
|
|
* in new invites, updates and cancellations for attendees, if that list |
25
|
|
|
* changed. |
26
|
|
|
* 3. On the receiving end, it can create a local iCalendar event based on |
27
|
|
|
* a received invite. |
28
|
|
|
* 4. It can also process an invite update on a local event, ensuring that any |
29
|
|
|
* overridden properties from attendees are retained. |
30
|
|
|
* 5. It can create a accepted or declined iTip reply based on an invite. |
31
|
|
|
* 6. It can process a reply from an invite and update an events attendee |
32
|
|
|
* status based on a reply. |
33
|
|
|
* |
34
|
|
|
* @copyright Copyright (C) fruux GmbH (https://fruux.com/) |
35
|
|
|
* @author Evert Pot (http://evertpot.com/) |
36
|
|
|
* @license http://sabre.io/license/ Modified BSD License |
37
|
|
|
*/ |
38
|
|
|
class Broker |
39
|
|
|
{ |
40
|
|
|
/** |
41
|
|
|
* This setting determines whether the rules for the SCHEDULE-AGENT |
42
|
|
|
* parameter should be followed. |
43
|
|
|
* |
44
|
|
|
* This is a parameter defined on ATTENDEE properties, introduced by RFC |
45
|
|
|
* 6638. This parameter allows a caldav client to tell the server 'Don't do |
46
|
|
|
* any scheduling operations'. |
47
|
|
|
* |
48
|
|
|
* If this setting is turned on, any attendees with SCHEDULE-AGENT set to |
49
|
|
|
* CLIENT will be ignored. This is the desired behavior for a CalDAV |
50
|
|
|
* server, but if you're writing an iTip application that doesn't deal with |
51
|
|
|
* CalDAV, you may want to ignore this parameter. |
52
|
|
|
* |
53
|
|
|
* @var bool |
54
|
|
|
*/ |
55
|
|
|
public $scheduleAgentServerRules = true; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* The broker will try during 'parseEvent' figure out whether the change |
59
|
|
|
* was significant. |
60
|
|
|
* |
61
|
|
|
* It uses a few different ways to do this. One of these ways is seeing if |
62
|
|
|
* certain properties changed values. This list of specified here. |
63
|
|
|
* |
64
|
|
|
* This list is taken from: |
65
|
|
|
* * http://tools.ietf.org/html/rfc5546#section-2.1.4 |
66
|
|
|
* |
67
|
|
|
* @var string[] |
68
|
|
|
*/ |
69
|
|
|
public $significantChangeProperties = [ |
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 ( |
898
|
|
|
$this->scheduleAgentServerRules && |
899
|
|
|
isset($attendee['SCHEDULE-AGENT']) && |
900
|
|
|
'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) |
901
|
|
|
) { |
902
|
|
|
continue; |
903
|
|
|
} |
904
|
|
|
$partStat = |
905
|
|
|
isset($attendee['PARTSTAT']) ? |
906
|
|
|
strtoupper($attendee['PARTSTAT']) : |
907
|
|
|
'NEEDS-ACTION'; |
908
|
|
|
|
909
|
|
|
$forceSend = |
910
|
|
|
isset($attendee['SCHEDULE-FORCE-SEND']) ? |
911
|
|
|
strtoupper($attendee['SCHEDULE-FORCE-SEND']) : |
912
|
|
|
null; |
913
|
|
|
|
914
|
|
|
if (isset($attendees[$attendee->getNormalizedValue()])) { |
915
|
|
|
$attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [ |
916
|
|
|
'id' => $recurId, |
917
|
|
|
'partstat' => $partStat, |
918
|
|
|
'forceSend' => $forceSend, |
919
|
|
|
]; |
920
|
|
|
} else { |
921
|
|
|
$attendees[$attendee->getNormalizedValue()] = [ |
922
|
|
|
'href' => $attendee->getNormalizedValue(), |
923
|
|
|
'instances' => [ |
924
|
|
|
$recurId => [ |
925
|
|
|
'id' => $recurId, |
926
|
|
|
'partstat' => $partStat, |
927
|
|
|
], |
928
|
|
|
], |
929
|
|
|
'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null, |
930
|
|
|
'forceSend' => $forceSend, |
931
|
|
|
]; |
932
|
|
|
} |
933
|
|
|
} |
934
|
|
|
$instances[$recurId] = $vevent; |
935
|
|
|
} |
936
|
|
|
|
937
|
|
|
foreach ($this->significantChangeProperties as $prop) { |
938
|
|
|
if (isset($vevent->$prop)) { |
939
|
|
|
$propertyValues = $vevent->select($prop); |
940
|
|
|
|
941
|
|
|
$eventSignificantChangeHash .= $prop . ':'; |
942
|
|
|
|
943
|
|
|
if ('EXDATE' === $prop) { |
944
|
|
|
$eventSignificantChangeHash .= implode(',', $exdate) . ';'; |
945
|
|
|
} elseif ('RRULE' === $prop) { |
946
|
|
|
$eventSignificantChangeHash .= implode(',', $rrule) . ';'; |
947
|
|
|
} else { |
948
|
|
|
foreach ($propertyValues as $val) { |
949
|
|
|
$eventSignificantChangeHash .= $val->getValue() . ';'; |
950
|
|
|
} |
951
|
|
|
} |
952
|
|
|
} |
953
|
|
|
} |
954
|
|
|
$significantChangeEventProperties[] = $eventSignificantChangeHash; |
955
|
|
|
} |
956
|
|
|
|
957
|
|
|
asort($significantChangeEventProperties); |
958
|
|
|
|
959
|
|
|
foreach ($significantChangeEventProperties as $eventSignificantChangeHash) { |
960
|
|
|
$significantChangeHash .= $eventSignificantChangeHash; |
961
|
|
|
} |
962
|
|
|
$significantChangeHash = md5($significantChangeHash); |
963
|
|
|
|
964
|
|
|
return compact( |
965
|
|
|
'uid', |
966
|
|
|
'organizer', |
967
|
|
|
'organizerName', |
968
|
|
|
'organizerScheduleAgent', |
969
|
|
|
'organizerForceSend', |
970
|
|
|
'instances', |
971
|
|
|
'attendees', |
972
|
|
|
'sequence', |
973
|
|
|
'exdate', |
974
|
|
|
'timezone', |
975
|
|
|
'significantChangeHash', |
976
|
|
|
'status' |
977
|
|
|
); |
978
|
|
|
} |
979
|
|
|
} |
980
|
|
|
|