Issues (752)

includes/modules/class.appointmentitemmodule.php (8 issues)

1
<?php
2
3
/**
4
 * Appointment ItemModule
5
 * Module which opens, creates, saves and deletes an item. It
6
 * extends the Module class.
7
 */
8
class AppointmentItemModule extends ItemModule {
9
	/**
10
	 * @var string client or server IANA timezone
11
	 */
12
	protected $tziana;
13
14
	/**
15
	 * @var bool|string client timezone definition
16
	 */
17
	protected $tzdef;
18
19
	/**
20
	 * @var array|bool client timezone definition array
21
	 */
22
	protected $tzdefObj;
23
24
	/**
25
	 * @var mixed client timezone effective rule id
26
	 */
27
	protected $tzEffRuleIdx;
28
29
	/**
30
	 * Constructor.
31
	 *
32
	 * @param int   $id   unique id
33
	 * @param array $data list of all actions
34
	 */
35
	public function __construct($id, $data) {
36
		parent::__construct($id, $data);
37
38
		$this->properties = $GLOBALS['properties']->getAppointmentProperties();
39
40
		$this->plaintext = true;
41
		$this->skipCopyProperties = [
42
			$this->properties['goid'],
43
			$this->properties['goid2'],
44
			$this->properties['request_sent'],
45
			PR_OWNER_APPT_ID,
46
		];
47
48
		$this->tziana = 'Etc/UTC';
49
		$this->tzdef = false;
50
		$this->tzdefObj = false;
51
	}
52
53
	#[Override]
54
	public function open($store, $entryid, $action) {
55
		if ($store && $entryid) {
56
			$data = [];
57
58
			$message = $GLOBALS['operations']->openMessage($store, $entryid);
59
60
			if (empty($message)) {
61
				return;
62
			}
63
64
			// Open embedded message if requested
65
			$attachNum = !empty($action['attach_num']) ? $action['attach_num'] : false;
66
			if ($attachNum) {
67
				// get message props of sub message
68
				$parentMessage = $message;
69
				$message = $GLOBALS['operations']->openMessage($store, $entryid, $attachNum);
70
71
				if (empty($message)) {
72
					return;
73
				}
74
75
				$data['item'] = $GLOBALS['operations']->getEmbeddedMessageProps($store, $message, $this->properties, $parentMessage, $attachNum);
76
			}
77
			else {
78
				// add all standard properties from the series/normal message
79
				$data['item'] = $GLOBALS['operations']->getMessageProps($store, $message, $this->properties, $this->plaintext);
80
			}
81
82
			if (!empty($action["timezone_iana"])) {
83
				$this->tziana = $action["timezone_iana"];
84
85
				try {
86
					$this->tzdef = mapi_ianatz_to_tzdef($action['timezone_iana']);
87
				}
88
				catch (Exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
89
				}
90
			}
91
92
			// if appointment is recurring then only we should get properties of occurrence if basedate is supplied
93
			if ($data['item']['props']['recurring'] === true) {
94
				if (!empty($action['basedate'])) {
95
					// check for occurrence/exception
96
					$basedate = $action['basedate'];
97
98
					$recur = new Recurrence($store, $message);
0 ignored issues
show
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...
99
100
					$exceptionatt = $recur->getExceptionAttachment($basedate);
101
102
					// Single occurrences are never recurring
103
					$data['item']['props']['recurring'] = false;
104
105
					if ($exceptionatt) {
106
						// Existing exception (open existing item, which includes basedate)
107
						$exceptionattProps = mapi_getprops($exceptionatt, [PR_ATTACH_NUM]);
108
						$exception = mapi_attach_openobj($exceptionatt, 0);
109
110
						// overwrite properties with the ones from the exception
111
						$exceptionProps = $GLOBALS['operations']->getMessageProps($store, $exception, $this->properties, $this->plaintext);
112
113
						/*
114
						 * If recurring item has set reminder to true then
115
						 * all occurrences before the 'flagdueby' value(of recurring item)
116
						 * should not show that reminder is set.
117
						 */
118
						if (isset($exceptionProps['props']['reminder']) && $data['item']['props']['reminder'] == true) {
119
							$flagDueByDay = $recur->dayStartOf($data['item']['props']['flagdueby']);
120
121
							if ($flagDueByDay > $basedate) {
122
								$exceptionProps['props']['reminder'] = false;
123
							}
124
						}
125
126
						// The properties must be merged, if the recipients or attachments are present in the exception
127
						// then that list should be used. Otherwise the list from the series must be applied (this
128
						// corresponds with OL2007).
129
						// @FIXME getMessageProps should not return empty string if exception doesn't contain body
130
						// by this change we can handle a situation where user has set empty string in the body explicitly
131
						if (!empty($exceptionProps['props']['body']) || !empty($exceptionProps['props']['html_body'])) {
132
							if (!empty($exceptionProps['props']['body'])) {
133
								$data['item']['props']['body'] = $exceptionProps['props']['body'];
134
							}
135
136
							if (!empty($exceptionProps['props']['html_body'])) {
137
								$data['item']['props']['html_body'] = $exceptionProps['props']['html_body'];
138
							}
139
140
							$data['item']['props']['isHTML'] = $exceptionProps['props']['isHTML'];
141
						}
142
						// remove properties from $exceptionProps so array_merge will not overwrite it
143
						unset($exceptionProps['props']['html_body'], $exceptionProps['props']['body'], $exceptionProps['props']['isHTML']);
144
145
						$data['item']['props'] = array_merge($data['item']['props'], $exceptionProps['props']);
146
						if (isset($exceptionProps['recipients'])) {
147
							$data['item']['recipients'] = $exceptionProps['recipients'];
148
						}
149
150
						if (isset($exceptionProps['attachments'])) {
151
							$data['item']['attachments'] = $exceptionProps['attachments'];
152
						}
153
154
						// Make sure we are using the passed basedate and not something wrong in the opened item
155
						$data['item']['props']['basedate'] = $basedate;
156
						$data['item']['attach_num'] = [$exceptionattProps[PR_ATTACH_NUM]];
157
					}
158
					elseif ($recur->isDeleteException($basedate)) {
159
						// Exception is deleted, should not happen, but if it the case then give error
160
						$this->sendFeedback(
161
							false,
162
							[
163
								'type' => ERROR_ZARAFA,
164
								'info' => [
165
									'original_message' => _('Could not open occurrence.'),
166
									'display_message' => _('Could not open occurrence, specific occurrence is probably deleted.'),
167
								],
168
							]
169
						);
170
171
						return;
172
					}
173
					else {
174
						// opening an occurrence of a recurring series (same as normal open, but add basedate, startdate and enddate)
175
						$data['item']['props']['basedate'] = $basedate;
176
						$data['item']['props']['startdate'] = $recur->getOccurrenceStart($basedate);
177
						$data['item']['props']['duedate'] = $recur->getOccurrenceEnd($basedate);
178
						$data['item']['props']['commonstart'] = $data['item']['props']['startdate'];
179
						$data['item']['props']['commonend'] = $data['item']['props']['duedate'];
180
						unset($data['item']['props']['reminder_time']);
181
182
						/*
183
						 * If recurring item has set reminder to true then
184
						 * all occurrences before the 'flagdueby' value(of recurring item)
185
						 * should not show that reminder is set.
186
						 */
187
						if (isset($exceptionProps['props']['reminder']) && $data['item']['props']['reminder'] == true) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $exceptionProps does not exist. Did you maybe mean $exception?
Loading history...
188
							$flagDueByDay = $recur->dayStartOf($data['item']['props']['flagdueby']);
189
190
							if ($flagDueByDay > $basedate) {
191
								$exceptionProps['props']['reminder'] = false;
192
							}
193
						}
194
					}
195
				}
196
				else {
197
					// Opening a recurring series, get the recurrence information
198
					$recur = new Recurrence($store, $message);
199
					$recurpattern = $recur->getRecurrence();
200
					$tz = $recur->tz; // no function to do this at the moment
201
202
					// Add the recurrence pattern to the data
203
					if (isset($recurpattern) && is_array($recurpattern)) {
204
						$data['item']['props'] += $recurpattern;
205
					}
206
207
					// Add the timezone information to the data
208
					if (isset($tz) && is_array($tz)) {
209
						$data['item']['props'] += $tz;
210
					}
211
				}
212
			}
213
214
			// Fix for all-day events which have a different timezone than the user's browser
215
			if ($data['item']['props']['alldayevent'] == 1 && $this->tzdef !== false) {
216
				$this->processAllDayItem($store, $data['item'], $message);
217
			}
218
219
			// Send the data
220
			$this->addActionData('item', $data);
221
			$GLOBALS['bus']->addData($this->getResponseData());
222
		}
223
	}
