Issues (752)

server/includes/modules/class.itemmodule.php (22 issues)

1
<?php
2
3
/**
4
 * ItemModule
5
 * Module which opens, creates, saves and deletes an item. It
6
 * extends the Module class.
7
 */
8
class ItemModule extends Module {
9
	/**
10
	 * The setting whether Meeting Requests should be booked directly or not.
11
	 */
12
	public $directBookingMeetingRequest;
13
14
	/**
15
	 * The array of properties which should not be copied during the copy() action.
16
	 */
17
	public $skipCopyProperties;
18
19
	/**
20
	 * Indicates that we are supporting only plain text body in the message props.
21
	 */
22
	public $plaintext;
23
24
	/**
25
	 * Constructor.
26
	 *
27
	 * @param int   $id   unique id
28
	 * @param array $data list of all actions
29
	 */
30
	public function __construct($id, $data) {
31
		$this->directBookingMeetingRequest = ENABLE_DIRECT_BOOKING;
32
		$this->skipCopyProperties = [];
33
		$this->plaintext = false;
34
35
		parent::__construct($id, $data);
36
	}
37
38
	/**
39
	 * Executes all the actions in the $data variable.
40
	 */
41
	#[Override]
42
	public function execute() {
43
		foreach ($this->data as $actionType => $action) {
44
			if (!isset($actionType)) {
45
				continue;
46
			}
47
48
			try {
49
				$store = $this->getActionStore($action);
50
				$parententryid = $this->getActionParentEntryID($action);
51
				$entryid = $this->getActionEntryID($action);
52
53
				switch ($actionType) {
54
					case "open":
55
						$this->open($store, $entryid, $action);
56
						break;
57
58
					case "save":
59
						if (!$store || !$parententryid) {
60
							/*
61
							 * if parententryid or storeentryid is not passed then we can take a guess that
62
							 * it would be  a save operation but instead of depending on server to get default
63
							 * parent and store client should always send parententryid and storeentryid
64
							 *
65
							 * we can also assume that user has permission to right in his own store
66
							 */
67
							$this->save($store, $parententryid, $entryid, $action);
0 ignored issues
show
$parententryid of type object is incompatible with the type string expected by parameter $parententryid of ItemModule::save(). ( Ignorable by Annotation )

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

67
							$this->save($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $action);
Loading history...
68
							break;
69
						}
70
						/*
71
						 * The "message_action" object has been set, check the action_type field for
72
						 * the exact action which must be taken.
73
						 * Supported actions:
74
						 *   - acceptmeetingrequest: attendee has accepted mr
75
						 *   - declineMeetingRequest: attendee has declined mr
76
						 */
77
						if (!isset($action["message_action"], $action["message_action"]["action_type"])) {
78
							$this->save($store, $parententryid, $entryid, $action);
79
							break;
80
						}
81
82
						switch ($action["message_action"]["action_type"]) {
83
							case "declineMeetingRequest":
84
							case "acceptMeetingRequest":
85
								$message = $GLOBALS["operations"]->openMessage($store, $entryid);
86
								$basedate = ($action['basedate'] ?? false);
87
								$delete = false;
88
89
								if ($basedate) {
90
									$recurrence = 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...
91
									$exceptionatt = $recurrence->getExceptionAttachment($basedate);
92
									if ($exceptionatt) {
93
										// get properties of existing exception.
94
										$exceptionattProps = mapi_getprops($exceptionatt, [PR_ATTACH_NUM]);
95
										$attach_num = $exceptionattProps[PR_ATTACH_NUM];
96
									}
97
								}
98
99
								/**
100
								 * Get message class from original message. This can be changed to
101
								 * IPM.Appointment if the item is a Meeting Request in the maillist.
102
								 * After Accepting/Declining the message is moved and changed.
103
								 */
104
								$originalMessageProps = mapi_getprops($message, [PR_MESSAGE_CLASS]);
105
								$req = new Meetingrequest($store, $message, $GLOBALS["mapisession"]->getSession(), $this->directBookingMeetingRequest);
0 ignored issues
show
The type Meetingrequest 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...
106
107
								// Update extra body information
108
								if (isset($action["message_action"]['meetingTimeInfo']) && !empty($action["message_action"]['meetingTimeInfo'])) {
109
									$req->setMeetingTimeInfo($action["message_action"]['meetingTimeInfo']);
110
									unset($action["message_action"]['meetingTimeInfo']);
111
								}
112
113
								// sendResponse flag if it is set then send the mail response to the organzer.
114
								$sendResponse = true;
115
								if (isset($action["message_action"]["sendResponse"]) && $action["message_action"]["sendResponse"] == false) {
116
									$sendResponse = false;
117
								}
118
119
								// @FIXME: fix body
120
								$body = false;
121
								if (isset($action["props"]["isHTML"]) && $action["props"]["isHTML"] === true) {
122
									$body = $action["props"]["html_body"] ?? false;
123
								}
124
								else {
125
									$body = $action["props"]["body"] ?? false;
126
								}
127
128
								if ($action["message_action"]["action_type"] == "acceptMeetingRequest") {
129
									$tentative = $action["message_action"]["responseType"] === olResponseTentative;
130
									$newProposedStartTime = $action["message_action"]["proposed_starttime"] ?? false;
131
									$newProposedEndTime = $action["message_action"]["proposed_endtime"] ?? false;
132
133
									// We are accepting MR from preview-read-mail so set delete the actual mail flag.
134
									$delete = $req->isMeetingRequest($originalMessageProps[PR_MESSAGE_CLASS]);
135
136
									$req->doAccept($tentative, $sendResponse, $delete, $newProposedStartTime, $newProposedEndTime, $body, true, $store, $basedate);
137
								}
138
								else {
139
									$delete = $req->doDecline($sendResponse, $basedate, $body);
140
								}
141
142
								/**
143
								 * Now if the item is the Meeting Request that was sent to the attendee
144
								 * it is removed when the user has clicked on Accept/Decline. If the
145
								 * item is the appointment in the calendar it will not be moved. To only
146
								 * notify the bus when the item is a Meeting Request we are going to
147
								 * check the PR_MESSAGE_CLASS and see if it is "IPM.Meeting*".
148
								 */
149
								$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
150
151
								// if opened appointment is exception then it will add
152
								// the attach_num and basedate in messageProps.
153
								if (isset($attach_num)) {
154
									$messageProps[PR_ATTACH_NUM] = [$attach_num];
155
									$messageProps[$this->properties["basedate"]] = $basedate;
156
								}
157
158
								if ($delete) {
159
									// send TABLE_DELETE event because the message has moved
160
									$this->sendFeedback(true);
161
									$GLOBALS["bus"]->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), TABLE_DELETE, $messageProps);
162
								}
163
								else {
164
									$this->addActionData("update", ["item" => Conversion::mapMAPI2XML($this->properties, $messageProps)]);
165
									$GLOBALS["bus"]->addData($this->getResponseData());
166
167
									// send TABLE_SAVE event because an occurrence is deleted
168
									$GLOBALS["bus"]->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageProps);
169
								}
170
171
								break;
172
173
							case "acceptTaskRequest":
174
							case "declineTaskRequest":
175
								$message = $GLOBALS["operations"]->openMessage($store, $entryid);
176
177
								if (isset($action["props"]) && !empty($action["props"])) {
178
									$properties = $GLOBALS["properties"]->getTaskProperties();
179
									mapi_setprops($message, Conversion::mapXML2MAPI($properties, $action["props"]));
180
									mapi_savechanges($message);
181
								}
182
								// The task may be a delegated task, do an update if needed (will fail for non-delegated tasks)
183
								$tr = new TaskRequest($store, $message, $GLOBALS["mapisession"]->getSession());
0 ignored issues
show
The type TaskRequest 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...
184
								$isAccept = $action["message_action"]["action_type"] == "acceptTaskRequest";
185
								if (isset($action["message_action"]["task_comments_info"]) && !empty($action["message_action"]["task_comments_info"])) {
186
									$tr->setTaskCommentsInfo($action["message_action"]["task_comments_info"]);
187
								}
188
								if ($isAccept) {
189
									$result = $tr->doAccept();
190
								}
191
								else {
192
									$result = $tr->doDecline();
193
								}
194
195
								$this->sendFeedback(true);
196
								if ($result !== false) {
197
									$GLOBALS["bus"]->notify(bin2hex((string) $result[PR_PARENT_ENTRYID]), TABLE_DELETE, $result);
198
								}
199
200
								$props = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
201
								if (!$tr->isTaskRequest()) {
202
									unset($props[PR_MESSAGE_CLASS]);
203
									$GLOBALS["bus"]->notify(bin2hex((string) $props[PR_PARENT_ENTRYID]), $isAccept ? TABLE_SAVE : TABLE_DELETE, $props);
204
								}
205
								break;
206
207
							case "copy":
208
							case "move":
209
								$this->copy($store, $parententryid, $entryid, $action);
0 ignored issues
show
$store of type object is incompatible with the type resource expected by parameter $store of ItemModule::copy(). ( Ignorable by Annotation )

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

209
								$this->copy(/** @scrutinizer ignore-type */ $store, $parententryid, $entryid, $action);
Loading history...
$parententryid of type object is incompatible with the type string expected by parameter $parententryid of ItemModule::copy(). ( Ignorable by Annotation )

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

209
								$this->copy($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $action);
Loading history...
210
								break;
211
212
							case "reply":
213
							case "replyall":
214
							case "forward":
215
							default:
216
								$this->save($store, $parententryid, $entryid, $action);
217
						}
218
						break;
219
220
					case "delete":
221
						$subActionType = false;
222
						if (isset($action["message_action"], $action["message_action"]["action_type"])) {
223
							$subActionType = $action["message_action"]["action_type"];
224
						}
225
226
						/*
227
						 * The "message_action" object has been set, check the action_type field for
228
						 * the exact action which must be taken.
229
						 * Supported actions:
230
						 *   - cancelInvitation: organizer cancels already scheduled meeting
231
						 *   - removeFromCalendar: attendee receives meeting cancellation and wants to remove item from calendar
232
						 */
233
						switch ($subActionType) {
234
							case "removeFromCalendar":
235
								$basedate = (isset($action['basedate']) && !empty($action['basedate'])) ? $action['basedate'] : false;
236
237
								$this->removeFromCalendar($store, $entryid, $basedate, $this->directBookingMeetingRequest);
0 ignored issues
show
It seems like $basedate can also be of type false; however, parameter $basedate of ItemModule::removeFromCalendar() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

237
								$this->removeFromCalendar($store, $entryid, /** @scrutinizer ignore-type */ $basedate, $this->directBookingMeetingRequest);
Loading history...
238
								$this->sendFeedback(true);
239
								break;
240
241
							case "cancelInvitation":
242
								$this->cancelInvitation($store, $entryid, $action, $this->directBookingMeetingRequest);
243
								$this->sendFeedback(true);
244
								break;
245
246
							case "declineMeeting":
247
								// @FIXME can we somehow merge declineMeeting and declineMeetingRequest sub actions?
248
								$message = $GLOBALS["operations"]->openMessage($store, $entryid);
249
								$basedate = (isset($action['basedate']) && !empty($action['basedate'])) ? $action['basedate'] : false;
250
251
								$req = new Meetingrequest($store, $message, $GLOBALS["mapisession"]->getSession(), $this->directBookingMeetingRequest);
252
253
								// @FIXME: may be we can remove this body check any get it while declining meeting 'body'
254
								$body = false;
255
								if (isset($action["props"]["isHTML"]) && $action["props"]["isHTML"] === true) {
256
									$body = $action["props"]["html_body"] ?? false;
257
								}
258
								else {
259
									$body = $action["props"]["body"] ?? false;
260
								}
261
								$req->doDecline(true, $basedate, $body);
262
263
								$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
264
								$GLOBALS["bus"]->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), $basedate ? TABLE_SAVE : TABLE_DELETE, $messageProps);
265
266
								break;
267
268
							case "snooze":
269
							case "dismiss":
270
								$this->delete($store, $parententryid, $entryid, $action);
0 ignored issues
show
$parententryid of type object is incompatible with the type string expected by parameter $parententryid of ItemModule::delete(). ( Ignorable by Annotation )

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

270
								$this->delete($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $action);
Loading history...
271
								break;
272
273
							default:
274
								// Deleting an occurrence means that we have to save the message to
275
								// generate an exception. So when the basedate is provided, we actually
276
								// perform a save rather then delete.
277
								if (isset($action['basedate']) && !empty($action['basedate'])) {
278
									$this->save($store, $parententryid, $entryid, $action, "delete");
0 ignored issues
show
The call to ItemModule::save() has too many arguments starting with 'delete'. ( Ignorable by Annotation )

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

278
									$this->/** @scrutinizer ignore-call */ 
279
                save($store, $parententryid, $entryid, $action, "delete");

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
279
								}
280
								else {
281
									$this->delete($store, $parententryid, $entryid, $action);
282
								}
283
								break;
284
						}
