Passed
Push — master ( d7fd99...ee5080 )
by
unknown
02:37 queued 13s
created

class.baserecurrence.php (11 issues)

1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
6
 */
7
8
/**
9
 * BaseRecurrence
10
 * this class is superclass for recurrence for appointments and tasks. This class provides all
11
 * basic features of recurrence.
12
 */
13
abstract class BaseRecurrence {
14
	/**
15
	 * @var resource Mapi Message Store (may be null if readonly)
16
	 */
17
	public $store;
18
19
	/**
20
	 * @var mixed Mapi Message (may be null if readonly)
21
	 */
22
	public $message;
23
24
	/**
25
	 * @var array Message Properties
26
	 */
27
	public $messageprops;
28
29
	/**
30
	 * @var array list of property tags
31
	 */
32
	public $proptags;
33
34
	/**
35
	 * @var mixed recurrence data of this calendar item
36
	 */
37
	public $recur;
38
39
	/**
40
	 * @var mixed Timezone data of this calendar item
41
	 */
42
	public $tz;
43
44
	/**
45
	 * Constructor.
46
	 *
47
	 * @param resource $store   MAPI Message Store Object
48
	 * @param mixed    $message the MAPI (appointment) message
49
	 */
50
	public function __construct($store, $message) {
51
		$this->store = $store;
52
53
		if (is_array($message)) {
54
			$this->messageprops = $message;
55
		}
56
		else {
57
			$this->message = $message;
58
			$this->messageprops = mapi_getprops($this->message, $this->proptags);
59
		}
60
61
		if (isset($this->messageprops[$this->proptags["recurring_data"]])) {
62
			// There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface
63
			if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) {
64
				$this->getFullRecurrenceBlob();
65
			}
66
67
			$this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]);
68
		}
69
		if (isset($this->proptags["timezone_data"], $this->messageprops[$this->proptags["timezone_data"]])) {
70
			$this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]);
71
		}
72
	}
73
74
	public function getRecurrence() {
75
		return $this->recur;
76
	}
77
78
	public function getFullRecurrenceBlob(): void {
79
		$message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);
0 ignored issues
show
$this->store of type resource is incompatible with the type resource expected by parameter $store of mapi_msgstore_openentry(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
		$message = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $this->store, $this->messageprops[PR_ENTRYID]);
Loading history...
80
81
		$recurrBlob = '';
82
		$stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
0 ignored issues
show
The call to mapi_openproperty() has too many arguments starting with IID_IStream. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

82
		$stream = /** @scrutinizer ignore-call */ mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
It seems like $message can also be of type false; however, parameter $any of mapi_openproperty() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
		$stream = mapi_openproperty(/** @scrutinizer ignore-type */ $message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
Loading history...
83
		$stat = mapi_stream_stat($stream);
0 ignored issues
show
$stream of type resource is incompatible with the type resource expected by parameter $stream of mapi_stream_stat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
		$stat = mapi_stream_stat(/** @scrutinizer ignore-type */ $stream);
Loading history...
84
85
		for ($i = 0; $i < $stat['cb']; $i += 1024) {
86
			$recurrBlob .= mapi_stream_read($stream, 1024);
0 ignored issues
show
$stream of type resource is incompatible with the type resource expected by parameter $stream of mapi_stream_read(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
			$recurrBlob .= mapi_stream_read(/** @scrutinizer ignore-type */ $stream, 1024);
Loading history...
87
		}
88
89
		if (!empty($recurrBlob)) {
90
			$this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob;
91
		}
92
	}
93
94
	/**
95
	 * Function for parsing the Recurrence value of a Calendar item.
96
	 *
97
	 * Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the
98
	 * data to this function
99
	 *
100
	 * Returns a structure containing the data:
101
	 *
102
	 * type  - type of recurrence: day=10, week=11, month=12, year=13
103
	 * subtype - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday)
104
	 * start - unix timestamp of first occurrence
105
	 * end  - unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1
106
	 * numoccur     - occurrences (may be very large when there is no end data)
107
	 *
108
	 * then, for each type:
109
	 *
110
	 * Daily:
111
	 *  everyn - every [everyn] days in minutes
112
	 *  regen - regenerating event (like tasks)
113
	 *
114
	 * Weekly:
115
	 *  everyn - every [everyn] weeks in weeks
116
	 *  regen - regenerating event (like tasks)
117
	 *  weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
118
	 *
119
	 * Monthly:
120
	 *  everyn - every [everyn] months
121
	 *  regen - regenerating event (like tasks)
122
	 *
123
	 *  subtype 2:
124
	 *   monthday - on day [monthday] of the month
125
	 *
126
	 *  subtype 3:
127
	 *   weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
128
	 *   nday - on [nday]'th [weekdays] of the month
129
	 *
130
	 * Yearly:
131
	 *  everyn - every [everyn] months (12, 24, 36, ...)
132
	 *  month - in month [month] (although the month is encoded in minutes since the startning of the year ........)
133
	 *  regen - regenerating event (like tasks)
134
	 *
135
	 *  subtype 2:
136
	 *   monthday - on day [monthday] of the month
137
	 *
138
	 *  subtype 3:
139
	 *   weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
140
	 *   nday - on [nday]'th [weekdays] of the month [month]
141
	 *
142
	 * @param string $rdata Binary string
143
	 *
144
	 * @return null|(((false|int|mixed|string)[]|int)[]|int|mixed)[] recurrence data
145
	 *
146
	 * @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
147
	 */
148
	public function parseRecurrence($rdata) {
149
		if (strlen($rdata) < 10) {
150
			return;
151
		}
152
153
		$ret = [];
154
		$ret["changed_occurrences"] = [];
155
		$ret["deleted_occurrences"] = [];
156
157
		$data = unpack("vReaderVersion/vWriterVersion/vrtype/vrtype2/vCalendarType", $rdata);
158
159
		// Do some recurrence validity checks
160
		if ($data['ReaderVersion'] != 0x3004 || $data['WriterVersion'] != 0x3004) {
161
			return $ret;
162
		}
163
164
		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])) {
165
			return $ret;
166
		}
167
168
		if (!in_array($data["rtype2"], [rptDay, rptWeek, rptMonth, rptMonthNth, rptMonthEnd, rptHjMonth, rptHjMonthNth, rptHjMonthEnd])) {
169
			return $ret;
170
		}
171
172
		if (!in_array($data['CalendarType'], [MAPI_CAL_DEFAULT, MAPI_CAL_GREGORIAN])) {
173
			return $ret;
174
		}
175
176
		$ret["type"] = (int) $data["rtype"] > 0x2000 ? (int) $data["rtype"] - 0x2000 : $data["rtype"];
177
		$ret["subtype"] = $data["rtype2"];
178
		$rdata = substr($rdata, 10);
179
180
		switch ($data["rtype"]) {
181
			case IDC_RCEV_PAT_ORB_DAILY:
182
				if (strlen($rdata) < 12) {
183
					return $ret;
184
				}
185
186
				$data = unpack("Vunknown/Veveryn/Vregen", $rdata);
187
				if ($data["everyn"] > 1438560) { // minutes for 999 days
188
					return $ret;
189
				}
190
				$ret["everyn"] = $data["everyn"];
191
				$ret["regen"] = $data["regen"];
192
193
				switch ($ret["subtype"]) {
194
					case rptDay:
195
						$rdata = substr($rdata, 12);
196
						break;
197
198
					case rptWeek:
199
						$rdata = substr($rdata, 16);
200
						break;
201
				}
202
203
				break;
204
205
			case IDC_RCEV_PAT_ORB_WEEKLY:
206
				if (strlen($rdata) < 16) {
207
					return $ret;
208
				}
209
210
				$data = unpack("Vconst1/Veveryn/Vregen", $rdata);
211
				if ($data["everyn"] > 99) {
212
					return $ret;
213
				}
214
215
				$rdata = substr($rdata, 12);
216
217
				$ret["everyn"] = $data["everyn"];
218
				$ret["regen"] = $data["regen"];
219
				$ret["weekdays"] = 0;
220
221
				if ($data["regen"] == 0) {
222
					$data = unpack("Vweekdays", $rdata);
223
					$rdata = substr($rdata, 4);
224
225
					$ret["weekdays"] = $data["weekdays"];
226
				}
227
				break;
228
229
			case IDC_RCEV_PAT_ORB_MONTHLY:
230
				if (strlen($rdata) < 16) {
231
					return $ret;
232
				}
233
234
				$data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata);
235
				if ($data["everyn"] > 99) {
236
					return $ret;
237
				}
238
239
				$ret["everyn"] = $data["everyn"];
240
				$ret["regen"] = $data["regen"];
241
242
				if ($ret["subtype"] == rptMonthNth) {
243
					$ret["weekdays"] = $data["monthday"];
244
				}
245
				else {
246
					$ret["monthday"] = $data["monthday"];
247
				}
248
249
				$rdata = substr($rdata, 16);
250
				if ($ret["subtype"] == rptMonthNth) {
251
					$data = unpack("Vnday", $rdata);
252
					$ret["nday"] = $data["nday"];
253
					$rdata = substr($rdata, 4);
254
				}
255
				break;
256
257
			case IDC_RCEV_PAT_ORB_YEARLY:
