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