285
						break;
286
287
					default:
288
						$this->handleUnknownActionType($actionType);
289
				}
290
			}
291
			catch (MAPIException $e) {
292
				$this->processException($e, $actionType, $store, $parententryid, $entryid, $action);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parententryid does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $entryid does not seem to be defined for all execution paths leading up to this point.
Loading history...
293
			}
294
		}
295
	}
296
297
	/**
298
	 * Function does customization of exception based on module data.
299
	 * like, here it will generate display message based on actionType
300
	 * for particular exception.
301
	 *
302
	 * @param object     $e             Exception object
303
	 * @param string     $actionType    the action type, sent by the client
304
	 * @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...
305
	 * @param string     $parententryid parent entryid of the message
306
	 * @param string     $entryid       entryid of the message
307
	 * @param array      $action        the action data, sent by the client
308
	 */
309
	#[Override]
310
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
311
		if (is_null($e->displayMessage)) {
312
			switch ($actionType) {
313
				case "open":
314
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
315
						$e->setDisplayMessage(_("You have insufficient privileges to open this message."));
316
					}
317
					elseif ($e->getCode() == MAPI_E_NOT_FOUND) {
318
						$e->setDisplayMessage(_("Could not find message, either it has been moved or deleted or you don't have access to open this message."));
319
						// Show error in console instead of a pop-up
320
						if (isset($action["message_action"]['suppress_exception']) && $action["message_action"]['suppress_exception'] === true) {
321
							$e->setNotificationType('console');
322
						}
323
					}
324
					else {
325
						$e->setDisplayMessage(_("Could not open message."));
326
						$e->allowToShowDetailsMessage = true;
327
					}
328
					break;
329
330
				case "save":
331
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
332
						if (!empty($action["message_action"]["action_type"])) {
333
							switch ($action["message_action"]["action_type"]) {
334
								case "declineMeetingRequest":
335
									$e->setDisplayMessage(_("You have insufficient privileges to decline this Meeting Request") . ".");
336
									break;
337
338
								case "acceptMeetingRequest":
339
									$e->setDisplayMessage(_("You have insufficient privileges to accept this Meeting Request") . ".");
340
									break;
341
342
								case "copy":
343
									$e->setDisplayMessage(_("Could not copy message") . ".");
344
									break;
345
346
								case "move":
347
									$e->setDisplayMessage(_("Could not move message") . ".");
348
									break;
349
							}
350
						}
351
352
						if (empty($e->displayMessage)) {
353
							$e->setDisplayMessage(_("You have insufficient privileges to save items in this folder") . ".");
354
						}
355
					}
