AppointmentListModule::processItems()   B
last analyzed

Complexity

Conditions 11
Paths 3

Size

Total Lines 70
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 11
eloc 36
c 4
b 1
f 0
nc 3
nop 5
dl 0
loc 70
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 string client or server IANA timezone
19
	 */
20
	protected $tziana;
21
22
	/**
23
	 * @var bool|string client timezone definition
24
	 */
25
	protected $tzdef;
26
27
	/**
28
	 * @var array|bool client timezone definition array
29
	 */
30
	protected $tzdefObj;
31
32
	/**
33
	 * @var mixed client timezone effective rule id
34
	 */
35
	protected $tzEffRuleIdx;
36
37
	/**
38
	 * Constructor.
39
	 *
40
	 * @param int   $id   unique id
41
	 * @param array $data list of all actions
42
	 */
43
	public function __construct($id, $data) {
44
		parent::__construct($id, $data);
45
46
		$this->properties = $GLOBALS["properties"]->getAppointmentListProperties();
47
48
		$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...
49
		$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...
50
		$this->tziana = 'Etc/UTC';
51
		$this->tzdef = false;
52
		$this->tzdefObj = false;
53
	}
54
55
	/**
56
	 * Creates the notifiers for this module,
57
	 * and register them to the Bus.
58
	 */
59
	public function createNotifiers() {
60
		$entryid = $this->getEntryID();
61
		$GLOBALS["bus"]->registerNotifier('appointmentlistnotifier', $entryid);
62
	}
63
64
	/**
65
	 * Executes all the actions in the $data variable.
66
	 */
67
	#[Override]
68
	public function execute() {
69
		foreach ($this->data as $actionType => $action) {
70
			if (!isset($actionType)) {
71
				continue;
72
			}
73
74
			try {
75
				$store = $this->getActionStore($action);
76
				$entryid = $this->getActionEntryID($action);
77
78
				match ($actionType) {
79
					"list" => $this->messageList($store, $entryid, $action, $actionType),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->messageList($stor..., $action, $actionType) targeting AppointmentListModule::messageList() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
80
					// @FIXME add functionality to handle private items
81
					"search" => $this->search($store, $entryid, $action, $actionType),
82
					"updatesearch" => $this->updatesearch($store, $entryid, $action),
83
					"stopsearch" => $this->stopSearch($store, $entryid, $action),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->stopSearch($store, $entryid, $action) targeting ListModule::stopSearch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
84
					default => $this->handleUnknownActionType($actionType),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleUnknownActionType($actionType) targeting Module::handleUnknownActionType() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
85
				};
86
			}
87
			catch (MAPIException $e) {
88
				if (isset($action['suppress_exception']) && $action['suppress_exception'] === true) {
89
					$e->setNotificationType('console');
90
				}
91
				$this->processException($e, $actionType);
92
			}
93
		}
94
	}
95
96
	/**
97
	 * Function which retrieves a list of calendar items in a calendar folder.
98
	 *
99
	 * @param object $store      MAPI Message Store Object
100
	 * @param string $entryid    entryid of the folder
101
	 * @param array  $action     the action data, sent by the client
102
	 * @param string $actionType the action type, sent by the client
103
	 */
104
	#[Override]
