Test Failed
Push — master ( 1ac602...f9946d )
by
unknown
12:58 queued 10:17
created

BaseRecurrence::getExceptionProperties()   B

Complexity

Conditions 8

Size

Total Lines 43
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 22
c 0
b 0
f 0
nop 1
dl 0
loc 43
rs 8.4444
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-2022 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
		 */
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() {
80
			$message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);
81
82
			$recurrBlob = '';
83
			$stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
84
			$stat = mapi_stream_stat($stream);
0 ignored issues
show
Bug introduced by
The function mapi_stream_stat was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

84
			$stat = /** @scrutinizer ignore-call */ mapi_stream_stat($stream);
Loading history...
85
86
			for ($i = 0; $i < $stat['cb']; $i += 1024) {
87
				$recurrBlob .= mapi_stream_read($stream, 1024);
0 ignored issues
show
Bug introduced by
The function mapi_stream_read was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

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

1051
							$dayofweek = gmdate("w", /** @scrutinizer ignore-type */ $occenddate);
Loading history...
1052
1053
							// Loop through the last occurrences until we have had them all
1054
							for ($j = 1; $restocc > 0; ++$j) {
1055
								// Jump to the next week (which may be N weeks away) when going over the week boundary
1056
								if ((($dayofweek + $j) % 7) == $weekstart) {
1057
									$occenddate += (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60;
1058
								}
1059
1060
								// If this is a matching day, once less occurrence to process
1061
								if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
1062
									--$restocc;
1063
								}
1064
1065
								// Next day
1066
								$occenddate += 24 * 60 * 60;
1067
							}
1068
1069
							break;
1070
1071
						case 0x0C: // monthly
1072
						case 0x0D: // yearly
1073
							$curyear = gmdate("Y", (int) $this->recur["start"]);
1074
							$curmonth = gmdate("n", (int) $this->recur["start"]);
1075
							// $forwardcount = months
1076
1077
							switch ((int) $this->recur["subtype"]) {
1078
								case 2: // on D day of every M month
1079
									while ($forwardcount > 0) {
1080
										$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1081
1082
										if ($curmonth >= 12) {
1083
											$curmonth = 1;
1084
											++$curyear;
1085
										}
1086
										else {
1087
											++$curmonth;
1088
										}
1089
										--$forwardcount;
1090
									}
1091
1092
									// compensation between 28 and 31
1093
									if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
1094
										gmdate("j", $occenddate) < ((int) $this->recur["monthday"])) {
1095
										if (gmdate("j", $occenddate) < 28) {
1096
											$occenddate -= gmdate("j", $occenddate) * 24 * 60 * 60;
1097
										}
1098
										else {
1099
											$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1100
										}
1101
									}
1102
1103
									break;
1104
1105
								case 3: // on Nth weekday of every M month
1106
									$nday = (int) $this->recur["nday"]; // 1 tot 5
1107
									$weekdays = (int) $this->recur["weekdays"];
1108
1109
									while ($forwardcount > 0) {
1110
										$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1111
										if ($curmonth >= 12) {
1112
											$curmonth = 1;
1113
											++$curyear;
1114
										}
1115
										else {
1116
											++$curmonth;
1117
										}
1118
1119
										--$forwardcount;
1120
									}
1121
1122
									if ($nday == 5) {
1123
										// Set date on the last day of the last month
1124
										$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1125
									}
1126
									else {
1127
										// Set date on the first day of the last month
1128
										$occenddate -= (gmdate("j", $occenddate) - 1) * 24 * 60 * 60;
1129
									}
1130
1131
									$dayofweek = gmdate("w", $occenddate);
1132
									for ($i = 0; $i < 7; ++$i) {
1133
										if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
1134
											$occenddate -= $i * 24 * 60 * 60;
1135
											break;
1136
										}
1137
										if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
1138
											$occenddate += ($i + (($nday - 1) * 7)) * 24 * 60 * 60;
1139
											break;
1140
										}
1141
									}
1142
1143
								break; // case 3:
1144
								}
1145
1146
							break;
1147
					}
1148
1149
					if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX) {
1150
						$occenddate = PHP_INT_MAX;
1151
					}
1152
1153
					$this->recur["end"] = $occenddate;
1154
1155
					$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1156
					break;
1157
				// Never ends
1158
				case 0x23:
1159
				default:
