Passed
Push — master ( 348e1a...a68187 )
by
unknown
05:49
created

AppointmentListModule::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 9
rs 10
c 1
b 1
f 0
1
<?php
2
3
/**
4
 * Appointment Module.
5
 */
6
class AppointmentListModule extends ListModule {
7
	/**
8
	 * @var date start interval of view visible
9
	 */
10
	private $startdate;
11
12
	/**
13
	 * @var date end interval of view visible
14
	 */
15
	private $enddate;
16
17
	/**
18
	 * @var bool|string client timezone definition
19
	 */
20
	protected $tzdef;
21
22
	/**
23
	 * @var array|bool client timezone definition array
24
	 */
25
	protected $tzdefObj;
26
27
	/**
28
	 * @var mixed client timezone effective rule id
29
	 */
30
	protected $tzEffRuleIdx;
31
32
	/**
33
	 * Constructor.
34
	 *
35
	 * @param int   $id   unique id
36
	 * @param array $data list of all actions
37
	 */
38
	public function __construct($id, $data) {
39
		parent::__construct($id, $data);
40
41
		$this->properties = $GLOBALS["properties"]->getAppointmentListProperties();
42
43
		$this->startdate = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type date of property $startdate.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
44
		$this->enddate = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type date of property $enddate.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
45
		$this->tzdef = false;
46
		$this->tzdefObj = false;
47
	}
48
49
	/**
50
	 * Creates the notifiers for this module,
51
	 * and register them to the Bus.
52
	 */
53
	public function createNotifiers() {
54
		$entryid = $this->getEntryID();
55
		$GLOBALS["bus"]->registerNotifier('appointmentlistnotifier', $entryid);
56
	}
57
58
	/**
59
	 * Executes all the actions in the $data variable.
60
	 */
61
	public function execute() {
62
		foreach ($this->data as $actionType => $action) {
63
			if (isset($actionType)) {
64
				try {
65
					$store = $this->getActionStore($action);
66
					$entryid = $this->getActionEntryID($action);
67
68
					switch ($actionType) {
69
						case "list":
70
							$this->messageList($store, $entryid, $action, $actionType);
71
							break;
72
73
						case "search":
74
							// @FIXME add functionality to handle private items
75
							$this->search($store, $entryid, $action, $actionType);
76
							break;
77
78
						case "updatesearch":
79
							$this->updatesearch($store, $entryid, $action);
80
							break;
81
82
						case "stopsearch":
83
							$this->stopSearch($store, $entryid, $action);
84
							break;
85
86
						default:
87
							$this->handleUnknownActionType($actionType);
88
					}
89
				}
90
				catch (MAPIException $e) {
91
					if (isset($action['suppress_exception']) && $action['suppress_exception'] === true) {
92
						$e->setNotificationType('console');
93
					}
94
					$this->processException($e, $actionType);
95
				}
96
			}
97
		}
98
	}
99
100
	/**
101
	 * Function which retrieves a list of calendar items in a calendar folder.
102
	 *
103
	 * @param object $store      MAPI Message Store Object
104
	 * @param string $entryid    entryid of the folder
105
	 * @param array  $action     the action data, sent by the client
106
	 * @param string $actionType the action type, sent by the client
107
	 */
108
	public function messageList($store, $entryid, $action, $actionType) {
109
		if ($store && $entryid) {
110
			// initialize start and due date with false value so it will not take values from previous request
111
			$this->startdate = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type date of property $startdate.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
112
			$this->enddate = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type date of property $enddate.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
113
114
			if (isset($action["restriction"])) {
115
				if (isset($action["restriction"]["startdate"])) {
116
					$this->startdate = $action["restriction"]["startdate"];
117
				}
118
119
				if (isset($action["restriction"]["duedate"])) {
120
					$this->enddate = $action["restriction"]["duedate"];
121
				}
122
			}
123
124
			if (!empty($action["timezone_iana"])) {
125
				try {
126
					$this->tzdef = mapi_ianatz_to_tzdef($action['timezone_iana']);
127
				}
128
				catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
129
				}
130
			}
131
132
			if ($this->startdate && $this->enddate) {
133
				$data = [];
134
135
				if (is_array($entryid) && !empty($entryid)) {
0 ignored issues
show
introduced by
The condition is_array($entryid) is always false.
Loading history...
136
					$data["item"] = [];
137
					for ($index = 0, $index2 = count($entryid); $index < $index2; ++$index) {
138
						$this->getDelegateFolderInfo($store[$index]);
139
140
						// Set the active store in properties class and get the props based on active store.
141
						// we need to do this because of multi server env where shared store belongs to the different server.
142
						// Here name space is different per server. e.g. There is user A and user B and both are belongs to
143
						// different server and user B is shared store of user A because of that user A has 'categories' => -2062020578
144
						// and user B 'categories' => -2062610402,
145
						$GLOBALS["properties"]->setActiveStore($store[$index]);
146
						$this->properties = $GLOBALS["properties"]->getAppointmentListProperties();
147
148
						$data["item"] = array_merge($data["item"], $this->getCalendarItems($store[$index], $entryid[$index], $this->startdate, $this->enddate));
149
					}
150
				}
151
				else {
152
					$this->getDelegateFolderInfo($store);
0 ignored issues
show
Bug introduced by
$store of type object is incompatible with the type resource expected by parameter $store of ListModule::getDelegateFolderInfo(). ( Ignorable by Annotation )

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

152
					$this->getDelegateFolderInfo(/** @scrutinizer ignore-type */ $store);
Loading history...
153
					$data["item"] = $this->getCalendarItems($store, $entryid, $this->startdate, $this->enddate);
154
				}
155
156
				$this->addActionData("list", $data);
157
				$GLOBALS["bus"]->addData($this->getResponseData());
158
			}
159
			else {
160
				// for list view in calendar as startdate and enddate is passed as false
161
				// this will set sorting and paging for items in listview.
162
163
				$this->getDelegateFolderInfo($store);
164
165
				/* This is an override for parent::messageList(), which ignores an array of entryids / stores.
166
				*	 The following block considers this possibly and merges the data of several folders / stores.
167
				*/
168
169
				$this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content
170
171
				if ($store && $entryid) {
172
					// Restriction
173
					$this->parseRestriction($action);
0 ignored issues
show
Bug introduced by
$action of type array is incompatible with the type object expected by parameter $action of ListModule::parseRestriction(). ( Ignorable by Annotation )

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

173
					$this->parseRestriction(/** @scrutinizer ignore-type */ $action);
Loading history...
174
175
					// Sort
176
					$this->parseSortOrder($action, null, true);
177
178
					$limit = false;
179
					if (isset($action['restriction']['limit'])) {
180
						$limit = $action['restriction']['limit'];
181
					}
182
					else {
183
						$limit = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
184
					}
185
186
					$isSearchFolder = isset($action['search_folder_entryid']);
187
					$entryid = $isSearchFolder ? hex2bin($action['search_folder_entryid']) : $entryid;
188
189
					if (!is_array($entryid) && !is_array($store)) {
0 ignored issues
show
introduced by
The condition is_array($store) is always false.
Loading history...
introduced by
The condition is_array($entryid) is always false.
Loading history...
190
						$entryid = [$entryid];
191
						$store = [$store];
192
					}
193
194
					// Get the table and merge the arrays
195
					$data = [];
196
					$items = [];
197
					for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
198
						$newItems = $GLOBALS["operations"]->getTable($store[$i], $entryid[$i], $this->properties, $this->sort, $this->start, $limit, $this->restriction);
199
						$items = array_merge($items, $newItems['item']);
200
						if (isset($newItems['page']['totalrowcount']) && $newItems['page']['totalrowcount'] > $limit) {
201
							$data['page'] = $newItems['page'];
202
						}
203
					}
204
205
					// If the request come from search folder then no need to send folder information
206
					if (!$isSearchFolder) {
207
						$contentCount = 0;
208
						$contentUnread = 0;
209
210
						// For each folder
211
						for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
212
							// Open folder
213
							$folder = mapi_msgstore_openentry($store[$i], $entryid[$i]);
214
							// Obtain some statistics from the folder contents
215
							$content = mapi_getprops($folder, [PR_CONTENT_COUNT, PR_CONTENT_UNREAD]);
216
							if (isset($content[PR_CONTENT_COUNT])) {
217
								$contentCount += $content[PR_CONTENT_COUNT];
218
							}
219
220
							if (isset($content[PR_CONTENT_UNREAD])) {
221
								$contentUnread += $content[PR_CONTENT_UNREAD];
222
							}
223
						}
224
225
						$data["folder"] = [];
226
						$data["folder"]["content_count"] = $contentCount;
227
						$data["folder"]["content_unread"] = $contentUnread;
228
					}
229
230
					$items = $this->filterPrivateItems(['item' => $items]);
231
					// unset will remove the value but will not regenerate array keys, so we need to
232
					// do it here
233
					$data["item"] = array_values($items["item"]);
234
235
					for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
236
						// Allowing to hook in just before the data sent away to be sent to the client
237
						$GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', [
238
							'moduleObject' => &$this,
239
							'store' => $store[$i],
240
							'entryid' => $entryid[$i],
241
							'action' => $action,
242
							'data' => &$data,
243
						]);