105
	public function messageList($store, $entryid, $action, $actionType) {
106
		if (!$store || !$entryid) {
0 ignored issues
show
introduced by
$store is of type object, thus it always evaluated to true.
Loading history...
107
			return;
108
		}
109
		// initialize start and due date with false value so it will not take values from previous request
110
		$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...
111
		$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...
112
113
		if (isset($action["restriction"])) {
114
			if (isset($action["restriction"]["startdate"])) {
115
				$this->startdate = $action["restriction"]["startdate"];
116
			}
117
118
			if (isset($action["restriction"]["duedate"])) {
119
				$this->enddate = $action["restriction"]["duedate"];
120
			}
121
		}
122
123
		if (!empty($action["timezone_iana"])) {
124
			$this->tziana = $action["timezone_iana"];
125
126
			try {
127
				$this->tzdef = mapi_ianatz_to_tzdef($action['timezone_iana']);
128
			}
129
			catch (Exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
130
			}
131
		}
132
133
		if ($this->startdate && $this->enddate) {
134
			$data = [];
135
136
			if (is_array($entryid) && !empty($entryid)) {
0 ignored issues
show
introduced by
The condition is_array($entryid) is always false.
Loading history...
137
				$data["item"] = [];
138
				for ($index = 0, $index2 = count($entryid); $index < $index2; ++$index) {
139
					$this->getDelegateFolderInfo($store[$index]);
140
141
					// Set the active store in properties class and get the props based on active store.
142
					// we need to do this because of multi server env where shared store belongs to the different server.
143
					// Here name space is different per server. e.g. There is user A and user B and both are belongs to
144
					// different server and user B is shared store of user A because of that user A has 'categories' => -2062020578
145
					// and user B 'categories' => -2062610402,
146
					$GLOBALS["properties"]->setActiveStore($store[$index]);
147
					$this->properties = $GLOBALS["properties"]->getAppointmentListProperties();
148
149
					$data["item"] = array_merge($data["item"], $this->getCalendarItems($store[$index], $entryid[$index], $this->startdate, $this->enddate));
150
				}
151
			}
152
			else {
153
				$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

153
				$this->getDelegateFolderInfo(/** @scrutinizer ignore-type */ $store);
Loading history...
154
				$data["item"] = $this->getCalendarItems($store, $entryid, $this->startdate, $this->enddate);
155
			}
156
157
			$this->addActionData("list", $data);
158
			$GLOBALS["bus"]->addData($this->getResponseData());
159
160
			return;
161
		}
162
		// for list view in calendar as startdate and enddate is passed as false
163
		// this will set sorting and paging for items in listview.
164
165
		$this->getDelegateFolderInfo($store);
166
167
		/* This is an override for parent::messageList(), which ignores an array of entryids / stores.
168
		*	 The following block considers this possibly and merges the data of several folders / stores.
169
		*/
170
171
		$this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content
172
173
		if (!$store || !$entryid) {
0 ignored issues
show
introduced by
$store is of type object, thus it always evaluated to true.
Loading history...
174
			return;
175
		}
176
177
		// Restriction
178
		$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

178
		$this->parseRestriction(/** @scrutinizer ignore-type */ $action);
Loading history...
179
180
		// Sort
181
		$this->parseSortOrder($action, null, true);
182
183
		$limit = false;
184
		if (isset($action['restriction']['limit'])) {
185
			$limit = $action['restriction']['limit'];
186
		}
187
		else {
188
			$limit = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
189
		}
190
191
		$isSearchFolder = isset($action['search_folder_entryid']);
192
		$entryid = $isSearchFolder ? hex2bin((string) $action['search_folder_entryid']) : $entryid;
193
194
		if (!is_array($entryid) && !is_array($store)) {
0 ignored issues
show
introduced by
The condition is_array($entryid) is always false.
Loading history...
introduced by
The condition is_array($store) is always false.
Loading history...
195
			$entryid = [$entryid];
196
			$store = [$store];
197
		}
198
199
		// Get the table and merge the arrays
200
		$data = [];
201
		$items = [];
202
		for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
203
			$newItems = $GLOBALS["operations"]->getTable($store[$i], $entryid[$i], $this->properties, $this->sort, $this->start, $limit, $this->restriction);
204
			$items = array_merge($items, $newItems['item']);
205
			if (isset($newItems['page']['totalrowcount']) && $newItems['page']['totalrowcount'] > $limit) {
206
				$data['page'] = $newItems['page'];
207
			}
208
		}
209
210
		// If the request come from search folder then no need to send folder information
211
		if (!$isSearchFolder) {
212
			$contentCount = 0;
213
			$contentUnread = 0;
214
215
			// For each folder
216
			for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
217
				// Open folder
218
				$folder = mapi_msgstore_openentry($store[$i], $entryid[$i]);
219
				// Obtain some statistics from the folder contents
220
				$content = mapi_getprops($folder, [PR_CONTENT_COUNT, PR_CONTENT_UNREAD]);
221
				if (isset($content[PR_CONTENT_COUNT])) {
222
					$contentCount += $content[PR_CONTENT_COUNT];
223
				}
224
225
				if (isset($content[PR_CONTENT_UNREAD])) {
226
					$contentUnread += $content[PR_CONTENT_UNREAD];
227
				}
228
			}
