Test Failed
Push — master ( 19e7e7...e85ccb )
by
unknown
07:48
created

AppointmentListModule::processItems()   B

Complexity

Conditions 11
Paths 31

Size

Total Lines 68
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 11
eloc 35
c 2
b 1
f 0
nc 31
nop 5
dl 0
loc 68
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
	public function execute() {
68
		foreach ($this->data as $actionType => $action) {
69
			if (isset($actionType)) {
70
				try {
71
					$store = $this->getActionStore($action);
72
					$entryid = $this->getActionEntryID($action);
73
74
					switch ($actionType) {
75
						case "list":
76
							$this->messageList($store, $entryid, $action, $actionType);
77
							break;
78
79
						case "search":
80
							// @FIXME add functionality to handle private items
81
							$this->search($store, $entryid, $action, $actionType);
82
							break;
83
84
						case "updatesearch":
85
							$this->updatesearch($store, $entryid, $action);
86
							break;
87
88
						case "stopsearch":
89
							$this->stopSearch($store, $entryid, $action);
90
							break;
91
92
						default:
93
							$this->handleUnknownActionType($actionType);
94
					}
95
				}
96
				catch (MAPIException $e) {
97
					if (isset($action['suppress_exception']) && $action['suppress_exception'] === true) {
98
						$e->setNotificationType('console');
99
					}
100
					$this->processException($e, $actionType);
101
				}
102
			}
103
		}
104
	}
105
106
	/**
107
	 * Function which retrieves a list of calendar items in a calendar folder.
108
	 *
109
	 * @param object $store      MAPI Message Store Object
110
	 * @param string $entryid    entryid of the folder
111
	 * @param array  $action     the action data, sent by the client
112
	 * @param string $actionType the action type, sent by the client
113
	 */
114
	public function messageList($store, $entryid, $action, $actionType) {
115
		if ($store && $entryid) {
116
			// initialize start and due date with false value so it will not take values from previous request
117
			$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...
118
			$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...
119
120
			if (isset($action["restriction"])) {
121
				if (isset($action["restriction"]["startdate"])) {
122
					$this->startdate = $action["restriction"]["startdate"];
123
				}
124
125
				if (isset($action["restriction"]["duedate"])) {
126
					$this->enddate = $action["restriction"]["duedate"];
127
				}
128
			}
129
130
			if (!empty($action["timezone_iana"])) {
131
				$this->tziana = $action["timezone_iana"];
132
				try {
133
					$this->tzdef = mapi_ianatz_to_tzdef($action['timezone_iana']);
134
				}
135
				catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
136
				}
137
			}
138
139
			if ($this->startdate && $this->enddate) {
140
				$data = [];
141
142
				if (is_array($entryid) && !empty($entryid)) {
0 ignored issues
show
introduced by
The condition is_array($entryid) is always false.
Loading history...
143
					$data["item"] = [];
144
					for ($index = 0, $index2 = count($entryid); $index < $index2; ++$index) {
145
						$this->getDelegateFolderInfo($store[$index]);
146
147
						// Set the active store in properties class and get the props based on active store.
148
						// we need to do this because of multi server env where shared store belongs to the different server.
149
						// Here name space is different per server. e.g. There is user A and user B and both are belongs to
150
						// different server and user B is shared store of user A because of that user A has 'categories' => -2062020578
151
						// and user B 'categories' => -2062610402,
152
						$GLOBALS["properties"]->setActiveStore($store[$index]);
153
						$this->properties = $GLOBALS["properties"]->getAppointmentListProperties();
154
155
						$data["item"] = array_merge($data["item"], $this->getCalendarItems($store[$index], $entryid[$index], $this->startdate, $this->enddate));
156
					}
157
				}
158
				else {
159
					$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

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

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