258
				if (strlen($rdata) < 16) {
259
					return $ret;
260
				}
261
262
				$data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata);
263
				// recurring yearly tasks have a period in months multiple by 12
264
				if ($data['regen'] && $data["everyn"] % 12 != 0) {
265
					return $ret;
266
				}
267
				if (!$data['regen'] && $data["everyn"] != 12) {
268
					return $ret;
269
				}
270
271
				$ret["month"] = $data["month"];
272
				$ret["everyn"] = $data["everyn"];
273
				$ret["regen"] = $data["regen"];
274
275
				if ($ret["subtype"] == rptMonthNth) {
276
					$ret["weekdays"] = $data["monthday"];
277
				}
278
				else {
279
					$ret["monthday"] = $data["monthday"];
280
				}
281
282
				$rdata = substr($rdata, 16);
283
284
				if ($ret["subtype"] == rptMonthNth) {
285
					$data = unpack("Vnday", $rdata);
286
					$ret["nday"] = $data["nday"];
287
					$rdata = substr($rdata, 4);
288
				}
289
				break;
290
		}
291
292
		if (strlen($rdata) < 16) {
293
			return $ret;
294
		}
295
296
		$data = unpack("Vterm/Vnumoccur/Vconst2/Vnumexcept", $rdata);
297
		$rdata = substr($rdata, 16);
298
		if (!in_array($data["term"], [IDC_RCEV_PAT_ERB_END, IDC_RCEV_PAT_ERB_AFTERNOCCUR, IDC_RCEV_PAT_ERB_NOEND, 0xFFFFFFFF])) {
299
			return $ret;
300
		}
301
302
		$ret["term"] = (int) $data["term"] > 0x2000 ? (int) $data["term"] - 0x2000 : $data["term"];
303
		$ret["numoccur"] = $data["numoccur"];
304
		$ret["numexcept"] = $data["numexcept"];
305
306
		// exc_base_dates are *all* the base dates that have been either deleted or modified
307
		$exc_base_dates = [];
308
		for ($i = 0; $i < $ret["numexcept"]; ++$i) {
309
			if (strlen($rdata) < 4) {
310
				// We shouldn't arrive here, because that implies
311
				// numexcept does not match the amount of data
312
				// which is available for the exceptions.
313
				return $ret;
314
			}
315
			$data = unpack("Vbasedate", $rdata);
316
			$rdata = substr($rdata, 4);
317
			$exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]);
318
		}
319
320
		if (strlen($rdata) < 4) {
321
			return $ret;
322
		}
323
324
		$data = unpack("Vnumexceptmod", $rdata);
325
		$rdata = substr($rdata, 4);
326
327
		$ret["numexceptmod"] = $data["numexceptmod"];
328
329
		// exc_changed are the base dates of *modified* occurrences. exactly what is modified
330
		// is in the attachments *and* in the data further down this function.
331
		$exc_changed = [];
332
		for ($i = 0; $i < $ret["numexceptmod"]; ++$i) {
333
			if (strlen($rdata) < 4) {
334
				// We shouldn't arrive here, because that implies
335
				// numexceptmod does not match the amount of data
336
				// which is available for the exceptions.
337
				return $ret;
338
			}
339
			$data = unpack("Vstartdate", $rdata);
340
			$rdata = substr($rdata, 4);
341
			$exc_changed[] = $this->recurDataToUnixData($data["startdate"]);
342
		}
343
344
		if (strlen($rdata) < 8) {
345
			return $ret;
346
		}
347
348
		$data = unpack("Vstart/Vend", $rdata);
349
		$rdata = substr($rdata, 8);
350
351
		$ret["start"] = $this->recurDataToUnixData($data["start"]);
352
		$ret["end"] = $this->recurDataToUnixData($data["end"]);
353
354
		// this is where task recurrence stop
355
		if (strlen($rdata) < 16) {
356
			return $ret;
357
		}
358
359
		$data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata);
360
		$rdata = substr($rdata, 16);
361
362
		$ret["startocc"] = $data["startmin"];
363
		$ret["endocc"] = $data["endmin"];
364
		$writerversion = $data["writerversion"];
365
366
		$data = unpack("vnumber", $rdata);
367
		$rdata = substr($rdata, 2);
368
369
		$nexceptions = $data["number"];
370
		$exc_changed_details = [];
371
372
		// Parse n modified exceptions
373
		for ($i = 0; $i < $nexceptions; ++$i) {
374
			$item = [];
375
376
			// Get exception startdate, enddate and basedate (the date at which the occurrence would have started)
377
			$data = unpack("Vstartdate/Venddate/Vbasedate", $rdata);
378
			$rdata = substr($rdata, 12);
379
380
			// Convert recurtimestamp to unix timestamp
381
			$startdate = $this->recurDataToUnixData($data["startdate"]);
382
			$enddate = $this->recurDataToUnixData($data["enddate"]);
383
			$basedate = $this->recurDataToUnixData($data["basedate"]);
384
385
			// Set the right properties
386
			$item["basedate"] = $this->dayStartOf($basedate);
387
			$item["start"] = $startdate;
388
			$item["end"] = $enddate;
389
390
			$data = unpack("vbitmask", $rdata);
391
			$rdata = substr($rdata, 2);
392
			$item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions
393
394
			// Bitmask to verify what properties are changed
395
			$bitmask = $data["bitmask"];
396
397
			// ARO_SUBJECT: 0x0001
398
			// Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject
399
			if ($bitmask & (1 << 0)) {
400
				$data = unpack("vnull_length/vlength", $rdata);
401
				$rdata = substr($rdata, 4);
402
403
				$length = $data["length"];
404
				$item["subject"] = ""; // Normalized subject
405
				for ($j = 0; $j < $length && strlen($rdata); ++$j) {
406
					$data = unpack("Cchar", $rdata);
407
					$rdata = substr($rdata, 1);
408
409
					$item["subject"] .= chr($data["char"]);
410
				}
411
			}
412
413
			// ARO_MEETINGTYPE: 0x0002
414
			if ($bitmask & (1 << 1)) {
415
				$rdata = substr($rdata, 4);
416
				// Attendees modified: no data here (only in attachment)
417
			}
418
419
			// ARO_REMINDERDELTA: 0x0004
420
			// Look for field: ReminderDelta (4b)
421
			if ($bitmask & (1 << 2)) {
422
				$data = unpack("Vremind_before", $rdata);
423
				$rdata = substr($rdata, 4);
424
425
				$item["remind_before"] = $data["remind_before"];
426
			}
427
428
			// ARO_REMINDER: 0x0008
429
			// Look field: ReminderSet (4b)
430
			if ($bitmask & (1 << 3)) {
431
				$data = unpack("Vreminder_set", $rdata);
432
				$rdata = substr($rdata, 4);
433
434
				$item["reminder_set"] = $data["reminder_set"];
435
			}
436
437
			// ARO_LOCATION: 0x0010
438
			// Look for fields: LocationLength (2b), LocationLength2 (2b) and Location
439
			// Similar to ARO_SUBJECT above.
440
			if ($bitmask & (1 << 4)) {
441
				$data = unpack("vnull_length/vlength", $rdata);
442
				$rdata = substr($rdata, 4);
443
444
				$item["location"] = "";
445
446
				$length = $data["length"];
447
				$data = substr($rdata, 0, $length);
448
				$rdata = substr($rdata, $length);
449
450
				$item["location"] .= $data;
451
			}
452
453
			// ARO_BUSYSTATUS: 0x0020
454
			// Look for field: BusyStatus (4b)
455
			if ($bitmask & (1 << 5)) {
456
				$data = unpack("Vbusystatus", $rdata);
457
				$rdata = substr($rdata, 4);
458
459
				$item["busystatus"] = $data["busystatus"];
460
			}
461
462
			// ARO_ATTACHMENT: 0x0040
463
			if ($bitmask & (1 << 6)) {
464
				// no data: RESERVED
465
				$rdata = substr($rdata, 4);
466
			}
467
468
			// ARO_SUBTYPE: 0x0080
469
			// Look for field: SubType (4b). Determines whether it is an allday event.
470
			if ($bitmask & (1 << 7)) {
471
				$data = unpack("Vallday", $rdata);
472
				$rdata = substr($rdata, 4);
473
474
				$item["alldayevent"] = $data["allday"];
475
			}
476
477
			// ARO_APPTCOLOR: 0x0100
478
			// Look for field: AppointmentColor (4b)
479
			if ($bitmask & (1 << 8)) {
480
				$data = unpack("Vlabel", $rdata);
481
				$rdata = substr($rdata, 4);
482
483
				$item["label"] = $data["label"];
484
			}
485
486
			// ARO_EXCEPTIONAL_BODY: 0x0200
487
			if ($bitmask & (1 << 9)) {
488
				// Notes or Attachments modified: no data here (only in attachment)
489
			}
490
491
			array_push($exc_changed_details, $item);
492
		}
493
494
		/**
495
		 * We now have $exc_changed, $exc_base_dates and $exc_changed_details
496
		 * We will ignore $exc_changed, as this information is available in $exc_changed_details
497
		 * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item
498
		 * has been deleted.
499
		 */
500
501
		// Find deleted occurrences
502
		$deleted_occurrences = [];