229
230
			$data["folder"] = [];
231
			$data["folder"]["content_count"] = $contentCount;
232
			$data["folder"]["content_unread"] = $contentUnread;
233
		}
234
235
		$items = $this->filterPrivateItems(['item' => $items]);
236
		// unset will remove the value but will not regenerate array keys, so we need to
237
		// do it here
238
		$data["item"] = array_values($items["item"]);
239
240
		for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
241
			// Allowing to hook in just before the data sent away to be sent to the client
242
			$GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', [
243
				'moduleObject' => &$this,
244
				'store' => $store[$i],
245
				'entryid' => $entryid[$i],
246
				'action' => $action,
247
				'data' => &$data,
248
			]);
249
		}
250
251
		$this->addActionData($actionType, $data);
252
		$GLOBALS["bus"]->addData($this->getResponseData());
253
	}
254
255
	/**
256
	 * Function to return all Calendar items in a given timeframe. This
257
	 * function also takes recurring items into account.
258
	 *
259
	 * @param object $store   message store
260
	 * @param mixed  $entryid entryid of the folder
261
	 * @param mixed  $start   startdate of the interval
262
	 * @param mixed  $end     enddate of the interval
263
	 */
264
	public function getCalendarItems($store, $entryid, $start, $end) {
265
		$restriction =
266
			// OR
267
			//  - Either we want all appointments which fall within the given range
268
			//	- Or we want all recurring items which we manually check if an occurrence
269
			//	  exists which will fall inside the range
270
			[RES_OR,
271
				[
272
					// OR
273
					//	- Either we want all properties which fall inside the range (or overlap the range somewhere)
274
					//		(start < appointmentEnd && due > appointmentStart)
275
					//	- Or we want all zero-minute appointments which fall at the start of the restriction.
276
					// Note that this will effectively exclude any appointments which have an enddate on the restriction
277
					// start date, as those are not useful for us. Secondly, we exclude all zero-minute appointments
278
					// which fall on the end of the restriction as the restriction is <start, end].
279
					[RES_OR,
280
						[
281
							// AND
282
							//	- The AppointmentEnd must fall after the start of the range
283
							//	- The AppointmentStart must fall before the end of the range
284
							[RES_AND,
285
								[
286
									// start < appointmentEnd
287
									[RES_PROPERTY,
288
										[RELOP => RELOP_GT,
289
											ULPROPTAG => $this->properties["duedate"],
290
											VALUE => $start,
291
										],
292
									],
293
									// due > appointmentStart
294
									[RES_PROPERTY,
295
										[RELOP => RELOP_LT,
296
											ULPROPTAG => $this->properties["startdate"],
297
											VALUE => $end,
298
										],
299
									],
300
								],
301
							],
302
							// AND
303
							//	- The AppointmentStart equals the start of the range
304
							//	- The AppointmentEnd equals the start of the range
305
							// In other words the zero-minute appointments on the start of the range
306
							[RES_AND,
307
								[
308
									// appointmentStart == start
309
									[RES_PROPERTY,
310
										[RELOP => RELOP_EQ,
311
											ULPROPTAG => $this->properties["startdate"],
312
											VALUE => $start,
313
										],
314
									],
315
									// appointmentEnd == start
316
									[RES_PROPERTY,
317
										[RELOP => RELOP_EQ,
318
											ULPROPTAG => $this->properties["duedate"],
319
											VALUE => $start,
320
										],
321
									],
322
								],
323
							],
324
						],
325
					],
326
					// OR
327
					// (item[isRecurring] == true)
328
					// Add one day to the start and the end of the periods to avoid
329
					// timezone offset related differences between start/clipstart
330
					// and end/clipend.
331
					[RES_AND,
332
						[
333
							[RES_PROPERTY,
334
								[RELOP => RELOP_EQ,
335
									ULPROPTAG => $this->properties["recurring"],
336
									VALUE => true,
337
								],
338
							],
339
							[RES_AND,
340
								[
341
									[RES_PROPERTY,
342
										[RELOP => RELOP_GT,
343
											ULPROPTAG => $this->properties["enddate_recurring"],
344
											VALUE => (int) $start - 86400,
345
										],
346
									],
347
									[RES_PROPERTY,
348
										[RELOP => RELOP_LT,
349
											ULPROPTAG => $this->properties["startdate_recurring"],
350
											VALUE => (int) $end + 86400,
351
										],
352
									],
353
								],
354
							],
355
						],
356
					],
357
				],
358
			]; // global OR
359
360
		try {
361
			$folder = mapi_msgstore_openentry($store, $entryid);
362
			$table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS);
363
			$calendaritems = mapi_table_queryallrows($table, $this->properties, $restriction);
364
365
			return $this->processItems($calendaritems, $store, $entryid, $start, $end);
366
		}