244
					}
245
246
					$this->addActionData($actionType, $data);
247
					$GLOBALS["bus"]->addData($this->getResponseData());
248
				}
249
			}
250
		}
251
	}
252
253
	/**
254
	 * Function to return all Calendar items in a given timeframe. This
255
	 * function also takes recurring items into account.
256
	 *
257
	 * @param object $store   message store
258
	 * @param mixed  $entryid entryid of the folder
259
	 * @param mixed  $start   startdate of the interval
260
	 * @param mixed  $end     enddate of the interval
261
	 */
262
	public function getCalendarItems($store, $entryid, $start, $end) {
263
		$restriction =
264
			// OR
265
			//  - Either we want all appointments which fall within the given range
266
			//	- Or we want all recurring items which we manually check if an occurrence
267
			//	  exists which will fall inside the range
268
			[RES_OR,
269
				[
270
					// OR
271
					//	- Either we want all properties which fall inside the range (or overlap the range somewhere)
272
					//		(start < appointmentEnd && due > appointmentStart)
273
					//	- Or we want all zero-minute appointments which fall at the start of the restriction.
274
					// Note that this will effectively exclude any appointments which have an enddate on the restriction
275
					// start date, as those are not useful for us. Secondly, we exclude all zero-minute appointments
276
					// which fall on the end of the restriction as the restriction is <start, end].
277
					[RES_OR,
278
						[
279
							// AND
280
							//	- The AppointmentEnd must fall after the start of the range
281
							//	- The AppointmentStart must fall before the end of the range
282
							[RES_AND,
283
								[
284
									// start < appointmentEnd
285
									[RES_PROPERTY,
286
										[RELOP => RELOP_GT,
287
											ULPROPTAG => $this->properties["duedate"],
288
											VALUE => $start,
289
										],
290
									],
291
									// due > appointmentStart
292
									[RES_PROPERTY,
293
										[RELOP => RELOP_LT,
294
											ULPROPTAG => $this->properties["startdate"],
295
											VALUE => $end,
296
										],
297
									],
298
								],
299
							],
300
							// AND
301
							//	- The AppointmentStart equals the start of the range
302
							//	- The AppointmentEnd equals the start of the range
303
							// In other words the zero-minute appointments on the start of the range
304
							[RES_AND,
305
								[
306
									// appointmentStart == start
307
									[RES_PROPERTY,
308
										[RELOP => RELOP_EQ,
309
											ULPROPTAG => $this->properties["startdate"],
310
											VALUE => $start,
311
										],
312
									],
313
									// appointmentEnd == start
314
									[RES_PROPERTY,
315
										[RELOP => RELOP_EQ,
316
											ULPROPTAG => $this->properties["duedate"],
317
											VALUE => $start,
318
										],
319
									],
320
								],
321
							],
322
						],
323
					],
324
					// OR
325
					// (item[isRecurring] == true)
326
					// Add one day to the start and the end of the periods to avoid
327
					// timezone offset related differences between start/clipstart
328
					// and end/clipend.
329
					[RES_AND,
330
						[
331
							[RES_PROPERTY,
332
								[RELOP => RELOP_EQ,
333
									ULPROPTAG => $this->properties["recurring"],
334
									VALUE => true,
335
								],
336
							],
337
							[RES_AND,
338
								[
339
									[RES_PROPERTY,
340
										[RELOP => RELOP_GT,
341
											ULPROPTAG => $this->properties["enddate_recurring"],
342
											VALUE => (int) $start - 86400,
343
										],
344
									],
345
									[RES_PROPERTY,
346
										[RELOP => RELOP_LT,
347
											ULPROPTAG => $this->properties["startdate_recurring"],
348
											VALUE => (int) $end + 86400,
349
										],
350
									],
351
								],
352
							],
353
						],
354
					],
355
				],
356
			]; // global OR
357
358
		try {
359
			$folder = mapi_msgstore_openentry($store, $entryid);
360
			$table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS);
361
			$calendaritems = mapi_table_queryallrows($table, $this->properties, $restriction);
362
363
			return $this->processItems($calendaritems, $store, $entryid, $start, $end);
364
		}