503
504
		foreach ($exc_base_dates as $base_date) {
505
			$found = false;
506
507
			foreach ($exc_changed_details as $details) {
508
				if ($details["basedate"] == $base_date) {
509
					$found = true;
510
					break;
511
				}
512
			}
513
			if (!$found) {
514
				// item was not in exc_changed_details, so it must be deleted
515
				$deleted_occurrences[] = $base_date;
516
			}
517
		}
518
519
		$ret["deleted_occurrences"] = $deleted_occurrences;
520
		$ret["changed_occurrences"] = $exc_changed_details;
521
522
		// enough data for normal exception (no extended data)
523
		if (strlen($rdata) < 8) {
524
			return $ret;
525
		}
526
527
		$data = unpack("Vreservedsize", $rdata);
528
		$rdata = substr($rdata, 4 + $data["reservedsize"]);
529
530
		for ($i = 0; $i < $nexceptions; ++$i) {
531
			// subject and location in ucs-2 to utf-8
532
			if ($writerversion >= 0x3009) {
533
				$data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4
534
				$rdata = substr($rdata, 4 + $data["size"]);
535
			}
536
537
			$data = unpack("Vreservedsize", $rdata);
538
			$rdata = substr($rdata, 4 + $data["reservedsize"]);
539
540
			// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
541
			if ($exc_changed_details[$i]["bitmask"] & 0x11) {
542
				$data = unpack("Vstart/Vend/Vorig", $rdata);
543
				$rdata = substr($rdata, 4 * 3);
544
545
				$exc_changed_details[$i]["ex_start_datetime"] = $data["start"];
546
				$exc_changed_details[$i]["ex_end_datetime"] = $data["end"];
547
				$exc_changed_details[$i]["ex_orig_date"] = $data["orig"];
548
			}
549
550
			// ARO_SUBJECT
551
			if ($exc_changed_details[$i]["bitmask"] & 0x01) {
552
				// decode ucs2 string to utf-8
553
				$data = unpack("vlength", $rdata);
554
				$rdata = substr($rdata, 2);
555
				$length = $data["length"];
556
				$data = substr($rdata, 0, $length * 2);
557
				$rdata = substr($rdata, $length * 2);
558
				$subject = iconv("UCS-2LE", "UTF-8", $data);
559
				// replace subject with unicode subject
560
				$exc_changed_details[$i]["subject"] = $subject;
561
			}
562
563
			// ARO_LOCATION
564
			if ($exc_changed_details[$i]["bitmask"] & 0x10) {
565
				// decode ucs2 string to utf-8
566
				$data = unpack("vlength", $rdata);
567
				$rdata = substr($rdata, 2);
568
				$length = $data["length"];
569
				$data = substr($rdata, 0, $length * 2);
570
				$rdata = substr($rdata, $length * 2);
571
				$location = iconv("UCS-2LE", "UTF-8", $data);
572
				// replace subject with unicode subject
573
				$exc_changed_details[$i]["location"] = $location;
574
			}
575
576
			// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
577
			if ($exc_changed_details[$i]["bitmask"] & 0x11) {
578
				$data = unpack("Vreservedsize", $rdata);
579
				$rdata = substr($rdata, 4 + $data["reservedsize"]);
580
			}
581
		}
582
583
		// update with extended data
584
		$ret["changed_occurrences"] = $exc_changed_details;
585
586
		return $ret;
587
	}
588
589
	/**
590
	 * Saves the recurrence data to the recurrence property.
591
	 */
592
	public function saveRecurrence(): void {
593
		// Only save if a message was passed
594
		if (!isset($this->message)) {
595
			return;
596
		}
597
598
		// Abort if no recurrence was set
599
		if (!isset(
600
			$this->recur["type"],
601
			$this->recur["subtype"],
602
			$this->recur["start"],
603
			$this->recur["end"],
604
			$this->recur["startocc"],
605
			$this->recur["endocc"])
606
		) {
607
			return;
608
		}
609
610
		$rtype = 0x2000 + (int) $this->recur["type"];
611
612
		// Don't allow invalid type and subtype values
613
		if (!in_array($rtype, [IDC_RCEV_PAT_ORB_DAILY, IDC_RCEV_PAT_ORB_WEEKLY, IDC_RCEV_PAT_ORB_MONTHLY, IDC_RCEV_PAT_ORB_YEARLY])) {
614
			return;
615
		}
616
617
		if (!in_array((int) $this->recur["subtype"], [rptDay, rptWeek, rptMonth, rptMonthNth, rptMonthEnd, rptHjMonth, rptHjMonthNth, rptHjMonthEnd])) {
618
			return;
619
		}
620
621
		$rdata = pack("vvvvv", 0x3004, 0x3004, $rtype, (int) $this->recur["subtype"], MAPI_CAL_DEFAULT);
622
		$weekstart = 1; // monday
623
		$forwardcount = 0;
624
		$count = 0;
625
		$restocc = 0;
626
		$dayofweek = (int) gmdate("w", (int) $this->recur["start"]); // 0 (for Sunday) through 6 (for Saturday)
627
628
		// Terminate
629
		$term = (int) $this->recur["term"] < 0x2000 ? 0x2000 + (int) $this->recur["term"] : (int) $this->recur["term"];
630
631
		switch ($rtype) {
632
			case IDC_RCEV_PAT_ORB_DAILY:
633
				if (!isset($this->recur["everyn"]) || (int) $this->recur["everyn"] > 1438560 || (int) $this->recur["everyn"] < 0) { // minutes for 999 days
634
					return;
635
				}
636
637
				if ($this->recur["subtype"] == rptWeek) {
638
					// Daily every workday
639
					$rdata .= pack("VVVV", 6 * 24 * 60, 1, 0, 0x3E);
640
				}
641
				else {
642
					// Calc first occ
643
					$firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]);
644
645
					$rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0);
646
				}
647
				break;
648
649
			case IDC_RCEV_PAT_ORB_WEEKLY:
650
				if (!isset($this->recur["everyn"]) || $this->recur["everyn"] > 99 || (int) $this->recur["everyn"] < 0) {
651
					return;
652
				}
653
654
				if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) {
655
					return;
656
				}
657
658
				// No need to calculate startdate if sliding flag was set.
659
				if (!$this->recur['regen']) {
660
					// Calculate start date of recurrence
661
662
					// Find the first day that matches one of the weekdays selected
663
					$daycount = 0;
664
					$dayskip = -1;
665
					for ($j = 0; $j < 7; ++$j) {
666
						if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
667
							if ($dayskip == -1) {
668
								$dayskip = $j;
669
							}
670
671
							++$daycount;
672
						}
673
					}
674
675
					// $dayskip is the number of days to skip from the startdate until the first occurrence
676
					// $daycount is the number of days per week that an occurrence occurs
677
678
					$weekskip = 0;
679
					if (($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek + $dayskip) > 6) {
680
						$weekskip = 1;
681
					}
682
683
					// Check if the recurrence ends after a number of occurrences, in that case we must calculate the
684
					// remaining occurrences based on the start of the recurrence.
685
					if ($term == IDC_RCEV_PAT_ERB_AFTERNOCCUR) {
686
						// $weekskip is the amount of weeks to skip from the startdate before the first occurrence
687
						// $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that
688
						// is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over
689
						// (eg when numoccur = 2, and daycount = 1)
690
						$forwardcount = floor((int) ($this->recur["numoccur"] - 1) / $daycount);
691
692
						// $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one
693
						// for the occurrence on the first day
694
						$restocc = ((int) $this->recur["numoccur"]) - ($forwardcount * $daycount) - 1;
695
696
						// $forwardcount is now the number of weeks we can go forward and still be inside the recurrence
697
						$forwardcount *= (int) $this->recur["everyn"];
698
					}
699
700
					// The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week)
701
					$this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24 * 60 * 60) + ($weekskip * (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60);
702
				}
703
704
				// Calc first occ
705
				$firstocc = $this->unixDataToRecurData($this->recur["start"]) % (((int) $this->recur["everyn"]) * 7 * 24 * 60);
706
707
				$firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60;
708
709
				if ($this->recur["regen"]) {
710
					$rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1);
711
				}
712
				else {
713
					$rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]);
714
				}
715
				break;
716
717
			case IDC_RCEV_PAT_ORB_MONTHLY:
718
			case IDC_RCEV_PAT_ORB_YEARLY:
719
				if (!isset($this->recur["everyn"])) {
720
					return;
721
				}
722
				if ($rtype == IDC_RCEV_PAT_ORB_YEARLY && !isset($this->recur["month"])) {
723
					return;
724
				}
725
726
				if ($rtype == IDC_RCEV_PAT_ORB_MONTHLY) {
727
					$everyn = (int) $this->recur["everyn"];
728
					if ($everyn > 99 || $everyn < 0) {
729
						return;
730
					}
731
				}
732
				else {
733
					$everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12;
734
				}
735
736
				// Get montday/month/year of original start
737
				$curmonthday = gmdate("j", (int) $this->recur["start"]);
738
				$curyear = gmdate("Y", (int) $this->recur["start"]);
739
				$curmonth = gmdate("n", (int) $this->recur["start"]);
740
741
				// Check if the recurrence ends after a number of occurrences, in that case we must calculate the
742
				// remaining occurrences based on the start of the recurrence.