367
		catch (Exception $e) {
368
			// MAPI_E_NOT_FOUND means missing permissions, try to get items via freebusy
369
			if ($e->getCode() == MAPI_E_NOT_FOUND) {
370
				return $this->getFreebusyItems($store, $entryid, $start, $end);
371
			}
372
		}
373
374
		return [];
375
	}
376
377
	/**
378
	 * Process calendar items to prepare them for being sent back to the client.
379
	 *
380
	 * @param array  $calendaritems array of appointments retrieved from the mapi tablwe
381
	 * @param object $store         message store
382
	 * @param mixed  $entryid
383
	 * @param mixed  $start         startdate of the interval
384
	 * @param mixed  $end           enddate of the interval
385
	 *
386
	 * @return array $items processed items
387
	 */
388
	public function processItems($calendaritems, $store, $entryid, $start, $end) {
389
		$items = [];
390
		$openedMessages = [];
391
		$proptags = $GLOBALS["properties"]->getRecurrenceProperties();
392
393
		foreach ($calendaritems as $calendaritem) {
394
			$item = null;
395
			if (!isset($calendaritem[$this->properties["recurring"]]) ||
396
			    !$calendaritem[$this->properties["recurring"]]) {
397
				$item = Conversion::mapMAPI2XML($this->properties, $calendaritem);
398
				$this->addItems($store, $item, $openedMessages, $start, $end, $items);
399
400
				continue;
401
			}
402
403
			// Fix for all-day events which have a different timezone than the user's browser
404
			$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...
405
			$recuritems = $recurrence->getItems($start, $end);
406
407
			foreach ($recuritems as $recuritem) {
408
				$item = Conversion::mapMAPI2XML($this->properties, $recuritem);
409
410
				// Single occurrences are never recurring
411
				$item['props']['recurring'] = false;
412
413
				if (isset($recuritem["exception"])) {
414
					$item["props"]["exception"] = true;
415
				}
416
417
				if (isset($recuritem["basedate"])) {
418
					$item["props"]["basedate"] = $recuritem["basedate"];
419
				}
420
421
				if (isset($recuritem["exception"])) {
422
					// Add categories if they are set on the exception
423
					// We will create a new Recurrence object with the opened message,
424
					// so we can open the attachments. The attachments for this exception
425
					// contains the categories property (if changed)
426
					$msgEntryid = bin2hex((string) $calendaritem[$this->properties["entryid"]]);
427
					if (!isset($openedMessages[$msgEntryid])) {
428
						// Open the message and add it to the openedMessages property
429
						$message = mapi_msgstore_openentry($store, $calendaritem[$this->properties["entryid"]]);
430
						$openedMessages[$msgEntryid] = $message;
431
					}
432
					else {
433
						// This message was already opened
434
						$message = $openedMessages[$msgEntryid];
435
					}
436
					// Now create a Recurrence object with the mapi message (instead of the message props)
437
					// so we can open the attachments
438
					$recurrence = new Recurrence($store, $message, $proptags);
439
					$exceptionatt = $recurrence->getExceptionAttachment($recuritem["basedate"]);
440
					if ($exceptionatt) {
441
						// Existing exception (open existing item, which includes basedate)
442
						$exception = mapi_attach_openobj($exceptionatt, 0);
443
						$exceptionProps = $GLOBALS['operations']->getMessageProps($store, $exception, ['categories' => $this->properties["categories"]]);
444
445
						if (isset($exceptionProps['props']['categories'])) {
446
							$item["props"]["categories"] = $exceptionProps['props']['categories'];
447
						}
448
					}
449
				}
450
451
				$this->addItems($store, $item, $openedMessages, $start, $end, $items);
452
			}
453
		}
454
455
		usort($items, ["AppointmentListModule", "compareCalendarItems"]);
456
457
		return $items;
458
	}