1160
					$this->recur["end"] = 0x7FFFFFFF; // max date -> 2038
1161
					$rdata .= pack("V", 0x5AE980DF);
1162
					break;
1163
			}
1164
1165
			// UTC date
1166
			$utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]);
1167
			$utcend = $this->toGMT($this->tz, (int) $this->recur["end"]);
1168
1169
			// utc date+time
1170
			$utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"]) * 60) : $utcstart;
1171
			$utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart;
1172
1173
			$propsToSet = [];
1174
			// update reminder time
1175
			$propsToSet[$this->proptags["reminder_time"]] = $utcfirstoccstartdatetime;
1176
1177
			// update first occurrence date
1178
			$propsToSet[$this->proptags["startdate"]] = $propsToSet[$this->proptags["commonstart"]] = $utcfirstoccstartdatetime;
1179
			$propsToSet[$this->proptags["duedate"]] = $propsToSet[$this->proptags["commonend"]] = $utcfirstoccenddatetime;
1180
1181
			// Set Outlook properties, if it is an appointment
1182
			if (isset($this->messageprops[$this->proptags["message_class"]]) && $this->messageprops[$this->proptags["message_class"]] == "IPM.Appointment") {
1183
				// update real begin and real end date
1184
				$propsToSet[$this->proptags["startdate_recurring"]] = $utcstart;
1185
				$propsToSet[$this->proptags["enddate_recurring"]] = $utcend;
1186
1187
				// recurrencetype
1188
				// Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
1189
				$propsToSet[$this->proptags["recurrencetype"]] = ((int) $this->recur["type"]) - 0x9;
1190
1191
				// set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
1192
				$propsToSet[$this->proptags["side_effects"]] = 369;
1193
			}
1194
			else {
1195
				$propsToSet[$this->proptags["side_effects"]] = 3441;
1196
			}
1197
1198
			// FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
1199
			// Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset
1200
			// to the 'next' occurrence; this makes sure that deleting the next occurrence will correctly set the reminder to
1201
			// the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time)
1202
			// with the reminder flag set.
1203
			$reminderprops = mapi_getprops($this->message, [$this->proptags["reminder_minutes"], $this->proptags["flagdueby"]]);
1204
			if (isset($reminderprops[$this->proptags["reminder_minutes"]])) {
1205
				$occ = false;
1206
				$occurrences = $this->getItems(time(), 0x7FF00000, 3, true);
1207
1208
				for ($i = 0, $len = count($occurrences); $i < $len; ++$i) {
1209
					// This will actually also give us appointments that have already started, but not yet ended. Since we want the next
1210
					// reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum
1211
					// number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case:
1212
					// time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item
1213
					// can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped.
1214
1215
					if (($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) {
1216
						$occ = $occurrences[$i];
1217
						break;
1218
					}
1219
				}
1220
1221
				if ($occ) {
1222
					if (isset($reminderprops[$this->proptags["flagdueby"]])) {
1223
						$propsToSet[$this->proptags["flagdueby"]] = $reminderprops[$this->proptags["flagdueby"]];
1224
					}
1225
					else {
1226
						$propsToSet[$this->proptags["flagdueby"]] = $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60);
1227
					}
1228
				}
1229
				else {
1230
					// Last reminder passed, no reminders any more.
1231
					$propsToSet[$this->proptags["reminder"]] = false;
1232
					$propsToSet[$this->proptags["flagdueby"]] = 0x7FF00000;
1233
				}
1234
			}
1235
1236
			// Default data
1237
			// Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information)
1238
			$rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00);
1239
1240
			if (isset($this->recur["startocc"], $this->recur["endocc"])) {
1241
				// Set start and endtime in minutes
1242
				$rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]);
1243
			}
1244
1245
			// Detailed exception data
1246
1247
			$changed_items = $this->recur["changed_occurrences"];
1248
1249
			$rdata .= pack("v", count($changed_items));