224
225
	/**
226
	 * Function does customization of exception based on module data.
227
	 * like, here it will generate display message based on actionType
228
	 * for particular exception.
229
	 *
230
	 * @param object     $e             Exception object
231
	 * @param string     $actionType    the action type, sent by the client
232
	 * @param MAPIobject $store         store object of message
0 ignored issues
show
The type MAPIobject 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...
233
	 * @param string     $parententryid parent entryid of the message
234
	 * @param string     $entryid       entryid of the message
235
	 * @param array      $action        the action data, sent by the client
236
	 */
237
	#[Override]
238
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
239
		if (is_null($e->displayMessage)) {
240
			switch ($actionType) {
241
				case "save":
242
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
243
						$message = mapi_msgstore_openentry($store, $entryid);
244
						$messageProps = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
245
						$messageClass = $messageProps[PR_MESSAGE_CLASS];
246
247
						$text = $messageClass !== "IPM.Appointment" ? _('a meeting request') : _('an appointment');
248
						$msg = _('You have insufficient privileges to move ' . $text . ' in this calendar. The calendar owner can set these using the \'permissions\'-tab of the folder properties (right click the calendar folder > properties > permissions)');
249
250
						$e->setDisplayMessage($msg);
251
						$e->setTitle(_('Insufficient privileges'));
252
253
						// Need this notification to refresh the calendar.
254
						$GLOBALS['bus']->notify(bin2hex((string) $parententryid), TABLE_DELETE, $messageProps);
255
					}
256
					break;
257
			}