365
		catch (Exception $e) {
366
			// MAPI_E_NOT_FOUND means missing permissions, try to get items via freebusy
367
			if ($e->getCode() == MAPI_E_NOT_FOUND) {
368
				return $this->getFreebusyItems($store, $entryid, $start, $end);
369
			}
370
		}
371
372
		return [];
373
	}
374
375
	/**
376
	 * Process calendar items to prepare them for being sent back to the client.
377
	 *
378
	 * @param array  $calendaritems array of appointments retrieved from the mapi tablwe
379
	 * @param object $store         message store
380
	 * @param mixed  $entryid
381
	 * @param mixed  $start         startdate of the interval
382
	 * @param mixed  $end           enddate of the interval
383
	 *
384
	 * @return array $items processed items
385
	 */
386
	public function processItems($calendaritems, $store, $entryid, $start, $end) {
387
		$items = [];
388
		$openedMessages = [];
389
		$proptags = $GLOBALS["properties"]->getRecurrenceProperties();
390
391
		foreach ($calendaritems as $calendaritem) {
392
			$item = null;
393
			// Fix for all-day events which have a different timezone than the user's browser
394
			if (isset($calendaritem[$this->properties["recurring"]]) && $calendaritem[$this->properties["recurring"]]) {
395
				$recurrence = new Recurrence($store, $calendaritem, $proptags);
0 ignored issues
show
Bug introduced by
The type Recurrence was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
396
				$recuritems = $recurrence->getItems($start, $end);
397
398
				foreach ($recuritems as $recuritem) {
399
					$item = Conversion::mapMAPI2XML($this->properties, $recuritem);
400
401
					// Single occurrences are never recurring
402
					$item['props']['recurring'] = false;
403
404
					if (isset($recuritem["exception"])) {
405
						$item["props"]["exception"] = true;
406
					}
407
408
					if (isset($recuritem["basedate"])) {
409
						$item["props"]["basedate"] = $recuritem["basedate"];
410
					}
411
412
					if (isset($recuritem["exception"])) {
413
						// Add categories if they are set on the exception
414
						// We will create a new Recurrence object with the opened message,
415
						// so we can open the attachments. The attachments for this exception
416
						// contains the categories property (if changed)
417
						$msgEntryid = bin2hex($calendaritem[$this->properties["entryid"]]);
418
						if (!isset($openedMessages[$msgEntryid])) {
419
							// Open the message and add it to the openedMessages property
420
							$message = mapi_msgstore_openentry($store, $calendaritem[$this->properties["entryid"]]);
421
							$openedMessages[$msgEntryid] = $message;
422
						}
423
						else {
424
							// This message was already opened
425
							$message = $openedMessages[$msgEntryid];
426
						}
427
						// Now create a Recurrence object with the mapi message (instead of the message props)
428
						// so we can open the attachments
429
						$recurrence = new Recurrence($store, $message, $proptags);
430
						$exceptionatt = $recurrence->getExceptionAttachment($recuritem["basedate"]);
431
						if ($exceptionatt) {
432
							// Existing exception (open existing item, which includes basedate)
433
							$exception = mapi_attach_openobj($exceptionatt, 0);
434
							$exceptionProps = $GLOBALS['operations']->getMessageProps($store, $exception, ['categories' => $this->properties["categories"]]);
435
436
							if (isset($exceptionProps['props']['categories'])) {
437
								$item["props"]["categories"] = $exceptionProps['props']['categories'];
438
							}
439
						}
440
					}
441
442
					$item = $this->processPrivateItem($item);
0 ignored issues
show
Bug introduced by
$item of type array<string,array> is incompatible with the type object expected by parameter $item of AppointmentListModule::processPrivateItem(). ( Ignorable by Annotation )

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

442
					$item = $this->processPrivateItem(/** @scrutinizer ignore-type */ $item);
Loading history...
443
444
					// only add it in response if its not removed by above function
445
					if (!empty($item)) {
446
						if (empty($item["props"]["commonstart"]) && isset($item["props"]["startdate"])) {
447
							$item["props"]["commonstart"] = $item["props"]["startdate"];
448
						}
449
						if (empty($item["props"]["commonend"]) && isset($item["props"]["duedate"])) {
450
							$item["props"]["commonend"] = $item["props"]["duedate"];
451
						}
452
						if (isset($item["props"]["alldayevent"]) && $item["props"]["alldayevent"]) {
453
							$this->processAllDayItem($store, $item, $openedMessages);
454
						}
455
						array_push($items, $item);
456
					}
457
				}
458
			}
459
			else {
460
				$item = Conversion::mapMAPI2XML($this->properties, $calendaritem);
461
462
				$item = $this->processPrivateItem($item);
463
464
				// only add it in response if its not removed by above function
465
				if (!empty($item)) {
466
					if (empty($item["props"]["commonstart"]) && isset($item["props"]["startdate"])) {
467
						$item["props"]["commonstart"] = $item["props"]["startdate"];
468
					}
469
					if (empty($item["props"]["commonend"]) && isset($item["props"]["duedate"])) {
470
						$item["props"]["commonend"] = $item["props"]["duedate"];
471
					}
472
					if (isset($item["props"]["alldayevent"]) && $item["props"]["alldayevent"]) {
473
						$this->processAllDayItem($store, $item, $openedMessages);
474
					}
475
					array_push($items, $item);
476
				}
477
			}
478
		}
479
480
		usort($items, ["AppointmentListModule", "compareCalendarItems"]);
481
482
		return $items;
483
	}