356
					elseif ($e->getCode() == MAPI_E_STORE_FULL) {
357
						$e->setDisplayMessage($this->getOverQuotaMessage($store));
358
					}
359
					else {
360
						$e->setDisplayMessage(_("Could not save message") . ".");
361
						$e->allowToShowDetailsMessage = true;
362
					}
363
					break;
364
365
				case 'delete':
366
					switch ($e->getCode()) {
367
						case MAPI_E_NO_ACCESS:
368
							if (!empty($action['message_action']['action_type'])) {
369
								switch ($action['message_action']['action_type']) {
370
									case 'removeFromCalendar':
371
										$e->setDisplayMessage(_('You have insufficient privileges to remove item from the calendar.'));
372
										break;
373
								}
374
							}
375
							break;
376
377
						case MAPI_E_NOT_IN_QUEUE:
378
							$e->setDisplayMessage(_('Message is no longer in the outgoing queue, typically because it has already been sent.'));
379
							break;
380
381
						case MAPI_E_UNABLE_TO_ABORT:
382
							$e->setDisplayMessage(_('Message cannot be aborted'));
383
							break;
384
					}
385
					if (empty($e->displayMessage)) {
386
						$e->setDisplayMessage(_("You have insufficient privileges to delete items in this folder") . ".");
387
					}
