Passed
Push — master ( d44c66...31a96e )
by
unknown
16:00 queued 12s
created

AppointmentListModule::createNotifiers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

177
						$this->parseRestriction(/** @scrutinizer ignore-type */ $action);
Loading history...
178
179
						// Sort
180
						$this->parseSortOrder($action, null, true);
181
182
						$limit = false;
183
						if (isset($action['restriction']['limit'])) {
184
							$limit = $action['restriction']['limit'];
185
						}
186
						else {
187
							$limit = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
188
						}
189
190
						$isSearchFolder = isset($action['search_folder_entryid']);
191
						$entryid = $isSearchFolder ? hex2bin($action['search_folder_entryid']) : $entryid;
192
193
						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...
194
							$entryid = [$entryid];
195
							$store = [$store];
196
						}
197
198
						// Get the table and merge the arrays
199
						$data = [];
200
						$items = [];
201
						for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
202
							$newItems = $GLOBALS["operations"]->getTable($store[$i], $entryid[$i], $this->properties, $this->sort, $this->start, $limit, $this->restriction);
203
							$items = array_merge($items, $newItems['item']);
204
							if (isset($newItems['page']['totalrowcount']) && $newItems['page']['totalrowcount'] > $limit) {
205
								$data['page'] = $newItems['page'];
206
							}
207
						}
208
209
						// If the request come from search folder then no need to send folder information
210
						if (!$isSearchFolder) {
211
							$contentCount = 0;
212
							$contentUnread = 0;
213
214
							// For each folder
215
							for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
216
								// Open folder
217
								$folder = mapi_msgstore_openentry($store[$i], $entryid[$i]);
218
								// Obtain some statistics from the folder contents
219
								$content = mapi_getprops($folder, [PR_CONTENT_COUNT, PR_CONTENT_UNREAD]);
220
								if (isset($content[PR_CONTENT_COUNT])) {
221
									$contentCount += $content[PR_CONTENT_COUNT];
222
								}
223
224
								if (isset($content[PR_CONTENT_UNREAD])) {
225
									$contentUnread += $content[PR_CONTENT_UNREAD];
226
								}
227
							}
228
229
							$data["folder"] = [];
230
							$data["folder"]["content_count"] = $contentCount;
231
							$data["folder"]["content_unread"] = $contentUnread;
232
						}
233
234
						$items = $this->filterPrivateItems($items);
235
						// unset will remove the value but will not regenerate array keys, so we need to
236
						// do it here
237
						$data["item"] = $items;
238
239
						for ($i = 0, $c = count($entryid); $i < $c; ++$i) {
240
							// Allowing to hook in just before the data sent away to be sent to the client
241
							$GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', [
242
								'moduleObject' => &$this,
243
								'store' => $store[$i],
244
								'entryid' => $entryid[$i],
245
								'action' => $action,
246
								'data' => &$data,
247
							]);
248
						}
249
250
						$this->addActionData($actionType, $data);
251
						$GLOBALS["bus"]->addData($this->getResponseData());
252
					}
253
				}
254
			}
255
		}
256
257
		/**
258
		 * Function to return all Calendar items in a given timeframe. This
259
		 * function also takes recurring items into account.
260
		 *
261
		 * @param object $store    message store
262
		 * @param object $calendar folder
263
		 * @param date   $start    startdate of the interval
264
		 * @param date   $end      enddate of the interval
265
		 * @param mixed  $entryid
266
		 */