484
485
	/**
486
	 * Function will be used to process private items in a list response, modules can
487
	 * can decide what to do with the private items, remove the entire row or just
488
	 * hide the data. This function will only hide the data of the private appointments.
489
	 *
490
	 * @param object $item item properties
491
	 *
492
	 * @return object item properties after processing private items
493
	 */
494
	public function processPrivateItem($item) {
495
		if ($this->startdate && $this->enddate) {
496
			if ($this->checkPrivateItem($item)) {
497
				$item['props']['subject'] = _('Private Appointment');
498
				$item['props']['normalized_subject'] = _('Private Appointment');
499
				$item['props']['location'] = '';
500
				$item['props']['reminder'] = 0;
501
				$item['props']['access'] = 0;
502
				$item['props']['sent_representing_name'] = '';
503
				$item['props']['sender_name'] = '';
504
505
				return $item;
506
			}
507
508
			return $item;
509
		}
510
511
		// if we are in list view then we need to follow normal procedure of other listviews
512
		return parent::processPrivateItem($item);
513
	}
514
515
	/**
516
	 * Function will sort items for the month view
517
	 * small startdate on top.
518
	 *
519
	 * @param mixed $a
520
	 * @param mixed $b
521
	 */
522
	public static function compareCalendarItems($a, $b) {
523
		$start_a = $a["props"]["startdate"];
524
		$start_b = $b["props"]["startdate"];
525
526
		if ($start_a == $start_b) {
527
			return 0;
528
		}
529
530
		return ($start_a < $start_b) ? -1 : 1;
531
	}