388
					break;
389
390
				case "attach_items":
391
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
392
						$e->setDisplayMessage(_("You have insufficient privileges to attach item as an attachment."));
393
					}
394
					else {
395
						$e->setDisplayMessage(_("Could not attach item as an attachment."));
396
					}
397
					break;
398
399
				case "reclaimownership":
400
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
401
						$e->setDisplayMessage(_("You have insufficient privileges to reclaim the ownership for the Task Request."));
402
					}
403
					else {
404
						$e->setDisplayMessage(_("Could not reclaim the ownership for the Task Request."));
405
					}
406
					break;
407
408
				case "acceptTaskRequest":
409
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
410
						$e->setDisplayMessage(_("You have insufficient privileges to accept this Task Request."));
411
					}
412
					else {
413
						$e->setDisplayMessage(_("Could not accept Task Request."));
414
					}
415
					break;
416
417
				case "declineTaskRequest":
418
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
419
						$e->setDisplayMessage(_("You have insufficient privileges to decline this Task Request."));
420
					}
421
					else {
422
						$e->setDisplayMessage(_("Could not decline Task Request."));
423
					}
424
					break;
425
			}
426
			Log::Write(
427
				LOGLEVEL_ERROR,
428
				"itemmodule::handleException():" . $actionType . ": " . $e->displayMessage
429
			);
430
		}
431
432
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
433
	}
434
435
	/**
436
	 * Function which opens an item.
437
	 *
438
	 * @param object $store   MAPI Message Store Object
439
	 * @param string $entryid entryid of the message
440
	 * @param array  $action  the action data, sent by the client
441
	 */