267
		public function getCalendarItems($store, $entryid, $start, $end) {
268
			// Create mapping for restriction used properties which should not be send to the client.
269
			$properties = [
270
				"clipstart" => "PT_SYSTIME:PSETID_Appointment:" . PidLidClipStart,
271
				"clipend" => "PT_SYSTIME:PSETID_Appointment:" . PidLidClipEnd,
272
			];
273
			$properties = getPropIdsFromStrings($store, $properties);
274
275
			$restriction =
276
				// OR
277
				//  - Either we want all appointments which fall within the given range
278
				//	- Or we want all recurring items which we manually check if an occurrence
279
				//	  exists which will fall inside the range
280
				[RES_OR,
281
					[
282
						// OR
283
						//	- Either we want all properties which fall inside the range (or overlap the range somewhere)
284
						//		(start < appointmentEnd && due > appointmentStart)
285
						//	- Or we want all zero-minute appointments which fall at the start of the restriction.
286
						// Note that this will effectively exclude any appointments which have an enddate on the restriction
287
						// start date, as those are not useful for us. Secondly, we exclude all zero-minute appointments
288
						// which fall on the end of the restriction as the restriction is <start, end].
289
						[RES_OR,
290
							[
291
								// AND
292
								//	- The AppointmentEnd must fall after the start of the range
293
								//	- The AppointmentStart must fall before the end of the range
294
								[RES_AND,
295
									[
296
										// start < appointmentEnd
297
										[RES_PROPERTY,
298
											[RELOP => RELOP_GT,
299
												ULPROPTAG => $this->properties["duedate"],
300
												VALUE => $start,
301
											],
302
										],
303
										// due > appointmentStart
304
										[RES_PROPERTY,
305
											[RELOP => RELOP_LT,
306
												ULPROPTAG => $this->properties["startdate"],
307
												VALUE => $end,
308
											],
309
										],
310
									],
311
								],
312
								// AND
313
								//	- The AppointmentStart equals the start of the range
314
								//	- The AppointmentEnd equals the start of the range
315
								// In other words the zero-minute appointments on the start of the range
316
								[RES_AND,
317
									[
318
										// appointmentStart == start
319
										[RES_PROPERTY,
320
											[RELOP => RELOP_EQ,
321
												ULPROPTAG => $this->properties["startdate"],
322
												VALUE => $start,
323
											],
324
										],
325
										// appointmentEnd == start
326
										[RES_PROPERTY,
327
											[RELOP => RELOP_EQ,
328
												ULPROPTAG => $this->properties["duedate"],
329
												VALUE => $start,
330
											],
331
										],
332
									],
333
								],
334
							],
335
						],
336
						// OR
337
						// (item[isRecurring] == true)
338
						[RES_AND,
339
							[
340
								[RES_PROPERTY,
341
									[RELOP => RELOP_EQ,
342
										ULPROPTAG => $this->properties["recurring"],
343
										VALUE => true,
344
									],
345
								],
346
								[RES_AND,
347
									[
348
										[RES_PROPERTY,
349
											[RELOP => RELOP_GT,
350
												ULPROPTAG => $properties["clipend"],
351
												VALUE => $start,
352
											],
353
										],
354
										[RES_PROPERTY,
355
											[RELOP => RELOP_LT,
356
												ULPROPTAG => $properties["clipstart"],
357
												VALUE => $end,
358
											],
359
										],
360
									],
361
								],
362
							],
363
						],
364
					],
365
				]; // global OR
366
367
			$folder = mapi_msgstore_openentry($store, $entryid);
368
			$table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS);
369
			$calendaritems = mapi_table_queryallrows($table, $this->properties, $restriction);
370
371
			return $this->processItems($calendaritems, $store, $entryid, $start, $end);
372
		}
373
374
		/**
375
		 * Process calendar items to prepare them for being sent back to the client.
376
		 *
377
		 * @param array  $calendaritems array of appointments retrieved from the mapi tablwe
378
		 * @param object $store         message store
379
		 * @param object $calendar      folder
380
		 * @param date   $start         startdate of the interval
381
		 * @param date   $end           enddate of the interval
382
		 * @param mixed  $entryid
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);
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment {Object} at position 0 could not be parsed: Unknown type name '{' at position 0 in {Object}.
Loading history...
491
		 *
492
		 * @return {Object} item properties after processing private items
0 ignored issues
show
Documentation Bug introduced by
The doc comment {Object} at position 0 could not be parsed: Unknown type name '{' at position 0 in {Object}.
Loading history...
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']['location'] = '';
499
					$item['props']['reminder'] = 0;
500
					$item['props']['access'] = 0;
501
					$item['props']['sent_representing_name'] = '';
502
					$item['props']['sender_name'] = '';
503
504
					return $item;
505
				}
506
507
				return $item;
508
			}
509
			// if we are in list view then we need to follow normal procedure of other listviews
510
			return parent::processPrivateItem($item);
511
		}
512
513
		/**
514
		 * Function will sort items for the month view
515
		 * small startdate on top.
516
		 *
517
		 * @param mixed $a
518
		 * @param mixed $b
519
		 */