743
				if ($term == IDC_RCEV_PAT_ERB_AFTERNOCCUR) {
744
					// $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus
745
					// one to make sure there are always at least one occurrence left)
746
					$forwardcount = ((((int) $this->recur["numoccur"]) - 1) * $everyn);
747
				}
748
749
				// Get month for yearly on D'th day of month M
750
				if ($rtype == IDC_RCEV_PAT_ORB_YEARLY) {
751
					$selmonth = floor(((int) $this->recur["month"]) / (24 * 60 * 29)) + 1; // 1=jan, 2=feb, eg
752
				}
753
754
				switch ((int) $this->recur["subtype"]) {
755
					// on D day of every M month
756
					case rptMonth:
757
						if (!isset($this->recur["monthday"])) {
758
							return;
759
						}
760
						// Recalc startdate
761
762
						// Set on the right begin day
763
764
						// Go the beginning of the month
765
						$this->recur["start"] -= ($curmonthday - 1) * 24 * 60 * 60;
766
						// Go the the correct month day
767
						$this->recur["start"] += (((int) $this->recur["monthday"]) - 1) * 24 * 60 * 60;
768
769
						// If the previous calculation gave us a start date different than the original start date, then we need to skip to the first occurrence
770
						if (($rtype == IDC_RCEV_PAT_ORB_MONTHLY && ((int) $this->recur["monthday"]) < $curmonthday) ||
771
							($rtype == IDC_RCEV_PAT_ORB_YEARLY && ($selmonth != $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday)))) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $selmonth does not seem to be defined for all execution paths leading up to this point.
Loading history...
772
							if ($rtype == IDC_RCEV_PAT_ORB_YEARLY) {
773
								if ($curmonth > $selmonth) {// go to next occurrence in 'everyn' months minus difference in first occurrence and original date
774
									$count = $everyn - ($curmonth - $selmonth);
775
								}
776
								elseif ($curmonth < $selmonth) {// go to next occurrence upto difference in first occurrence and original date
777
									$count = $selmonth - $curmonth;
778
								}
779
								else {
780
									// Go to next occurrence while recurrence start date is greater than occurrence date but within same month
781
									if (((int) $this->recur["monthday"]) < $curmonthday) {
782
										$count = $everyn;
783
									}
784
								}
785
							}
786
							else {
787
								$count = $everyn; // Monthly, go to next occurrence in 'everyn' months
788
							}
789
790
							// Forward by $count months. This is done by getting the number of days in that month and forwarding that many days
791
							for ($i = 0; $i < $count; ++$i) {
792
								$this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth);
793
794
								if ($curmonth == 12) {
795
									++$curyear;
796
									$curmonth = 0;
797
								}
798
								++$curmonth;
799
							}
800
						}
801
802
						// "start" is now pointing to the first occurrence, except that it will overshoot if the
803
						// month in which it occurs has less days than specified as the day of the month. So 31st
804
						// of each month will overshoot in february (29 days). We compensate for that by checking
805
						// if the day of the month we got is wrong, and then back up to the last day of the previous
806
						// month.
807
						if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
808
							gmdate("j", (int) $this->recur["start"]) < ((int) $this->recur["monthday"])) {
809
							$this->recur["start"] -= gmdate("j", (int) $this->recur["start"]) * 24 * 60 * 60;
810
						}
811
812
						// "start" is now the first occurrence