442
	public function open($store, $entryid, $action) {
443
		$data = [];
444
445
		if ($entryid) {
446
			if ($store) {
0 ignored issues
show
$store is of type object, thus it always evaluated to true.
Loading history...
447
				$message = $GLOBALS['operations']->openMessage($store, $entryid);
448
			}
449
			else {
450
				// store is not passed so we need to open the message first to get the store resource
451
				$message = $GLOBALS['mapisession']->openMessage($entryid);
452
453
				$messageStoreInfo = mapi_getprops($message, [PR_STORE_ENTRYID]);
454
				$store = $GLOBALS['mapisession']->openMessageStore($messageStoreInfo[PR_STORE_ENTRYID]);
455
			}
456
		}
457
458
		if (empty($message)) {
459
			return;
460
		}
461
462
		// Decode smime signed messages on this message
463
		parse_smime($store, $message);
464
465
		// Open embedded message if requested
466
		$attachNum = !empty($action['attach_num']) ? $action['attach_num'] : false;
467
		if ($attachNum) {
468
			// get message props of sub message
469
			$parentMessage = $message;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $message does not seem to be defined for all execution paths leading up to this point.
Loading history...
470
			$message = $GLOBALS['operations']->openMessage($store, $entryid, $attachNum, true);
471
472
			if (empty($message)) {
473
				return;
474
			}
475
476
			$data['item'] = $GLOBALS['operations']->getEmbeddedMessageProps($store, $message, $this->properties, $parentMessage, $attachNum);
477
		}
478
		else {
479
			// get message props of the message
480
			$data['item'] = $GLOBALS['operations']->getMessageProps($store, $message, $this->properties, $this->plaintext);
481
			$messageClass = $data['item']['props']['message_class'] ?? '';
482
483
			// Check for meeting request, do processing if necessary
484
			if (stripos($messageClass, 'IPM.Schedule.Meeting') !== false) {
485
				$req = new Meetingrequest($store, $message, $GLOBALS['mapisession']->getSession(), $this->directBookingMeetingRequest);
486
487
				try {
488
					if ($req->isMeetingRequestResponse($messageClass)) {
489
						if ($req->isLocalOrganiser()) {
490
							// We received a meeting request response, and we're the delegate/organiser
491
							$req->processMeetingRequestResponse();
492
						}
493
					}
494
					elseif ($req->isMeetingRequest($messageClass)) {
495
						if (!$req->isLocalOrganiser()) {
496
							if ($req->isMeetingOutOfDate()) {
497
								// we know that meeting is out of date so directly set this properties
498
								$data['item']['props']['meetingtype'] = mtgOutOfDate;
499
								$data['item']['props']['icon_index'] = 1033;
500
501
								// send update to maillistmodule that meeting request is updated with out of date flag
502
								$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
503
								$GLOBALS['bus']->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageProps);
504
							}
505
							else {
506
								/*
507
								 * if meeting request is not out of date then process it for the first time
508
								 * which will create corresponding appointment in the user's calendar
509
								 */
510
								$calItemEntryid = $req->doAccept(true, false, false);
511
								// we need to set timezone information for all-day events
512
								if (isset($data['item']['props']['alldayevent']) &&
513
									$data['item']['props']['alldayevent'] == true &&
514
									!empty($action["timezone_iana"]) &&
515
									is_string($calItemEntryid)) {
516
									try {
517
										$tzdef = mapi_ianatz_to_tzdef($action['timezone_iana']);
518
										$tzdefObj = $GLOBALS['entryid']->createTimezoneDefinitionObject($tzdef);
519
										$tzEffRuleIdx = getEffectiveTzreg($tzdefObj['rules']);
520
										if (!is_null($tzEffRuleIdx)) {
521
											$localStart = $data['item']['props']['appointment_startdate'] + $tzdefObj['rules'][$tzEffRuleIdx]['bias'] * 60;
522
											if (isDst($tzdefObj['rules'][$tzEffRuleIdx], $data['item']['props']['appointment_startdate'])) {
523
												$localStart += $tzdefObj['rules'][$tzEffRuleIdx]['dstbias'] * 60;
524
											}
525
											$duration = $data['item']['props']['appointment_duedate'] - $data['item']['props']['appointment_startdate'];
526
527
											$calItem = $GLOBALS['mapisession']->openMessage($calItemEntryid);
528
											mapi_setprops($calItem, [
529
												$this->properties['appointment_startdate'] => $localStart,
530
												$this->properties['appointment_duedate'] => $localStart + $duration,
531
												$this->properties['tzdefstart'] => $tzdef,
532
												$this->properties['tzdefend'] => $tzdef,
533
											]);
534
											mapi_savechanges($calItem);
535
											$data['item']['props']['appointment_startdate'] = $localStart;
536
											$data['item']['props']['appointment_duedate'] = $localStart + $duration;
537
										}
538
									}
539
									catch (Exception $e) {
540
										error_log(sprintf("Error setting timezone to an all-day event: %s", $e));
541
									}
542
								}
543
							}
544
545
							// Show user whether meeting request conflict with other appointment or not.
546
							$meetingConflicts = $req->isMeetingConflicting();
547
548
							/**
549
							 * if $meetingConflicts is boolean and true then its a normal meeting.
550
							 * if $meetingConflicts is integer then it indicates no of instances of a recurring meeting which conflicts with Calendar.
551
							 */
552
							if ($meetingConflicts !== false) {
553
								if ($meetingConflicts === true) {
554
									$data['item']['props']['conflictinfo'] = _('Conflicts with another appointment.');
555
								}
556
								else {
557
									$data['item']['props']['conflictinfo'] = sprintf(ngettext('%s occurrence of this recurring appointment conflicts with other appointment.', '%s occurrences of this recurring appointment conflicts with other appointments.', $meetingConflicts), $meetingConflicts);
558
								}
559
							}
560
						}
561
					}
562
					elseif ($req->isMeetingCancellation()) {
563
						$req->processMeetingCancellation();
564
					}
565
566
					if ($req->isInCalendar()) {
567
						$calendarItemProps = $this->getCalendarItemProps($req);
568
						if (!empty($calendarItemProps)) {
569
							$data['item']['props'] = array_merge($data['item']['props'], $calendarItemProps);
570
						}
571
					}
572
					else {
573
						$data['item']['props']['appointment_not_found'] = true;
574
					}
575
				}
576
				catch (MAPIException $e) {
577
					// if quota is exceeded or or we don't have permission to write in calendar folder than ignore the exception.
578
					if ($e->getCode() !== MAPI_E_STORE_FULL && $e->getCode() !== MAPI_E_NO_ACCESS) {
579
						// re-throw the exception if it is not one of quota/calendar permission.
580
						throw $e;
581
					}
582
				}
583
				if (!empty($data['item']['props']['appointment_recurring']) &&
584
					empty($data['item']['props']['appointment_recurring_pattern'])) {
585
					$recurr = new Recurrence($store, $message);
586
					$data['item']['props']['appointment_recurring_pattern'] = $recurr->saveRecurrencePattern();
587
				}
588
			}
589
			elseif (stripos($messageClass, 'REPORT.IPM.NOTE.NDR') !== false) {
590
				// check if this message is a NDR (mail)message, if so, generate a new body message
591
				$data['item']['props']['isHTML'] = false;
592
				$data['item']['props']['body'] = $this->getNDRbody($message);
593
			}
594
		}
595
596
		$userEntryId = '';
597
		if (isset($data['item']['props']['sent_representing_entryid'])) {
598
			$userEntryId = hex2bin($data['item']['props']['sent_representing_entryid']);
599
		}
600
		elseif (isset($data['item']['props']['sender_entryid'])) {
601
			$userEntryId = hex2bin($data['item']['props']['sender_entryid']);
602
		}
603
604
		// get user image saved in LDAP.
605
		if (!empty($userEntryId)) {
606
			$data['item']['props']['user_image'] = $GLOBALS['operations']->getCompressedUserImage($userEntryId);
607
		}
608
609
		// Allowing to hook in just before the data sent away to be sent to the client
610
		$GLOBALS['PluginManager']->triggerHook('server.module.itemmodule.open.after', [
611
			'moduleObject' => &$this,
612
			'store' => $store,
613
			'entryid' => $entryid,
614
			'action' => $action,
615
			'message' => &$message,
616
			'data' => &$data,
617
		]);
618
619
		// ugly workaround to show clip icon for the signed/encrypted emails
620
		if (isset($data['item']['props']['smime'])) {
621
			$data['item']['props']['hasattach'] = true;
622
			unset($data['item']['props']['hide_attachments']);
623
		}
624
625
		$this->addActionData('item', $data);
626
		$GLOBALS['bus']->addData($this->getResponseData());
627
	}