1250
1251
			foreach ($changed_items as $changed_item) {
1252
				// Set start and end time of exception
1253
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1254
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1255
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1256
1257
				// Bitmask
1258
				$bitmask = 0;
1259
1260
				// Check for changed strings
1261
				if (isset($changed_item["subject"])) {
1262
					$bitmask |= 1 << 0;
1263
				}
1264
1265
				if (isset($changed_item["remind_before"])) {
1266
					$bitmask |= 1 << 2;
1267
				}
1268
1269
				if (isset($changed_item["reminder_set"])) {
1270
					$bitmask |= 1 << 3;
1271
				}
1272
1273
				if (isset($changed_item["location"])) {
1274
					$bitmask |= 1 << 4;
1275
				}
1276
1277
				if (isset($changed_item["busystatus"])) {
1278
					$bitmask |= 1 << 5;
1279
				}
1280
1281
				if (isset($changed_item["alldayevent"])) {
1282
					$bitmask |= 1 << 7;
1283
				}
1284
1285
				if (isset($changed_item["label"])) {
1286
					$bitmask |= 1 << 8;
1287
				}
1288
1289
				$rdata .= pack("v", $bitmask);
1290
1291
				// Set "subject"
1292
				if (isset($changed_item["subject"])) {
1293
					// convert utf-8 to non-unicode blob string (us-ascii?)
1294
					$subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
1295
					$length = strlen($subject);
1296
					$rdata .= pack("vv", $length + 1, $length);
1297
					$rdata .= pack("a" . $length, $subject);
1298
				}
1299
1300
				if (isset($changed_item["remind_before"])) {
1301
					$rdata .= pack("V", $changed_item["remind_before"]);
1302
				}
1303
1304
				if (isset($changed_item["reminder_set"])) {
1305
					$rdata .= pack("V", $changed_item["reminder_set"]);
1306
				}
1307
1308
				if (isset($changed_item["location"])) {
1309
					$location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
1310
					$length = strlen($location);
1311
					$rdata .= pack("vv", $length + 1, $length);
1312
					$rdata .= pack("a" . $length, $location);
1313
				}
1314
1315
				if (isset($changed_item["busystatus"])) {
1316
					$rdata .= pack("V", $changed_item["busystatus"]);
1317
				}
1318
1319
				if (isset($changed_item["alldayevent"])) {
1320
					$rdata .= pack("V", $changed_item["alldayevent"]);
1321
				}
1322
1323
				if (isset($changed_item["label"])) {
1324
					$rdata .= pack("V", $changed_item["label"]);
1325
				}
1326
			}
1327
1328
			$rdata .= pack("V", 0);
1329
1330
			// write extended data
1331
			foreach ($changed_items as $changed_item) {
1332
				$rdata .= pack("V", 0);
1333
				if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1334
					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1335
					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1336
					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1337
				}
1338
1339
				if (isset($changed_item["subject"])) {
1340
					$subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
1341
					$length = iconv_strlen($subject, "UCS-2LE");
1342
					$rdata .= pack("v", $length);
1343
					$rdata .= pack("a" . $length * 2, $subject);
1344
				}
1345
1346
				if (isset($changed_item["location"])) {
1347
					$location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
1348
					$length = iconv_strlen($location, "UCS-2LE");
1349
					$rdata .= pack("v", $length);
1350
					$rdata .= pack("a" . $length * 2, $location);
1351
				}
1352
1353
				if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1354
					$rdata .= pack("V", 0);
1355
				}
1356
			}
1357
1358
			$rdata .= pack("V", 0);
1359
1360
			// Set props
1361
			$propsToSet[$this->proptags["recurring_data"]] = $rdata;
1362
			$propsToSet[$this->proptags["recurring"]] = true;
1363
			if (isset($this->tz) && $this->tz) {
1364
				$timezone = "GMT";
1365
				if ($this->tz["timezone"] != 0) {
1366
					// Create user readable timezone information
1367
					$timezone = sprintf(
1368
						"(GMT %s%02d:%02d)",
1369
						(-$this->tz["timezone"] > 0 ? "+" : "-"),
1370
						abs($this->tz["timezone"] / 60),
1371
						abs($this->tz["timezone"] % 60)
1372
					);
1373
				}
1374
				$propsToSet[$this->proptags["timezone_data"]] = $this->getTimezoneData($this->tz);
1375
				$propsToSet[$this->proptags["timezone"]] = $timezone;
1376
			}
1377
			mapi_setprops($this->message, $propsToSet);
1378
		}
1379
1380
		/**
1381
		 * Function which converts a recurrence date timestamp to an unix date timestamp.
1382
		 *
1383
		 * @author Steve Hardy
1384
		 *
1385
		 * @param int $rdate the date which will be converted
1386
		 *
1387
		 * @return int the converted date
1388
		 */
