1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only |
5
|
|
|
* SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH |
6
|
|
|
* SPDX-FileCopyrightText: Copyright 2020-2025 grommunio GmbH |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* BaseRecurrence |
11
|
|
|
* this class is superclass for recurrence for appointments and tasks. This class provides all |
12
|
|
|
* basic features of recurrence. |
13
|
|
|
*/ |
14
|
|
|
abstract class BaseRecurrence { |
15
|
|
|
/** |
16
|
|
|
* @var resource Mapi Message Store (may be null if readonly) |
17
|
|
|
*/ |
18
|
|
|
public $store; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var mixed Mapi Message (may be null if readonly) |
22
|
|
|
*/ |
23
|
|
|
public $message; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array Message Properties |
27
|
|
|
*/ |
28
|
|
|
public $messageprops; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var array list of property tags |
32
|
|
|
*/ |
33
|
|
|
public $proptags; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var mixed recurrence data of this calendar item |
37
|
|
|
*/ |
38
|
|
|
public $recur; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var mixed Timezone data of this calendar item |
42
|
|
|
*/ |
43
|
|
|
public $tz; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Constructor. |
47
|
|
|
* |
48
|
|
|
* @param resource $store MAPI Message Store Object |
49
|
|
|
* @param mixed $message the MAPI (appointment) message |
50
|
|
|
*/ |
51
|
|
|
public function __construct($store, $message) { |
52
|
|
|
$this->store = $store; |
53
|
|
|
|
54
|
|
|
if (is_array($message)) { |
55
|
|
|
$this->messageprops = $message; |
56
|
|
|
} |
57
|
|
|
else { |
58
|
|
|
$this->message = $message; |
59
|
|
|
$this->messageprops = mapi_getprops($this->message, $this->proptags); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
if (isset($this->messageprops[$this->proptags["recurring_data"]])) { |
63
|
|
|
// There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface |
64
|
|
|
if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) { |
65
|
|
|
$this->getFullRecurrenceBlob(); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
$this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]); |
69
|
|
|
} |
70
|
|
|
if (isset($this->proptags["timezone_data"], $this->messageprops[$this->proptags["timezone_data"]])) { |
71
|
|
|
$this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]); |
72
|
|
|
} |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
public function getRecurrence() { |
76
|
|
|
return $this->recur; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
public function getFullRecurrenceBlob(): void { |
80
|
|
|
$message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]); |
|
|
|
|
81
|
|
|
|
82
|
|
|
$recurrBlob = ''; |
83
|
|
|
$stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0); |
84
|
|
|
$stat = mapi_stream_stat($stream); |
85
|
|
|
|
86
|
|
|
for ($i = 0; $i < $stat['cb']; $i += 1024) { |
87
|
|
|
$recurrBlob .= mapi_stream_read($stream, 1024); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
if (!empty($recurrBlob)) { |
91
|
|
|
$this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob; |
92
|
|
|
} |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Function for parsing the Recurrence value of a Calendar item. |
97
|
|
|
* |
98
|
|
|
* Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the |
99
|
|
|
* data to this function |
100
|
|
|
* |
101
|
|
|
* Returns a structure containing the data: |
102
|
|
|
* |
103
|
|
|
* type - type of recurrence: day=10, week=11, month=12, year=13 |
104
|
|
|
* subtype - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday) |
105
|
|
|
* start - unix timestamp of first occurrence |
106
|
|
|
* end - unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1 |
107
|
|
|
* numoccur - occurrences (may be very large when there is no end data) |
108
|
|
|
* |
109
|
|
|
* then, for each type: |
110
|
|
|
* |
111
|
|
|
* Daily: |
112
|
|
|
* everyn - every [everyn] days in minutes |
113
|
|
|
* regen - regenerating event (like tasks) |
114
|
|
|
* |
115
|
|
|
* Weekly: |
116
|
|
|
* everyn - every [everyn] weeks in weeks |
117
|
|
|
* regen - regenerating event (like tasks) |
118
|
|
|
* weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc) |
119
|
|
|
* |
120
|
|
|
* Monthly: |
121
|
|
|
* everyn - every [everyn] months |
122
|
|
|
* regen - regenerating event (like tasks) |
123
|
|
|
* |
124
|
|
|
* subtype 2: |
125
|
|
|
* monthday - on day [monthday] of the month |
126
|
|
|
* |
127
|
|
|
* subtype 3: |
128
|
|
|
* weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc) |
129
|
|
|
* nday - on [nday]'th [weekdays] of the month |
130
|
|
|
* |
131
|
|
|
* Yearly: |
132
|
|
|
* everyn - every [everyn] months (12, 24, 36, ...) |
133
|
|
|
* month - in month [month] (although the month is encoded in minutes since the startning of the year ........) |
134
|
|
|
* regen - regenerating event (like tasks) |
135
|
|
|
* |
136
|
|
|
* subtype 2: |
137
|
|
|
* monthday - on day [monthday] of the month |
138
|
|
|
* |
139
|
|
|
* subtype 3: |
140
|
|
|
* weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc) |
141
|
|
|
* nday - on [nday]'th [weekdays] of the month [month] |
142
|
|
|
* |
143
|
|
|
* @param string $rdata Binary string |
144
|
|
|
* |
145
|
|
|
* @return null|(((false|int|mixed|string)[]|int)[]|int|mixed)[] recurrence data |
146
|
|
|
* |
147
|
|
|
* @psalm-return array{changed_occurrences: array<int, array{basedate: false|int, start: int, end: int, bitmask: mixed, subject?: false|string, remind_before?: mixed, reminder_set?: mixed, location?: false|string, busystatus?: mixed, alldayevent?: mixed, label?: mixed, ex_start_datetime?: mixed, ex_end_datetime?: mixed, ex_orig_date?: mixed}>, deleted_occurrences: list<int>, type?: int|mixed, subtype?: mixed, month?: mixed, everyn?: mixed, regen?: mixed, monthday?: mixed, weekdays?: 0|mixed, nday?: mixed, term?: int|mixed, numoccur?: mixed, numexcept?: mixed, numexceptmod?: mixed, start?: int, end?: int, startocc?: mixed, endocc?: mixed}|null |
148
|
|
|
*/ |
149
|
|
|
public function parseRecurrence($rdata) { |
150
|
|
|
if (strlen($rdata) < 10) { |
151
|
|
|
return; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
$ret = []; |
155
|
|
|
$ret["changed_occurrences"] = []; |
156
|
|
|
$ret["deleted_occurrences"] = []; |
157
|
|
|
|
158
|
|
|
$data = unpack("vReaderVersion/vWriterVersion/vrtype/vrtype2/vCalendarType", $rdata); |
159
|
|
|
|
160
|
|
|
// Do some recurrence validity checks |
161
|
|
|
if ($data['ReaderVersion'] != 0x3004 || $data['WriterVersion'] != 0x3004) { |
162
|
|
|
return $ret; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
if (!in_array($data["rtype"], [IDC_RCEV_PAT_ORB_DAILY, IDC_RCEV_PAT_ORB_WEEKLY, IDC_RCEV_PAT_ORB_MONTHLY, IDC_RCEV_PAT_ORB_YEARLY])) { |
166
|
|
|
return $ret; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
if (!in_array($data["rtype2"], [rptDay, rptWeek, rptMonth, rptMonthNth, rptMonthEnd, rptHjMonth, rptHjMonthNth, rptHjMonthEnd])) { |
170
|
|
|
return $ret; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
if (!in_array($data['CalendarType'], [MAPI_CAL_DEFAULT, MAPI_CAL_GREGORIAN])) { |
174
|
|
|
return $ret; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$ret["type"] = (int) $data["rtype"] > 0x2000 ? (int) $data["rtype"] - 0x2000 : $data["rtype"]; |
178
|
|
|
$ret["subtype"] = $data["rtype2"]; |
179
|
|
|
$rdata = substr($rdata, 10); |
180
|
|
|
|
181
|
|
|
switch ($data["rtype"]) { |
182
|
|
|
case IDC_RCEV_PAT_ORB_DAILY: |
183
|
|
|
if (strlen($rdata) < 12) { |
184
|
|
|
return $ret; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$data = unpack("Vunknown/Veveryn/Vregen", $rdata); |
188
|
|
|
if ($data["everyn"] > 1438560) { // minutes for 999 days |
189
|
|
|
return $ret; |
190
|
|
|
} |
191
|
|
|
$ret["everyn"] = $data["everyn"]; |
192
|
|
|
$ret["regen"] = $data["regen"]; |
193
|
|
|
|
194
|
|
|
switch ($ret["subtype"]) { |
195
|
|
|
case rptDay: |
196
|
|
|
$rdata = substr($rdata, 12); |
197
|
|
|
break; |
198
|
|
|
|
199
|
|
|
case rptWeek: |
200
|
|
|
$rdata = substr($rdata, 16); |
201
|
|
|
break; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
break; |
205
|
|
|
|
206
|
|
|
case IDC_RCEV_PAT_ORB_WEEKLY: |
207
|
|
|
if (strlen($rdata) < 16) { |
208
|
|
|
return $ret; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$data = unpack("Vconst1/Veveryn/Vregen", $rdata); |
212
|
|
|
if ($data["everyn"] > 99) { |
213
|
|
|
return $ret; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
$rdata = substr($rdata, 12); |
217
|
|
|
|
218
|
|
|
$ret["everyn"] = $data["everyn"]; |
219
|
|
|
$ret["regen"] = $data["regen"]; |
220
|
|
|
$ret["weekdays"] = 0; |
221
|
|
|
|
222
|
|
|
if ($data["regen"] == 0) { |
223
|
|
|
$data = unpack("Vweekdays", $rdata); |
224
|
|
|
$rdata = substr($rdata, 4); |
225
|
|
|
|
226
|
|
|
$ret["weekdays"] = $data["weekdays"]; |
227
|
|
|
} |
228
|
|
|
break; |
229
|
|
|
|
230
|
|
|
case IDC_RCEV_PAT_ORB_MONTHLY: |
231
|
|
|
if (strlen($rdata) < 16) { |
232
|
|
|
return $ret; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
$data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata); |
236
|
|
|
if ($data["everyn"] > 99) { |
237
|
|
|
return $ret; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
$ret["everyn"] = $data["everyn"]; |
241
|
|
|
$ret["regen"] = $data["regen"]; |
242
|
|
|
|
243
|
|
|
if ($ret["subtype"] == rptMonthNth) { |
244
|
|
|
$ret["weekdays"] = $data["monthday"]; |
245
|
|
|
} |
246
|
|
|
else { |
247
|
|
|
$ret["monthday"] = $data["monthday"]; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$rdata = substr($rdata, 16); |
251
|
|
|
if ($ret["subtype"] == rptMonthNth) { |
252
|
|
|
$data = unpack("Vnday", $rdata); |
253
|
|
|
// Sanity check for valid values (and opportunistically try to fix) |
254
|
|
|
if ($data["nday"] == 0xFFFFFFFF || $data["nday"] == -1) { |
255
|
|
|
$data["nday"] = 5; |
256
|
|
|
} |
257
|
|
|
elseif ($data["nday"] < 0 || $data["nday"] > 5) { |
258
|
|
|
$data["nday"] = 0; |
259
|
|
|
} |
260
|
|
|
$ret["nday"] = $data["nday"]; |
261
|
|
|
$rdata = substr($rdata, 4); |
262
|
|
|
} |
263
|
|
|
break; |
264
|
|
|
|
265
|
|
|
case IDC_RCEV_PAT_ORB_YEARLY: |
266
|
|
|
if (strlen($rdata) < 16) { |
267
|
|
|
return $ret; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata); |
271
|
|
|
// recurring yearly tasks have a period in months multiple by 12 |
272
|
|
|
if ($data['regen'] && $data["everyn"] % 12 != 0) { |
273
|
|
|
return $ret; |
274
|
|
|
} |
275
|
|
|
if (!$data['regen'] && $data["everyn"] != 12) { |
276
|
|
|
return $ret; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
$ret["month"] = $data["month"]; |
280
|
|
|
$ret["everyn"] = $data["everyn"]; |
281
|
|
|
$ret["regen"] = $data["regen"]; |
282
|
|
|
|
283
|
|
|
if ($ret["subtype"] == rptMonthNth) { |
284
|
|
|
$ret["weekdays"] = $data["monthday"]; |
285
|
|
|
} |
286
|
|
|
else { |
287
|
|
|
$ret["monthday"] = $data["monthday"]; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
$rdata = substr($rdata, 16); |
291
|
|
|
|
292
|
|
|
if ($ret["subtype"] == rptMonthNth) { |
293
|
|
|
$data = unpack("Vnday", $rdata); |
294
|
|
|
// Sanity check for valid values (and opportunistically try to fix) |
295
|
|
|
if ($data["nday"] == 0xFFFFFFFF || $data["nday"] == -1) { |
296
|
|
|
$data["nday"] = 5; |
297
|
|
|
} |
298
|
|
|
elseif ($data["nday"] < 0 || $data["nday"] > 5) { |
299
|
|
|
$data["nday"] = 0; |
300
|
|
|
} |
301
|
|
|
$ret["nday"] = $data["nday"]; |
302
|
|
|
$rdata = substr($rdata, 4); |
303
|
|
|
} |
304
|
|
|
break; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
if (strlen($rdata) < 16) { |
308
|
|
|
return $ret; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
$data = unpack("Vterm/Vnumoccur/Vconst2/Vnumexcept", $rdata); |
312
|
|
|
$rdata = substr($rdata, 16); |
313
|
|
|
if (!in_array($data["term"], [IDC_RCEV_PAT_ERB_END, IDC_RCEV_PAT_ERB_AFTERNOCCUR, IDC_RCEV_PAT_ERB_NOEND, 0xFFFFFFFF])) { |
314
|
|
|
return $ret; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
$ret["term"] = (int) $data["term"] > 0x2000 ? (int) $data["term"] - 0x2000 : $data["term"]; |
318
|
|
|
$ret["numoccur"] = $data["numoccur"]; |
319
|
|
|
$ret["numexcept"] = $data["numexcept"]; |
320
|
|
|
|
321
|
|
|
// exc_base_dates are *all* the base dates that have been either deleted or modified |
322
|
|
|
$exc_base_dates = []; |
323
|
|
|
for ($i = 0; $i < $ret["numexcept"]; ++$i) { |
324
|
|
|
if (strlen($rdata) < 4) { |
325
|
|
|
// We shouldn't arrive here, because that implies |
326
|
|
|
// numexcept does not match the amount of data |
327
|
|
|
// which is available for the exceptions. |
328
|
|
|
return $ret; |
329
|
|
|
} |
330
|
|
|
$data = unpack("Vbasedate", $rdata); |
331
|
|
|
$rdata = substr($rdata, 4); |
332
|
|
|
$exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
if (strlen($rdata) < 4) { |
336
|
|
|
return $ret; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
$data = unpack("Vnumexceptmod", $rdata); |
340
|
|
|
$rdata = substr($rdata, 4); |
341
|
|
|
|
342
|
|
|
$ret["numexceptmod"] = $data["numexceptmod"]; |
343
|
|
|
|
344
|
|
|
// exc_changed are the base dates of *modified* occurrences. exactly what is modified |
345
|
|
|
// is in the attachments *and* in the data further down this function. |
346
|
|
|
$exc_changed = []; |
347
|
|
|
for ($i = 0; $i < $ret["numexceptmod"]; ++$i) { |
348
|
|
|
if (strlen($rdata) < 4) { |
349
|
|
|
// We shouldn't arrive here, because that implies |
350
|
|
|
// numexceptmod does not match the amount of data |
351
|
|
|
// which is available for the exceptions. |
352
|
|
|
return $ret; |
353
|
|
|
} |
354
|
|
|
$data = unpack("Vstartdate", $rdata); |
355
|
|
|
$rdata = substr($rdata, 4); |
356
|
|
|
$exc_changed[] = $this->recurDataToUnixData($data["startdate"]); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
if (strlen($rdata) < 8) { |
360
|
|
|
return $ret; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
$data = unpack("Vstart/Vend", $rdata); |
364
|
|
|
$rdata = substr($rdata, 8); |
365
|
|
|
|
366
|
|
|
$ret["start"] = $this->recurDataToUnixData($data["start"]); |
367
|
|
|
$ret["end"] = $this->recurDataToUnixData($data["end"]); |
368
|
|
|
|
369
|
|
|
// this is where task recurrence stop |
370
|
|
|
if (strlen($rdata) < 16) { |
371
|
|
|
return $ret; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
$data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata); |
375
|
|
|
$rdata = substr($rdata, 16); |
376
|
|
|
|
377
|
|
|
$ret["startocc"] = $data["startmin"]; |
378
|
|
|
$ret["endocc"] = $data["endmin"]; |
379
|
|
|
$writerversion = $data["writerversion"]; |
380
|
|
|
|
381
|
|
|
$data = unpack("vnumber", $rdata); |
382
|
|
|
$rdata = substr($rdata, 2); |
383
|
|
|
|
384
|
|
|
$nexceptions = $data["number"]; |
385
|
|
|
$exc_changed_details = []; |
386
|
|
|
|
387
|
|
|
// Parse n modified exceptions |
388
|
|
|
for ($i = 0; $i < $nexceptions; ++$i) { |
389
|
|
|
$item = []; |
390
|
|
|
|
391
|
|
|
// Get exception startdate, enddate and basedate (the date at which the occurrence would have started) |
392
|
|
|
$data = unpack("Vstartdate/Venddate/Vbasedate", $rdata); |
393
|
|
|
$rdata = substr($rdata, 12); |
394
|
|
|
|
395
|
|
|
// Convert recurtimestamp to unix timestamp |
396
|
|
|
$startdate = $this->recurDataToUnixData($data["startdate"]); |
397
|
|
|
$enddate = $this->recurDataToUnixData($data["enddate"]); |
398
|
|
|
$basedate = $this->recurDataToUnixData($data["basedate"]); |
399
|
|
|
|
400
|
|
|
// Set the right properties |
401
|
|
|
$item["basedate"] = $this->dayStartOf($basedate); |
402
|
|
|
$item["start"] = $startdate; |
403
|
|
|
$item["end"] = $enddate; |
404
|
|
|
|
405
|
|
|
$data = unpack("vbitmask", $rdata); |
406
|
|
|
$rdata = substr($rdata, 2); |
407
|
|
|
$item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions |
408
|
|
|
|
409
|
|
|
// Bitmask to verify what properties are changed |
410
|
|
|
$bitmask = $data["bitmask"]; |
411
|
|
|
|
412
|
|
|
// ARO_SUBJECT: 0x0001 |
413
|
|
|
// Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject |
414
|
|
|
if ($bitmask & (1 << 0)) { |
415
|
|
|
$data = unpack("vnull_length/vlength", $rdata); |
416
|
|
|
$rdata = substr($rdata, 4); |
417
|
|
|
|
418
|
|
|
$length = $data["length"]; |
419
|
|
|
$item["subject"] = ""; // Normalized subject |
420
|
|
|
for ($j = 0; $j < $length && strlen($rdata); ++$j) { |
421
|
|
|
$data = unpack("Cchar", $rdata); |
422
|
|
|
$rdata = substr($rdata, 1); |
423
|
|
|
|
424
|
|
|
$item["subject"] .= chr($data["char"]); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
// ARO_MEETINGTYPE: 0x0002 |
429
|
|
|
if ($bitmask & (1 << 1)) { |
430
|
|
|
$rdata = substr($rdata, 4); |
431
|
|
|
// Attendees modified: no data here (only in attachment) |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
// ARO_REMINDERDELTA: 0x0004 |
435
|
|
|
// Look for field: ReminderDelta (4b) |
436
|
|
|
if ($bitmask & (1 << 2)) { |
437
|
|
|
$data = unpack("Vremind_before", $rdata); |
438
|
|
|
$rdata = substr($rdata, 4); |
439
|
|
|
|
440
|
|
|
$item["remind_before"] = $data["remind_before"]; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
// ARO_REMINDER: 0x0008 |
444
|
|
|
// Look field: ReminderSet (4b) |
445
|
|
|
if ($bitmask & (1 << 3)) { |
446
|
|
|
$data = unpack("Vreminder_set", $rdata); |
447
|
|
|
$rdata = substr($rdata, 4); |
448
|
|
|
|
449
|
|
|
$item["reminder_set"] = $data["reminder_set"]; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
// ARO_LOCATION: 0x0010 |
453
|
|
|
// Look for fields: LocationLength (2b), LocationLength2 (2b) and Location |
454
|
|
|
// Similar to ARO_SUBJECT above. |
455
|
|
|
if ($bitmask & (1 << 4)) { |
456
|
|
|
$data = unpack("vnull_length/vlength", $rdata); |
457
|
|
|
$rdata = substr($rdata, 4); |
458
|
|
|
|
459
|
|
|
$item["location"] = ""; |
460
|
|
|
|
461
|
|
|
$length = $data["length"]; |
462
|
|
|
$data = substr($rdata, 0, $length); |
463
|
|
|
$rdata = substr($rdata, $length); |
464
|
|
|
|
465
|
|
|
$item["location"] .= $data; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
// ARO_BUSYSTATUS: 0x0020 |
469
|
|
|
// Look for field: BusyStatus (4b) |
470
|
|
|
if ($bitmask & (1 << 5)) { |
471
|
|
|
$data = unpack("Vbusystatus", $rdata); |
472
|
|
|
$rdata = substr($rdata, 4); |
473
|
|
|
|
474
|
|
|
$item["busystatus"] = $data["busystatus"]; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
// ARO_ATTACHMENT: 0x0040 |
478
|
|
|
if ($bitmask & (1 << 6)) { |
479
|
|
|
// no data: RESERVED |
480
|
|
|
$rdata = substr($rdata, 4); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
// ARO_SUBTYPE: 0x0080 |
484
|
|
|
// Look for field: SubType (4b). Determines whether it is an allday event. |
485
|
|
|
if ($bitmask & (1 << 7)) { |
486
|
|
|
$data = unpack("Vallday", $rdata); |
487
|
|
|
$rdata = substr($rdata, 4); |
488
|
|
|
|
489
|
|
|
$item["alldayevent"] = $data["allday"]; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
// ARO_APPTCOLOR: 0x0100 |
493
|
|
|
// Look for field: AppointmentColor (4b) |
494
|
|
|
if ($bitmask & (1 << 8)) { |
495
|
|
|
$data = unpack("Vlabel", $rdata); |
496
|
|
|
$rdata = substr($rdata, 4); |
497
|
|
|
|
498
|
|
|
$item["label"] = $data["label"]; |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
// ARO_EXCEPTIONAL_BODY: 0x0200 |
502
|
|
|
if ($bitmask & (1 << 9)) { |
503
|
|
|
// Notes or Attachments modified: no data here (only in attachment) |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
array_push($exc_changed_details, $item); |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* We now have $exc_changed, $exc_base_dates and $exc_changed_details |
511
|
|
|
* We will ignore $exc_changed, as this information is available in $exc_changed_details |
512
|
|
|
* also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item |
513
|
|
|
* has been deleted. |
514
|
|
|
*/ |
515
|
|
|
|
516
|
|
|
// Find deleted occurrences |
517
|
|
|
$deleted_occurrences = []; |
518
|
|
|
|
519
|
|
|
foreach ($exc_base_dates as $base_date) { |
520
|
|
|
$found = false; |
521
|
|
|
|
522
|
|
|
foreach ($exc_changed_details as $details) { |
523
|
|
|
if ($details["basedate"] == $base_date) { |
524
|
|
|
$found = true; |
525
|
|
|
break; |
526
|
|
|
} |
527
|
|
|
} |
528
|
|
|
if (!$found) { |
529
|
|
|
// item was not in exc_changed_details, so it must be deleted |
530
|
|
|
$deleted_occurrences[] = $base_date; |
531
|
|
|
} |
532
|
|
|
} |
533
|
|
|
|
534
|
|
|
$ret["deleted_occurrences"] = $deleted_occurrences; |
535
|
|
|
$ret["changed_occurrences"] = $exc_changed_details; |
536
|
|
|
|
537
|
|
|
// enough data for normal exception (no extended data) |
538
|
|
|
if (strlen($rdata) < 8) { |
539
|
|
|
return $ret; |
540
|
|
|
} |
541
|
|
|
|
542
|
|
|
$data = unpack("Vreservedsize", $rdata); |
543
|
|
|
$rdata = substr($rdata, 4 + $data["reservedsize"]); |
544
|
|
|
|
545
|
|
|
for ($i = 0; $i < $nexceptions; ++$i) { |
546
|
|
|
// subject and location in ucs-2 to utf-8 |
547
|
|
|
if ($writerversion >= 0x3009) { |
548
|
|
|
$data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4 |
549
|
|
|
$rdata = substr($rdata, 4 + $data["size"]); |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
$data = unpack("Vreservedsize", $rdata); |
553
|
|
|
$rdata = substr($rdata, 4 + $data["reservedsize"]); |
554
|
|
|
|
555
|
|
|
// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10) |
556
|
|
|
if ($exc_changed_details[$i]["bitmask"] & 0x11) { |
557
|
|
|
$data = unpack("Vstart/Vend/Vorig", $rdata); |
558
|
|
|
$rdata = substr($rdata, 4 * 3); |
559
|
|
|
|
560
|
|
|
$exc_changed_details[$i]["ex_start_datetime"] = $data["start"]; |
561
|
|
|
$exc_changed_details[$i]["ex_end_datetime"] = $data["end"]; |
562
|
|
|
$exc_changed_details[$i]["ex_orig_date"] = $data["orig"]; |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
// ARO_SUBJECT |
566
|
|
|
if ($exc_changed_details[$i]["bitmask"] & 0x01) { |
567
|
|
|
// decode ucs2 string to utf-8 |
568
|
|
|
$data = unpack("vlength", $rdata); |
569
|
|
|
$rdata = substr($rdata, 2); |
570
|
|
|
$length = $data["length"]; |
571
|
|
|
$data = substr($rdata, 0, $length * 2); |
572
|
|
|
$rdata = substr($rdata, $length * 2); |
573
|
|
|
$subject = iconv("UCS-2LE", "UTF-8", $data); |
574
|
|
|
// replace subject with unicode subject |
575
|
|
|
$exc_changed_details[$i]["subject"] = $subject; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
// ARO_LOCATION |
579
|
|
|
if ($exc_changed_details[$i]["bitmask"] & 0x10) { |
580
|
|
|
// decode ucs2 string to utf-8 |
581
|
|
|
$data = unpack("vlength", $rdata); |
582
|
|
|
$rdata = substr($rdata, 2); |
583
|
|
|
$length = $data["length"]; |
584
|
|
|
$data = substr($rdata, 0, $length * 2); |
585
|
|
|
$rdata = substr($rdata, $length * 2); |
586
|
|
|
$location = iconv("UCS-2LE", "UTF-8", $data); |
587
|
|
|
// replace subject with unicode subject |
588
|
|
|
$exc_changed_details[$i]["location"] = $location; |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10) |
592
|
|
|
if ($exc_changed_details[$i]["bitmask"] & 0x11) { |
593
|
|
|
$data = unpack("Vreservedsize", $rdata); |
594
|
|
|
$rdata = substr($rdata, 4 + $data["reservedsize"]); |
595
|
|
|
} |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
// update with extended data |
599
|
|
|
$ret["changed_occurrences"] = $exc_changed_details; |
600
|
|
|
|
601
|
|
|
return $ret; |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
/** |
605
|
|
|
* Saves the recurrence data to the recurrence property. |
606
|
|
|
*/ |
607
|
|
|
public function saveRecurrence(): void { |
608
|
|
|
// Only save if a message was passed |
609
|
|
|
if (!isset($this->message)) { |
610
|
|
|
return; |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
// Abort if no recurrence was set |
614
|
|
|
if (!isset( |
615
|
|
|
$this->recur["type"], |
616
|
|
|
$this->recur["subtype"], |
617
|
|
|
$this->recur["start"], |
618
|
|
|
$this->recur["end"], |
619
|
|
|
$this->recur["startocc"], |
620
|
|
|
$this->recur["endocc"]) |
621
|
|
|
) { |
622
|
|
|
return; |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
$rtype = 0x2000 + (int) $this->recur["type"]; |
626
|
|
|
|
627
|
|
|
// Don't allow invalid type and subtype values |
628
|
|
|
if (!in_array($rtype, [IDC_RCEV_PAT_ORB_DAILY, IDC_RCEV_PAT_ORB_WEEKLY, IDC_RCEV_PAT_ORB_MONTHLY, IDC_RCEV_PAT_ORB_YEARLY])) { |
629
|
|
|
return; |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
if (!in_array((int) $this->recur["subtype"], [rptDay, rptWeek, rptMonth, rptMonthNth, rptMonthEnd, rptHjMonth, rptHjMonthNth, rptHjMonthEnd])) { |
633
|
|
|
return; |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
$rdata = pack("vvvvv", 0x3004, 0x3004, $rtype, (int) $this->recur["subtype"], MAPI_CAL_DEFAULT); |
637
|
|
|
$weekstart = 1; // monday |
638
|
|
|
$forwardcount = 0; |
639
|
|
|
$count = 0; |
640
|
|
|
$restocc = 0; |
641
|
|
|
$dayofweek = (int) gmdate("w", (int) $this->recur["start"]); // 0 (for Sunday) through 6 (for Saturday) |
642
|
|
|
|
643
|
|
|
// Terminate |
644
|
|
|
$term = (int) $this->recur["term"] < 0x2000 ? 0x2000 + (int) $this->recur["term"] : (int) $this->recur["term"]; |
645
|
|
|
|
646
|
|
|
switch ($rtype) { |
647
|
|
|
case IDC_RCEV_PAT_ORB_DAILY: |
648
|
|
|
if (!isset($this->recur["everyn"]) || (int) $this->recur["everyn"] > 1438560 || (int) $this->recur["everyn"] < 0) { // minutes for 999 days |
649
|
|
|
return; |
650
|
|
|
} |
651
|
|
|
|
652
|
|
|
if ($this->recur["subtype"] == rptWeek) { |
653
|
|
|
// Daily every workday |
654
|
|
|
$rdata .= pack("VVVV", 6 * 24 * 60, 1, 0, 0x3E); |
655
|
|
|
} |
656
|
|
|
else { |
657
|
|
|
// Calc first occ |
658
|
|
|
$firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]); |
659
|
|
|
|
660
|
|
|
$rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0); |
661
|
|
|
} |
662
|
|
|
break; |
663
|
|
|
|
664
|
|
|
case IDC_RCEV_PAT_ORB_WEEKLY: |
665
|
|
|
if (!isset($this->recur["everyn"]) || $this->recur["everyn"] > 99 || (int) $this->recur["everyn"] < 0) { |
666
|
|
|
return; |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) { |
670
|
|
|
return; |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
// No need to calculate startdate if sliding flag was set. |
674
|
|
|
if (!$this->recur['regen']) { |
675
|
|
|
// Calculate start date of recurrence |
676
|
|
|
|
677
|
|
|
// Find the first day that matches one of the weekdays selected |
678
|
|
|
$daycount = 0; |
679
|
|
|
$dayskip = -1; |
680
|
|
|
for ($j = 0; $j < 7; ++$j) { |
681
|
|
|
if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) { |
682
|
|
|
if ($dayskip == -1) { |
683
|
|
|
$dayskip = $j; |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
++$daycount; |
687
|
|
|
} |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
// $dayskip is the number of days to skip from the startdate until the first occurrence |
691
|
|
|
// $daycount is the number of days per week that an occurrence occurs |
692
|
|
|
|
693
|
|
|
$weekskip = 0; |
694
|
|
|
if (($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek + $dayskip) > 6) { |
695
|
|
|
$weekskip = 1; |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
// Check if the recurrence ends after a number of occurrences, in that case we must calculate the |
699
|
|
|
// remaining occurrences based on the start of the recurrence. |
700
|
|
|
if ($term == IDC_RCEV_PAT_ERB_AFTERNOCCUR) { |
701
|
|
|
// $weekskip is the amount of weeks to skip from the startdate before the first occurrence |
702
|
|
|
// $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that |
703
|
|
|
// is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over |
704
|
|
|
// (eg when numoccur = 2, and daycount = 1) |
705
|
|
|
$forwardcount = floor((int) ($this->recur["numoccur"] - 1) / $daycount); |
706
|
|
|
|
707
|
|
|
// $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one |
708
|
|
|
// for the occurrence on the first day |
709
|
|
|
$restocc = ((int) $this->recur["numoccur"]) - ($forwardcount * $daycount) - 1; |
710
|
|
|
|
711
|
|
|
// $forwardcount is now the number of weeks we can go forward and still be inside the recurrence |
712
|
|
|
$forwardcount *= (int) $this->recur["everyn"]; |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
// The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week) |
716
|
|
|
$this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24 * 60 * 60) + ($weekskip * (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60); |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
// Calc first occ |
720
|
|
|
$firstocc = $this->unixDataToRecurData($this->recur["start"]) % (((int) $this->recur["everyn"]) * 7 * 24 * 60); |
721
|
|
|
|
722
|
|
|
$firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60; |
723
|
|
|
|
724
|
|
|
if ($this->recur["regen"]) { |
725
|
|
|
$rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1); |
726
|
|
|
} |
727
|
|
|
else { |
728
|
|
|
$rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]); |
729
|
|
|
} |
730
|
|
|
break; |
731
|
|
|
|
732
|
|
|
case IDC_RCEV_PAT_ORB_MONTHLY: |
733
|
|
|
case IDC_RCEV_PAT_ORB_YEARLY: |
734
|
|
|
if (!isset($this->recur["everyn"])) { |
735
|
|
|
return; |
736
|
|
|
} |
737
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_YEARLY && !isset($this->recur["month"])) { |
738
|
|
|
return; |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_MONTHLY) { |
742
|
|
|
$everyn = (int) $this->recur["everyn"]; |
743
|
|
|
if ($everyn > 99 || $everyn < 0) { |
744
|
|
|
return; |
745
|
|
|
} |
746
|
|
|
} |
747
|
|
|
else { |
748
|
|
|
$everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12; |
749
|
|
|
} |
750
|
|
|
|
751
|
|
|
// Get montday/month/year of original start |
752
|
|
|
$curmonthday = gmdate("j", (int) $this->recur["start"]); |
753
|
|
|
$curyear = gmdate("Y", (int) $this->recur["start"]); |
754
|
|
|
$curmonth = gmdate("n", (int) $this->recur["start"]); |
755
|
|
|
|
756
|
|
|
// Check if the recurrence ends after a number of occurrences, in that case we must calculate the |
757
|
|
|
// remaining occurrences based on the start of the recurrence. |
758
|
|
|
if ($term == IDC_RCEV_PAT_ERB_AFTERNOCCUR) { |
759
|
|
|
// $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus |
760
|
|
|
// one to make sure there are always at least one occurrence left) |
761
|
|
|
$forwardcount = ((((int) $this->recur["numoccur"]) - 1) * $everyn); |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
// Get month for yearly on D'th day of month M |
765
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_YEARLY) { |
766
|
|
|
$selmonth = floor(((int) $this->recur["month"]) / (24 * 60 * 29)) + 1; // 1=jan, 2=feb, eg |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
switch ((int) $this->recur["subtype"]) { |
770
|
|
|
// on D day of every M month |
771
|
|
|
case rptMonth: |
772
|
|
|
if (!isset($this->recur["monthday"])) { |
773
|
|
|
return; |
774
|
|
|
} |
775
|
|
|
// Recalc startdate |
776
|
|
|
|
777
|
|
|
// Set on the right begin day |
778
|
|
|
|
779
|
|
|
// Go the beginning of the month |
780
|
|
|
$this->recur["start"] -= ($curmonthday - 1) * 24 * 60 * 60; |
781
|
|
|
// Go the the correct month day |
782
|
|
|
$this->recur["start"] += (((int) $this->recur["monthday"]) - 1) * 24 * 60 * 60; |
783
|
|
|
|
784
|
|
|
// If the previous calculation gave us a start date different than the original start date, then we need to skip to the first occurrence |
785
|
|
|
if (($rtype == IDC_RCEV_PAT_ORB_MONTHLY && ((int) $this->recur["monthday"]) < $curmonthday) || |
786
|
|
|
($rtype == IDC_RCEV_PAT_ORB_YEARLY && ($selmonth != $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday)))) { |
|
|
|
|
787
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_YEARLY) { |
788
|
|
|
if ($curmonth > $selmonth) {// go to next occurrence in 'everyn' months minus difference in first occurrence and original date |
789
|
|
|
$count = $everyn - ($curmonth - $selmonth); |
790
|
|
|
} |
791
|
|
|
elseif ($curmonth < $selmonth) {// go to next occurrence upto difference in first occurrence and original date |
792
|
|
|
$count = $selmonth - $curmonth; |
793
|
|
|
} |
794
|
|
|
else { |
795
|
|
|
// Go to next occurrence while recurrence start date is greater than occurrence date but within same month |
796
|
|
|
if (((int) $this->recur["monthday"]) < $curmonthday) { |
797
|
|
|
$count = $everyn; |
798
|
|
|
} |
799
|
|
|
} |
800
|
|
|
} |
801
|
|
|
else { |
802
|
|
|
$count = $everyn; // Monthly, go to next occurrence in 'everyn' months |
803
|
|
|
} |
804
|
|
|
|
805
|
|
|
// Forward by $count months. This is done by getting the number of days in that month and forwarding that many days |
806
|
|
|
for ($i = 0; $i < $count; ++$i) { |
807
|
|
|
$this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth); |
808
|
|
|
|
809
|
|
|
if ($curmonth == 12) { |
810
|
|
|
++$curyear; |
811
|
|
|
$curmonth = 0; |
812
|
|
|
} |
813
|
|
|
++$curmonth; |
814
|
|
|
} |
815
|
|
|
} |
816
|
|
|
|
817
|
|
|
// "start" is now pointing to the first occurrence, except that it will overshoot if the |
818
|
|
|
// month in which it occurs has less days than specified as the day of the month. So 31st |
819
|
|
|
// of each month will overshoot in february (29 days). We compensate for that by checking |
820
|
|
|
// if the day of the month we got is wrong, and then back up to the last day of the previous |
821
|
|
|
// month. |
822
|
|
|
if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 && |
823
|
|
|
gmdate("j", (int) $this->recur["start"]) < ((int) $this->recur["monthday"])) { |
824
|
|
|
$this->recur["start"] -= gmdate("j", (int) $this->recur["start"]) * 24 * 60 * 60; |
825
|
|
|
} |
826
|
|
|
|
827
|
|
|
// "start" is now the first occurrence |
828
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_MONTHLY) { |
829
|
|
|
// Calc first occ |
830
|
|
|
$monthIndex = ((((12 % $everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn; |
831
|
|
|
|
832
|
|
|
$firstocc = 0; |
833
|
|
|
for ($i = 0; $i < $monthIndex; ++$i) { |
834
|
|
|
$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60; |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]); |
838
|
|
|
} |
839
|
|
|
else { |
840
|
|
|
// Calc first occ |
841
|
|
|
$firstocc = 0; |
842
|
|
|
$monthIndex = (int) gmdate("n", $this->recur["start"]); |
843
|
|
|
for ($i = 1; $i < $monthIndex; ++$i) { |
844
|
|
|
$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60; |
845
|
|
|
} |
846
|
|
|
|
847
|
|
|
$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]); |
848
|
|
|
} |
849
|
|
|
break; |
850
|
|
|
|
851
|
|
|
case rptMonthNth: |
852
|
|
|
// monthly: on Nth weekday of every M month |
853
|
|
|
// yearly: on Nth weekday of M month |
854
|
|
|
if (!isset($this->recur["weekdays"], $this->recur["nday"])) { |
855
|
|
|
return; |
856
|
|
|
} |
857
|
|
|
|
858
|
|
|
$weekdays = (int) $this->recur["weekdays"]; |
859
|
|
|
$nday = (int) $this->recur["nday"]; |
860
|
|
|
|
861
|
|
|
// Calc startdate |
862
|
|
|
$monthbegindow = (int) $this->recur["start"]; |
863
|
|
|
|
864
|
|
|
if ($nday == 5) { |
865
|
|
|
// Set date on the last day of the last month |
866
|
|
|
$monthbegindow += (gmdate("t", $monthbegindow) - gmdate("j", $monthbegindow)) * 24 * 60 * 60; |
867
|
|
|
} |
868
|
|
|
else { |
869
|
|
|
// Set on the first day of the month |
870
|
|
|
$monthbegindow -= ((gmdate("j", $monthbegindow) - 1) * 24 * 60 * 60); |
871
|
|
|
} |
872
|
|
|
|
873
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_YEARLY) { |
874
|
|
|
// Set on right month |
875
|
|
|
if ($selmonth < $curmonth) { |
876
|
|
|
$tmp = 12 - $curmonth + $selmonth; |
877
|
|
|
} |
878
|
|
|
else { |
879
|
|
|
$tmp = ($selmonth - $curmonth); |
880
|
|
|
} |
881
|
|
|
|
882
|
|
|
for ($i = 0; $i < $tmp; ++$i) { |
883
|
|
|
$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth); |
884
|
|
|
|
885
|
|
|
if ($curmonth == 12) { |
886
|
|
|
++$curyear; |
887
|
|
|
$curmonth = 0; |
888
|
|
|
} |
889
|
|
|
++$curmonth; |
890
|
|
|
} |
891
|
|
|
} |
892
|
|
|
else { |
893
|
|
|
// Check or you exist in the right month |
894
|
|
|
|
895
|
|
|
$dayofweek = gmdate("w", $monthbegindow); |
896
|
|
|
for ($i = 0; $i < 7; ++$i) { |
897
|
|
|
if ($nday == 5 && (($dayofweek - $i) % 7 >= 0) && (1 << (($dayofweek - $i) % 7)) & $weekdays) { |
898
|
|
|
$day = gmdate("j", $monthbegindow) - $i; |
899
|
|
|
break; |
900
|
|
|
} |
901
|
|
|
if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) { |
902
|
|
|
$day = (($nday - 1) * 7) + ($i + 1); |
903
|
|
|
break; |
904
|
|
|
} |
905
|
|
|
} |
906
|
|
|
|
907
|
|
|
// Goto the next X month |
908
|
|
|
if (isset($day) && ($day < gmdate("j", (int) $this->recur["start"]))) { |
909
|
|
|
if ($nday == 5) { |
910
|
|
|
$monthbegindow += 24 * 60 * 60; |
911
|
|
|
if ($curmonth == 12) { |
912
|
|
|
++$curyear; |
913
|
|
|
$curmonth = 0; |
914
|
|
|
} |
915
|
|
|
++$curmonth; |
916
|
|
|
} |
917
|
|
|
|
918
|
|
|
for ($i = 0; $i < $everyn; ++$i) { |
919
|
|
|
$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth); |
920
|
|
|
|
921
|
|
|
if ($curmonth == 12) { |
922
|
|
|
++$curyear; |
923
|
|
|
$curmonth = 0; |
924
|
|
|
} |
925
|
|
|
++$curmonth; |
926
|
|
|
} |
927
|
|
|
|
928
|
|
|
if ($nday == 5) { |
929
|
|
|
$monthbegindow -= 24 * 60 * 60; |
930
|
|
|
} |
931
|
|
|
} |
932
|
|
|
} |
933
|
|
|
|
934
|
|
|
// FIXME: weekstart? |
935
|
|
|
|
936
|
|
|
$day = 0; |
937
|
|
|
// Set start on the right day |
938
|
|
|
$dayofweek = gmdate("w", $monthbegindow); |
939
|
|
|
for ($i = 0; $i < 7; ++$i) { |
940
|
|
|
if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) { |
941
|
|
|
$day = $i; |
942
|
|
|
break; |
943
|
|
|
} |
944
|
|
|
if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) { |
945
|
|
|
$day = ($nday - 1) * 7 + ($i + 1); |
946
|
|
|
break; |
947
|
|
|
} |
948
|
|
|
} |
949
|
|
|
if ($nday == 5) { |
950
|
|
|
$monthbegindow -= $day * 24 * 60 * 60; |
951
|
|
|
} |
952
|
|
|
else { |
953
|
|
|
$monthbegindow += ($day - 1) * 24 * 60 * 60; |
954
|
|
|
} |
955
|
|
|
|
956
|
|
|
$firstocc = 0; |
957
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_MONTHLY) { |
958
|
|
|
// Calc first occ |
959
|
|
|
$monthIndex = ((((12 % $everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn; |
960
|
|
|
|
961
|
|
|
for ($i = 0; $i < $monthIndex; ++$i) { |
962
|
|
|
$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60; |
963
|
|
|
} |
964
|
|
|
|
965
|
|
|
$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday); |
966
|
|
|
} |
967
|
|
|
else { |
968
|
|
|
// Calc first occ |
969
|
|
|
$monthIndex = (int) gmdate("n", $this->recur["start"]); |
970
|
|
|
|
971
|
|
|
for ($i = 1; $i < $monthIndex; ++$i) { |
972
|
|
|
$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60; |
973
|
|
|
} |
974
|
|
|
|
975
|
|
|
$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday); |
976
|
|
|
} |
977
|
|
|
break; |
978
|
|
|
} |
979
|
|
|
break; |
980
|
|
|
} |
981
|
|
|
|
982
|
|
|
if (!isset($this->recur["term"])) { |
983
|
|
|
return; |
984
|
|
|
} |
985
|
|
|
|
986
|
|
|
$rdata .= pack("V", $term); |
987
|
|
|
|
988
|
|
|
switch ($term) { |
989
|
|
|
// After the given enddate |
990
|
|
|
case IDC_RCEV_PAT_ERB_END: |
991
|
|
|
$rdata .= pack("V", 10); |
992
|
|
|
break; |
993
|
|
|
|
994
|
|
|
// After a number of times |
995
|
|
|
case IDC_RCEV_PAT_ERB_AFTERNOCCUR: |
996
|
|
|
if (!isset($this->recur["numoccur"])) { |
997
|
|
|
return; |
998
|
|
|
} |
999
|
|
|
|
1000
|
|
|
$rdata .= pack("V", (int) $this->recur["numoccur"]); |
1001
|
|
|
break; |
1002
|
|
|
|
1003
|
|
|
// Never ends |
1004
|
|
|
case IDC_RCEV_PAT_ERB_NOEND: |
1005
|
|
|
$rdata .= pack("V", 0); |
1006
|
|
|
break; |
1007
|
|
|
} |
1008
|
|
|
|
1009
|
|
|
// Strange little thing for the recurrence type "every workday" |
1010
|
|
|
if ($rtype == IDC_RCEV_PAT_ORB_WEEKLY && ((int) $this->recur["subtype"]) == 1) { |
1011
|
|
|
$rdata .= pack("V", 1); |
1012
|
|
|
} |
1013
|
|
|
else { // Other recurrences |
1014
|
|
|
$rdata .= pack("V", 0); |
1015
|
|
|
} |
1016
|
|
|
|
1017
|
|
|
// Exception data |
1018
|
|
|
|
1019
|
|
|
// Get all exceptions |
1020
|
|
|
$deleted_items = $this->recur["deleted_occurrences"]; |
1021
|
|
|
$changed_items = $this->recur["changed_occurrences"]; |
1022
|
|
|
|
1023
|
|
|
// Merge deleted and changed items into one list |
1024
|
|
|
$items = $deleted_items; |
1025
|
|
|
|
1026
|
|
|
foreach ($changed_items as $changed_item) { |
1027
|
|
|
array_push($items, $this->dayStartOf($changed_item["basedate"])); |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
|
|
sort($items); |
1031
|
|
|
|
1032
|
|
|
// Add the merged list in to the rdata |
1033
|
|
|
$rdata .= pack("V", count($items)); |
1034
|
|
|
foreach ($items as $item) { |
1035
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($item)); |
1036
|
|
|
} |
1037
|
|
|
|
1038
|
|
|
// Loop through the changed exceptions (not deleted) |
1039
|
|
|
$rdata .= pack("V", count($changed_items)); |
1040
|
|
|
$items = []; |
1041
|
|
|
|
1042
|
|
|
foreach ($changed_items as $changed_item) { |
1043
|
|
|
$items[] = $this->dayStartOf($changed_item["start"]); |
1044
|
|
|
} |
1045
|
|
|
|
1046
|
|
|
sort($items); |
1047
|
|
|
|
1048
|
|
|
// Add the changed items list int the rdata |
1049
|
|
|
foreach ($items as $item) { |
1050
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($item)); |
1051
|
|
|
} |
1052
|
|
|
|
1053
|
|
|
// Set start date |
1054
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"])); |
1055
|
|
|
|
1056
|
|
|
// Set enddate |
1057
|
|
|
switch ($term) { |
1058
|
|
|
// After the given enddate |
1059
|
|
|
case IDC_RCEV_PAT_ERB_END: |
1060
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"])); |
1061
|
|
|
break; |
1062
|
|
|
|
1063
|
|
|
// After a number of times |
1064
|
|
|
case IDC_RCEV_PAT_ERB_AFTERNOCCUR: |
1065
|
|
|
// @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour |
1066
|
|
|
$occenddate = (int) $this->recur["start"]; |
1067
|
|
|
|
1068
|
|
|
switch ($rtype) { |
1069
|
|
|
case IDC_RCEV_PAT_ORB_DAILY: |
1070
|
|
|
if ($this->recur["subtype"] == rptWeek) { |
1071
|
|
|
// Daily every workday |
1072
|
|
|
$restocc = (int) $this->recur["numoccur"]; |
1073
|
|
|
|
1074
|
|
|
// Get starting weekday |
1075
|
|
|
$nowtime = $this->gmtime($occenddate); |
1076
|
|
|
$j = $nowtime["tm_wday"]; |
1077
|
|
|
|
1078
|
|
|
while (1) { |
1079
|
|
|
if (($j % 7) > 0 && ($j % 7) < 6) { |
1080
|
|
|
--$restocc; |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
++$j; |
1084
|
|
|
|
1085
|
|
|
if ($restocc <= 0) { |
1086
|
|
|
break; |
1087
|
|
|
} |
1088
|
|
|
|
1089
|
|
|
$occenddate += 24 * 60 * 60; |
1090
|
|
|
} |
1091
|
|
|
} |
1092
|
|
|
else { |
1093
|
|
|
// -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence) |
1094
|
|
|
$occenddate += (((int) $this->recur["everyn"]) * 60 * ((int) $this->recur["numoccur"] - 1)); |
1095
|
|
|
} |
1096
|
|
|
break; |
1097
|
|
|
|
1098
|
|
|
case IDC_RCEV_PAT_ORB_WEEKLY: |
1099
|
|
|
// Needed values |
1100
|
|
|
// $forwardcount - number of weeks we can skip forward |
1101
|
|
|
// $restocc - number of remaining occurrences after the week skip |
1102
|
|
|
|
1103
|
|
|
// Add the weeks till the last item |
1104
|
|
|
$occenddate += ($forwardcount * 7 * 24 * 60 * 60); |
1105
|
|
|
|
1106
|
|
|
$dayofweek = gmdate("w", $occenddate); |
|
|
|
|
1107
|
|
|
|
1108
|
|
|
// Loop through the last occurrences until we have had them all |
1109
|
|
|
for ($j = 1; $restocc > 0; ++$j) { |
1110
|
|
|
// Jump to the next week (which may be N weeks away) when going over the week boundary |
1111
|
|
|
if ((($dayofweek + $j) % 7) == $weekstart) { |
1112
|
|
|
$occenddate += (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60; |
1113
|
|
|
} |
1114
|
|
|
|
1115
|
|
|
// If this is a matching day, once less occurrence to process |
1116
|
|
|
if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) { |
1117
|
|
|
--$restocc; |
1118
|
|
|
} |
1119
|
|
|
|
1120
|
|
|
// Next day |
1121
|
|
|
$occenddate += 24 * 60 * 60; |
1122
|
|
|
} |
1123
|
|
|
|
1124
|
|
|
break; |
1125
|
|
|
|
1126
|
|
|
case IDC_RCEV_PAT_ORB_MONTHLY: |
1127
|
|
|
case IDC_RCEV_PAT_ORB_YEARLY: |
1128
|
|
|
$curyear = gmdate("Y", (int) $this->recur["start"]); |
1129
|
|
|
$curmonth = gmdate("n", (int) $this->recur["start"]); |
1130
|
|
|
// $forwardcount = months |
1131
|
|
|
|
1132
|
|
|
switch ((int) $this->recur["subtype"]) { |
1133
|
|
|
case rptMonth: // on D day of every M month |
1134
|
|
|
while ($forwardcount > 0) { |
1135
|
|
|
$occenddate += $this->getMonthInSeconds($curyear, $curmonth); |
1136
|
|
|
|
1137
|
|
|
if ($curmonth >= 12) { |
1138
|
|
|
$curmonth = 1; |
1139
|
|
|
++$curyear; |
1140
|
|
|
} |
1141
|
|
|
else { |
1142
|
|
|
++$curmonth; |
1143
|
|
|
} |
1144
|
|
|
--$forwardcount; |
1145
|
|
|
} |
1146
|
|
|
|
1147
|
|
|
// compensation between 28 and 31 |
1148
|
|
|
if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 && |
1149
|
|
|
gmdate("j", $occenddate) < ((int) $this->recur["monthday"])) { |
1150
|
|
|
if (gmdate("j", $occenddate) < 28) { |
1151
|
|
|
$occenddate -= gmdate("j", $occenddate) * 24 * 60 * 60; |
1152
|
|
|
} |
1153
|
|
|
else { |
1154
|
|
|
$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60; |
1155
|
|
|
} |
1156
|
|
|
} |
1157
|
|
|
|
1158
|
|
|
break; |
1159
|
|
|
|
1160
|
|
|
case rptMonthNth: // on Nth weekday of every M month |
1161
|
|
|
$nday = (int) $this->recur["nday"]; // 1 tot 5 |
1162
|
|
|
$weekdays = (int) $this->recur["weekdays"]; |
1163
|
|
|
|
1164
|
|
|
while ($forwardcount > 0) { |
1165
|
|
|
$occenddate += $this->getMonthInSeconds($curyear, $curmonth); |
1166
|
|
|
if ($curmonth >= 12) { |
1167
|
|
|
$curmonth = 1; |
1168
|
|
|
++$curyear; |
1169
|
|
|
} |
1170
|
|
|
else { |
1171
|
|
|
++$curmonth; |
1172
|
|
|
} |
1173
|
|
|
|
1174
|
|
|
--$forwardcount; |
1175
|
|
|
} |
1176
|
|
|
|
1177
|
|
|
if ($nday == 5) { |
1178
|
|
|
// Set date on the last day of the last month |
1179
|
|
|
$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60; |
1180
|
|
|
} |
1181
|
|
|
else { |
1182
|
|
|
// Set date on the first day of the last month |
1183
|
|
|
$occenddate -= (gmdate("j", $occenddate) - 1) * 24 * 60 * 60; |
1184
|
|
|
} |
1185
|
|
|
|
1186
|
|
|
$dayofweek = gmdate("w", $occenddate); |
1187
|
|
|
for ($i = 0; $i < 7; ++$i) { |
1188
|
|
|
if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) { |
1189
|
|
|
$occenddate -= $i * 24 * 60 * 60; |
1190
|
|
|
break; |
1191
|
|
|
} |
1192
|
|
|
if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) { |
1193
|
|
|
$occenddate += ($i + (($nday - 1) * 7)) * 24 * 60 * 60; |
1194
|
|
|
break; |
1195
|
|
|
} |
1196
|
|
|
} |
1197
|
|
|
|
1198
|
|
|
break; // case rptMonthNth |
1199
|
|
|
} |
1200
|
|
|
|
1201
|
|
|
break; |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
|
|
if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX) { |
1205
|
|
|
$occenddate = PHP_INT_MAX; |
1206
|
|
|
} |
1207
|
|
|
|
1208
|
|
|
$this->recur["end"] = $occenddate; |
1209
|
|
|
|
1210
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"])); |
1211
|
|
|
break; |
1212
|
|
|
|
1213
|
|
|
// Never ends |
1214
|
|
|
case IDC_RCEV_PAT_ERB_NOEND: |
1215
|
|
|
default: |
1216
|
|
|
$this->recur["end"] = 0x7FFFFFFF; // max date -> 2038 |
1217
|
|
|
$rdata .= pack("V", 0x5AE980DF); |
1218
|
|
|
break; |
1219
|
|
|
} |
1220
|
|
|
|
1221
|
|
|
// UTC date |
1222
|
|
|
$utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]); |
1223
|
|
|
$utcend = $this->toGMT($this->tz, (int) $this->recur["end"]); |
1224
|
|
|
|
1225
|
|
|
// utc date+time |
1226
|
|
|
$utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"]) * 60) : $utcstart; |
1227
|
|
|
$utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart; |
1228
|
|
|
|
1229
|
|
|
$propsToSet = []; |
1230
|
|
|
// update reminder time |
1231
|
|
|
$propsToSet[$this->proptags["reminder_time"]] = $utcfirstoccstartdatetime; |
1232
|
|
|
|
1233
|
|
|
// update first occurrence date |
1234
|
|
|
$propsToSet[$this->proptags["startdate"]] = $propsToSet[$this->proptags["commonstart"]] = $utcfirstoccstartdatetime; |
1235
|
|
|
$propsToSet[$this->proptags["duedate"]] = $propsToSet[$this->proptags["commonend"]] = $utcfirstoccenddatetime; |
1236
|
|
|
|
1237
|
|
|
// Set Outlook properties, if it is an appointment |
1238
|
|
|
if (isset($this->messageprops[$this->proptags["message_class"]]) && $this->messageprops[$this->proptags["message_class"]] == "IPM.Appointment") { |
1239
|
|
|
// update real begin and real end date |
1240
|
|
|
$propsToSet[$this->proptags["startdate_recurring"]] = $utcstart; |
1241
|
|
|
$propsToSet[$this->proptags["enddate_recurring"]] = $utcend; |
1242
|
|
|
|
1243
|
|
|
// recurrencetype |
1244
|
|
|
// Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype |
1245
|
|
|
$propsToSet[$this->proptags["recurrencetype"]] = ((int) $this->recur["type"]) - 0x9; |
1246
|
|
|
|
1247
|
|
|
// set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting |
1248
|
|
|
$propsToSet[$this->proptags["side_effects"]] = 369; |
1249
|
|
|
} |
1250
|
|
|
else { |
1251
|
|
|
$propsToSet[$this->proptags["side_effects"]] = 3441; |
1252
|
|
|
} |
1253
|
|
|
|
1254
|
|
|
// FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog |
1255
|
|
|
// Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset |
1256
|
|
|
// to the 'next' occurrence; this makes sure that deleting the next occurrence will correctly set the reminder to |
1257
|
|
|
// the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time) |
1258
|
|
|
// with the reminder flag set. |
1259
|
|
|
$reminderprops = mapi_getprops($this->message, [$this->proptags["reminder_minutes"], $this->proptags["flagdueby"]]); |
1260
|
|
|
if (isset($reminderprops[$this->proptags["reminder_minutes"]])) { |
1261
|
|
|
$occ = false; |
1262
|
|
|
$occurrences = $this->getItems(time(), 0x7FF00000, 3, true); |
1263
|
|
|
|
1264
|
|
|
for ($i = 0, $len = count($occurrences); $i < $len; ++$i) { |
1265
|
|
|
// This will actually also give us appointments that have already started, but not yet ended. Since we want the next |
1266
|
|
|
// reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum |
1267
|
|
|
// number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case: |
1268
|
|
|
// time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item |
1269
|
|
|
// can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped. |
1270
|
|
|
|
1271
|
|
|
if (($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) { |
1272
|
|
|
$occ = $occurrences[$i]; |
1273
|
|
|
break; |
1274
|
|
|
} |
1275
|
|
|
} |
1276
|
|
|
|
1277
|
|
|
if ($occ) { |
1278
|
|
|
if (isset($reminderprops[$this->proptags["flagdueby"]])) { |
1279
|
|
|
$propsToSet[$this->proptags["flagdueby"]] = $reminderprops[$this->proptags["flagdueby"]]; |
1280
|
|
|
} |
1281
|
|
|
else { |
1282
|
|
|
$propsToSet[$this->proptags["flagdueby"]] = $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60); |
1283
|
|
|
} |
1284
|
|
|
} |
1285
|
|
|
else { |
1286
|
|
|
// Last reminder passed, no reminders any more. |
1287
|
|
|
$propsToSet[$this->proptags["reminder"]] = false; |
1288
|
|
|
$propsToSet[$this->proptags["flagdueby"]] = 0x7FF00000; |
1289
|
|
|
} |
1290
|
|
|
} |
1291
|
|
|
|
1292
|
|
|
// Default data |
1293
|
|
|
// Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information) |
1294
|
|
|
$rdata .= pack("VV", 0x3006, 0x3008); |
1295
|
|
|
if (isset($this->recur["startocc"], $this->recur["endocc"])) { |
1296
|
|
|
// Set start and endtime in minutes |
1297
|
|
|
$rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]); |
1298
|
|
|
} |
1299
|
|
|
|
1300
|
|
|
// Detailed exception data |
1301
|
|
|
|
1302
|
|
|
$changed_items = $this->recur["changed_occurrences"]; |
1303
|
|
|
|
1304
|
|
|
$rdata .= pack("v", count($changed_items)); |
1305
|
|
|
|
1306
|
|
|
foreach ($changed_items as $changed_item) { |
1307
|
|
|
// Set start and end time of exception |
1308
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"])); |
1309
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"])); |
1310
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"])); |
1311
|
|
|
|
1312
|
|
|
// Bitmask |
1313
|
|
|
$bitmask = 0; |
1314
|
|
|
|
1315
|
|
|
// Check for changed strings |
1316
|
|
|
if (isset($changed_item["subject"])) { |
1317
|
|
|
$bitmask |= 1 << 0; |
1318
|
|
|
} |
1319
|
|
|
|
1320
|
|
|
if (isset($changed_item["remind_before"])) { |
1321
|
|
|
$bitmask |= 1 << 2; |
1322
|
|
|
} |
1323
|
|
|
|
1324
|
|
|
if (isset($changed_item["reminder_set"])) { |
1325
|
|
|
$bitmask |= 1 << 3; |
1326
|
|
|
} |
1327
|
|
|
|
1328
|
|
|
if (isset($changed_item["location"])) { |
1329
|
|
|
$bitmask |= 1 << 4; |
1330
|
|
|
} |
1331
|
|
|
|
1332
|
|
|
if (isset($changed_item["busystatus"])) { |
1333
|
|
|
$bitmask |= 1 << 5; |
1334
|
|
|
} |
1335
|
|
|
|
1336
|
|
|
if (isset($changed_item["alldayevent"])) { |
1337
|
|
|
$bitmask |= 1 << 7; |
1338
|
|
|
} |
1339
|
|
|
|
1340
|
|
|
if (isset($changed_item["label"])) { |
1341
|
|
|
$bitmask |= 1 << 8; |
1342
|
|
|
} |
1343
|
|
|
|
1344
|
|
|
$rdata .= pack("v", $bitmask); |
1345
|
|
|
|
1346
|
|
|
// Set "subject" |
1347
|
|
|
if (isset($changed_item["subject"])) { |
1348
|
|
|
// convert utf-8 to non-unicode blob string (us-ascii?) |
1349
|
|
|
$subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]); |
1350
|
|
|
$length = strlen($subject); |
1351
|
|
|
$rdata .= pack("vv", $length + 1, $length); |
1352
|
|
|
$rdata .= pack("a" . $length, $subject); |
1353
|
|
|
} |
1354
|
|
|
|
1355
|
|
|
if (isset($changed_item["remind_before"])) { |
1356
|
|
|
$rdata .= pack("V", $changed_item["remind_before"]); |
1357
|
|
|
} |
1358
|
|
|
|
1359
|
|
|
if (isset($changed_item["reminder_set"])) { |
1360
|
|
|
$rdata .= pack("V", $changed_item["reminder_set"]); |
1361
|
|
|
} |
1362
|
|
|
|
1363
|
|
|
if (isset($changed_item["location"])) { |
1364
|
|
|
$location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]); |
1365
|
|
|
$length = strlen($location); |
1366
|
|
|
$rdata .= pack("vv", $length + 1, $length); |
1367
|
|
|
$rdata .= pack("a" . $length, $location); |
1368
|
|
|
} |
1369
|
|
|
|
1370
|
|
|
if (isset($changed_item["busystatus"])) { |
1371
|
|
|
$rdata .= pack("V", $changed_item["busystatus"]); |
1372
|
|
|
} |
1373
|
|
|
|
1374
|
|
|
if (isset($changed_item["alldayevent"])) { |
1375
|
|
|
$rdata .= pack("V", $changed_item["alldayevent"]); |
1376
|
|
|
} |
1377
|
|
|
|
1378
|
|
|
if (isset($changed_item["label"])) { |
1379
|
|
|
$rdata .= pack("V", $changed_item["label"]); |
1380
|
|
|
} |
1381
|
|
|
} |
1382
|
|
|
|
1383
|
|
|
$rdata .= pack("V", 0); |
1384
|
|
|
|
1385
|
|
|
// write extended data |
1386
|
|
|
foreach ($changed_items as $changed_item) { |
1387
|
|
|
$rdata .= pack("V", 0); |
1388
|
|
|
if (isset($changed_item["subject"]) || isset($changed_item["location"])) { |
1389
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"])); |
1390
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"])); |
1391
|
|
|
$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"])); |
1392
|
|
|
} |
1393
|
|
|
|
1394
|
|
|
if (isset($changed_item["subject"])) { |
1395
|
|
|
$subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]); |
1396
|
|
|
$length = iconv_strlen($subject, "UCS-2LE"); |
1397
|
|
|
$rdata .= pack("v", $length); |
1398
|
|
|
$rdata .= pack("a" . $length * 2, $subject); |
1399
|
|
|
} |
1400
|
|
|
|
1401
|
|
|
if (isset($changed_item["location"])) { |
1402
|
|
|
$location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]); |
1403
|
|
|
$length = iconv_strlen($location, "UCS-2LE"); |
1404
|
|
|
$rdata .= pack("v", $length); |
1405
|
|
|
$rdata .= pack("a" . $length * 2, $location); |
1406
|
|
|
} |
1407
|
|
|
|
1408
|
|
|
if (isset($changed_item["subject"]) || isset($changed_item["location"])) { |
1409
|
|
|
$rdata .= pack("V", 0); |
1410
|
|
|
} |
1411
|
|
|
} |
1412
|
|
|
|
1413
|
|
|
$rdata .= pack("V", 0); |
1414
|
|
|
|
1415
|
|
|
// Set props |
1416
|
|
|
$propsToSet[$this->proptags["recurring_data"]] = $rdata; |
1417
|
|
|
$propsToSet[$this->proptags["recurring"]] = true; |
1418
|
|
|
if (isset($this->tz) && $this->tz) { |
1419
|
|
|
$timezone = "GMT"; |
1420
|
|
|
if ($this->tz["timezone"] != 0) { |
1421
|
|
|
// Create user readable timezone information |
1422
|
|
|
$timezone = sprintf( |
1423
|
|
|
"(GMT %s%02d:%02d)",- |
1424
|
|
|
$this->tz["timezone"] > 0 ? "+" : "-", |
1425
|
|
|
abs($this->tz["timezone"] / 60), |
1426
|
|
|
abs($this->tz["timezone"] % 60) |
1427
|
|
|
); |
1428
|
|
|
} |
1429
|
|
|
$propsToSet[$this->proptags["timezone_data"]] = $this->getTimezoneData($this->tz); |
1430
|
|
|
$propsToSet[$this->proptags["timezone"]] = $timezone; |
1431
|
|
|
} |
1432
|
|
|
mapi_setprops($this->message, $propsToSet); |
1433
|
|
|
} |
1434
|
|
|
|
1435
|
|
|
/** |
1436
|
|
|
* Function which converts a recurrence date timestamp to an unix date timestamp. |
1437
|
|
|
* |
1438
|
|
|
* @author Steve Hardy |
1439
|
|
|
* |
1440
|
|
|
* @param int $rdate the date which will be converted |
1441
|
|
|
* |
1442
|
|
|
* @return int the converted date |
1443
|
|
|
*/ |
1444
|
|
|
public function recurDataToUnixData($rdate) { |
1445
|
|
|
return ($rdate - 194074560) * 60; |
1446
|
|
|
} |
1447
|
|
|
|
1448
|
|
|
/** |
1449
|
|
|
* Function which converts an unix date timestamp to recurrence date timestamp. |
1450
|
|
|
* |
1451
|
|
|
* @author Johnny Biemans |
1452
|
|
|
* |
1453
|
|
|
* @param int $date the date which will be converted |
1454
|
|
|
* |
1455
|
|
|
* @return float|int the converted date in minutes |
1456
|
|
|
*/ |
1457
|
|
|
public function unixDataToRecurData($date) { |
1458
|
|
|
return ($date / 60) + 194074560; |
1459
|
|
|
} |
1460
|
|
|
|
1461
|
|
|
/** |
1462
|
|
|
* gmtime() doesn't exist in standard PHP, so we have to implement it ourselves. |
1463
|
|
|
* |
1464
|
|
|
* @author Steve Hardy |
1465
|
|
|
* |
1466
|
|
|
* @param mixed $ts |
1467
|
|
|
* |
1468
|
|
|
* @return float|int |
1469
|
|
|
*/ |
1470
|
|
|
public function GetTZOffset($ts) { |
1471
|
|
|
$Offset = date("O", $ts); |
1472
|
|
|
|
1473
|
|
|
$Parity = $Offset < 0 ? -1 : 1; |
1474
|
|
|
$Offset = $Parity * $Offset; |
1475
|
|
|
$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100; |
1476
|
|
|
|
1477
|
|
|
return $Parity * $Offset; |
1478
|
|
|
} |
1479
|
|
|
|
1480
|
|
|
/** |
1481
|
|
|
* gmtime() doesn't exist in standard PHP, so we have to implement it ourselves. |
1482
|
|
|
* |
1483
|
|
|
* @author Steve Hardy |
1484
|
|
|
* |
1485
|
|
|
* @param int $time |
1486
|
|
|
* |
1487
|
|
|
* @return array GMT Time |
1488
|
|
|
*/ |
1489
|
|
|
public function gmtime($time) { |
1490
|
|
|
$TZOffset = $this->GetTZOffset($time); |
1491
|
|
|
|
1492
|
|
|
$t_time = $time - $TZOffset * 60; # Counter adjust for localtime() |
1493
|
|
|
|
1494
|
|
|
return localtime($t_time, 1); |
1495
|
|
|
} |
1496
|
|
|
|
1497
|
|
|
/** |
1498
|
|
|
* @param float|string $year |
1499
|
|
|
*/ |
1500
|
|
|
public function isLeapYear($year): bool { |
1501
|
|
|
return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0); |
1502
|
|
|
} |
1503
|
|
|
|
1504
|
|
|
/** |
1505
|
|
|
* @param float|string $year |
1506
|
|
|
* @param int|string $month |
1507
|
|
|
*/ |
1508
|
|
|
public function getMonthInSeconds($year, $month): int { |
1509
|
|
|
if (in_array($month, [1, 3, 5, 7, 8, 10, 12])) { |
1510
|
|
|
$day = 31; |
1511
|
|
|
} |
1512
|
|
|
elseif (in_array($month, [4, 6, 9, 11])) { |
1513
|
|
|
$day = 30; |
1514
|
|
|
} |
1515
|
|
|
else { |
1516
|
|
|
$day = 28; |
1517
|
|
|
if ($this->isLeapYear($year) == 1) { |
1518
|
|
|
++$day; |
1519
|
|
|
} |
1520
|
|
|
} |
1521
|
|
|
|
1522
|
|
|
return $day * 24 * 60 * 60; |
1523
|
|
|
} |
1524
|
|
|
|
1525
|
|
|
/** |
1526
|
|
|
* Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour. |
1527
|
|
|
* |
1528
|
|
|
* @param int $year |
1529
|
|
|
* @param int $month |
1530
|
|
|
* @param int $week |
1531
|
|
|
* @param int $day |
1532
|
|
|
* @param int $hour |
1533
|
|
|
* |
1534
|
|
|
* @return int the timestamp of the given date, timezone-independent |
1535
|
|
|
*/ |
1536
|
|
|
public function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour) { |
1537
|
|
|
// get first day of month |
1538
|
|
|
$date = gmmktime(0, 0, 0, $month, 0, $year + 1900); |
1539
|
|
|
|
1540
|
|
|
// get wday info |
1541
|
|
|
$gmdate = $this->gmtime($date); |
1542
|
|
|
|
1543
|
|
|
$date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week |
1544
|
|
|
|
1545
|
|
|
$date += $week * 7 * 24 * 60 * 60; // go to correct week nr |
1546
|
|
|
$date += $day * 24 * 60 * 60; |
1547
|
|
|
$date += $hour * 60 * 60; |
1548
|
|
|
|
1549
|
|
|
$gmdate = $this->gmtime($date); |
1550
|
|
|
|
1551
|
|
|
// if we are in the next month, then back up a week, because week '5' means |
1552
|
|
|
// 'last week of month' |
1553
|
|
|
|
1554
|
|
|
if ($month != $gmdate["tm_mon"] + 1) { |
1555
|
|
|
$date -= 7 * 24 * 60 * 60; |
1556
|
|
|
} |
1557
|
|
|
|
1558
|
|
|
return $date; |
1559
|
|
|
} |
1560
|
|
|
|
1561
|
|
|
/** |
1562
|
|
|
* getTimezone gives the timezone offset (in minutes) of the given |
1563
|
|
|
* local date/time according to the given TZ info. |
1564
|
|
|
* |
1565
|
|
|
* @param mixed $tz |
1566
|
|
|
* @param mixed $date |
1567
|
|
|
*/ |
1568
|
|
|
public function getTimezone($tz, $date) { |
1569
|
|
|
// No timezone -> GMT (+0) |
1570
|
|
|
if (!isset($tz["timezone"])) { |
1571
|
|
|
return 0; |
1572
|
|
|
} |
1573
|
|
|
|
1574
|
|
|
$dst = false; |
1575
|
|
|
$gmdate = $this->gmtime($date); |
1576
|
|
|
|
1577
|
|
|
$dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]); |
1578
|
|
|
$dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]); |
1579
|
|
|
|
1580
|
|
|
if ($dststart <= $dstend) { |
1581
|
|
|
// Northern hemisphere, eg DST is during Mar-Oct |
1582
|
|
|
if ($date > $dststart && $date < $dstend) { |
1583
|
|
|
$dst = true; |
1584
|
|
|
} |
1585
|
|
|
} |
1586
|
|
|
else { |
1587
|
|
|
// Southern hemisphere, eg DST is during Oct-Mar |
1588
|
|
|
if ($date < $dstend || $date > $dststart) { |
1589
|
|
|
$dst = true; |
1590
|
|
|
} |
1591
|
|
|
} |
1592
|
|
|
|
1593
|
|
|
if ($dst) { |
1594
|
|
|
return $tz["timezone"] + $tz["timezonedst"]; |
1595
|
|
|
} |
1596
|
|
|
|
1597
|
|
|
return $tz["timezone"]; |
1598
|
|
|
} |
1599
|
|
|
|
1600
|
|
|
/** |
1601
|
|
|
* parseTimezone parses the timezone as specified in named property 0x8233 |
1602
|
|
|
* in Outlook calendar messages. Returns the timezone in minutes negative |
1603
|
|
|
* offset (GMT +2:00 -> -120). |
1604
|
|
|
* |
1605
|
|
|
* @param mixed $data |
1606
|
|
|
* |
1607
|
|
|
* @return null|array|false |
1608
|
|
|
*/ |
1609
|
|
|
public function parseTimezone($data) { |
1610
|
|
|
if (strlen($data) < 48) { |
1611
|
|
|
return; |
1612
|
|
|
} |
1613
|
|
|
|
1614
|
|
|
return unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data); |
1615
|
|
|
} |
1616
|
|
|
|
1617
|
|
|
/** |
1618
|
|
|
* @param mixed $tz |
1619
|
|
|
* |
1620
|
|
|
* @return false|string |
1621
|
|
|
*/ |
1622
|
|
|
public function getTimezoneData($tz) { |
1623
|
|
|
return pack("lllllvvllvlvvlv", $tz["timezone"], 0, $tz["timezonedst"], 0, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendhour"], 0, 0, 0, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststarthour"], 0, 0); |
1624
|
|
|
} |
1625
|
|
|
|
1626
|
|
|
/** |
1627
|
|
|
* toGMT returns a timestamp in GMT time for the time and timezone given. |
1628
|
|
|
* |
1629
|
|
|
* @param mixed $tz |
1630
|
|
|
* @param mixed $date |
1631
|
|
|
*/ |
1632
|
|
|
public function toGMT($tz, $date) { |
1633
|
|
|
if (!isset($tz['timezone'])) { |
1634
|
|
|
return $date; |
1635
|
|
|
} |
1636
|
|
|
$offset = $this->getTimezone($tz, $date); |
1637
|
|
|
|
1638
|
|
|
return $date + $offset * 60; |
1639
|
|
|
} |
1640
|
|
|
|
1641
|
|
|
/** |
1642
|
|
|
* fromGMT returns a timestamp in the local timezone given from the GMT time given. |
1643
|
|
|
* |
1644
|
|
|
* @param mixed $tz |
1645
|
|
|
* @param mixed $date |
1646
|
|
|
*/ |
1647
|
|
|
public function fromGMT($tz, $date) { |
1648
|
|
|
$offset = $this->getTimezone($tz, $date); |
1649
|
|
|
|
1650
|
|
|
return $date - $offset * 60; |
1651
|
|
|
} |
1652
|
|
|
|
1653
|
|
|
/** |
1654
|
|
|
* Function to get timestamp of the beginning of the day of the timestamp given. |
1655
|
|
|
* |
1656
|
|
|
* @param mixed $date |
1657
|
|
|
* |
1658
|
|
|
* @return false|int timestamp referring to same day but at 00:00:00 |
1659
|
|
|
*/ |
1660
|
|
|
public function dayStartOf($date) { |
1661
|
|
|
$time1 = $this->gmtime($date); |
1662
|
|
|
|
1663
|
|
|
return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900); |
1664
|
|
|
} |
1665
|
|
|
|
1666
|
|
|
/** |
1667
|
|
|
* Function to get timestamp of the beginning of the month of the timestamp given. |
1668
|
|
|
* |
1669
|
|
|
* @param mixed $date |
1670
|
|
|
* |
1671
|
|
|
* @return false|int Timestamp referring to same month but on the first day, and at 00:00:00 |
1672
|
|
|
*/ |
1673
|
|
|
public function monthStartOf($date) { |
1674
|
|
|
$time1 = $this->gmtime($date); |
1675
|
|
|
|
1676
|
|
|
return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900); |
1677
|
|
|
} |
1678
|
|
|
|
1679
|
|
|
/** |
1680
|
|
|
* Function to get timestamp of the beginning of the year of the timestamp given. |
1681
|
|
|
* |
1682
|
|
|
* @param mixed $date |
1683
|
|
|
* |
1684
|
|
|
* @return false|int Timestamp referring to the same year but on Jan 01, at 00:00:00 |
1685
|
|
|
*/ |
1686
|
|
|
public function yearStartOf($date) { |
1687
|
|
|
$time1 = $this->gmtime($date); |
1688
|
|
|
|
1689
|
|
|
return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900); |
1690
|
|
|
} |
1691
|
|
|
|
1692
|
|
|
/** |
1693
|
|
|
* Function which returns the items in a given interval. This included expansion of the recurrence and |
1694
|
|
|
* processing of exceptions (modified and deleted). |
1695
|
|
|
* |
1696
|
|
|
* @param int $start start time of the interval (GMT) |
1697
|
|
|
* @param int $end end time of the interval (GMT) |
1698
|
|
|
* @param mixed $limit |
1699
|
|
|
* @param mixed $remindersonly |
1700
|
|
|
* |
1701
|
|
|
* @return (array|mixed)[] |
1702
|
|
|
* |
1703
|
|
|
* @psalm-return array<int, T|array> |
1704
|
|
|
*/ |
1705
|
|
|
public function getItems($start, $end, $limit = 0, $remindersonly = false): array { |
1706
|
|
|
$items = []; |
1707
|
|
|
$firstday = 0; |
1708
|
|
|
|
1709
|
|
|
if (!isset($this->recur)) { |
1710
|
|
|
return $items; |
1711
|
|
|
} |
1712
|
|
|
|
1713
|
|
|
// Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which |
1714
|
|
|
// exceptions are in range and have a reminder set |
1715
|
|
|
if ($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) { |
1716
|
|
|
// Sort exceptions by start time |
1717
|
|
|
uasort($this->recur["changed_occurrences"], [$this, "sortExceptionStart"]); |
1718
|
|
|
|
1719
|
|
|
// Loop through all changed exceptions |
1720
|
|
|
foreach ($this->recur["changed_occurrences"] as $exception) { |
1721
|
|
|
// Check reminder set |
1722
|
|
|
if (!isset($exception["reminder"]) || $exception["reminder"] == false) { |
1723
|
|
|
continue; |
1724
|
|
|
} |
1725
|
|
|
|
1726
|
|
|
// Convert to GMT |
1727
|
|
|
$occstart = $this->toGMT($this->tz, $exception["start"]); |
1728
|
|
|
$occend = $this->toGMT($this->tz, $exception["end"]); |
1729
|
|
|
|
1730
|
|
|
// Check range criterium |
1731
|
|
|
if ($occstart > $end || $occend < $start) { |
1732
|
|
|
continue; |
1733
|
|
|
} |
1734
|
|
|
|
1735
|
|
|
// OK, add to items. |
1736
|
|
|
array_push($items, $this->getExceptionProperties($exception)); |
1737
|
|
|
if ($limit && (count($items) == $limit)) { |
1738
|
|
|
break; |
1739
|
|
|
} |
1740
|
|
|
} |
1741
|
|
|
|
1742
|
|
|
uasort($items, [$this, "sortStarttime"]); |
1743
|
|
|
|
1744
|
|
|
return $items; |
1745
|
|
|
} |
1746
|
|
|
|
1747
|
|
|
// From here on, the dates of the occurrences are calculated in local time, so the days we're looking |
1748
|
|
|
// at are calculated from the local time dates of $start and $end |
1749
|
|
|
|
1750
|
|
|
if (isset($this->recur['regen'], $this->action['datecompleted']) && $this->recur['regen']) { |
1751
|
|
|
$daystart = $this->dayStartOf($this->action['datecompleted']); |
1752
|
|
|
} |
1753
|
|
|
else { |
1754
|
|
|
$daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence |
1755
|
|
|
} |
1756
|
|
|
|
1757
|
|
|
// Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view |
1758
|
|
|
// or the end of the recurrence, whichever comes first |
1759
|
|
|
if ($end > $this->toGMT($this->tz, $this->recur["end"])) { |
1760
|
|
|
$rangeend = $this->toGMT($this->tz, $this->recur["end"]); |
1761
|
|
|
} |
1762
|
|
|
else { |
1763
|
|
|
$rangeend = $end; |
1764
|
|
|
} |
1765
|
|
|
|
1766
|
|
|
$dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend)); |
1767
|
|
|
|
1768
|
|
|
// Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range. |
1769
|
|
|
$recurType = (int) $this->recur["type"] < 0x2000 ? (int) $this->recur["type"] + 0x2000 : (int) $this->recur["type"]; |
1770
|
|
|
|
1771
|
|
|
switch ($recurType) { |
1772
|
|
|
case IDC_RCEV_PAT_ORB_DAILY: |
1773
|
|
|
if ($this->recur["everyn"] <= 0) { |
1774
|
|
|
$this->recur["everyn"] = 1440; |
1775
|
|
|
} |
1776
|
|
|
|
1777
|
|
|
if ($this->recur["subtype"] == rptDay) { |
1778
|
|
|
// Every Nth day |
1779
|
|
|
for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) { |
1780
|
|
|
$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1781
|
|
|
} |
1782
|
|
|
break; |
1783
|
|
|
} |
1784
|
|
|
// Every workday |
1785
|
|
|
for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440) { |
1786
|
|
|
$nowtime = $this->gmtime($now); |
1787
|
|
|
if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace |
1788
|
|
|
$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1789
|
|
|
} |
1790
|
|
|
} |
1791
|
|
|
break; |
1792
|
|
|
|
1793
|
|
|
case IDC_RCEV_PAT_ORB_WEEKLY: |
1794
|
|
|
if ($this->recur["everyn"] <= 0) { |
1795
|
|
|
$this->recur["everyn"] = 1; |
1796
|
|
|
} |
1797
|
|
|
|
1798
|
|
|
// If sliding flag is set then move to 'n' weeks |
1799
|
|
|
if ($this->recur['regen']) { |
1800
|
|
|
$daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]); |
1801
|
|
|
} |
1802
|
|
|
|
1803
|
|
|
for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"])) { |
1804
|
|
|
if ($this->recur['regen']) { |
1805
|
|
|
$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1806
|
|
|
continue; |
1807
|
|
|
} |
1808
|
|
|
// Loop through the whole following week to the first occurrence of the week, add each day that is specified |
1809
|
|
|
for ($wday = 0; $wday < 7; ++$wday) { |
1810
|
|
|
$daynow = $now + $wday * 60 * 60 * 24; |
1811
|
|
|
// checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item |
1812
|
|
|
if ($daynow > $dayend) { |
1813
|
|
|
continue; |
1814
|
|
|
} |
1815
|
|
|
$nowtime = $this->gmtime($daynow); // Get the weekday of the current day |
1816
|
|
|
if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ? |
1817
|
|
|
$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1818
|
|
|
} |
1819
|
|
|
} |
1820
|
|
|
} |
1821
|
|
|
break; |
1822
|
|
|
|
1823
|
|
|
case IDC_RCEV_PAT_ORB_MONTHLY: |
1824
|
|
|
if ($this->recur["everyn"] <= 0) { |
1825
|
|
|
$this->recur["everyn"] = 1; |
1826
|
|
|
} |
1827
|
|
|
|
1828
|
|
|
// Loop through all months from start to end of occurrence, starting at beginning of first month |
1829
|
|
|
for ($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) { |
|
|
|
|
1830
|
|
|
if (isset($this->recur["monthday"]) && ($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months |
1831
|
|
|
$difference = 1; |
1832
|
|
|
if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) { |
1833
|
|
|
$difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1; |
1834
|
|
|
} |
1835
|
|
|
$daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60); |
1836
|
|
|
// checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item |
1837
|
|
|
if ($daynow <= $dayend) { |
1838
|
|
|
$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
|
|
|
|
1839
|
|
|
} |
1840
|
|
|
} |
1841
|
|
|
elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] of every N months |
1842
|
|
|
// Sanitize input |
1843
|
|
|
if ($this->recur["weekdays"] == 0) { |
1844
|
|
|
$this->recur["weekdays"] = 1; |
1845
|
|
|
} |
1846
|
|
|
|
1847
|
|
|
// If nday is not set to the last day in the month |
1848
|
|
|
if ($this->recur["nday"] < 5) { |
1849
|
|
|
// keep the track of no. of time correct selection pattern (like 2nd weekday, 4th friday, etc.) is matched |
1850
|
|
|
$ndaycounter = 0; |
1851
|
|
|
// Find matching weekday in this month |
1852
|
|
|
for ($day = 0, $total = $this->daysInMonth($now, 1); $day < $total; ++$day) { |
1853
|
|
|
$daynow = $now + $day * 60 * 60 * 24; |
1854
|
|
|
$nowtime = $this->gmtime($daynow); // Get the weekday of the current day |
1855
|
|
|
|
1856
|
|
|
if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ? |
1857
|
|
|
++$ndaycounter; |
1858
|
|
|
} |
1859
|
|
|
// check the selected pattern is same as asked Nth weekday,If so set the firstday |
1860
|
|
|
if ($this->recur["nday"] == $ndaycounter) { |
1861
|
|
|
$firstday = $day; |
1862
|
|
|
break; |
1863
|
|
|
} |
1864
|
|
|
} |
1865
|
|
|
// $firstday is the day of the month on which the asked pattern of nth weekday matches |
1866
|
|
|
$daynow = $now + $firstday * 60 * 60 * 24; |
1867
|
|
|
} |
1868
|
|
|
else { |
1869
|
|
|
// Find last day in the month ($now is the firstday of the month) |
1870
|
|
|
$NumDaysInMonth = $this->daysInMonth($now, 1); |
1871
|
|
|
$daynow = $now + (($NumDaysInMonth - 1) * 24 * 60 * 60); |
1872
|
|
|
|
1873
|
|
|
$nowtime = $this->gmtime($daynow); |
|
|
|
|
1874
|
|
|
while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) == 0) { |
1875
|
|
|
$daynow -= 86400; |
1876
|
|
|
$nowtime = $this->gmtime($daynow); |
1877
|
|
|
} |
1878
|
|
|
} |
1879
|
|
|
|
1880
|
|
|
/* |
1881
|
|
|
* checks weather the next coming day in recurrence pattern is less than or equal to end day of the * recurring item.Also check weather the coming day in recurrence pattern is greater than or equal to start * of recurring pattern, so that appointment that fall under the recurrence range are only displayed. |
1882
|
|
|
*/ |
1883
|
|
|
if ($daynow <= $dayend && $daynow >= $daystart) { |
1884
|
|
|
$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1885
|
|
|
} |
1886
|
|
|
} |
1887
|
|
|
elseif ($this->recur['regen']) { |
1888
|
|
|
$next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60); |
1889
|
|
|
$now = $daystart + ($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60); |
1890
|
|
|
|
1891
|
|
|
if ($now <= $dayend) { |
1892
|
|
|
$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1893
|
|
|
} |
1894
|
|
|
} |
1895
|
|
|
} |
1896
|
|
|
break; |
1897
|
|
|
|
1898
|
|
|
case IDC_RCEV_PAT_ORB_YEARLY: |
1899
|
|
|
if ($this->recur["everyn"] <= 0) { |
1900
|
|
|
$this->recur["everyn"] = 12; |
1901
|
|
|
} |
1902
|
|
|
|
1903
|
|
|
for ($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) { |
1904
|
|
|
if (isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month |
1905
|
|
|
// recur["month"] is in minutes since the beginning of the year |
1906
|
|
|
$month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11] |
1907
|
|
|
$monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31] |
1908
|
|
|
$monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month |
1909
|
|
|
if ($monthday > $this->daysInMonth($monthstart, 1)) { |
1910
|
|
|
$monthday = $this->daysInMonth($monthstart, 1); |
1911
|
|
|
} // Cap $monthday on month length (eg 28 feb instead of 29 feb) |
1912
|
|
|
$daynow = $monthstart + ($monthday - 1) * 24 * 60 * 60; |
1913
|
|
|
$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1914
|
|
|
} |
1915
|
|
|
elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] in month X of every N years |
1916
|
|
|
// Go the correct month |
1917
|
|
|
$monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60; |
1918
|
|
|
|
1919
|
|
|
// Find first matching weekday in this month |
1920
|
|
|
for ($wday = 0; $wday < 7; ++$wday) { |
1921
|
|
|
$daynow = $monthnow + $wday * 60 * 60 * 24; |
1922
|
|
|
$nowtime = $this->gmtime($daynow); // Get the weekday of the current day |
1923
|
|
|
|
1924
|
|
|
if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ? |
1925
|
|
|
$firstday = $wday; |
1926
|
|
|
break; |
1927
|
|
|
} |
1928
|
|
|
} |
1929
|
|
|
|
1930
|
|
|
// Same as above (monthly) |
1931
|
|
|
$daynow = $monthnow + ($firstday + ($this->recur["nday"] - 1) * 7) * 60 * 60 * 24; |
1932
|
|
|
|
1933
|
|
|
while ($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) { |
1934
|
|
|
$daynow -= 7 * 60 * 60 * 24; |
1935
|
|
|
} |
1936
|
|
|
|
1937
|
|
|
$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1938
|
|
|
} |
1939
|
|
|
elseif ($this->recur['regen']) { |
1940
|
|
|
$year_starttime = $this->gmtime($now); |
1941
|
|
|
$is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1); // +1 next year |
1942
|
|
|
$now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /* year in seconds */); |
1943
|
|
|
|
1944
|
|
|
if ($now <= $dayend) { |
1945
|
|
|
$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly); |
1946
|
|
|
} |
1947
|
|
|
} |
1948
|
|
|
} |
1949
|
|
|
break; |
1950
|
|
|
} |
1951
|
|
|
// to get all exception items |
1952
|
|
|
if (!empty($this->recur['changed_occurrences'])) { |
1953
|
|
|
$this->processExceptionItems($items, $start, $end); |
|
|
|
|
1954
|
|
|
} |
1955
|
|
|
|
1956
|
|
|
// sort items on starttime |
1957
|
|
|
usort($items, [$this, "sortStarttime"]); |
1958
|
|
|
|
1959
|
|
|
// Return the MAPI-compatible list of items for this object |
1960
|
|
|
return $items; |
1961
|
|
|
} |
1962
|
|
|
|
1963
|
|
|
/** |
1964
|
|
|
* @psalm-return -1|0|1 |
1965
|
|
|
* |
1966
|
|
|
* @param mixed $a |
1967
|
|
|
* @param mixed $b |
1968
|
|
|
*/ |
1969
|
|
|
public function sortStarttime($a, $b): int { |
1970
|
|
|
$aTime = $a[$this->proptags["startdate"]]; |
1971
|
|
|
$bTime = $b[$this->proptags["startdate"]]; |
1972
|
|
|
|
1973
|
|
|
return $aTime == $bTime ? 0 : ($aTime > $bTime ? 1 : -1); |
1974
|
|
|
} |
1975
|
|
|
|
1976
|
|
|
/** |
1977
|
|
|
* daysInMonth. |
1978
|
|
|
* |
1979
|
|
|
* Returns the number of days in the upcoming number of months. If you specify 1 month as |
1980
|
|
|
* $months it will give you the number of days in the month of $date. If you specify more it |
1981
|
|
|
* will also count the days in the upcoming months and add that to the number of days. So |
1982
|
|
|
* if you have a date in march and you specify $months as 2 it will return 61. |
1983
|
|
|
* |
1984
|
|
|
* @param int $date specified date as timestamp from which you want to know the number |
1985
|
|
|
* of days in the month |
1986
|
|
|
* @param int $months number of months you want to know the number of days in |
1987
|
|
|
* |
1988
|
|
|
* @return float|int number of days in the specified amount of months |
1989
|
|
|
*/ |
1990
|
|
|
public function daysInMonth($date, $months) { |
1991
|
|
|
$days = 0; |
1992
|
|
|
|
1993
|
|
|
for ($i = 0; $i < $months; ++$i) { |
1994
|
|
|
$days += date("t", $date + $days * 24 * 60 * 60); |
1995
|
|
|
} |
1996
|
|
|
|
1997
|
|
|
return $days; |
1998
|
|
|
} |
1999
|
|
|
|
2000
|
|
|
// Converts MAPI-style 'minutes' into the month of the year [0..11] |
2001
|
|
|
public function monthOfYear($minutes) { |
2002
|
|
|
$d = gmmktime(0, 0, 0, 1, 1, 2001); // The year 2001 was a non-leap year, and the minutes provided are always in non-leap-year-minutes |
2003
|
|
|
|
2004
|
|
|
$d += $minutes * 60; |
2005
|
|
|
|
2006
|
|
|
$dtime = $this->gmtime($d); |
2007
|
|
|
|
2008
|
|
|
return $dtime["tm_mon"]; |
2009
|
|
|
} |
2010
|
|
|
|
2011
|
|
|
/** |
2012
|
|
|
* @psalm-return -1|0|1 |
2013
|
|
|
* |
2014
|
|
|
* @param mixed $a |
2015
|
|
|
* @param mixed $b |
2016
|
|
|
*/ |
2017
|
|
|
public function sortExceptionStart($a, $b): int { |
2018
|
|
|
return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1); |
2019
|
|
|
} |
2020
|
|
|
|
2021
|
|
|
/** |
2022
|
|
|
* Function to get all properties of a single changed exception. |
2023
|
|
|
* |
2024
|
|
|
* @param mixed $exception |
2025
|
|
|
* |
2026
|
|
|
* @return (mixed|true)[] associative array of properties for the exception |
2027
|
|
|
* |
2028
|
|
|
* @psalm-return array<mixed|true> |
2029
|
|
|
*/ |
2030
|
|
|
public function getExceptionProperties($exception): array { |
2031
|
|
|
// Exception has same properties as main object, with some properties overridden: |
2032
|
|
|
$item = $this->messageprops; |
2033
|
|
|
|
2034
|
|
|
// Special properties |
2035
|
|
|
$item["exception"] = true; |
2036
|
|
|
$item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time ! |
2037
|
|
|
|
2038
|
|
|
// MAPI-compatible properties (you can handle an exception as a normal calendar item like this) |
2039
|
|
|
$item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]); |
2040
|
|
|
$item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]); |
2041
|
|
|
$item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]]; |
2042
|
|
|
$item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]]; |
2043
|
|
|
|
2044
|
|
|
if (isset($exception["subject"])) { |
2045
|
|
|
$item[$this->proptags["subject"]] = $exception["subject"]; |
2046
|
|
|
} |
2047
|
|
|
|
2048
|
|
|
if (isset($exception["label"])) { |
2049
|
|
|
$item[$this->proptags["label"]] = $exception["label"]; |
2050
|
|
|
} |
2051
|
|
|
|
2052
|
|
|
if (isset($exception["alldayevent"])) { |
2053
|
|
|
$item[$this->proptags["alldayevent"]] = $exception["alldayevent"]; |
2054
|
|
|
} |
2055
|
|
|
|
2056
|
|
|
if (isset($exception["location"])) { |
2057
|
|
|
$item[$this->proptags["location"]] = $exception["location"]; |
2058
|
|
|
} |
2059
|
|
|
|
2060
|
|
|
if (isset($exception["remind_before"])) { |
2061
|
|
|
$item[$this->proptags["reminder_minutes"]] = $exception["remind_before"]; |
2062
|
|
|
} |
2063
|
|
|
|
2064
|
|
|
if (isset($exception["reminder_set"])) { |
2065
|
|
|
$item[$this->proptags["reminder"]] = $exception["reminder_set"]; |
2066
|
|
|
} |
2067
|
|
|
|
2068
|
|
|
if (isset($exception["busystatus"])) { |
2069
|
|
|
$item[$this->proptags["busystatus"]] = $exception["busystatus"]; |
2070
|
|
|
} |
2071
|
|
|
|
2072
|
|
|
return $item; |
2073
|
|
|
} |
2074
|
|
|
|
2075
|
|
|
/** |
2076
|
|
|
* @param false|int $start |
2077
|
|
|
* @param false|int $basedate |
2078
|
|
|
* @param mixed $startocc |
2079
|
|
|
* @param mixed $endocc |
2080
|
|
|
* @param mixed $tz |
2081
|
|
|
* @param mixed $reminderonly |
2082
|
|
|
*/ |
2083
|
|
|
abstract public function processOccurrenceItem(array &$items, $start, int $end, $basedate, $startocc, $endocc, $tz, $reminderonly); |
2084
|
|
|
} |
2085
|
|
|
|