628
629
	/**
630
	 * Function which saves an item.
631
	 *
632
	 * @param object $store         MAPI Message Store Object
633
	 * @param string $parententryid parent entryid of the message
634
	 * @param array  $action        the action data, sent by the client
635
	 * @param mixed  $entryid
636
	 */
637
	public function save($store, $parententryid, $entryid, $action) {
638
		$result = false;
639
640
		if (isset($action["props"])) {
641
			if (!$store) {
0 ignored issues
show
$store is of type object, thus it always evaluated to true.
Loading history...
642
				$store = $GLOBALS['mapisession']->getDefaultMessageStore();
643
			}
644
			if (!$parententryid) {
645
				if (isset($action['props']['message_class'])) {
646
					$parententryid = $this->getDefaultFolderEntryID($store, $action['props']['message_class']);
647
				}
648
				else {
649
					$parententryid = $this->getDefaultFolderEntryID($store, '');
650
				}
651
			}
652
653
			if ($store && $parententryid) {
654
				$props = Conversion::mapXML2MAPI($this->properties, $action["props"]);
655
656
				$messageProps = []; // props returned from saveMessage
657
658
				// Save message
659
				if (!empty($props)) {
660
					$result = $GLOBALS["operations"]->saveMessage($store, $entryid, $parententryid, $props, $messageProps, [], !empty($action['attachments']) ? $action['attachments'] : []);
661
				}
662
663
				if ($result) {
664
					$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_SAVE, $messageProps);
665
666
					$this->addActionData("update", ["item" => Conversion::mapMAPI2XML($this->properties, $messageProps)]);
667
					$GLOBALS["bus"]->addData($this->getResponseData());
668
				}
669
				else {
670
					$this->sendFeedback(false);
671
				}
672
			}
673
		}
674
	}
675
676
	/**
677
	 * Function which deletes an item.
678
	 *
679
	 * @param object $store         MAPI Message Store Object
680
	 * @param string $parententryid parent entryid of the message
681
	 * @param string $entryid       entryid of the message
682
	 * @param array  $action        the action data, sent by the client
683
	 */
684
	public function delete($store, $parententryid, $entryid, $action) {
685
		if (!$store || !$parententryid || !$entryid) {
0 ignored issues
show
$store is of type object, thus it always evaluated to true.
Loading history...
686
			return;
687
		}
688
		$props = [];
689
		$props[PR_PARENT_ENTRYID] = $parententryid;
690
		$props[PR_ENTRYID] = $entryid;
691
692
		$storeprops = mapi_getprops($store, [PR_ENTRYID]);
693
		$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
694
695
		$soft = $action['message_action']['soft_delete'] ?? false;
696
		$unread = $action['message_action']['non_read_notify'] ?? false;
697
		$result = $GLOBALS["operations"]->deleteMessages($store, $parententryid, $entryid, $soft, $unread);
698
		if ($result) {
699
			$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_DELETE, $props);
700
			$this->sendFeedback(true);
701
		}
702
	}
703
704
	/**
705
	 * Function which returns the entryid of a default folder.
706
	 *
707
	 * @param object $store        MAPI Message Store Object
708
	 * @param string $messageClass the class of the folder
709
	 *
710
	 * @return string entryid of a default folder, false if not found
711
	 */
712
	public function getDefaultFolderEntryID($store, $messageClass) {
713
		$entryid = false;
714
715
		if ($store) {
0 ignored issues
show
$store is of type object, thus it always evaluated to true.
Loading history...
716
			$rootcontainer = mapi_msgstore_openentry($store);
717
			$rootcontainerprops = mapi_getprops($rootcontainer, [PR_IPM_DRAFTS_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID]);
718
719
			switch ($messageClass) {
720
				case "IPM.Appointment":
721
					if (isset($rootcontainerprops[PR_IPM_APPOINTMENT_ENTRYID])) {
722
						$entryid = $rootcontainerprops[PR_IPM_APPOINTMENT_ENTRYID];
723
					}
724
					break;
725
726
				case "IPM.Contact":
727
				case "IPM.DistList":
728
					if (isset($rootcontainerprops[PR_IPM_CONTACT_ENTRYID])) {
729
						$entryid = $rootcontainerprops[PR_IPM_CONTACT_ENTRYID];
730
					}
731
					break;
732
733
				case "IPM.StickyNote":
734
					if (isset($rootcontainerprops[PR_IPM_NOTE_ENTRYID])) {
735
						$entryid = $rootcontainerprops[PR_IPM_NOTE_ENTRYID];
736
					}
737
					break;
738
739
				case "IPM.Task":
740
					if (isset($rootcontainerprops[PR_IPM_TASK_ENTRYID])) {
741
						$entryid = $rootcontainerprops[PR_IPM_TASK_ENTRYID];
742
					}
743
					break;
744
745
				default:
746
					if (isset($rootcontainerprops[PR_IPM_DRAFTS_ENTRYID])) {
747
						$entryid = $rootcontainerprops[PR_IPM_DRAFTS_ENTRYID];
748
					}
749
					break;
750
			}
751
		}
752
753
		return $entryid;
754
	}
755
756
	/**
757
	 * Function which copies or moves one or more items.
758
	 *
759
	 * @param resource $store         MAPI Message Store Object
760
	 * @param string   $parententryid entryid of the folder
761
	 * @param mixed    $entryids      list of entryids which will be copied or moved (in binary format)
762
	 * @param array    $action        the action data, sent by the client
763
	 */