1389
		public function recurDataToUnixData($rdate) {
1390
			return ($rdate - 194074560) * 60;
1391
		}
1392
1393
		/**
1394
		 * Function which converts an unix date timestamp to recurrence date timestamp.
1395
		 *
1396
		 * @author Johnny Biemans
1397
		 *
1398
		 * @param int $date the date which will be converted
1399
		 *
1400
		 * @return int the converted date in minutes
1401
		 */
1402
		public function unixDataToRecurData($date) {
1403
			return ($date / 60) + 194074560;
1404
		}
1405
1406
		/**
1407
		 * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1408
		 *
1409
		 * @author Steve Hardy
1410
		 *
1411
		 * @param mixed $ts
1412
		 */
1413
		public function GetTZOffset($ts) {
1414
			$Offset = date("O", $ts);
1415
1416
			$Parity = $Offset < 0 ? -1 : 1;
1417
			$Offset = $Parity * $Offset;
1418
			$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1419
1420
			return $Parity * $Offset;
1421
		}
1422
1423
		/**
1424
		 * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1425
		 *
1426
		 * @author Steve Hardy
1427
		 *
1428
		 * @param int $time
1429
		 *
1430
		 * @return array GMT Time
1431
		 */
1432
		public function gmtime($time) {
1433
			$TZOffset = $this->GetTZOffset($time);
1434
1435
			$t_time = $time - $TZOffset * 60; # Counter adjust for localtime()
1436
1437
			return localtime($t_time, 1);
1438
		}
1439
1440
		public function isLeapYear($year) {
1441
			return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0);
1442
		}
1443
1444
		public function getMonthInSeconds($year, $month) {
1445
			if (in_array($month, [1, 3, 5, 7, 8, 10, 12])) {
1446
				$day = 31;
1447
			}
1448
			elseif (in_array($month, [4, 6, 9, 11])) {
1449
				$day = 30;
1450
			}
1451
			else {
1452
				$day = 28;
1453
				if ($this->isLeapYear($year) == 1) {
1454
					++$day;
1455
				}
1456
			}
1457
1458
			return $day * 24 * 60 * 60;
1459
		}
1460
1461
		/**
1462
		 * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour.
1463
		 *
1464
		 * @param int $year
1465
		 * @param int $month
1466
		 * @param int $week
1467
		 * @param int $day
1468
		 * @param int $hour
1469
		 *
1470
		 * @return int the timestamp of the given date, timezone-independent
1471
		 */
1472
		public function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour) {
1473
			// get first day of month
1474
			$date = gmmktime(0, 0, 0, $month, 0, $year + 1900);
1475
1476
			// get wday info
1477
			$gmdate = $this->gmtime($date);
1478
1479
			$date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week
1480
1481
			$date += $week * 7 * 24 * 60 * 60; // go to correct week nr
1482
			$date += $day * 24 * 60 * 60;
1483
			$date += $hour * 60 * 60;
1484
1485
			$gmdate = $this->gmtime($date);
1486
1487
			// if we are in the next month, then back up a week, because week '5' means
1488
			// 'last week of month'
1489
1490
			if ($month != $gmdate["tm_mon"] + 1) {
1491
				$date -= 7 * 24 * 60 * 60;
1492
			}
1493
1494
			return $date;
1495
		}
1496
1497
		/**
1498
		 * getTimezone gives the timezone offset (in minutes) of the given
1499
		 * local date/time according to the given TZ info.
1500
		 *
1501
		 * @param mixed $tz
1502
		 * @param mixed $date
1503
		 */
1504
		public function getTimezone($tz, $date) {
1505
			// No timezone -> GMT (+0)
1506
			if (!isset($tz["timezone"])) {
1507
				return 0;
1508
			}
1509
1510
			$dst = false;
1511
			$gmdate = $this->gmtime($date);
1512
1513
			$dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
1514
			$dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);
1515
1516
			if ($dststart <= $dstend) {
1517
				// Northern hemisphere, eg DST is during Mar-Oct
1518
				if ($date > $dststart && $date < $dstend) {
1519
					$dst = true;
1520
				}
1521
			}
1522
			else {
1523
				// Southern hemisphere, eg DST is during Oct-Mar
1524
				if ($date < $dstend || $date > $dststart) {
1525
					$dst = true;
1526
				}
1527
			}