459
460
	/**
461
	 * Function will be used to process private items in a list response, modules can
462
	 * can decide what to do with the private items, remove the entire row or just
463
	 * hide the data. This function will only hide the data of the private appointments.
464
	 *
465
	 * @param object $item item properties
466
	 *
467
	 * @return object item properties after processing private items
468
	 */
469
	#[Override]
470
	public function processPrivateItem($item) {
471
		if (!$this->startdate || !$this->enddate) {
472
			// if we are in list view then we need to follow normal procedure of other listviews
473
			return parent::processPrivateItem($item);
474
		}
475
476
		if (!$this->checkPrivateItem($item)) {
477
			return $item;
478
		}
479
480
		$item['props']['subject'] = _('Private Appointment');
481
		$item['props']['normalized_subject'] = _('Private Appointment');
482
		$item['props']['location'] = '';
483
		$item['props']['reminder'] = 0;
484
		$item['props']['access'] = 0;
485
		$item['props']['sent_representing_name'] = '';
486
		$item['props']['sender_name'] = '';
487
488
		return $item;
489
	}
490
491
	/**
492
	 * Function will sort items for the month view
493
	 * small startdate on top.
494
	 *
495
	 * @param mixed $a
496
	 * @param mixed $b
497
	 */
498
	public static function compareCalendarItems($a, $b) {
499
		$start_a = $a["props"]["startdate"];
500
		$start_b = $b["props"]["startdate"];
501
502
		return $start_a <=> $start_b;
503
	}
504
505
	/**
506
	 * Processes an all-day item and calculates the correct starttime if necessary.
507
	 *
508
	 * @param object $store
509
	 * @param array  $calendaritem
510
	 * @param array  $openedMessages
511
	 */