520
		public static function compareCalendarItems($a, $b) {
521
			$start_a = $a["props"]["startdate"];
522
			$start_b = $b["props"]["startdate"];
523
524
			if ($start_a == $start_b) {
525
				return 0;
526
			}
527
528
			return ($start_a < $start_b) ? -1 : 1;
529
		}
530
531
		/**
532
		 * Processes an all-day item and calculates the correct starttime if necessary.
533
		 *
534
		 * @param object $store
535
		 * @param array  $calendaritem
536
		 * @param array  $openedMessages
537
		 */
538
		private function processAllDayItem($store, &$calendaritem, &$openedMessages) {
539
			if (!isset($calendaritem['props']['tzdefstart'])) {
540
				return;
541
			}
542
			$tzdefstart = hex2bin($calendaritem['props']['tzdefstart']);
543
544
			// queryrows only returns 510 chars max, so if tzdef is longer than that
545
			// it was probably silently truncated. In such case we need to open
546
			// the message and read the prop value as stream.
547
			if (strlen($calendaritem['props']['tzdefstart']) > 500) {
548
				if (!isset($openedMessages[$calendaritem['entryid']])) {
549
					// Open the message and add it to the openedMessages property
550
					$openedMessages[$calendaritem['entryid']] = mapi_msgstore_openentry($store, hex2bin($calendaritem['entryid']));
551
				}
552
				$tzdefstart = streamProperty($openedMessages[$calendaritem['entryid']], $this->properties['tzdefstart']);
553
			}
554
555
			// Compare the timezone definitions of the client and the appointment.
556
			// Further processing is only required if they don't match.
557
			if (!$GLOBALS['entryid']->compareEntryIds($this->tzdef, $tzdefstart)) {
558
				if ($this->tzdefObj === false) {
559
					$this->tzdefObj = $GLOBALS['entryid']->createTimezoneDefinitionObject($this->tzdef);
560
				}
561
				$this->tzEffRuleIdx = getEffectiveTzreg($this->tzdefObj['rules']);
562
563
				$appTzDefStart = $GLOBALS['entryid']->createTimezoneDefinitionObject($tzdefstart);
564
				// Find TZRULE_FLAG_EFFECTIVE_TZREG rule for the appointment's timezone
565
				$appTzEffRuleIdx = getEffectiveTzreg($appTzDefStart['rules']);
566
567
				if (!is_null($this->tzEffRuleIdx) && !is_null($appTzEffRuleIdx)) {
568
					// first apply the bias of the appointment timezone and the bias of the browser
569
					$localStart = $calendaritem['props']['startdate'] - $appTzDefStart['rules'][$appTzEffRuleIdx]['bias'] * 60 + $this->tzdefObj['rules'][$this->tzEffRuleIdx]['bias'] * 60;
570
					if (isDst($appTzDefStart['rules'][$appTzEffRuleIdx], $calendaritem['props']['startdate'])) {
571
						$localStart -= $appTzDefStart['rules'][$appTzEffRuleIdx]['dstbias'] * 60;
572
					}
573
					if (isDst($this->tzdefObj['rules'][$this->tzEffRuleIdx], $calendaritem['props']['startdate'])) {
574
						$localStart += $this->tzdefObj['rules'][$this->tzEffRuleIdx]['dstbias'] * 60;
575
					}
576
					$duration = $calendaritem['props']['duedate'] - $calendaritem['props']['startdate'];
577
					$calendaritem['props']['startdate'] = $calendaritem['props']['commonstart'] = $localStart;
578
					$calendaritem['props']['duedate'] = $calendaritem['props']['commonend'] = $localStart + $duration;
579
				}
580
			}
581
		}
582
	}
583