1528
1529
			if ($dst) {
1530
				return $tz["timezone"] + $tz["timezonedst"];
1531
			}
1532
1533
			return $tz["timezone"];
1534
		}
1535
1536
		/**
1537
		 * getWeekNr() returns the week nr of the month (ie first week of february is 1).
1538
		 *
1539
		 * @param mixed $date
1540
		 */
1541
		public function getWeekNr($date) {
1542
			$gmdate = gmtime($date);
0 ignored issues
show
Bug introduced by
The function gmtime was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1542
			$gmdate = /** @scrutinizer ignore-call */ gmtime($date);
Loading history...
1543
			$gmdate["tm_mday"] = 0;
1544
1545
			return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1;
1546
		}
1547
1548
		/**
1549
		 * parseTimezone parses the timezone as specified in named property 0x8233
1550
		 * in Outlook calendar messages. Returns the timezone in minutes negative
1551
		 * offset (GMT +2:00 -> -120).
1552
		 *
1553
		 * @param mixed $data
1554
		 */
1555
		public function parseTimezone($data) {
1556
			if (strlen($data) < 48) {
1557
				return;
1558
			}
1559
1560
			return unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
1561
		}
1562
1563
		public function getTimezoneData($tz) {
1564
			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);
1565
		}
1566
1567
		/**
1568
		 * createTimezone creates the timezone as specified in the named property 0x8233
1569
		 * see also parseTimezone()
1570
		 * $tz is an array with the timezone data.
1571
		 *
1572
		 * @param mixed $tz
1573
		 */
1574
		public function createTimezone($tz) {
1575
			return pack(
1576
				"lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx",
1577
				$tz["timezone"],
1578
				array_key_exists("timezonedst", $tz) ? $tz["timezonedst"] : 0,
1579
				array_key_exists("dstendmonth", $tz) ? $tz["dstendmonth"] : 0,
1580
				array_key_exists("dstendweek", $tz) ? $tz["dstendweek"] : 0,
1581
				array_key_exists("dstendhour", $tz) ? $tz["dstendhour"] : 0,
1582
				array_key_exists("dststartmonth", $tz) ? $tz["dststartmonth"] : 0,
1583
				array_key_exists("dststartweek", $tz) ? $tz["dststartweek"] : 0,
1584
				array_key_exists("dststarthour", $tz) ? $tz["dststarthour"] : 0
1585
			);
1586
		}
1587
1588
		/**
1589
		 * toGMT returns a timestamp in GMT time for the time and timezone given.
1590
		 *
1591
		 * @param mixed $tz
1592
		 * @param mixed $date
1593
		 */
1594
		public function toGMT($tz, $date) {
1595
			if (!isset($tz['timezone'])) {
1596
				return $date;
1597
			}
1598
			$offset = $this->getTimezone($tz, $date);
1599
1600
			return $date + $offset * 60;
1601
		}
1602
1603
		/**
1604
		 * fromGMT returns a timestamp in the local timezone given from the GMT time given.
1605
		 *
1606
		 * @param mixed $tz
1607
		 * @param mixed $date
1608
		 */
1609
		public function fromGMT($tz, $date) {
1610
			$offset = $this->getTimezone($tz, $date);
1611
1612
			return $date - $offset * 60;
1613
		}
1614
1615
		/**
1616
		 * Function to get timestamp of the beginning of the day of the timestamp given.
1617
		 *
1618
		 * @param mixed $date
1619
		 *
1620
		 * @return int|false timestamp referring to same day but at 00:00:00
1621
		 */
1622
		public function dayStartOf($date) {
1623
			$time1 = $this->gmtime($date);
1624
1625
			return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
1626
		}
1627
1628
		/**
1629
		 * Function to get timestamp of the beginning of the month of the timestamp given.
1630
		 *
1631
		 * @param mixed $date
1632
		 *
1633
		 * @return int|false Timestamp referring to same month but on the first day, and at 00:00:00
1634
		 */
1635
		public function monthStartOf($date) {
1636
			$time1 = $this->gmtime($date);
1637
1638
			return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
1639
		}
1640
1641
		/**
1642
		 * Function to get timestamp of the beginning of the year of the timestamp given.
1643
		 *
1644
		 * @param mixed $date
1645
		 *
1646
		 * @return int|false Timestamp referring to the same year but on Jan 01, at 00:00:00
1647
		 */
