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