813
						if ($rtype == IDC_RCEV_PAT_ORB_MONTHLY) {
814
							// Calc first occ
815
							$monthIndex = ((((12 % $everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
816
817
							$firstocc = 0;
818
							for ($i = 0; $i < $monthIndex; ++$i) {
819
								$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
820
							}
821
822
							$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
823
						}
824
						else {
825
							// Calc first occ
826
							$firstocc = 0;
827
							$monthIndex = (int) gmdate("n", $this->recur["start"]);
828
							for ($i = 1; $i < $monthIndex; ++$i) {
829
								$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
830
							}
831
832
							$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
833
						}
834
						break;
835
836
					case rptMonthNth:
837
						// monthly: on Nth weekday of every M month
838
						// yearly: on Nth weekday of M month
839
						if (!isset($this->recur["weekdays"], $this->recur["nday"])) {
840
							return;
841
						}
842
843
						$weekdays = (int) $this->recur["weekdays"];
844
						$nday = (int) $this->recur["nday"];
845
846
						// Calc startdate
847
						$monthbegindow = (int) $this->recur["start"];
848
849
						if ($nday == 5) {
850
							// Set date on the last day of the last month
851
							$monthbegindow += (gmdate("t", $monthbegindow) - gmdate("j", $monthbegindow)) * 24 * 60 * 60;
852
						}
853
						else {
854
							// Set on the first day of the month
855
							$monthbegindow -= ((gmdate("j", $monthbegindow) - 1) * 24 * 60 * 60);
856
						}
857
858
						if ($rtype == IDC_RCEV_PAT_ORB_YEARLY) {
859
							// Set on right month
860
							if ($selmonth < $curmonth) {
861
								$tmp = 12 - $curmonth + $selmonth;
862
							}
863
							else {
864
								$tmp = ($selmonth - $curmonth);
865
							}
866
867
							for ($i = 0; $i < $tmp; ++$i) {
868
								$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
869
870
								if ($curmonth == 12) {
871
									++$curyear;
872
									$curmonth = 0;
873
								}
874
								++$curmonth;
875
							}
876
						}
877
						else {
878
							// Check or you exist in the right month
879
880
							$dayofweek = gmdate("w", $monthbegindow);
881
							for ($i = 0; $i < 7; ++$i) {
882
								if ($nday == 5 && (($dayofweek - $i) % 7 >= 0) && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
883
									$day = gmdate("j", $monthbegindow) - $i;
884
									break;
885
								}
886
								if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
887
									$day = (($nday - 1) * 7) + ($i + 1);
888
									break;
889
								}
890
							}
891
892
							// Goto the next X month
893
							if (isset($day) && ($day < gmdate("j", (int) $this->recur["start"]))) {
894
								if ($nday == 5) {
895
									$monthbegindow += 24 * 60 * 60;
896
									if ($curmonth == 12) {
897
										++$curyear;
898
										$curmonth = 0;
899
									}
900
									++$curmonth;
901
								}
902
903
								for ($i = 0; $i < $everyn; ++$i) {
904
									$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
905
906
									if ($curmonth == 12) {
907
										++$curyear;
908
										$curmonth = 0;
909
									}
910
									++$curmonth;
911
								}
912
913
								if ($nday == 5) {
914
									$monthbegindow -= 24 * 60 * 60;
915
								}
916
							}
917
						}
918
919
						// FIXME: weekstart?
920
921
						$day = 0;
922
						// Set start on the right day
923
						$dayofweek = gmdate("w", $monthbegindow);
924
						for ($i = 0; $i < 7; ++$i) {
925
							if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
926
								$day = $i;
927
								break;
928
							}
929
							if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
930
								$day = ($nday - 1) * 7 + ($i + 1);
931
								break;
932
							}
933
						}
934
						if ($nday == 5) {
935
							$monthbegindow -= $day * 24 * 60 * 60;
936
						}
937
						else {
938
							$monthbegindow += ($day - 1) * 24 * 60 * 60;
939
						}
940
941
						$firstocc = 0;
942
						if ($rtype == IDC_RCEV_PAT_ORB_MONTHLY) {
943
							// Calc first occ
944
							$monthIndex = ((((12 % $everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
945
946
							for ($i = 0; $i < $monthIndex; ++$i) {
947
								$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
948
							}
949
950
							$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
951
						}
952
						else {
953
							// Calc first occ
954
							$monthIndex = (int) gmdate("n", $this->recur["start"]);
955
956
							for ($i = 1; $i < $monthIndex; ++$i) {
957
								$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
958
							}
959
960
							$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
961
						}
962
						break;
963
				}
964
				break;
965
		}
966
967
		if (!isset($this->recur["term"])) {
968
			return;
969
		}
970
971
		$rdata .= pack("V", $term);
972
973
		switch ($term) {
974
			// After the given enddate
975
			case IDC_RCEV_PAT_ERB_END:
976
				$rdata .= pack("V", 10);
977
				break;
978
979
				// After a number of times
980
			case IDC_RCEV_PAT_ERB_AFTERNOCCUR:
981
				if (!isset($this->recur["numoccur"])) {
982
					return;
983
				}
984
985
				$rdata .= pack("V", (int) $this->recur["numoccur"]);
986
				break;
987
988
				// Never ends
989
			case IDC_RCEV_PAT_ERB_NOEND:
990
				$rdata .= pack("V", 0);
991
				break;
992
		}
993
994
		// Strange little thing for the recurrence type "every workday"
995
		if ($rtype == IDC_RCEV_PAT_ORB_WEEKLY && ((int) $this->recur["subtype"]) == 1) {
996
			$rdata .= pack("V", 1);
997
		}
998
		else { // Other recurrences
999
			$rdata .= pack("V", 0);
1000
		}
1001
1002
		// Exception data
1003
1004
		// Get all exceptions
1005
		$deleted_items = $this->recur["deleted_occurrences"];
1006
		$changed_items = $this->recur["changed_occurrences"];
1007
1008
		// Merge deleted and changed items into one list
1009
		$items = $deleted_items;
1010
1011
		foreach ($changed_items as $changed_item) {
1012
			array_push($items, $this->dayStartOf($changed_item["basedate"]));
1013
		}
1014
1015
		sort($items);
1016
1017
		// Add the merged list in to the rdata
1018
		$rdata .= pack("V", count($items));
1019
		foreach ($items as $item) {
1020
			$rdata .= pack("V", $this->unixDataToRecurData($item));
1021
		}
1022
1023
		// Loop through the changed exceptions (not deleted)
1024
		$rdata .= pack("V", count($changed_items));
1025
		$items = [];
1026
1027
		foreach ($changed_items as $changed_item) {
1028
			$items[] = $this->dayStartOf($changed_item["start"]);
1029
		}
1030
1031
		sort($items);
1032
1033
		// Add the changed items list int the rdata
1034
		foreach ($items as $item) {
1035
			$rdata .= pack("V", $this->unixDataToRecurData($item));
1036
		}
1037
1038
		// Set start date
1039
		$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"]));
1040
1041
		// Set enddate
1042
		switch ($term) {
1043
			// After the given enddate
1044
			case IDC_RCEV_PAT_ERB_END:
1045
				$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1046
				break;
1047
1048
				// After a number of times
1049
			case IDC_RCEV_PAT_ERB_AFTERNOCCUR:
1050
				// @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour
1051
				$occenddate = (int) $this->recur["start"];
1052
1053
				switch ($rtype) {
1054
					case IDC_RCEV_PAT_ORB_DAILY:
1055
						if ($this->recur["subtype"] == rptWeek) {
1056
							// Daily every workday
1057
							$restocc = (int) $this->recur["numoccur"];
1058
1059
							// Get starting weekday
1060
							$nowtime = $this->gmtime($occenddate);
1061
							$j = $nowtime["tm_wday"];
1062
1063
							while (1) {
1064
								if (($j % 7) > 0 && ($j % 7) < 6) {
1065
									--$restocc;
1066
								}
1067
1068
								++$j;
1069
1070
								if ($restocc <= 0) {
1071
									break;
1072
								}
1073
1074
								$occenddate += 24 * 60 * 60;
1075
							}
1076
						}
1077
						else {
1078
							// -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence)
1079
							$occenddate += (((int) $this->recur["everyn"]) * 60 * ((int) $this->recur["numoccur"] - 1));
1080
						}
1081
						break;
1082
1083
					case IDC_RCEV_PAT_ORB_WEEKLY:
1084
						// Needed values
1085
						// $forwardcount - number of weeks we can skip forward
1086
						// $restocc - number of remaining occurrences after the week skip
1087
1088
						// Add the weeks till the last item
1089
						$occenddate += ($forwardcount * 7 * 24 * 60 * 60);
1090
1091
						$dayofweek = gmdate("w", $occenddate);
0 ignored issues
show
$occenddate of type double is incompatible with the type integer|null expected by parameter $timestamp of gmdate(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1091
						$dayofweek = gmdate("w", /** @scrutinizer ignore-type */ $occenddate);
Loading history...
1092
1093
						// Loop through the last occurrences until we have had them all
1094
						for ($j = 1; $restocc > 0; ++$j) {
1095
							// Jump to the next week (which may be N weeks away) when going over the week boundary
1096
							if ((($dayofweek + $j) % 7) == $weekstart) {
1097
								$occenddate += (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60;
1098
							}
1099
1100
							// If this is a matching day, once less occurrence to process
1101
							if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
1102
								--$restocc;
1103
							}
1104
1105
							// Next day
1106
							$occenddate += 24 * 60 * 60;
1107
						}
1108
1109
						break;
1110
1111
					case IDC_RCEV_PAT_ORB_MONTHLY:
1112
					case IDC_RCEV_PAT_ORB_YEARLY:
1113
						$curyear = gmdate("Y", (int) $this->recur["start"]);
1114
						$curmonth = gmdate("n", (int) $this->recur["start"]);
1115
						// $forwardcount = months
1116
1117
						switch ((int) $this->recur["subtype"]) {
1118
							case rptMonth: // on D day of every M month
1119
								while ($forwardcount > 0) {
1120
									$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1121
1122
									if ($curmonth >= 12) {
1123
										$curmonth = 1;
1124
										++$curyear;
1125
									}
1126
									else {
1127
										++$curmonth;
1128
									}
1129
									--$forwardcount;
1130
								}
1131
1132
								// compensation between 28 and 31
1133
								if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
1134
									gmdate("j", $occenddate) < ((int) $this->recur["monthday"])) {
1135
									if (gmdate("j", $occenddate) < 28) {
1136
										$occenddate -= gmdate("j", $occenddate) * 24 * 60 * 60;
1137
									}
1138
									else {
1139
										$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1140
									}
1141
								}
1142
1143
								break;
1144
1145
							case rptMonthNth: // on Nth weekday of every M month
1146
								$nday = (int) $this->recur["nday"]; // 1 tot 5
1147
								$weekdays = (int) $this->recur["weekdays"];
1148
1149
								while ($forwardcount > 0) {
1150
									$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1151
									if ($curmonth >= 12) {
1152
										$curmonth = 1;
1153
										++$curyear;
1154
									}
1155
									else {
1156
										++$curmonth;
1157
									}
1158
1159
									--$forwardcount;
1160
								}
1161
1162
								if ($nday == 5) {
1163
									// Set date on the last day of the last month
1164
									$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1165
								}
1166
								else {
1167
									// Set date on the first day of the last month
1168
									$occenddate -= (gmdate("j", $occenddate) - 1) * 24 * 60 * 60;
1169
								}
1170
1171
								$dayofweek = gmdate("w", $occenddate);
1172
								for ($i = 0; $i < 7; ++$i) {
1173
									if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
1174
										$occenddate -= $i * 24 * 60 * 60;
1175
										break;
1176
									}
1177
									if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
1178
										$occenddate += ($i + (($nday - 1) * 7)) * 24 * 60 * 60;
1179
										break;
1180
									}
1181
								}
1182
1183
								break; // case rptMonthNth
1184
						}
1185
1186
						break;
1187
				}
1188
1189
				if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX) {
1190
					$occenddate = PHP_INT_MAX;
1191
				}
1192
1193
				$this->recur["end"] = $occenddate;
1194
1195
				$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1196
				break;
1197
1198
				// Never ends
1199
			case IDC_RCEV_PAT_ERB_NOEND:
1200
			default:
1201
				$this->recur["end"] = 0x7FFFFFFF; // max date -> 2038
1202
				$rdata .= pack("V", 0x5AE980DF);
1203
				break;
1204
		}
1205
1206
		// UTC date
1207
		$utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]);
1208
		$utcend = $this->toGMT($this->tz, (int) $this->recur["end"]);
1209
1210
		// utc date+time
1211
		$utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"]) * 60) : $utcstart;
1212
		$utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart;
1213
1214
		$propsToSet = [];
1215
		// update reminder time
1216
		$propsToSet[$this->proptags["reminder_time"]] = $utcfirstoccstartdatetime;
1217
1218
		// update first occurrence date
1219
		$propsToSet[$this->proptags["startdate"]] = $propsToSet[$this->proptags["commonstart"]] = $utcfirstoccstartdatetime;
1220
		$propsToSet[$this->proptags["duedate"]] = $propsToSet[$this->proptags["commonend"]] = $utcfirstoccenddatetime;
1221
1222
		// Set Outlook properties, if it is an appointment
1223
		if (isset($this->messageprops[$this->proptags["message_class"]]) && $this->messageprops[$this->proptags["message_class"]] == "IPM.Appointment") {
1224
			// update real begin and real end date
1225
			$propsToSet[$this->proptags["startdate_recurring"]] = $utcstart;
1226
			$propsToSet[$this->proptags["enddate_recurring"]] = $utcend;
1227
1228
			// recurrencetype
1229
			// Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
1230
			$propsToSet[$this->proptags["recurrencetype"]] = ((int) $this->recur["type"]) - 0x9;
1231
1232
			// set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
1233
			$propsToSet[$this->proptags["side_effects"]] = 369;
1234
		}
1235
		else {
1236
			$propsToSet[$this->proptags["side_effects"]] = 3441;
1237
		}
1238
1239
		// FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
1240
		// Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset
1241
		// to the 'next' occurrence; this makes sure that deleting the next occurrence will correctly set the reminder to
1242
		// the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time)
1243
		// with the reminder flag set.
1244
		$reminderprops = mapi_getprops($this->message, [$this->proptags["reminder_minutes"], $this->proptags["flagdueby"]]);
1245
		if (isset($reminderprops[$this->proptags["reminder_minutes"]])) {
1246
			$occ = false;
1247
			$occurrences = $this->getItems(time(), 0x7FF00000, 3, true);
1248
1249
			for ($i = 0, $len = count($occurrences); $i < $len; ++$i) {
1250
				// This will actually also give us appointments that have already started, but not yet ended. Since we want the next
1251
				// reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum
1252
				// number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case:
1253
				// time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item
1254
				// can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped.
1255
1256
				if (($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) {
1257
					$occ = $occurrences[$i];
1258
					break;
1259
				}
1260
			}
1261
1262
			if ($occ) {
1263
				if (isset($reminderprops[$this->proptags["flagdueby"]])) {
1264
					$propsToSet[$this->proptags["flagdueby"]] = $reminderprops[$this->proptags["flagdueby"]];
1265
				}
1266
				else {
1267
					$propsToSet[$this->proptags["flagdueby"]] = $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60);
1268
				}
1269
			}
1270
			else {
1271
				// Last reminder passed, no reminders any more.
1272
				$propsToSet[$this->proptags["reminder"]] = false;
1273
				$propsToSet[$this->proptags["flagdueby"]] = 0x7FF00000;
1274
			}
1275
		}