1648
		public function yearStartOf($date) {
1649
			$time1 = $this->gmtime($date);
1650
1651
			return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
1652
		}
1653
1654
		/**
1655
		 * Function which returns the items in a given interval. This included expansion of the recurrence and
1656
		 * processing of exceptions (modified and deleted).
1657
		 *
1658
		 * @param int   $start         start time of the interval (GMT)
1659
		 * @param int   $end           end time of the interval (GMT)
1660
		 * @param mixed  $limit
1661
		 * @param mixed  $remindersonly
1662
		 *
1663
		 * @return array
1664
		 */
1665
		public function getItems($start, $end, $limit = 0, $remindersonly = false) {
1666
			$items = [];
1667
1668
			if (isset($this->recur)) {
1669
				// Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
1670
				// exceptions are in range and have a reminder set
1671
				if ($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
1672
					// Sort exceptions by start time
1673
					uasort($this->recur["changed_occurrences"], [$this, "sortExceptionStart"]);
1674
1675
					// Loop through all changed exceptions
1676
					foreach ($this->recur["changed_occurrences"] as $exception) {
1677
						// Check reminder set
1678
						if (!isset($exception["reminder"]) || $exception["reminder"] == false) {
1679
							continue;
1680
						}
1681
1682
						// Convert to GMT
1683
						$occstart = $this->toGMT($this->tz, $exception["start"]);
1684
						$occend = $this->toGMT($this->tz, $exception["end"]);
1685
1686
						// Check range criterium
1687
						if ($occstart > $end || $occend < $start) {
1688
							continue;
1689
						}
1690
1691
						// OK, add to items.
1692
						array_push($items, $this->getExceptionProperties($exception));
1693
						if ($limit && (count($items) == $limit)) {
1694
							break;
1695
						}
1696
					}
1697
1698
					uasort($items, [$this, "sortStarttime"]);
1699
1700
					return $items;
1701
				}
1702
1703
				// From here on, the dates of the occurrences are calculated in local time, so the days we're looking
1704
				// at are calculated from the local time dates of $start and $end
1705
1706
				if (isset($this->recur['regen'], $this->action['datecompleted']) && $this->recur['regen']) {
1707
					$daystart = $this->dayStartOf($this->action['datecompleted']);
1708
				}
1709
				else {
1710
					$daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
1711
				}
1712
1713
				// Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
1714
				// or the end of the recurrence, whichever comes first
1715
				if ($end > $this->toGMT($this->tz, $this->recur["end"])) {
1716
					$rangeend = $this->toGMT($this->tz, $this->recur["end"]);
1717
				}
1718
				else {
1719
					$rangeend = $end;
1720
				}
1721
1722
				$dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));
1723
1724
				// Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.
1725
1726
				switch ($this->recur["type"]) {
1727
					case 10:
1728
						// Daily
1729
						if ($this->recur["everyn"] <= 0) {
1730
							$this->recur["everyn"] = 1440;
1731
						}
1732
1733
						if ($this->recur["subtype"] == 0) {
1734
							// Every Nth day
1735
							for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) {
1736
								$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1737
							}
1738
						}
1739
						else {
1740
							// Every workday
1741
							for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440) {
1742
								$nowtime = $this->gmtime($now);
1743
								if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace
1744
									$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1745
								}
1746
							}
1747
						}
1748
						break;
1749
1750
					case 11:
1751
						// Weekly
1752
						if ($this->recur["everyn"] <= 0) {
1753
							$this->recur["everyn"] = 1;
1754
						}
1755
1756
						// If sliding flag is set then move to 'n' weeks
1757
						if ($this->recur['regen']) {
1758
							$daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);
1759
						}
1760
1761
						for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"])) {
1762
							if ($this->recur['regen']) {
1763
								$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1764
							}
1765
							else {
1766
								// Loop through the whole following week to the first occurrence of the week, add each day that is specified
1767
								for ($wday = 0; $wday < 7; ++$wday) {
1768
									$daynow = $now + $wday * 60 * 60 * 24;
1769
									// checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
1770
									if ($daynow <= $dayend) {
1771
										$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1772
										if (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))) { // Selected ?
1773
											$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1774
										}
1775
									}
1776
								}
1777
							}