764
	public function copy($store, $parententryid, $entryids, $action) {
765
		$result = false;
0 ignored issues
show
The assignment to $result is dead and can be removed.
Loading history...
766
767
		if ($store && $parententryid && $entryids) {
0 ignored issues
show
$store is of type resource, thus it always evaluated to false.
Loading history...
768
			$dest_store = $store;
769
			if (isset($action["message_action"]["destination_store_entryid"])) {
770
				$dest_storeentryid = hex2bin($action["message_action"]["destination_store_entryid"]);
771
				$dest_store = $GLOBALS["mapisession"]->openMessageStore($dest_storeentryid);
772
			}
773
774
			$dest_folderentryid = false;
775
			if (isset($action["message_action"]["destination_parent_entryid"])) {
776
				$dest_folderentryid = hex2bin($action["message_action"]["destination_parent_entryid"]);
777
			}
778
779
			$moveMessages = false;
780
			if (isset($action["message_action"]["action_type"]) && $action["message_action"]["action_type"] == "move") {
781
				$moveMessages = true;
782
			}
783
784
			// if item has some set of props that need to be saved into the newly copied/moved item
785
			$copyProps = [];
786
			if (isset($action["message_action"]["dropmodifications"])) {
787
				$copyProps = Conversion::mapXML2MAPI($this->properties, $action["message_action"]["dropmodifications"]);
788
			}
789
790
			// if item has some changes made before choosing different calendar from create-in dropdown
791
			if (isset($action["props"]) && !empty($action["props"])) {
792
				$copyProps = Conversion::mapXML2MAPI($this->properties, $action["props"]);
793
			}
794
795
			$props = [];
796
			$props[PR_PARENT_ENTRYID] = $parententryid;
797
			$props[PR_ENTRYID] = $entryids;
798
799
			$storeprops = mapi_getprops($store, [PR_ENTRYID]);
800
			$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
801
802
			$skipCopyProperties = [];
803
			if (isset($action["message_action"]["unset_Private"])) {
804
				if ($moveMessages) {
805
					array_push($skipCopyProperties, $this->properties["private"], $this->properties["sensitivity"]);
806
				}
807
				else {
808
					array_push($this->skipCopyProperties, $this->properties["private"], $this->properties["sensitivity"]);
809
				}
810
			}
811
812
			$result = $GLOBALS["operations"]->copyMessages($store, $parententryid, $dest_store, $dest_folderentryid, $entryids, $moveMessages ? $skipCopyProperties : $this->skipCopyProperties, $moveMessages, $copyProps);
813
814
			if ($result) {
815
				if ($moveMessages) {
816
					$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_DELETE, $props);
817
				}
818
819
				// Delete the PR_ENTRYID, the copied or moved message has a new entryid,
820
				// and at this time we have no idea what that might be. So make sure
821
				// we unset it, otherwise the notification handlers get weird ideas
822
				// and could reset the PR_PARENT_ENTRYID to the old folder again.
823
				unset($props[PR_ENTRYID]);
824
				$props[PR_PARENT_ENTRYID] = $dest_folderentryid;
825
				$props[PR_STORE_ENTRYID] = $dest_storeentryid;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $dest_storeentryid does not seem to be defined for all execution paths leading up to this point.
Loading history...
826
				$GLOBALS["bus"]->notify(bin2hex($dest_folderentryid), TABLE_SAVE, $props);
827
			}
828
829
			$this->sendFeedback($result, []);
830
		}
831
	}
832
833
	/**
834
	 * Function returns correspondent calendar item's properties attached
835
	 * with the meeting request/response/cancellation.
836
	 *
837
	 * @param Meetingrequest $meetingRequestObject the meeting request object
838
	 *                                             using which function will fetch meeting request properties and return them
839
	 */
840
	public function getCalendarItemProps($meetingRequestObject) {
841
		$calendarItem = $meetingRequestObject->getCorrespondentCalendarItem();
842
		$props = [];
843
		if ($calendarItem !== false) {
844
			$calendarItemProps = mapi_getprops($calendarItem, [PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $meetingRequestObject->proptags['updatecounter'], $meetingRequestObject->proptags['goid']]);
845
846
			// Store calendar item's necessary properties in props array.
847
			$props['appointment_store_entryid'] = bin2hex((string) $calendarItemProps[PR_STORE_ENTRYID]);
848
			$props['appointment_parent_entryid'] = bin2hex((string) $calendarItemProps[PR_PARENT_ENTRYID]);
849
			$props['appointment_entryid'] = bin2hex((string) $calendarItemProps[PR_ENTRYID]);
850
851
			$props['appointment_updatecounter'] = $calendarItemProps[$meetingRequestObject->proptags['updatecounter']] ?? 0;
852
853
			$messageProps = mapi_getprops($meetingRequestObject->message, [$meetingRequestObject->proptags['goid']]);
854
855
			$basedate = $meetingRequestObject->getBasedateFromGlobalID($messageProps[$meetingRequestObject->proptags['goid']]);
856
857
			if ($basedate !== false) {
858
				$props['appointment_basedate'] = $basedate;
859
860
				// if basedate is provided then it is exception, so get update counter of the exception
861
				$exception = $meetingRequestObject->getExceptionItem($calendarItem, $basedate);
862
863
				if ($exception !== false) {
864
					// we are able to find the exception then get updatecounter
865
					$exceptionProps = mapi_getprops($exception, [$meetingRequestObject->proptags['updatecounter']]);
866
					$props['appointment_updatecounter'] = $exceptionProps[$meetingRequestObject->proptags['updatecounter']] ?? 0;
867
				}
868
			}
869
870
			if ($meetingRequestObject->isMeetingRequestResponse()) {
871
				$props['meeting_updated'] = $meetingRequestObject->isMeetingUpdated($basedate);
872
			}
873
874
			return $props;
875
		}
876
877
		return false;
878
	}