512
	private function processAllDayItem($store, &$calendaritem, &$openedMessages) {
513
		// If the appointment doesn't have tzdefstart property, it was probably
514
		// created on a mobile device (mobile devices do not send a timezone for
515
		// all-day events) or was imported from a system which doesn't set it.
516
		$isTzdefstartSet = isset($calendaritem['props']['tzdefstart']);
517
		$tzdefstart = $isTzdefstartSet ?
518
			hex2bin((string) $calendaritem['props']['tzdefstart']) :
519
			mapi_ianatz_to_tzdef("Etc/UTC");
520
521
		// queryrows only returns 510 chars max, so if tzdef is longer than that
522
		// it was probably silently truncated. In such case we need to open
523
		// the message and read the prop value as stream.
524
		if (strlen($tzdefstart) > 500 && $isTzdefstartSet) {
525
			if (!isset($openedMessages[$calendaritem['entryid']])) {
526
				// Open the message and add it to the openedMessages property
527
				$openedMessages[$calendaritem['entryid']] = mapi_msgstore_openentry($store, hex2bin((string) $calendaritem['entryid']));
528
			}
529
			$tzdefstart = streamProperty($openedMessages[$calendaritem['entryid']], $this->properties['tzdefstart']);
530
		}
531
532
		$duration = $calendaritem['props']['duedate'] - $calendaritem['props']['startdate'];
533
		// Set the start and endtimes to the midnight of the client's timezone
534
		// if that's not the case already.
535
		if (!$isTzdefstartSet) {
536
			$calItemStart = new DateTime();
537
			$calItemStart->setTimestamp($calendaritem['props']['startdate']);
538
			$clientDate = DateTime::createFromInterface($calItemStart);
539
			$clientDate->setTimezone(new DateTimeZone($this->tziana));
540
			// It's only necessary to calculate new start and end times
541
			// if the appointment does not start at midnight
542
			if ((int) $clientDate->format("His") != 0) {
543
				$clientMidnight = DateTimeImmutable::createFromFormat(
544
					"Y-m-d H:i:s",
545
					$clientDate->format("Y-m-d ") . "00:00:00",
546
					$clientDate->getTimezone()
547
				);
548
				$interval = $clientDate->getTimestamp() - $clientMidnight->getTimestamp();
549
				// The code here is based on assumption that if the interval
550
				// is greater than 12 hours then the appointment takes place
551
				// on the day before or after. This should be fine for all the
552
				// timezones which do not exceed 12 hour difference to UTC.
553
				$localStart = $interval > 0 ?
554
					$calendaritem['props']['startdate'] - ($interval < 43200 ? $interval : $interval - 86400) :
555
					$calendaritem['props']['startdate'] + ($interval > -43200 ? $interval : $interval - 86400);
556
				$calendaritem['props']['startdate'] = $calendaritem['props']['commonstart'] = $localStart;
557
				$calendaritem['props']['duedate'] = $calendaritem['props']['commonend'] = $localStart + $duration;
558
			}
559
		}
560
		// Compare the timezone definitions of the client and the appointment.
561
		// Further processing is only required if they don't match.
562
		elseif ($isTzdefstartSet && !$GLOBALS['entryid']->compareEntryIds($this->tzdef, $tzdefstart)) {
563
			if ($this->tzdefObj === false) {
564
				$this->tzdefObj = $GLOBALS['entryid']->createTimezoneDefinitionObject($this->tzdef);
565
			}
566
			$this->tzEffRuleIdx = getEffectiveTzreg($this->tzdefObj['rules']);
567
568
			$appTzDefStart = $GLOBALS['entryid']->createTimezoneDefinitionObject($tzdefstart);
569
			// Find TZRULE_FLAG_EFFECTIVE_TZREG rule for the appointment's timezone
570
			$appTzEffRuleIdx = getEffectiveTzreg($appTzDefStart['rules']);
571
572
			if (!is_null($this->tzEffRuleIdx) && !is_null($appTzEffRuleIdx)) {
573
				// first apply the bias of the appointment timezone and the bias of the browser
574
				$localStart = $calendaritem['props']['startdate'] - $appTzDefStart['rules'][$appTzEffRuleIdx]['bias'] * 60 + $this->tzdefObj['rules'][$this->tzEffRuleIdx]['bias'] * 60;
575
				if (isDst($appTzDefStart['rules'][$appTzEffRuleIdx], $calendaritem['props']['startdate'])) {
576
					$localStart -= $appTzDefStart['rules'][$appTzEffRuleIdx]['dstbias'] * 60;
577
				}
578
				if (isDst($this->tzdefObj['rules'][$this->tzEffRuleIdx], $calendaritem['props']['startdate'])) {
579
					$localStart += $this->tzdefObj['rules'][$this->tzEffRuleIdx]['dstbias'] * 60;
580
				}
581
				$calendaritem['props']['startdate'] = $calendaritem['props']['commonstart'] = $localStart;
582
				$calendaritem['props']['duedate'] = $calendaritem['props']['commonend'] = $localStart + $duration;
583
			}
584
		}
585
	}
586
587
	/**
588
	 * Adds items to return items list.
589
	 *
590
	 * @param object $store
591
	 * @param array  $openedMessages
592
	 * @param mixed  $start          startdate of the interval
593
	 * @param mixed  $end            enddate of the interval
594
	 * @param array  $items
595
	 * @param mixed  $item
596
	 */
597
	private function addItems($store, &$item, &$openedMessages, $start, $end, &$items) {
598
		$item = $this->processPrivateItem($item);
599
600
		// only add it in response if its not removed by above function
601
		if (empty($item)) {
602
			return;
603
		}
604
		if (empty($item["props"]["commonstart"]) && isset($item["props"]["startdate"])) {
605
			$item["props"]["commonstart"] = $item["props"]["startdate"];
606
		}
607
		if (empty($item["props"]["commonend"]) && isset($item["props"]["duedate"])) {
608
			$item["props"]["commonend"] = $item["props"]["duedate"];
609
		}
610
		if (isset($item["props"]["alldayevent"]) && $item["props"]["alldayevent"]) {
611
			$this->processAllDayItem($store, $item, $openedMessages);
612
		}
613
		// After processing the all-day events, their start and due dates
614
		// may have changed, so it's necessary to check again if they are
615
		// still in the requested interval.
616
		if (($start <= $item["props"]["startdate"] && $end > $item['props']['startdate']) ||
617
			($start < $item["props"]["duedate"] && $end >= $item['props']['duedate']) ||
618
			($start > $item["props"]["startdate"] && $end < $item['props']['duedate'])) {
619
			array_push($items, $item);
620
		}
621
	}