1778
						}
1779
						break;
1780
1781
					case 12:
1782
						// Monthly
1783
						if ($this->recur["everyn"] <= 0) {
1784
							$this->recur["everyn"] = 1;
1785
						}
1786
1787
						// Loop through all months from start to end of occurrence, starting at beginning of first month
1788
						for ($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1789
							if (isset($this->recur["monthday"]) && ($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
1790
								$difference = 1;
1791
								if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) {
1792
									$difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
1793
								}
1794
								$daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
1795
								// checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item
1796
								if ($daynow <= $dayend) {
1797
									$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1798
								}
1799
							}
1800
							elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] of every N months
1801
								// Sanitize input
1802
								if ($this->recur["weekdays"] == 0) {
1803
									$this->recur["weekdays"] = 1;
1804
								}
1805
1806
								// If nday is not set to the last day in the month
1807
								if ($this->recur["nday"] < 5) {
1808
									// keep the track of no. of time correct selection pattern (like 2nd weekday, 4th friday, etc.) is matched
1809
									$ndaycounter = 0;
1810
									// Find matching weekday in this month
1811
									for ($day = 0, $total = $this->daysInMonth($now, 1); $day < $total; ++$day) {
1812
										$daynow = $now + $day * 60 * 60 * 24;
1813
										$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1814
1815
										if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1816
											++$ndaycounter;
1817
										}
1818
										// check the selected pattern is same as asked Nth weekday,If so set the firstday
1819
										if ($this->recur["nday"] == $ndaycounter) {
1820
											$firstday = $day;
1821
											break;
1822
										}
1823
									}
1824
									// $firstday is the day of the month on which the asked pattern of nth weekday matches
1825
									$daynow = $now + $firstday * 60 * 60 * 24;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $firstday does not seem to be defined for all execution paths leading up to this point.
Loading history...
1826
								}
1827
								else {
1828
									// Find last day in the month ($now is the firstday of the month)
1829
									$NumDaysInMonth = $this->daysInMonth($now, 1);
1830
									$daynow = $now + (($NumDaysInMonth - 1) * 24 * 60 * 60);
1831
1832
									$nowtime = $this->gmtime($daynow);
1833
									while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) == 0) {
1834
										$daynow -= 86400;
1835
										$nowtime = $this->gmtime($daynow);
1836
									}
1837
								}
1838
1839
								/*
1840
								* 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.
1841
								*/
1842
								if ($daynow <= $dayend && $daynow >= $daystart) {
1843
									$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1844
								}
1845
							}
1846
							elseif ($this->recur['regen']) {
1847
								$next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
1848
								$now = $daystart + ($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);
1849
1850
								if ($now <= $dayend) {
1851
									$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1852
								}
1853
							}
1854
						}
1855
						break;
1856
1857
					case 13:
1858
					// Yearly
1859
					if ($this->recur["everyn"] <= 0) {
1860
						$this->recur["everyn"] = 12;
1861
					}
1862
1863
					for ($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1864
						if (isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
1865
							// recur["month"] is in minutes since the beginning of the year
1866
							$month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
1867
							$monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
1868
							$monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
1869
							if ($monthday > $this->daysInMonth($monthstart, 1)) {
1870
								$monthday = $this->daysInMonth($monthstart, 1);
1871
							}	// Cap $monthday on month length (eg 28 feb instead of 29 feb)
1872
							$daynow = $monthstart + ($monthday - 1) * 24 * 60 * 60;
1873
							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1874
						}
1875
						elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] in month X of every N years
1876
							// Go the correct month
1877
							$monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;
1878
1879
							// Find first matching weekday in this month
1880
							for ($wday = 0; $wday < 7; ++$wday) {
1881
								$daynow = $monthnow + $wday * 60 * 60 * 24;
1882
								$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1883
1884
								if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1885
									$firstday = $wday;
1886
									break;
1887
								}
1888
							}
1889
1890
							// Same as above (monthly)
1891
							$daynow = $monthnow + ($firstday + ($this->recur["nday"] - 1) * 7) * 60 * 60 * 24;
1892
1893
							while ($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) {
1894
								$daynow -= 7 * 60 * 60 * 24;
1895
							}
1896
1897
							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1898
						}
1899
						elseif ($this->recur['regen']) {
1900
							$year_starttime = $this->gmtime($now);
1901
							$is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1);	// +1 next year
