1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only |
4
|
|
|
* SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH |
5
|
|
|
* SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH |
6
|
|
|
* |
7
|
|
|
* WBXML appointment entities that can be parsed directly (as a stream) from |
8
|
|
|
* WBXML. It is automatically decoded according to $mapping and the Sync |
9
|
|
|
* WBXML mappings. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
class SyncAppointment extends SyncObject { |
13
|
|
|
public $timezone; |
14
|
|
|
public $dtstamp; |
15
|
|
|
public $starttime; |
16
|
|
|
public $subject; |
17
|
|
|
public $uid; |
18
|
|
|
public $organizername; |
19
|
|
|
public $organizeremail; |
20
|
|
|
public $location; |
21
|
|
|
public $location2; // AS 16: SyncLocation object |
22
|
|
|
public $endtime; |
23
|
|
|
public $recurrence; |
24
|
|
|
public $sensitivity; |
25
|
|
|
public $busystatus; |
26
|
|
|
public $alldayevent; |
27
|
|
|
public $reminder; |
28
|
|
|
public $rtf; |
29
|
|
|
public $meetingstatus; |
30
|
|
|
public $attendees; |
31
|
|
|
public $body; |
32
|
|
|
public $bodytruncated; |
33
|
|
|
public $exceptions; |
34
|
|
|
public $deleted; |
35
|
|
|
public $exceptionstarttime; |
36
|
|
|
public $categories; |
37
|
|
|
|
38
|
|
|
// AS 12.0 props |
39
|
|
|
public $asbody; |
40
|
|
|
public $nativebodytype; |
41
|
|
|
|
42
|
|
|
// AS 14.0 props |
43
|
|
|
public $disallownewtimeprop; |
44
|
|
|
public $responsetype; |
45
|
|
|
public $responserequested; |
46
|
|
|
|
47
|
|
|
// AS 14.1 props |
48
|
|
|
public $onlineMeetingConfLink; |
49
|
|
|
public $onlineMeetingExternalLink; |
50
|
|
|
|
51
|
|
|
// AS 16.0 props |
52
|
|
|
public $asattachments; |
53
|
|
|
public $clientuid; |
54
|
|
|
public $instanceid; |
55
|
|
|
public $instanceiddelete; |
56
|
|
|
|
57
|
|
|
public function __construct() { |
58
|
|
|
$mapping = [ |
59
|
|
|
SYNC_POOMCAL_TIMEZONE => [ |
60
|
|
|
self::STREAMER_VAR => "timezone", |
61
|
|
|
self::STREAMER_RONOTIFY => true, |
62
|
|
|
], |
63
|
|
|
SYNC_POOMCAL_DTSTAMP => [ |
64
|
|
|
self::STREAMER_VAR => "dtstamp", |
65
|
|
|
self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, |
66
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETZERO], |
67
|
|
|
], |
68
|
|
|
SYNC_POOMCAL_STARTTIME => [ |
69
|
|
|
self::STREAMER_VAR => "starttime", |
70
|
|
|
self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, |
71
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_CMPLOWER => SYNC_POOMCAL_ENDTIME], |
72
|
|
|
self::STREAMER_RONOTIFY => true, |
73
|
|
|
], |
74
|
|
|
SYNC_POOMCAL_SUBJECT => [ |
75
|
|
|
self::STREAMER_VAR => "subject", |
76
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETEMPTY], |
77
|
|
|
self::STREAMER_RONOTIFY => true, |
78
|
|
|
self::STREAMER_PRIVATE => self::STRIP_PRIVATE_SUBSTITUTE, |
79
|
|
|
], |
80
|
|
|
SYNC_POOMCAL_UID => [self::STREAMER_VAR => "uid"], |
81
|
|
|
SYNC_POOMCAL_ORGANIZERNAME => [ |
82
|
|
|
self::STREAMER_VAR => "organizername", // verified below |
83
|
|
|
self::STREAMER_PRIVATE => 'Undisclosed Organizer', |
84
|
|
|
], |
85
|
|
|
SYNC_POOMCAL_ORGANIZEREMAIL => [ |
86
|
|
|
self::STREAMER_VAR => "organizeremail", // verified below |
87
|
|
|
self::STREAMER_PRIVATE => 'undisclosed@localhost', |
88
|
|
|
], |
89
|
|
|
SYNC_POOMCAL_LOCATION => [ |
90
|
|
|
self::STREAMER_VAR => "location", |
91
|
|
|
self::STREAMER_RONOTIFY => true, |
92
|
|
|
self::STREAMER_PRIVATE => true, |
93
|
|
|
], |
94
|
|
|
SYNC_POOMCAL_ENDTIME => [ |
95
|
|
|
self::STREAMER_VAR => "endtime", |
96
|
|
|
self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, |
97
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_CMPHIGHER => SYNC_POOMCAL_STARTTIME], |
98
|
|
|
self::STREAMER_RONOTIFY => true, |
99
|
|
|
], |
100
|
|
|
SYNC_POOMCAL_RECURRENCE => [ |
101
|
|
|
self::STREAMER_VAR => "recurrence", |
102
|
|
|
self::STREAMER_TYPE => "SyncRecurrence", |
103
|
|
|
self::STREAMER_RONOTIFY => true, |
104
|
|
|
], |
105
|
|
|
// Sensitivity values |
106
|
|
|
// 0 = Normal |
107
|
|
|
// 1 = Personal |
108
|
|
|
// 2 = Private |
109
|
|
|
// 3 = Confident |
110
|
|
|
SYNC_POOMCAL_SENSITIVITY => [ |
111
|
|
|
self::STREAMER_VAR => "sensitivity", |
112
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_ONEVALUEOF => [0, 1, 2, 3]], |
113
|
|
|
self::STREAMER_RONOTIFY => true, |
114
|
|
|
self::STREAMER_VALUEMAP => [0 => "Normal", |
115
|
|
|
1 => "Personal", |
116
|
|
|
2 => "Private", |
117
|
|
|
3 => "Confident", |
118
|
|
|
], |
119
|
|
|
], |
120
|
|
|
// Busystatus values |
121
|
|
|
// 0 = Free |
122
|
|
|
// 1 = Tentative |
123
|
|
|
// 2 = Busy |
124
|
|
|
// 3 = Out of office |
125
|
|
|
// 4 = Working Elsewhere |
126
|
|
|
SYNC_POOMCAL_BUSYSTATUS => [ |
127
|
|
|
self::STREAMER_VAR => "busystatus", |
128
|
|
|
self::STREAMER_CHECKS => [ |
129
|
|
|
self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETTWO, |
130
|
|
|
self::STREAMER_CHECK_ONEVALUEOF => [0, 1, 2, 3, 4], |
131
|
|
|
], |
132
|
|
|
self::STREAMER_RONOTIFY => true, |
133
|
|
|
// if private is stripped, value will be set to 2 (busy) |
134
|
|
|
self::STREAMER_PRIVATE => 2, |
135
|
|
|
self::STREAMER_VALUEMAP => [ |
136
|
|
|
0 => "Free", |
137
|
|
|
1 => "Tentative", |
138
|
|
|
2 => "Busy", |
139
|
|
|
3 => "Out of office", |
140
|
|
|
4 => "Working Elsewhere", |
141
|
|
|
], |
142
|
|
|
], |
143
|
|
|
SYNC_POOMCAL_ALLDAYEVENT => [ |
144
|
|
|
self::STREAMER_VAR => "alldayevent", |
145
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO], |
146
|
|
|
self::STREAMER_RONOTIFY => true, |
147
|
|
|
self::STREAMER_VALUEMAP => [ |
148
|
|
|
0 => "No", |
149
|
|
|
1 => "Yes", |
150
|
|
|
], |
151
|
|
|
], |
152
|
|
|
SYNC_POOMCAL_REMINDER => [ |
153
|
|
|
self::STREAMER_VAR => "reminder", |
154
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_CMPHIGHER => -1], |
155
|
|
|
self::STREAMER_RONOTIFY => true, |
156
|
|
|
// if private is stripped, value will be unset (no reminder) |
157
|
|
|
self::STREAMER_PRIVATE => true, |
158
|
|
|
], |
159
|
|
|
// Meetingstatus values |
160
|
|
|
// 0 = is not a meeting |
161
|
|
|
// 1 = is a meeting |
162
|
|
|
// 3 = Meeting received |
163
|
|
|
// 5 = Meeting is canceled |
164
|
|
|
// 7 = Meeting is canceled and received |
165
|
|
|
// 9 = as 1 |
166
|
|
|
// 11 = as 3 |
167
|
|
|
// 13 = as 5 |
168
|
|
|
// 15 = as 7 |
169
|
|
|
SYNC_POOMCAL_MEETINGSTATUS => [ |
170
|
|
|
self::STREAMER_VAR => "meetingstatus", |
171
|
|
|
self::STREAMER_CHECKS => [self::STREAMER_CHECK_ONEVALUEOF => [0, 1, 3, 5, 7, 9, 11, 13, 15]], |
172
|
|
|
self::STREAMER_RONOTIFY => true, |
173
|
|
|
self::STREAMER_VALUEMAP => [ |
174
|
|
|
0 => "Not a meeting", |
175
|
|
|
1 => "Meeting", |
176
|
|
|
3 => "Meeting received", |
177
|
|
|
5 => "Meeting canceled", |
178
|
|
|
7 => "Meeting canceled and received", |
179
|
|
|
9 => "Meeting", |
180
|
|
|
11 => "Meeting received", |
181
|
|
|
13 => "Meeting canceled", |
182
|
|
|
15 => "Meeting canceled and received", |
183
|
|
|
], |
184
|
|
|
], |
185
|
|
|
SYNC_POOMCAL_ATTENDEES => [ |
186
|
|
|
self::STREAMER_VAR => "attendees", |
187
|
|
|
self::STREAMER_TYPE => "SyncAttendee", |
188
|
|
|
self::STREAMER_ARRAY => SYNC_POOMCAL_ATTENDEE, |
189
|
|
|
self::STREAMER_RONOTIFY => true, |
190
|
|
|
self::STREAMER_PRIVATE => true, |
191
|
|
|
], |
192
|
|
|
SYNC_POOMCAL_BODY => [ |
193
|
|
|
self::STREAMER_VAR => "body", |
194
|
|
|
self::STREAMER_RONOTIFY => true, |
195
|
|
|
self::STREAMER_PRIVATE => true, |
196
|
|
|
], |
197
|
|
|
SYNC_POOMCAL_BODYTRUNCATED => [ |
198
|
|
|
self::STREAMER_VAR => "bodytruncated", |
199
|
|
|
self::STREAMER_PRIVATE => true, |
200
|
|
|
], |
201
|
|
|
SYNC_POOMCAL_EXCEPTIONS => [ |
202
|
|
|
self::STREAMER_VAR => "exceptions", |
203
|
|
|
self::STREAMER_TYPE => "SyncAppointmentException", |
204
|
|
|
self::STREAMER_ARRAY => SYNC_POOMCAL_EXCEPTION, |
205
|
|
|
self::STREAMER_RONOTIFY => true, |
206
|
|
|
], |
207
|
|
|
SYNC_POOMCAL_CATEGORIES => [ |
208
|
|
|
self::STREAMER_VAR => "categories", |
209
|
|
|
self::STREAMER_ARRAY => SYNC_POOMCAL_CATEGORY, |
210
|
|
|
self::STREAMER_RONOTIFY => true, |
211
|
|
|
self::STREAMER_PRIVATE => true, |
212
|
|
|
], |
213
|
|
|
]; |
214
|
|
|
|
215
|
|
|
if (Request::GetProtocolVersion() >= 12.0) { |
216
|
|
|
$mapping[SYNC_AIRSYNCBASE_BODY] = [ |
217
|
|
|
self::STREAMER_VAR => "asbody", |
218
|
|
|
self::STREAMER_TYPE => "SyncBaseBody", |
219
|
|
|
self::STREAMER_RONOTIFY => true, |
220
|
|
|
self::STREAMER_PRIVATE => true, |
221
|
|
|
]; |
222
|
|
|
|
223
|
|
|
$mapping[SYNC_AIRSYNCBASE_NATIVEBODYTYPE] = [self::STREAMER_VAR => "nativebodytype"]; |
224
|
|
|
|
225
|
|
|
// unset these properties because airsyncbase body and attachments will be used instead |
226
|
|
|
unset($mapping[SYNC_POOMCAL_BODY], $mapping[SYNC_POOMCAL_BODYTRUNCATED]); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
if (Request::GetProtocolVersion() >= 14.0) { |
230
|
|
|
$mapping[SYNC_POOMCAL_DISALLOWNEWTIMEPROPOSAL] = [ |
231
|
|
|
self::STREAMER_VAR => "disallownewtimeprop", |
232
|
|
|
self::STREAMER_RONOTIFY => true, |
233
|
|
|
self::STREAMER_PRIVATE => 1, |
234
|
|
|
]; // don't permit new time proposal |
235
|
|
|
$mapping[SYNC_POOMCAL_RESPONSEREQUESTED] = [ |
236
|
|
|
self::STREAMER_VAR => "responserequested", |
237
|
|
|
self::STREAMER_RONOTIFY => true, |
238
|
|
|
]; |
239
|
|
|
$mapping[SYNC_POOMCAL_RESPONSETYPE] = [ |
240
|
|
|
self::STREAMER_VAR => "responsetype", |
241
|
|
|
self::STREAMER_RONOTIFY => true, |
242
|
|
|
]; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
if (Request::GetProtocolVersion() >= 14.1) { |
246
|
|
|
$mapping[SYNC_POOMCAL_ONLINEMEETINGCONFLINK] = [ |
247
|
|
|
self::STREAMER_VAR => "onlineMeetingConfLink", |
248
|
|
|
self::STREAMER_RONOTIFY => true, |
249
|
|
|
]; |
250
|
|
|
$mapping[SYNC_POOMCAL_ONLINEMEETINGEXTERNALLINK] = [ |
251
|
|
|
self::STREAMER_VAR => "onlineMeetingExternalLink", |
252
|
|
|
self::STREAMER_RONOTIFY => true, |
253
|
|
|
]; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
if (Request::GetProtocolVersion() >= 16.0) { |
257
|
|
|
$mapping[SYNC_AIRSYNCBASE_ATTACHMENTS] = [ |
258
|
|
|
self::STREAMER_VAR => "asattachments", |
259
|
|
|
// Different tags can be used to encapsulate the SyncBaseAttachmentSubtypes depending on its usecase |
260
|
|
|
self::STREAMER_ARRAY => [ |
261
|
|
|
SYNC_AIRSYNCBASE_ATTACHMENT => "SyncBaseAttachment", |
262
|
|
|
SYNC_AIRSYNCBASE_ADD => "SyncBaseAttachmentAdd", |
263
|
|
|
SYNC_AIRSYNCBASE_DELETE => "SyncBaseAttachmentDelete", |
264
|
|
|
], |
265
|
|
|
]; |
266
|
|
|
$mapping[SYNC_AIRSYNCBASE_LOCATION] = [ |
267
|
|
|
self::STREAMER_VAR => "location2", |
268
|
|
|
self::STREAMER_TYPE => "SyncLocation", |
269
|
|
|
self::STREAMER_RONOTIFY => true, |
270
|
|
|
]; |
271
|
|
|
$mapping[SYNC_POOMCAL_CLIENTUID] = [ |
272
|
|
|
self::STREAMER_VAR => "clientuid", |
273
|
|
|
self::STREAMER_RONOTIFY => true, |
274
|
|
|
]; |
275
|
|
|
// Placeholder for the InstanceId (recurrence exceptions) and its deletion request |
276
|
|
|
$mapping[SYNC_AIRSYNCBASE_INSTANCEID] = [ |
277
|
|
|
self::STREAMER_VAR => "instanceid", |
278
|
|
|
self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE, |
279
|
|
|
]; |
280
|
|
|
$mapping[SYNC_AIRSYNCBASE_INSTANCEID_DELETE] = [ |
281
|
|
|
self::STREAMER_VAR => "instanceiddelete", |
282
|
|
|
self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE, |
283
|
|
|
]; |
284
|
|
|
|
285
|
|
|
// unset these properties because airsyncbase location will be used instead |
286
|
|
|
unset($mapping[SYNC_POOMCAL_LOCATION]); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
parent::__construct($mapping); |
290
|
|
|
|
291
|
|
|
// Indicates that this SyncObject supports the private flag and stripping of private data. |
292
|
|
|
$this->supportsPrivateStripping = true; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Method checks if the object has the minimum of required parameters |
297
|
|
|
* and fulfills semantic dependencies. |
298
|
|
|
* |
299
|
|
|
* This overloads the general check() with special checks to be executed |
300
|
|
|
* Checks if SYNC_POOMCAL_ORGANIZERNAME and SYNC_POOMCAL_ORGANIZEREMAIL are correctly set |
301
|
|
|
* |
302
|
|
|
* @param bool $logAsDebug (opt) default is false, so messages are logged in WARN log level |
303
|
|
|
* |
304
|
|
|
* @return bool |
305
|
|
|
*/ |
306
|
|
|
public function Check($logAsDebug = false) { |
307
|
|
|
// Fix starttime and endtime if they are not set on NEW appointments. |
308
|
|
|
if ($this->flags === SYNC_NEWMESSAGE) { |
|
|
|
|
309
|
|
|
$time = time(); |
310
|
|
|
$calcstart = $time + 1800 - ($time % 1800); // round up to the next half hour |
311
|
|
|
|
312
|
|
|
// Check error cases first |
313
|
|
|
// Case 2: starttime not set, endtime in the past |
314
|
|
|
if (!isset($this->starttime) && isset($this->endtime) && $this->endtime < $time) { |
315
|
|
|
SLog::Write(LOGLEVEL_WARN, "SyncAppointment->Check(): Parameter 'starttime' not set while 'endtime' is in the past (case 2). Aborting."); |
316
|
|
|
|
317
|
|
|
return false; |
318
|
|
|
} |
319
|
|
|
// Case 3b: starttime not set, endtime in the future (3) but before the calculated starttime (3b) |
320
|
|
|
if (!isset($this->starttime) && isset($this->endtime) && $this->endtime > $time && $this->endtime < $calcstart) { |
321
|
|
|
SLog::Write(LOGLEVEL_WARN, "SyncAppointment->Check(): Parameter 'starttime' not set while 'endtime' is in the future but before the calculated starttime (case 3b). Aborting."); |
322
|
|
|
|
323
|
|
|
return false; |
324
|
|
|
} |
325
|
|
|
// Case 5: starttime in the future but no endtime set |
326
|
|
|
if (isset($this->starttime) && $this->starttime > $time && !isset($this->endtime)) { |
327
|
|
|
SLog::Write(LOGLEVEL_WARN, "SyncAppointment->Check(): Parameter 'starttime' is in the future but 'endtime' is not set (case 5). Aborting."); |
328
|
|
|
|
329
|
|
|
return false; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
// Set starttime to the rounded up next half hour |
333
|
|
|
// Case 1, 3a (endtime won't be changed as it's set) |
334
|
|
|
if (!isset($this->starttime)) { |
335
|
|
|
$this->starttime = $calcstart; |
336
|
|
|
SLog::Write(LOGLEVEL_WBXML, sprintf("SyncAppointment->Check(): Parameter 'starttime' was not set, setting it to %d (%s).", $this->starttime, Utils::FormatDate($this->starttime))); |
337
|
|
|
} |
338
|
|
|
// Case 1, 4 |
339
|
|
|
if (!isset($this->endtime)) { |
340
|
|
|
$this->endtime = $calcstart + 1800; // 30 min after calcstart |
341
|
|
|
SLog::Write(LOGLEVEL_WBXML, sprintf("SyncAppointment->Check(): Parameter 'endtime' was not set, setting it to %d (%s).", $this->endtime, Utils::FormatDate($this->endtime))); |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
$ret = parent::Check($logAsDebug); |
346
|
|
|
|
347
|
|
|
// semantic checks general "turn off switch" |
348
|
|
|
if (defined("DO_SEMANTIC_CHECKS") && DO_SEMANTIC_CHECKS === false) { |
|
|
|
|
349
|
|
|
return $ret; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
if (!$ret) { |
353
|
|
|
return false; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
if ($this->meetingstatus > 0) { |
357
|
|
|
if (!isset($this->organizername) || !isset($this->organizeremail)) { |
358
|
|
|
SLog::Write(LOGLEVEL_INFO, "SyncAppointment->Check(): Parameter 'organizername' and 'organizeremail' should be set for a meeting request"); |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
// do not sync a recurrent appointment without a timezone (except all day events) |
363
|
|
|
if (isset($this->recurrence) && !isset($this->timezone) && empty($this->alldayevent)) { |
364
|
|
|
SLog::Write(LOGLEVEL_INFO, "SyncAppointment->Check(): timezone for a recurring appointment is not set."); |
365
|
|
|
|
366
|
|
|
return false; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
if (isset($this->busystatus) && $this->busystatus == 0xFFFFFFFF) { |
370
|
|
|
SLog::Write(LOGLEVEL_INFO, "SyncAppointment->Check(): rewriting busystatus -1 (0xFFFFFFFF) to fbBusy (2)."); |
371
|
|
|
$this->busystatus = fbBusy; |
|
|
|
|
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
return true; |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
class SyncAppointmentResponse extends SyncAppointment { |
379
|
|
|
use ResponseTrait; |
380
|
|
|
} |
381
|
|
|
|