1276
1277
		// Default data
1278
		// Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information)
1279
		$rdata .= pack("VV", 0x3006, 0x3008);
1280
		if (isset($this->recur["startocc"], $this->recur["endocc"])) {
1281
			// Set start and endtime in minutes
1282
			$rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]);
1283
		}
1284
1285
		// Detailed exception data
1286
1287
		$changed_items = $this->recur["changed_occurrences"];
1288
1289
		$rdata .= pack("v", count($changed_items));
1290
1291
		foreach ($changed_items as $changed_item) {
1292
			// Set start and end time of exception
1293
			$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1294
			$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1295
			$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1296
1297
			// Bitmask
1298
			$bitmask = 0;
1299
1300
			// Check for changed strings
1301
			if (isset($changed_item["subject"])) {
1302
				$bitmask |= 1 << 0;
1303
			}
1304
1305
			if (isset($changed_item["remind_before"])) {
1306
				$bitmask |= 1 << 2;
1307
			}
1308
1309
			if (isset($changed_item["reminder_set"])) {
1310
				$bitmask |= 1 << 3;
1311
			}
1312
1313
			if (isset($changed_item["location"])) {
1314
				$bitmask |= 1 << 4;
1315
			}
1316
1317
			if (isset($changed_item["busystatus"])) {
1318
				$bitmask |= 1 << 5;
1319
			}
1320
1321
			if (isset($changed_item["alldayevent"])) {
1322
				$bitmask |= 1 << 7;
1323
			}
1324
1325
			if (isset($changed_item["label"])) {
1326
				$bitmask |= 1 << 8;
1327
			}
1328
1329
			$rdata .= pack("v", $bitmask);
1330
1331
			// Set "subject"
1332
			if (isset($changed_item["subject"])) {
1333
				// convert utf-8 to non-unicode blob string (us-ascii?)
1334
				$subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
1335
				$length = strlen($subject);
1336
				$rdata .= pack("vv", $length + 1, $length);
1337
				$rdata .= pack("a" . $length, $subject);
1338
			}
1339
1340
			if (isset($changed_item["remind_before"])) {
1341
				$rdata .= pack("V", $changed_item["remind_before"]);
1342
			}
1343
1344
			if (isset($changed_item["reminder_set"])) {
1345
				$rdata .= pack("V", $changed_item["reminder_set"]);
1346
			}
1347
1348
			if (isset($changed_item["location"])) {
1349
				$location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
1350
				$length = strlen($location);
1351
				$rdata .= pack("vv", $length + 1, $length);
1352
				$rdata .= pack("a" . $length, $location);
1353
			}
1354
1355
			if (isset($changed_item["busystatus"])) {
1356
				$rdata .= pack("V", $changed_item["busystatus"]);
1357
			}
1358
1359
			if (isset($changed_item["alldayevent"])) {
1360
				$rdata .= pack("V", $changed_item["alldayevent"]);
1361
			}
1362
1363
			if (isset($changed_item["label"])) {
1364
				$rdata .= pack("V", $changed_item["label"]);
1365
			}
1366
		}
1367
1368
		$rdata .= pack("V", 0);
1369
1370
		// write extended data
1371
		foreach ($changed_items as $changed_item) {
1372
			$rdata .= pack("V", 0);
1373
			if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1374
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1375
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1376
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1377
			}
1378
1379
			if (isset($changed_item["subject"])) {
1380
				$subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
1381
				$length = iconv_strlen($subject, "UCS-2LE");
1382
				$rdata .= pack("v", $length);
1383
				$rdata .= pack("a" . $length * 2, $subject);
1384
			}
1385
1386
			if (isset($changed_item["location"])) {
1387
				$location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
1388
				$length = iconv_strlen($location, "UCS-2LE");
1389
				$rdata .= pack("v", $length);
1390
				$rdata .= pack("a" . $length * 2, $location);
1391
			}
1392
1393
			if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1394
				$rdata .= pack("V", 0);
1395
			}
1396
		}
1397
1398
		$rdata .= pack("V", 0);
1399
1400
		// Set props
1401
		$propsToSet[$this->proptags["recurring_data"]] = $rdata;
1402
		$propsToSet[$this->proptags["recurring"]] = true;
1403
		if (isset($this->tz) && $this->tz) {
1404
			$timezone = "GMT";
1405
			if ($this->tz["timezone"] != 0) {
1406
				// Create user readable timezone information
1407
				$timezone = sprintf(
1408
					"(GMT %s%02d:%02d)",-
1409
					$this->tz["timezone"] > 0 ? "+" : "-",
1410
					abs($this->tz["timezone"] / 60),
1411
					abs($this->tz["timezone"] % 60)
1412
				);
1413
			}
1414
			$propsToSet[$this->proptags["timezone_data"]] = $this->getTimezoneData($this->tz);
1415
			$propsToSet[$this->proptags["timezone"]] = $timezone;
1416
		}
1417
		mapi_setprops($this->message, $propsToSet);
1418
	}
1419
1420
	/**
1421
	 * Function which converts a recurrence date timestamp to an unix date timestamp.
1422
	 *
1423
	 * @author Steve Hardy
1424
	 *
1425
	 * @param int $rdate the date which will be converted
1426
	 *
1427
	 * @return int the converted date
1428
	 */
1429
	public function recurDataToUnixData($rdate) {
1430
		return ($rdate - 194074560) * 60;
1431
	}
1432
1433
	/**
1434
	 * Function which converts an unix date timestamp to recurrence date timestamp.
1435
	 *
1436
	 * @author Johnny Biemans
1437
	 *
1438
	 * @param int $date the date which will be converted
1439
	 *
1440
	 * @return float|int the converted date in minutes
1441
	 */
1442
	public function unixDataToRecurData($date) {
1443
		return ($date / 60) + 194074560;
1444
	}
1445
1446
	/**
1447
	 * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1448
	 *
1449
	 * @author Steve Hardy
1450
	 *
1451
	 * @param mixed $ts
1452
	 *
1453
	 * @return float|int
1454
	 */
1455
	public function GetTZOffset($ts) {
1456
		$Offset = date("O", $ts);
1457
1458
		$Parity = $Offset < 0 ? -1 : 1;
1459
		$Offset = $Parity * $Offset;
1460
		$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1461
1462
		return $Parity * $Offset;
1463
	}
1464
1465
	/**
1466
	 * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1467
	 *
1468
	 * @author Steve Hardy
1469
	 *
1470
	 * @param int $time
1471
	 *
1472
	 * @return array GMT Time
1473
	 */
1474
	public function gmtime($time) {
1475
		$TZOffset = $this->GetTZOffset($time);
1476
1477
		$t_time = $time - $TZOffset * 60; # Counter adjust for localtime()
1478
1479
		return localtime($t_time, 1);
1480
	}
1481
1482
	/**
1483
	 * @param float|string $year
1484
	 */
1485
	public function isLeapYear($year): bool {
1486
		return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0);
1487
	}
1488
1489
	/**
1490
	 * @param float|string $year
1491
	 * @param int|string   $month
1492
	 */
1493
	public function getMonthInSeconds($year, $month): int {
1494
		if (in_array($month, [1, 3, 5, 7, 8, 10, 12])) {
1495
			$day = 31;
1496
		}
1497
		elseif (in_array($month, [4, 6, 9, 11])) {
1498
			$day = 30;
1499
		}
1500
		else {
1501
			$day = 28;
1502
			if ($this->isLeapYear($year) == 1) {
1503
				++$day;
1504
			}
1505
		}
1506
1507
		return $day * 24 * 60 * 60;
1508
	}
1509
1510
	/**
1511
	 * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour.
1512
	 *
1513
	 * @param int $year
1514
	 * @param int $month
1515
	 * @param int $week
1516
	 * @param int $day
1517
	 * @param int $hour
1518
	 *
1519
	 * @return int the timestamp of the given date, timezone-independent
1520
	 */
1521
	public function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour) {
1522
		// get first day of month
1523
		$date = gmmktime(0, 0, 0, $month, 0, $year + 1900);
1524
1525
		// get wday info
1526
		$gmdate = $this->gmtime($date);
1527
1528
		$date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week
1529
1530
		$date += $week * 7 * 24 * 60 * 60; // go to correct week nr
1531
		$date += $day * 24 * 60 * 60;
1532
		$date += $hour * 60 * 60;
1533
1534
		$gmdate = $this->gmtime($date);
1535
1536
		// if we are in the next month, then back up a week, because week '5' means
1537
		// 'last week of month'
1538
1539
		if ($month != $gmdate["tm_mon"] + 1) {
1540
			$date -= 7 * 24 * 60 * 60;
1541
		}
1542
1543
		return $date;
1544
	}