1902
							$now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /* year in seconds */);
1903
1904
							if ($now <= $dayend) {
1905
								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1906
							}
1907
						}
1908
					}
1909
				}
1910
				// to get all exception items
1911
				if (!empty($this->recur['changed_occurrences'])) {
1912
					$this->processExceptionItems($items, $start, $end);
0 ignored issues
show
Bug introduced by
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

1912
					$this->/** @scrutinizer ignore-call */ 
1913
            processExceptionItems($items, $start, $end);
Loading history...
1913
				}
1914
			}
1915
1916
			// sort items on starttime
1917
			usort($items, [$this, "sortStarttime"]);
1918
1919
			// Return the MAPI-compatible list of items for this object
1920
			return $items;
1921
		}
1922
1923
		public function sortStarttime($a, $b) {
1924
			$aTime = $a[$this->proptags["startdate"]];
1925
			$bTime = $b[$this->proptags["startdate"]];
1926
1927
			return $aTime == $bTime ? 0 : ($aTime > $bTime ? 1 : -1);
1928
		}
1929
1930
		/**
1931
		 * daysInMonth.
1932
		 *
1933
		 * Returns the number of days in the upcoming number of months. If you specify 1 month as
1934
		 * $months it will give you the number of days in the month of $date. If you specify more it
1935
		 * will also count the days in the upcoming months and add that to the number of days. So
1936
		 * if you have a date in march and you specify $months as 2 it will return 61.
1937
		 *
1938
		 * @param int $date   specified date as timestamp from which you want to know the number
1939
		 *                    of days in the month
1940
		 * @param int $months number of months you want to know the number of days in
1941
		 *
1942
		 * @return int Number of days in the specified amount of months.
1943
		 */
1944
		public function daysInMonth($date, $months) {
1945
			$days = 0;
1946
1947
			for ($i = 0; $i < $months; ++$i) {
1948
				$days += date("t", $date + $days * 24 * 60 * 60);
1949
			}
1950
1951
			return $days;
1952
		}
1953
1954
		// Converts MAPI-style 'minutes' into the month of the year [0..11]
1955
		public function monthOfYear($minutes) {
1956
			$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
1957
1958
			$d += $minutes * 60;
1959
1960
			$dtime = $this->gmtime($d);
1961
1962
			return $dtime["tm_mon"];
1963
		}
1964
1965
		public function sortExceptionStart($a, $b) {
1966
			return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1);
1967
		}
1968
1969
		/**
1970
		 * Function to get all properties of a single changed exception.
1971
		 *
1972
		 * @param mixed $exception
1973
		 *
1974
		 * @return array associative array of properties for the exception, compatible with
1975
		 */
1976
		public function getExceptionProperties($exception) {
1977
			// Exception has same properties as main object, with some properties overridden:
1978
			$item = $this->messageprops;
1979
1980
			// Special properties
1981
			$item["exception"] = true;
1982
			$item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time !
1983
1984
			// MAPI-compatible properties (you can handle an exception as a normal calendar item like this)
1985
			$item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]);
1986
			$item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]);
1987
			$item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]];
1988
			$item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]];
1989
1990
			if (isset($exception["subject"])) {
1991
				$item[$this->proptags["subject"]] = $exception["subject"];
1992
			}
1993
1994
			if (isset($exception["label"])) {
1995
				$item[$this->proptags["label"]] = $exception["label"];
1996
			}
1997
1998
			if (isset($exception["alldayevent"])) {
1999
				$item[$this->proptags["alldayevent"]] = $exception["alldayevent"];
2000
			}
2001
2002
			if (isset($exception["location"])) {
2003
				$item[$this->proptags["location"]] = $exception["location"];
2004
			}
2005
2006
			if (isset($exception["remind_before"])) {
2007
				$item[$this->proptags["reminder_minutes"]] = $exception["remind_before"];
2008
			}
2009
2010
			if (isset($exception["reminder_set"])) {
2011
				$item[$this->proptags["reminder"]] = $exception["reminder_set"];
2012
			}
2013
2014
			if (isset($exception["busystatus"])) {
2015
				$item[$this->proptags["busystatus"]] = $exception["busystatus"];
2016
			}
2017
2018
			return $item;
2019
		}
2020
2021
		abstract public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly);
2022
	}
2023