532
533
	/**
534
	 * Processes an all-day item and calculates the correct starttime if necessary.
535
	 *
536
	 * @param object $store
537
	 * @param array  $calendaritem
538
	 * @param array  $openedMessages
539
	 */
540
	private function processAllDayItem($store, &$calendaritem, &$openedMessages) {
541
		// If the appointment doesn't have tzdefstart property, it was probably
542
		// created on a mobile device (mobile devices do not send a timezone for
543
		// all-day events).
544
		$tzdefstart = isset($calendaritem['props']['tzdefstart']) ?
545
			hex2bin($calendaritem['props']['tzdefstart']) :
546
			mapi_ianatz_to_tzdef("Etc/UTC");
547
548
		// queryrows only returns 510 chars max, so if tzdef is longer than that
549
		// it was probably silently truncated. In such case we need to open
550
		// the message and read the prop value as stream.
551
		if (strlen($tzdefstart) > 500 && isset($calendaritem['props']['tzdefstart'])) {
552
			if (!isset($openedMessages[$calendaritem['entryid']])) {
553
				// Open the message and add it to the openedMessages property
554
				$openedMessages[$calendaritem['entryid']] = mapi_msgstore_openentry($store, hex2bin($calendaritem['entryid']));
555
			}
556
			$tzdefstart = streamProperty($openedMessages[$calendaritem['entryid']], $this->properties['tzdefstart']);
557
		}
558
559
		// Compare the timezone definitions of the client and the appointment.
560
		// Further processing is only required if they don't match.
561
		if (!$GLOBALS['entryid']->compareEntryIds($this->tzdef, $tzdefstart)) {
562
			if ($this->tzdefObj === false) {
563
				$this->tzdefObj = $GLOBALS['entryid']->createTimezoneDefinitionObject($this->tzdef);
564
			}
565
			$this->tzEffRuleIdx = getEffectiveTzreg($this->tzdefObj['rules']);
566
567
			$appTzDefStart = $GLOBALS['entryid']->createTimezoneDefinitionObject($tzdefstart);
568
			// Find TZRULE_FLAG_EFFECTIVE_TZREG rule for the appointment's timezone
569
			$appTzEffRuleIdx = getEffectiveTzreg($appTzDefStart['rules']);
570
571
			if (!is_null($this->tzEffRuleIdx) && !is_null($appTzEffRuleIdx)) {
572
				// first apply the bias of the appointment timezone and the bias of the browser
573
				$localStart = $calendaritem['props']['startdate'] - $appTzDefStart['rules'][$appTzEffRuleIdx]['bias'] * 60 + $this->tzdefObj['rules'][$this->tzEffRuleIdx]['bias'] * 60;
574
				if (isDst($appTzDefStart['rules'][$appTzEffRuleIdx], $calendaritem['props']['startdate'])) {
575
					$localStart -= $appTzDefStart['rules'][$appTzEffRuleIdx]['dstbias'] * 60;
576
				}
577
				if (isDst($this->tzdefObj['rules'][$this->tzEffRuleIdx], $calendaritem['props']['startdate'])) {
578
					$localStart += $this->tzdefObj['rules'][$this->tzEffRuleIdx]['dstbias'] * 60;
579
				}
580
				$duration = $calendaritem['props']['duedate'] - $calendaritem['props']['startdate'];
581
				$calendaritem['props']['startdate'] = $calendaritem['props']['commonstart'] = $localStart;
582
				$calendaritem['props']['duedate'] = $calendaritem['props']['commonend'] = $localStart + $duration;
583
			}
584
		}
585
	}