622
623
	/**
624
	 * Gets items using freebusy entry point.
625
	 *
626
	 * @param object $store         message store
627
	 * @param mixed  $folderEntryid entryid of the folder
628
	 * @param mixed  $start         startdate of the interval
629
	 * @param mixed  $end           enddate of the interval
630
	 */
631
	public function getFreebusyItems($store, $folderEntryid, $start, $end) {
632
		$items = [];
633
		$storeProps = mapi_getprops($store, [PR_ENTRYID, PR_MAILBOX_OWNER_ENTRYID]);
634
		$folderEntryid = bin2hex((string) $folderEntryid);
635
		$storeEntryid = bin2hex((string) $storeProps[PR_ENTRYID]);
636
		// if start was not set, get items one month back
637
		if ($start === false) {
638
			$start = time() - 2592000;
639
		}
640
		// if end was not set, get items 3 months ahead
641
		if ($end === false) {
642
			$end = time() + 7776000;
643
		}
644
		$fbdata = mapi_getuserfreebusy($GLOBALS['mapisession']->getSession(), $storeProps[PR_MAILBOX_OWNER_ENTRYID], $start, $end);
645
		if (empty($fbdata['fbevents'])) {
646
			return $items;
647
		}
648
649
		foreach ($fbdata['fbevents'] as $fbEvent) {
650
			// check if the event is in start - end range
651
			if ($fbEvent['end'] < $start || $fbEvent['start'] > $end) {
652
				continue;
653
			}
654
			$isPrivate = $fbEvent['private'] ?? false;
655
			$items[] = [
656
				// entryid is required, generate a fake one if a real is not available
657
				'entryid' => isset($fbEvent['id']) ? bin2hex($fbEvent['id']) : bin2hex(random_bytes(16)),
658
				'parent_entryid' => $folderEntryid,
659
				'store_entryid' => $storeEntryid,
660
				'props' => [
661
					'access' => 0,
662
					'subject' => $isPrivate ? _('Private Appointment') : ($fbEvent['subject'] ?? _('Busy')),
663
					'normalized_subject' => $isPrivate ? _('Private Appointment') : ($fbEvent['subject'] ?? _('Busy')),
664
					'location' => $isPrivate ? '' : ($fbEvent['location'] ?? ''),
665
					'startdate' => $fbEvent['start'],
666
					'duedate' => $fbEvent['end'],
667
					'commonstart' => $fbEvent['start'],
668
					'commonend' => $fbEvent['end'],
669
					'message_class' => 'IPM.Appointment',
670
					'object_type' => MAPI_MESSAGE,
671
					'icon_index' => 1024,
672
					'display_to' => '',
673
					'display_cc' => '',
674
					'display_bcc' => '',
675
					'importance' => 1,
676
					'sensitivity' => 0,
677
					'message_size' => 0,
678
					'hasattach' => false,
679
					'sent_representing_entryid' => '',
680
					'sent_representing_name' => '',
681
					'sent_representing_address_type' => '',
682
					'sent_representing_email_address' => '',
683
					'sent_representing_search_key' => '',
684
					'sender_email_address' => '',
685
					'sender_name' => '',
686
					'sender_address_type' => '',
687
					'sender_entryid' => '',
688
					'sender_search_key' => '',
689
					'recurring' => false,
690
					'recurring_data' => '',
691
					'recurring_pattern' => '',
692
					'meeting' => $fbEvent['meeting'] ?? false,
693
					'reminder' => 0,
694
					'reminder_minutes' => 0,
695
					'private' => $isPrivate,
696
				],
697
			];
698
		}
699
700
		return $items;
701
	}
702
}
703