1545
1546
	/**
1547
	 * getTimezone gives the timezone offset (in minutes) of the given
1548
	 * local date/time according to the given TZ info.
1549
	 *
1550
	 * @param mixed $tz
1551
	 * @param mixed $date
1552
	 */
1553
	public function getTimezone($tz, $date) {
1554
		// No timezone -> GMT (+0)
1555
		if (!isset($tz["timezone"])) {
1556
			return 0;
1557
		}
1558
1559
		$dst = false;
1560
		$gmdate = $this->gmtime($date);
1561
1562
		$dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
1563
		$dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);
1564
1565
		if ($dststart <= $dstend) {
1566
			// Northern hemisphere, eg DST is during Mar-Oct
1567
			if ($date > $dststart && $date < $dstend) {
1568
				$dst = true;
1569
			}
1570
		}
1571
		else {
1572
			// Southern hemisphere, eg DST is during Oct-Mar
1573
			if ($date < $dstend || $date > $dststart) {
1574
				$dst = true;
1575
			}
1576
		}
1577
1578
		if ($dst) {
1579
			return $tz["timezone"] + $tz["timezonedst"];
1580
		}
1581
1582
		return $tz["timezone"];
1583
	}
1584
1585
	/**
1586
	 * parseTimezone parses the timezone as specified in named property 0x8233
1587
	 * in Outlook calendar messages. Returns the timezone in minutes negative
1588
	 * offset (GMT +2:00 -> -120).
1589
	 *
1590
	 * @param mixed $data
1591
	 *
1592
	 * @return null|array|false
1593
	 */
1594
	public function parseTimezone($data) {
1595
		if (strlen($data) < 48) {
1596
			return;
1597
		}
1598
1599
		return unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
1600
	}
1601
1602
	/**
1603
	 * @param mixed $tz
1604
	 *
1605
	 * @return false|string
1606
	 */
1607
	public function getTimezoneData($tz) {
1608
		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);
1609
	}
1610
1611
	/**
1612
	 * toGMT returns a timestamp in GMT time for the time and timezone given.
1613
	 *
1614
	 * @param mixed $tz
1615
	 * @param mixed $date
1616
	 */
1617
	public function toGMT($tz, $date) {
1618
		if (!isset($tz['timezone'])) {
1619
			return $date;
1620
		}
1621
		$offset = $this->getTimezone($tz, $date);
1622
1623
		return $date + $offset * 60;
1624
	}
1625
1626
	/**
1627
	 * fromGMT returns a timestamp in the local timezone given from the GMT time given.
1628
	 *
1629
	 * @param mixed $tz
1630
	 * @param mixed $date
1631
	 */
1632
	public function fromGMT($tz, $date) {
1633
		$offset = $this->getTimezone($tz, $date);
1634
1635
		return $date - $offset * 60;
1636
	}
1637
1638
	/**
1639
	 * Function to get timestamp of the beginning of the day of the timestamp given.
1640
	 *
1641
	 * @param mixed $date
1642
	 *
1643
	 * @return false|int timestamp referring to same day but at 00:00:00
1644
	 */
1645
	public function dayStartOf($date) {
1646
		$time1 = $this->gmtime($date);
1647
1648
		return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
1649
	}
1650
1651
	/**
1652
	 * Function to get timestamp of the beginning of the month of the timestamp given.
1653
	 *
1654
	 * @param mixed $date
1655
	 *
1656
	 * @return false|int Timestamp referring to same month but on the first day, and at 00:00:00
1657
	 */
1658
	public function monthStartOf($date) {
1659
		$time1 = $this->gmtime($date);
1660
1661
		return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
1662
	}
1663
1664
	/**
1665
	 * Function to get timestamp of the beginning of the year of the timestamp given.
1666
	 *
1667
	 * @param mixed $date
1668
	 *
1669
	 * @return false|int Timestamp referring to the same year but on Jan 01, at 00:00:00
1670
	 */
1671
	public function yearStartOf($date) {
1672
		$time1 = $this->gmtime($date);
1673
1674
		return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
1675
	}
1676
1677
	/**
1678
	 * Function which returns the items in a given interval. This included expansion of the recurrence and
1679
	 * processing of exceptions (modified and deleted).
1680
	 *
1681
	 * @param int   $start         start time of the interval (GMT)
1682
	 * @param int   $end           end time of the interval (GMT)
1683
	 * @param mixed $limit
1684
	 * @param mixed $remindersonly
1685
	 *
1686
	 * @return (array|mixed)[]
1687
	 *
1688
	 * @psalm-return array<int, T|array>
1689
	 */
1690
	public function getItems($start, $end, $limit = 0, $remindersonly = false): array {
1691
		$items = [];
1692
		$firstday = 0;
1693
1694
		if (isset($this->recur)) {
1695
			// Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
1696
			// exceptions are in range and have a reminder set
1697
			if ($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
1698
				// Sort exceptions by start time
1699
				uasort($this->recur["changed_occurrences"], [$this, "sortExceptionStart"]);
1700
1701
				// Loop through all changed exceptions
1702
				foreach ($this->recur["changed_occurrences"] as $exception) {
1703
					// Check reminder set
1704
					if (!isset($exception["reminder"]) || $exception["reminder"] == false) {
1705
						continue;
1706
					}
1707
1708
					// Convert to GMT
1709
					$occstart = $this->toGMT($this->tz, $exception["start"]);
1710
					$occend = $this->toGMT($this->tz, $exception["end"]);
1711
1712
					// Check range criterium
1713
					if ($occstart > $end || $occend < $start) {
1714
						continue;
1715
					}
1716
1717
					// OK, add to items.
1718
					array_push($items, $this->getExceptionProperties($exception));
1719
					if ($limit && (count($items) == $limit)) {
1720
						break;
1721
					}
1722
				}
1723
1724
				uasort($items, [$this, "sortStarttime"]);
1725
1726
				return $items;
1727
			}
1728
1729
			// From here on, the dates of the occurrences are calculated in local time, so the days we're looking
1730
			// at are calculated from the local time dates of $start and $end
1731
1732
			if (isset($this->recur['regen'], $this->action['datecompleted']) && $this->recur['regen']) {
1733
				$daystart = $this->dayStartOf($this->action['datecompleted']);
1734
			}
1735
			else {
1736
				$daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
1737
			}
1738
1739
			// Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
1740
			// or the end of the recurrence, whichever comes first
1741
			if ($end > $this->toGMT($this->tz, $this->recur["end"])) {
1742
				$rangeend = $this->toGMT($this->tz, $this->recur["end"]);
1743
			}
1744
			else {
1745
				$rangeend = $end;
1746
			}
1747
1748
			$dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));
1749
1750
			// Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.
1751
			$recurType = (int) $this->recur["type"] < 0x2000 ? (int) $this->recur["type"] + 0x2000 : (int) $this->recur["type"];
1752
1753
			switch ($recurType) {
1754
				case IDC_RCEV_PAT_ORB_DAILY:
1755
					if ($this->recur["everyn"] <= 0) {
1756
						$this->recur["everyn"] = 1440;
1757
					}
1758
1759
					if ($this->recur["subtype"] == rptDay) {
1760
						// Every Nth day
1761
						for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) {
1762
							$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1763
						}
1764
					}
1765
					else {
1766
						// Every workday
1767
						for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440) {
1768
							$nowtime = $this->gmtime($now);
1769
							if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace
1770
								$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1771
							}
1772
						}
1773
					}
1774
					break;
1775
1776
				case IDC_RCEV_PAT_ORB_WEEKLY:
1777
					if ($this->recur["everyn"] <= 0) {
1778
						$this->recur["everyn"] = 1;
1779
					}
1780
1781
					// If sliding flag is set then move to 'n' weeks
1782
					if ($this->recur['regen']) {
1783
						$daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);
1784
					}
1785
1786
					for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"])) {
1787
						if ($this->recur['regen']) {
1788
							$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1789
						}
1790
						else {
1791
							// Loop through the whole following week to the first occurrence of the week, add each day that is specified
1792
							for ($wday = 0; $wday < 7; ++$wday) {
1793
								$daynow = $now + $wday * 60 * 60 * 24;
1794
								// checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
1795
								if ($daynow <= $dayend) {
1796
									$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1797
									if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1798
										$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1799
									}
1800
								}
1801
							}
1802
						}
1803
					}
1804
					break;
1805
1806
				case IDC_RCEV_PAT_ORB_MONTHLY:
1807
					if ($this->recur["everyn"] <= 0) {
1808
						$this->recur["everyn"] = 1;
1809
					}
1810
1811
					// Loop through all months from start to end of occurrence, starting at beginning of first month