879
880
	/**
881
	 * Get a text body for a Non-Delivery report.
882
	 *
883
	 * This function reads the necessary properties from the passed message and constructs
884
	 * a user-readable NDR message from those properties
885
	 *
886
	 * @param mapimessage $message The NDR message to read the information from
0 ignored issues
show
The type mapimessage 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...
887
	 *
888
	 * @return string NDR body message as plaintext message
889
	 */
890
	public function getNDRbody($message) {
891
		$message_props = mapi_getprops($message, [PR_ORIGINAL_SUBJECT, PR_ORIGINAL_SUBMIT_TIME, PR_BODY]);
892
		$body = '';
893
894
		// use PR_BODY if it's there, otherwise create a recipient failed message
895
		if (isset($message_props[PR_BODY]) || propIsError(PR_BODY, $message_props) == MAPI_E_NOT_ENOUGH_MEMORY) {
896
			$body = mapi_openproperty($message, PR_BODY);
897
		}
898
899
		if (empty($body)) {
900
			$body = _("Your message did not reach some or all of the intended recipients") . "\n\n";
901
			$body .= "\t" . _("Subject") . ": " . $message_props[PR_ORIGINAL_SUBJECT] . "\n";
902
			$body .= "\t" . _("Sent") . ":    " . date(DATE_RFC2822, $message_props[PR_ORIGINAL_SUBMIT_TIME]) . "\n\n";
903
			$body .= _("The following recipient(s) could not be reached") . ":\n";
904
905
			$recipienttable = mapi_message_getrecipienttable($message);
906
			$recipientrows = mapi_table_queryallrows($recipienttable, [PR_DISPLAY_NAME, PR_REPORT_TIME, PR_REPORT_TEXT]);
907
			foreach ($recipientrows as $recipient) {
908
				$body .= "\n\t" . $recipient[PR_DISPLAY_NAME] . " on " . date(DATE_RFC2822, $recipient[PR_REPORT_TIME]) . "\n";
909
				$body .= "\t\t" . $recipient[PR_REPORT_TEXT] . "\n";
910
			}
911
		}
912
913
		return $body;
914
	}
915
916
	/**
917
	 * Send a meeting cancellation.
918
	 *
919
	 * This function sends a meeting cancellation for the meeting references by the passed entryid. It
920
	 * will send the meeting cancellation and move the item itself to the waste basket.
921
	 *
922
	 * @param mapistore $store                       The store in which the meeting request resides
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...
923
	 * @param string    $entryid                     entryid of the appointment for which the cancellation should be sent
924
	 * @param object    $action                      data sent by client
925
	 * @param bool      $directBookingMeetingRequest Indicates if a Meeting Request should use direct booking or not
926
	 */
927
	public function cancelInvitation($store, $entryid, $action, $directBookingMeetingRequest) {
928
		$message = $GLOBALS['operations']->openMessage($store, $entryid);
929
930
		// @TODO move this to meeting request class ?
931
		$req = new Meetingrequest($store, $message, $GLOBALS['mapisession']->getSession(), $directBookingMeetingRequest);
932
933
		// Update extra body information
934
		if (isset($action['message_action']['meetingTimeInfo']) && !empty($action['message_action']['meetingTimeInfo'])) {
935
			$req->setMeetingTimeInfo($action["message_action"]['meetingTimeInfo']);
936
			unset($action["message_action"]['meetingTimeInfo']);
937
		}
938
939
		// get basedate from action data and pass to meeting request class
940
		$basedate = !empty($action['basedate']) ? $action['basedate'] : false;
941
942
		$req->doCancelInvitation($basedate);
943
944
		if ($basedate !== false) {
945
			// if basedate is specified then we have created exception in recurring meeting request
946
			// so send notification of creation of exception
947
			$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
948
			$GLOBALS["bus"]->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageProps);
949
		}
950
		else {
951
			// for normal/recurring meetings send delete notification
952
			$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
953
			$GLOBALS["bus"]->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), TABLE_DELETE, $messageProps);
954
		}
955
	}
956
957
	/**
958
	 * Remove all appointments for a certain meeting request.
959
	 *
960
	 * This function searches the default calendar for all meeting requests for the specified
961
	 * meeting. All those appointments are then removed.
962
	 *
963
	 * @param mapistore $store                       Mapi store in which the meeting request and the calendar reside
964
	 * @param string    $entryid                     Entryid of the meeting request or appointment for which all items should be deleted
965
	 * @param string    $basedate                    if specified contains starttime of day of an occurrence
966
	 * @param bool      $directBookingMeetingRequest Indicates if a Meeting Request should use direct booking or not
967
	 */
968
	public function removeFromCalendar($store, $entryid, $basedate, $directBookingMeetingRequest) {
969
		$message = $GLOBALS["operations"]->openMessage($store, $entryid);
970
971
		$req = new Meetingrequest($store, $message, $GLOBALS["mapisession"]->getSession(), $directBookingMeetingRequest);
972
973
		$req->doRemoveFromCalendar($basedate);
974
975
		// Notify the bus that the message has been deleted
976
		$messageProps = mapi_getprops($message, [PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID]);
977
		$GLOBALS["bus"]->notify(bin2hex((string) $messageProps[PR_PARENT_ENTRYID]), $basedate ? TABLE_SAVE : TABLE_DELETE, $messageProps);
978
	}
979
}
980