258
		}
259
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
260
	}
261
262
	/**
263
	 * Save the give appointment or meeting request to the calendar.
264
	 *
265
	 * @param mapistore $store         MAPI store of the message
0 ignored issues
show
The type mapistore 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...
266
	 * @param string    $parententryid Parent entryid of the message (folder entryid, NOT message entryid)
267
	 * @param string    $entryid       entryid of the message
268
	 * @param array     $action        Action array containing json request
269
	 * @param string    $actionType    The action type which triggered this action
270
	 */
271
	#[Override]
272
	public function save($store, $parententryid, $entryid, $action, $actionType = 'save') {
273
		$result = false;
274
275
		// Save appointment (saveAppointment takes care of creating/modifying exceptions to recurring
276
		// items if necessary)
277
		$messageProps = $GLOBALS['operations']->saveAppointment($store, $entryid, $parententryid, $action, $actionType, $this->directBookingMeetingRequest);
278
279
		// Notify the bus if the save was OK
280
		if ($messageProps && !(is_array($messageProps) && isset($messageProps['error'])) && !isset($messageProps['remindertimeerror'])) {
281
			$GLOBALS['bus']->notify(bin2hex($parententryid), TABLE_SAVE, $messageProps);
282
			$result = true;
283
		}
284
285
		$errorMsg = false;
286
		if (!$result && isset($messageProps['remindertimeerror']) && !$messageProps['remindertimeerror']) {
287
			$errorMsg = _('Cannot set a reminder to appear before the previous occurrence. Reset reminder to save the change');
288
		}
289
		elseif (isset($messageProps['isexceptionallowed']) && $messageProps['isexceptionallowed'] === false) {
290
			$errorMsg = _('Two occurrences cannot occur on the same day');
291
		}
292
		elseif (is_array($messageProps) && isset($messageProps['error'])) {
293
			$errorMsg = match ($messageProps['error']) {
294
				1 => sprintf(_('You marked \'%s\' as a resource. You cannot schedule a meeting with \'%s\' because you do not have the appropriate permissions for that account. Either enter the name as a required or optional attendee or talk to your administrator about giving you permission to schedule \'%s\'.'), $messageProps['displayname'], $messageProps['displayname'], $messageProps['displayname']),
295
				2 => sprintf(_('\'%s\' has declined your meeting because \'%s\' does not automatically accept meeting requests.'), $messageProps['displayname'], $messageProps['displayname']),
296
				3 => sprintf(_('\'%s\' has declined your meeting because it is recurring. You must book each meeting separately with this resource.'), $messageProps['displayname']),
297
				4 => sprintf(_('\'%s\' is already booked for this specified time. You must use another time or find another resource.'), $messageProps['displayname']),
298
				default => _('Meeting was not scheduled.'),
299
			};
300
		}
301
		else {
302
			// Recurring but non-existing exception (same as normal open, but add basedate, startdate and enddate)
303
			$data = [];
304
			if ($result) {
305
				$data = Conversion::mapMAPI2XML($this->properties, $messageProps);
306
307
				// Get recipient information from the saved appointment to update client side
308
				// according to the latest recipient related changes only if changes requested from client.
309
				$savedAppointment = $GLOBALS['operations']->openMessage($store, $messageProps[PR_ENTRYID]);
310
				if (!empty($action['recipients'])) {
311
					$recipients = $GLOBALS["operations"]->getRecipientsInfo($savedAppointment);
312
					if (!empty($recipients)) {
313
						$data["recipients"] = [
314
							"item" => $recipients,
315
						];
316
					}
317
				}
318
319
				// Get attachments information from the saved appointment to update client side
320
				// according to the latest attachments related changes only if changes requested from client.
321
				if (!empty($action['attachments'])) {
322
					$attachments = $GLOBALS["operations"]->getAttachmentsInfo($savedAppointment);
323
					if (!empty($attachments)) {
324
						$data["attachments"] = [
325
							"item" => $attachments,
326
						];
327
					}
328
				}
329
330
				$data['action_response'] = [
331
					'resources_booked' => $this->directBookingMeetingRequest,
332
				];
333
334
				if (isset($action['message_action'], $action['message_action']['paste'])) {
335
					$data['action_response']['resources_pasted'] = true;
336
				}
337
			}
338
			else {
339
				if (!empty($action['message_action']['send'])) {
340
					$errorMsg = _('Meeting could not be sent.');
341
				}
342
				else {
343
					$errorMsg = _('Meeting could not be saved.');
344
				}
345
			}
346
		}
347
348
		if ($errorMsg === false) {
349
			$this->addActionData('update', ['item' => $data]);
350
			$GLOBALS['bus']->addData($this->getResponseData());
351
		}
352
		else {
353
			$this->sendFeedback(false, [
354
				'type' => ERROR_ZARAFA,
355
				'info' => [
356
					'display_message' => $errorMsg,
357
				],
358
			]);
359
		}
360
	}