586
587
	/**
588
	 * Gets items using freebusy entry point.
589
	 *
590
	 * @param object $store         message store
591
	 * @param mixed  $folderEntryid entryid of the folder
592
	 * @param mixed  $start         startdate of the interval
593
	 * @param mixed  $end           enddate of the interval
594
	 */
595
	public function getFreebusyItems($store, $folderEntryid, $start, $end) {
596
		$items = [];
597
		$storeProps = mapi_getprops($store, [PR_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]);
598
		$folderEntryid = bin2hex($folderEntryid);
599
		$storeEntryid = bin2hex($storeProps[PR_ENTRYID]);
600
		// if start was not set, get items one month back
601
		if ($start === false) {
602
			$start = time() - 2592000;
603
		}
604
		// if end was not set, get items 3 months ahead
605
		if ($end === false) {
606
			$end = time() + 7776000;
607
		}
608
		$fbdata = mapi_getuserfreebusy($GLOBALS['mapisession']->getSession(), $storeProps[PR_MAILBOX_OWNER_ENTRYID], $start, $end);
609
		if (!empty($fbdata['fbevents'])) {
610
			foreach ($fbdata['fbevents'] as $fbEvent) {
611
				// check if the event is in start - end range
612
				if ($fbEvent['end'] < $start || $fbEvent['start'] > $end) {
613
					continue;
614
				}
615
				$isPrivate = $fbEvent['private'] ?? false;
616
				$items[] = [
617
					// entryid is required, generate a fake one if a real is not available
618
					'entryid' => isset($fbEvent['id']) ? bin2hex($fbEvent['id']) : bin2hex(random_bytes(16)),
619
					'parent_entryid' => $folderEntryid,
620
					'store_entryid' => $storeEntryid,
621
					'props' => [
622
						'access' => 0,
623
						'subject' => $isPrivate ? _('Private Appointment') : ($fbEvent['subject'] ?? _('Busy')),
624
						'normalized_subject' => $isPrivate ? _('Private Appointment') : ($fbEvent['subject'] ?? _('Busy')),
625
						'location' => $isPrivate ? '' : ($fbEvent['location'] ?? ''),
626
						'startdate' => $fbEvent['start'],
627
						'duedate' => $fbEvent['end'],
628
						'commonstart' => $fbEvent['start'],
629
						'commonend' => $fbEvent['end'],
630
						'message_class' => 'IPM.Appointment',
631
						'object_type' => MAPI_MESSAGE,
632
						'icon_index' => 1024,
633
						'display_to' => '',
634
						'display_cc' => '',
635
						'display_bcc' => '',
636
						'importance' => 1,
637
						'sensitivity' => 0,
638
						'message_size' => 0,
639
						'hasattach' => false,
640
						'sent_representing_entryid' => '',
641
						'sent_representing_name' => '',
642
						'sent_representing_address_type' => '',
643
						'sent_representing_email_address' => '',
644
						'sent_representing_search_key' => '',
645
						'sender_email_address' => '',
646
						'sender_name' => '',
647
						'sender_address_type' => '',
648
						'sender_entryid' => '',
649
						'sender_search_key' => '',
650
						'recurring' => false,
651
						'recurring_data' => '',
652
						'recurring_pattern' => '',
653
						'meeting' => $fbEvent['meeting'] ?? false,
654
						'reminder' => 0,
655
						'reminder_minutes' => 0,
656
						'private' => $isPrivate,
657
					],
658
				];
659
			}
660
		}
661
662
		return $items;
663
	}
664
}
665