Issues (207)

class.baserecurrence.php (6 issues)

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]);
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

80
		$message = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $this->store, $this->messageprops[PR_ENTRYID]);
Loading history...
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)))) {
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...
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);
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

1106
						$dayofweek = gmdate("w", /** @scrutinizer ignore-type */ $occenddate);
Loading history...
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) {
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

1829
				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...
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);
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

1838
							$this->processOccurrenceItem($items, $start, $end, /** @scrutinizer ignore-type */ $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
Loading history...
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);
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

1873
							$nowtime = $this->gmtime(/** @scrutinizer ignore-type */ $daynow);
Loading history...
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