361
362
	/**
363
	 * Processes an all-day item and calculates the correct starttime if necessary.
364
	 *
365
	 * @param object $store
366
	 * @param array  $calendaritem
367
	 * @param object $message
368
	 */
369
	private function processAllDayItem($store, &$calendaritem, $message) {
0 ignored issues
show
The parameter $message is not used and could be removed. ( Ignorable by Annotation )

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

369
	private function processAllDayItem($store, &$calendaritem, /** @scrutinizer ignore-unused */ $message) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $store is not used and could be removed. ( Ignorable by Annotation )

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

369
	private function processAllDayItem(/** @scrutinizer ignore-unused */ $store, &$calendaritem, $message) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
370
		// If the appointment doesn't have tzdefstart property, it was probably
371
		// created on a mobile device (mobile devices do not send a timezone for
372
		// all-day events).
373
		$isTzdefstartSet = isset($calendaritem['props']['tzdefstart']);
374
		$tzdefstart = $isTzdefstartSet ?
375
			hex2bin((string) $calendaritem['props']['tzdefstart']) :
376
			mapi_ianatz_to_tzdef("Etc/UTC");
377
378
		// Compare the timezone definitions of the client and the appointment.
379
		// Further processing is only required if they don't match.
380
		if ($isTzdefstartSet && $GLOBALS['entryid']->compareEntryIds($this->tzdef, $tzdefstart)) {
381
			return;
382
		}
383
384
		$duration = $calendaritem['props']['duedate'] - $calendaritem['props']['startdate'];
385
		$localStart = $calendaritem['props']['startdate'];
0 ignored issues
show
The assignment to $localStart is dead and can be removed.
Loading history...
386
		if (!$isTzdefstartSet) {
387
			$localStart = getLocalStart($calendaritem['props']['startdate'], $this->tziana);
388
		}
389
		else {
390
			if ($this->tzdefObj === false) {
391
				$this->tzdefObj = $GLOBALS['entryid']->createTimezoneDefinitionObject($this->tzdef);
392
			}
393
			$this->tzEffRuleIdx = getEffectiveTzreg($this->tzdefObj['rules']);
394
395
			$appTzDefStart = $GLOBALS['entryid']->createTimezoneDefinitionObject($tzdefstart);
396
397
			// Find TZRULE_FLAG_EFFECTIVE_TZREG rule for the appointment's timezone
398
			$appTzEffRuleIdx = getEffectiveTzreg($appTzDefStart['rules']);
399
400
			if (is_null($this->tzEffRuleIdx) && !is_null($appTzEffRuleIdx)) {
401
				return;
402
			}
403
			// first apply the bias of the appointment timezone and the bias of the browser
404
			$localStart = $calendaritem['props']['startdate'] - $appTzDefStart['rules'][$appTzEffRuleIdx]['bias'] * 60 + $this->tzdefObj['rules'][$this->tzEffRuleIdx]['bias'] * 60;
405
			if (isDst($appTzDefStart['rules'][$appTzEffRuleIdx], $calendaritem['props']['startdate'])) {
406
				$localStart -= $appTzDefStart['rules'][$appTzEffRuleIdx]['dstbias'] * 60;
407
			}
408
			if (isDst($this->tzdefObj['rules'][$this->tzEffRuleIdx], $calendaritem['props']['startdate'])) {
409
				$localStart += $this->tzdefObj['rules'][$this->tzEffRuleIdx]['dstbias'] * 60;
410
			}
411
		}
412
		$calendaritem['props']['startdate'] = $calendaritem['props']['commonstart'] = $localStart;
413
		$calendaritem['props']['duedate'] = $calendaritem['props']['commonend'] = $localStart + $duration;
414
	}
415
}
416