1812
					for ($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
0 ignored issues
show
$now of type double is incompatible with the type integer expected by parameter $date of BaseRecurrence::daysInMonth(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1812
					for ($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth(/** @scrutinizer ignore-type */ $now, $this->recur["everyn"]) * 24 * 60 * 60) {
Loading history...
1813
						if (isset($this->recur["monthday"]) && ($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
1814
							$difference = 1;
1815
							if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) {
1816
								$difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
1817
							}
1818
							$daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
1819
							// checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item
1820
							if ($daynow <= $dayend) {
1821
								$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
0 ignored issues
show
$daynow of type double is incompatible with the type false|integer expected by parameter $basedate of BaseRecurrence::processOccurrenceItem(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1821
								$this->processOccurrenceItem($items, $start, $end, /** @scrutinizer ignore-type */ $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
Loading history...
1822
							}
1823
						}
1824
						elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] of every N months
1825
							// Sanitize input
1826
							if ($this->recur["weekdays"] == 0) {
1827
								$this->recur["weekdays"] = 1;
1828
							}
1829
1830
							// If nday is not set to the last day in the month
1831
							if ($this->recur["nday"] < 5) {
1832
								// keep the track of no. of time correct selection pattern (like 2nd weekday, 4th friday, etc.) is matched
1833
								$ndaycounter = 0;
1834
								// Find matching weekday in this month
1835
								for ($day = 0, $total = $this->daysInMonth($now, 1); $day < $total; ++$day) {
1836
									$daynow = $now + $day * 60 * 60 * 24;
1837
									$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1838
1839
									if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1840
										++$ndaycounter;
1841
									}
1842
									// check the selected pattern is same as asked Nth weekday,If so set the firstday
1843
									if ($this->recur["nday"] == $ndaycounter) {
1844
										$firstday = $day;
1845
										break;
1846
									}
1847
								}
1848
								// $firstday is the day of the month on which the asked pattern of nth weekday matches
1849
								$daynow = $now + $firstday * 60 * 60 * 24;
1850
							}
1851
							else {
1852
								// Find last day in the month ($now is the firstday of the month)
1853
								$NumDaysInMonth = $this->daysInMonth($now, 1);
1854
								$daynow = $now + (($NumDaysInMonth - 1) * 24 * 60 * 60);
1855
1856
								$nowtime = $this->gmtime($daynow);
0 ignored issues
show
$daynow of type double is incompatible with the type integer expected by parameter $time of BaseRecurrence::gmtime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1856
								$nowtime = $this->gmtime(/** @scrutinizer ignore-type */ $daynow);
Loading history...
1857
								while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) == 0) {
1858
									$daynow -= 86400;
1859
									$nowtime = $this->gmtime($daynow);
1860
								}
1861
							}
1862
1863
							/*
1864
							* 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.
1865
							*/
1866
							if ($daynow <= $dayend && $daynow >= $daystart) {
1867
								$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1868
							}
1869
						}
1870
						elseif ($this->recur['regen']) {
1871
							$next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
1872
							$now = $daystart + ($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);
1873
1874
							if ($now <= $dayend) {
1875
								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1876
							}
1877
						}
1878
					}
1879
					break;
1880
1881
				case IDC_RCEV_PAT_ORB_YEARLY:
1882
					if ($this->recur["everyn"] <= 0) {
1883
						$this->recur["everyn"] = 12;
1884
					}
1885
1886
					for ($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1887
						if (isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
1888
							// recur["month"] is in minutes since the beginning of the year
1889
							$month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
1890
							$monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
1891
							$monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
1892
							if ($monthday > $this->daysInMonth($monthstart, 1)) {
1893
								$monthday = $this->daysInMonth($monthstart, 1);
1894
							}	// Cap $monthday on month length (eg 28 feb instead of 29 feb)
1895
							$daynow = $monthstart + ($monthday - 1) * 24 * 60 * 60;
1896
							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1897
						}
1898
						elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] in month X of every N years
1899
							// Go the correct month
1900
							$monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;
1901
1902
							// Find first matching weekday in this month
1903
							for ($wday = 0; $wday < 7; ++$wday) {
1904
								$daynow = $monthnow + $wday * 60 * 60 * 24;
1905
								$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1906
1907
								if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1908
									$firstday = $wday;
1909
									break;
1910
								}
1911
							}
1912
1913
							// Same as above (monthly)
1914
							$daynow = $monthnow + ($firstday + ($this->recur["nday"] - 1) * 7) * 60 * 60 * 24;
1915
1916
							while ($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) {
1917
								$daynow -= 7 * 60 * 60 * 24;
1918
							}
1919
1920
							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1921
						}
1922
						elseif ($this->recur['regen']) {
1923
							$year_starttime = $this->gmtime($now);
1924
							$is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1);	// +1 next year
1925
							$now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /* year in seconds */);
1926
1927
							if ($now <= $dayend) {
1928
								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1929
							}
1930
						}
1931
					}
1932
			}
1933
			// to get all exception items
1934
			if (!empty($this->recur['changed_occurrences'])) {
1935
				$this->processExceptionItems($items, $start, $end);
0 ignored issues
show
The method processExceptionItems() does not exist on BaseRecurrence. It seems like you code against a sub-type of BaseRecurrence such as Recurrence. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1935
				$this->/** @scrutinizer ignore-call */ 
1936
           processExceptionItems($items, $start, $end);
Loading history...
1936
			}
1937
		}
1938
1939
		// sort items on starttime
1940
		usort($items, [$this, "sortStarttime"]);
1941
1942
		// Return the MAPI-compatible list of items for this object
1943
		return $items;
1944
	}
1945
1946
	/**
1947
	 * @psalm-return -1|0|1
1948
	 *
1949
	 * @param mixed $a
1950
	 * @param mixed $b
1951
	 */
1952
	public function sortStarttime($a, $b): int {
1953
		$aTime = $a[$this->proptags["startdate"]];
1954
		$bTime = $b[$this->proptags["startdate"]];
1955
1956
		return $aTime == $bTime ? 0 : ($aTime > $bTime ? 1 : -1);
1957
	}
1958
1959
	/**
1960
	 * daysInMonth.
1961
	 *
1962
	 * Returns the number of days in the upcoming number of months. If you specify 1 month as
1963
	 * $months it will give you the number of days in the month of $date. If you specify more it
1964
	 * will also count the days in the upcoming months and add that to the number of days. So
1965
	 * if you have a date in march and you specify $months as 2 it will return 61.
1966
	 *
1967
	 * @param int $date   specified date as timestamp from which you want to know the number
1968
	 *                    of days in the month
1969
	 * @param int $months number of months you want to know the number of days in
1970
	 *
1971
	 * @return float|int number of days in the specified amount of months
1972
	 */
1973
	public function daysInMonth($date, $months) {
1974
		$days = 0;
1975
1976
		for ($i = 0; $i < $months; ++$i) {
1977
			$days += date("t", $date + $days * 24 * 60 * 60);
1978
		}
1979
1980
		return $days;
1981
	}
1982
1983
	// Converts MAPI-style 'minutes' into the month of the year [0..11]
1984
	public function monthOfYear($minutes) {
1985
		$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
1986
1987
		$d += $minutes * 60;
1988
1989
		$dtime = $this->gmtime($d);
1990
1991
		return $dtime["tm_mon"];
1992
	}
1993
1994
	/**
1995
	 * @psalm-return -1|0|1
1996
	 *
1997
	 * @param mixed $a
1998
	 * @param mixed $b
1999
	 */
2000
	public function sortExceptionStart($a, $b): int {
2001
		return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1);
2002
	}
2003
2004
	/**
2005
	 * Function to get all properties of a single changed exception.
2006
	 *
2007
	 * @param mixed $exception
2008
	 *
2009
	 * @return (mixed|true)[] associative array of properties for the exception
2010
	 *
2011
	 * @psalm-return array<mixed|true>
2012
	 */
2013
	public function getExceptionProperties($exception): array {
2014
		// Exception has same properties as main object, with some properties overridden:
2015
		$item = $this->messageprops;
2016
2017
		// Special properties
2018
		$item["exception"] = true;
2019
		$item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time !
2020
2021
		// MAPI-compatible properties (you can handle an exception as a normal calendar item like this)
2022
		$item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]);
2023
		$item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]);
2024
		$item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]];
2025
		$item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]];
2026
2027
		if (isset($exception["subject"])) {
2028
			$item[$this->proptags["subject"]] = $exception["subject"];
2029
		}
2030
2031
		if (isset($exception["label"])) {
2032
			$item[$this->proptags["label"]] = $exception["label"];
2033
		}
2034
2035
		if (isset($exception["alldayevent"])) {
2036
			$item[$this->proptags["alldayevent"]] = $exception["alldayevent"];
2037
		}
2038
2039
		if (isset($exception["location"])) {
2040
			$item[$this->proptags["location"]] = $exception["location"];
2041
		}
2042
2043
		if (isset($exception["remind_before"])) {
2044
			$item[$this->proptags["reminder_minutes"]] = $exception["remind_before"];
2045
		}
2046
2047
		if (isset($exception["reminder_set"])) {
2048
			$item[$this->proptags["reminder"]] = $exception["reminder_set"];
2049
		}
2050
2051
		if (isset($exception["busystatus"])) {
2052
			$item[$this->proptags["busystatus"]] = $exception["busystatus"];
2053
		}
2054
2055
		return $item;
2056
	}
2057
2058
	/**
2059
	 * @param false|int $start
2060
	 * @param false|int $basedate
2061
	 * @param mixed     $startocc
2062
	 * @param mixed     $endocc
2063
	 * @param mixed     $tz
2064
	 * @param mixed     $reminderonly
2065
	 */
2066
	abstract public function processOccurrenceItem(array &$items, $start, int $end, $basedate, $startocc, $endocc, $tz, $reminderonly);
2067
}
2068