Issues (752)

server/includes/modules/class.hierarchymodule.php (19 issues)

1
<?php
2
3
/**
4
 * Hierarchy Module.
5
 *
6
 * @todo
7
 * - Check the code at deleteFolder and at copyFolder. Looks the same.
8
 */
9
class HierarchyModule extends Module {
10
	private $store_entryid;
11
12
	/**
13
	 * Constructor.
14
	 *
15
	 * @param int   $id   unique id
16
	 * @param array $data list of all actions
17
	 */
18
	public function __construct($id, $data) {
19
		$this->properties = $GLOBALS["properties"]->getFolderProperties();
20
		$this->list_properties = $GLOBALS["properties"]->getFolderListProperties();
21
22
		parent::__construct($id, $data);
23
	}
24
25
	/**
26
	 * Creates the notifiers for this module,
27
	 * and register them to the Bus.
28
	 */
29
	public function createNotifiers() {
30
		$entryid = $this->getEntryID();
31
		$GLOBALS["bus"]->registerNotifier('hierarchynotifier', $entryid, true);
32
		$GLOBALS["bus"]->registerNotifier('hierarchynotifier', REQUEST_ENTRYID);
33
		$GLOBALS["bus"]->registerNotifier('newmailnotifier', REQUEST_ENTRYID);
34
	}
35
36
	/**
37
	 * Function which returns a list of entryids, which is used to register this module. It
38
	 * returns the ipm_subtree entryids of every message store.
39
	 *
40
	 * @return array list of entryids
41
	 */
42
	#[Override]
43
	public function getEntryID() {
44
		$entryids = [];
45
		$storelist = $GLOBALS["mapisession"]->getAllMessageStores();
46
47
		foreach ($storelist as $entryid => $store) {
48
			$entryids[] = bin2hex((string) $entryid);
49
		}
50
51
		return $entryids;
52
	}
53
54
	/**
55
	 * Executes all the actions in the $data variable.
56
	 */
57
	#[Override]
58
	public function execute() {
59
		foreach ($this->data as $actionType => $action) {
60
			if (!isset($actionType)) {
61
				continue;
62
			}
63
64
			try {
65
				$store = $this->getActionStore($action);
66
				$parententryid = $this->getActionParentEntryID($action);
67
				$entryid = $this->getActionEntryID($action);
68
				$this->store_entryid = $action["store_entryid"] ?? '';
69
70
				switch ($actionType) {
71
					case "keepalive":
72
						/*
73
						 * as we haven't done any processing here but still we need to send
74
						 * success message to client so client can know that there isn't any problem
75
						 * on server side (this will also make bus class happy as it will cry when
76
						 * there isn't any data to send to client).
77
						 */
78
						$this->sendFeedback(true);
79
						break;
80
81
					case "destroysession":
82
						// This actiontype should never get this far, but should already have been
83
						// intercepted by the Session class.
84
						// Nevertheless implement processing here for unforeseen cases.
85
						$this->sendFeedback(true);
86
						break;
87
88
					case "list":
89
						$this->hierarchyList();
90
						break;
91
92
					case "open":
93
						$folder = mapi_msgstore_openentry($store, $entryid);
94
						$data = $this->getFolderProps($store, $folder);
95
96
						// return response
97
						$this->addActionData("item", $data);
98
						$GLOBALS["bus"]->addData($this->getResponseData());
99
						break;
100
101
					case "foldersize":
102
						$folders = [];
103
104
						$folder = mapi_msgstore_openentry($store, $entryid);
105
						$data = $this->getFolderProps($store, $folder);
106
107
						$info = $this->getFolderSize($store, $folder, '', $folders);
108
109
						// It could be that the $props already contains the data,
110
						// this happens when the folder is the IPM_SUBTREE and the
111
						// folder size is read directly from the store. Adjust
112
						// total_size accordingly.
113
						if (isset($data["props"]["store_size"])) {
114
							if (!isset($data["props"]["message_size"])) {
115
								$data["props"]["message_size"] = $data["props"]["store_size"];
116
							}
117
							$data["props"]["total_message_size"] = $data["props"]["store_size"] + $info["total_size"];
118
						}
119
						else {
120
							$data["props"]["message_size"] = $info["size"];
121
							$data["props"]["total_message_size"] = $info["total_size"];
122
						}
123
						$data["folders"] = [
124
							"item" => $folders,
125
						];
126
127
						// return response
128
						$this->addActionData("item", $data);
129
						$GLOBALS["bus"]->addData($this->getResponseData());
130
						break;
131
132
					case "delete":
133
						if (!$store || !$parententryid || !$entryid) {
134
							break;
135
						}
136
						if (!isset($action["message_action"], $action["message_action"]["action_type"]) ||
137
							$action["message_action"]["action_type"] !== "removefavorites") {
138
							$this->deleteFolder($store, $parententryid, $entryid, $action);
0 ignored issues
show
$parententryid of type object is incompatible with the type string expected by parameter $parententryid of HierarchyModule::deleteFolder(). ( Ignorable by Annotation )

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

138
							$this->deleteFolder($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $action);
Loading history...
139
							break;
140
						}
141
						if (!isset($action["message_action"]["isSearchFolder"]) ||
142
							!$action["message_action"]["isSearchFolder"]) {
143
							$this->removeFromFavorite($entryid);
144
							break;
145
						}
146
						$result = $this->deleteSearchFolder($store, $parententryid, $entryid, $action);
0 ignored issues
show
$parententryid of type object is incompatible with the type array expected by parameter $parententryid of HierarchyModule::deleteSearchFolder(). ( Ignorable by Annotation )

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

146
						$result = $this->deleteSearchFolder($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $action);
Loading history...
147
						dump($result, '$result');
148
						if ($result) {
149
							$this->sendFeedback(true);
150
						}
151
						break;
152
153
					case "save":
154
						if (!$store || !$parententryid) {
155
							break;
156
						}
157
						if ($entryid) {
158
							// The "message_action" object has been set, check the action_type field for
159
							// the exact action which must be taken.
160
							// Supported actions:
161
							//   - copy: Copy the folder to the new destination folder
162
							//   - move: Move the folder to the new destination folder
163
							//   - emptyfolder: Delete all items within the folder
164
							//   - readflags: Mark all items within the folder as read
165
							//   - addtofavorites: Add the folder to "favorites"
166
							if (!isset($action["message_action"]["isSearchFolder"])) {
167
								$folder = mapi_msgstore_openentry($store, $entryid);
168
								$data = $this->getFolderProps($store, $folder);
169
							}
170
							if (isset($action["message_action"], $action["message_action"]["action_type"])) {
171
								switch ($action["message_action"]["action_type"]) {
172
									case "copy":
173
									case "move":
174
										$destentryid = false;
175
										if (isset($action["message_action"]["destination_parent_entryid"])) {
176
											$destentryid = hex2bin($action["message_action"]["destination_parent_entryid"]);
177
										}
178
179
										$deststore = $store;
180
										if (isset($action["message_action"]["destination_store_entryid"])) {
181
											$deststore = $GLOBALS['mapisession']->openMessageStore(hex2bin($action["message_action"]["destination_store_entryid"]));
182
										}
183
184
										if ($destentryid && $deststore) {
185
											$this->copyFolder($store, $parententryid, $entryid, $destentryid, $deststore, $action["message_action"]["action_type"] == "move");
0 ignored issues
show
$parententryid of type object is incompatible with the type string expected by parameter $parententryid of HierarchyModule::copyFolder(). ( Ignorable by Annotation )

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

185
											$this->copyFolder($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $destentryid, $deststore, $action["message_action"]["action_type"] == "move");
Loading history...
186
										}
187
										if ($data["props"]["container_class"] === "IPF.Contact") {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
188
											$GLOBALS["bus"]->notify(ADDRESSBOOK_ENTRYID, OBJECT_SAVE);
189
										}
190
										break;
191
192
									case "emptyfolder":
193
										$this->emptyFolder($store, $entryid);
194
										break;
195
196
									case "readflags":
197
										$this->setReadFlags($store, $entryid);
198
										break;
199
200
									case "addtofavorites":
201
										if (isset($action["message_action"]["isSearchFolder"]) && $action["message_action"]["isSearchFolder"]) {
202
											$searchStoreEntryId = $action["message_action"]["search_store_entryid"];
203
											// Set display name to search folder.
204
											$searchStore = $GLOBALS["mapisession"]->openMessageStore(hex2bin((string) $searchStoreEntryId));
205
											$searchFolder = mapi_msgstore_openentry($searchStore, $entryid);
206
											mapi_setprops($searchFolder, [
207
												PR_DISPLAY_NAME => $action["props"]["display_name"],
208
											]);
209
											mapi_savechanges($searchFolder);
210
											$this->createLinkedSearchFolder($searchFolder);
211
										}
212
										else {
213
											$this->addToFavorite($store, $entryid);
0 ignored issues
show
$store of type object is incompatible with the type string expected by parameter $store of HierarchyModule::addToFavorite(). ( Ignorable by Annotation )

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

213
											$this->addToFavorite(/** @scrutinizer ignore-type */ $store, $entryid);
Loading history...
214
										}
215
										break;
216
								}
217
							}
218
							else {
219
								// save folder
220
								$folder = mapi_msgstore_openentry($store, hex2bin((string) $action["entryid"]));
221
								$this->save($store, $folder, $action);
222
								if ($data["props"]["container_class"] === "IPF.Contact") {
223
									$GLOBALS["bus"]->notify(ADDRESSBOOK_ENTRYID, OBJECT_SAVE);
224
								}
225
								$this->sendFeedback(true, []);
226
							}
227
						}
228
						else {
229
							// no entryid, create new folder
230
							if ($store && $parententryid && isset($action["props"]["display_name"], $action["props"]["container_class"])) {
231
								if (isset($action["message_action"], $action["message_action"]["action_type"])) {
232
									// We need to create new search folder under the favorites folder
233
									// based on give search folder info.
234
									if ($action["message_action"]["action_type"] === "addtofavorites") {
235
										$storeEntryId = $action["message_action"]["search_store_entryid"];
236
										$searchFolderEntryId = $action["message_action"]["search_folder_entryid"];
237
238
										// Get the search folder and search criteria using $storeEntryId and $searchFolderEntryId.
239
										$Store = $GLOBALS["mapisession"]->openMessageStore(hex2bin((string) $storeEntryId));
240
										$searchFolder = mapi_msgstore_openentry($Store, hex2bin((string) $searchFolderEntryId));
241
										$searchCriteria = mapi_folder_getsearchcriteria($searchFolder);
242
243
										// Get FINDERS_ROOT folder from store.
244
										$finderRootFolder = mapi_getprops($Store, [PR_FINDER_ENTRYID]);
245
										$searchFolderRoot = mapi_msgstore_openentry($Store, $finderRootFolder[PR_FINDER_ENTRYID]);
246
247
										// Create new search folder in FINDERS_ROOT folder and set the search
248
										// criteria in newly created search folder.
249
										$newSearchFolder = mapi_folder_createfolder($searchFolderRoot, $action["props"]["display_name"], '', 0, FOLDER_SEARCH);
250
										$subfolder_flag = 0;
251
										if (isset($action["subfolders"]) && $action["subfolders"] == "true") {
252
											$subfolder_flag = RECURSIVE_SEARCH;
253
										}
254
										mapi_folder_setsearchcriteria($newSearchFolder, $searchCriteria['restriction'], $searchCriteria['folderlist'], $subfolder_flag);
255
256
										// Sleep for 1 seconds initially, since it usually takes ~  1 seconds to fill the search folder.
257
										sleep(1);
258
										$this->createLinkedSearchFolder($newSearchFolder);
259
									}
260
								}
261
								else {
262
									$this->addFolder($store, $parententryid, $action["props"]["display_name"], $action["props"]["container_class"]);
0 ignored issues
show
$parententryid of type object is incompatible with the type string expected by parameter $parententryid of HierarchyModule::addFolder(). ( Ignorable by Annotation )

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

262
									$this->addFolder($store, /** @scrutinizer ignore-type */ $parententryid, $action["props"]["display_name"], $action["props"]["container_class"]);
Loading history...
263
								}
264
							}
265
							if ($action["props"]["container_class"] === "IPF.Contact") {
266
								$GLOBALS["bus"]->notify(ADDRESSBOOK_ENTRYID, OBJECT_SAVE);
267
							}
268
						}
269
						break;
270
271
					case "closesharedfolder":
272
						if (isset($action["folder_type"]) && $action["folder_type"] != "all") {
273
							// We're closing a Shared folder, check if we still have other
274
							// folders for the same user opened, if not we can safely close
275
							// the usrstore.
276
							$stores = $GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/shared_stores/" . strtolower(bin2hex((string) $action["user_name"])));
277
							if (!isset($stores) || empty($stores) || (count($stores) == 1 && isset($stores[$action["folder_type"]]))) {
278
								$entryid = $GLOBALS["mapisession"]->removeUserStore($action["user_name"]);
279
							}
280
							else {
281
								$entryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser($action["user_name"]);
282
								$this->removeFromFavorite(hex2bin((string) $action["entryid"]), $store, PR_WLINK_ENTRYID, false);
283
							}
284
						}
285
						else {
286
							// We're closing a Shared Store, simply remove it from the session.
287
							$entryid = $GLOBALS["mapisession"]->removeUserStore($action["user_name"]);
288
289
							if (isset($action["remove_favorites"]) && $action["remove_favorites"]) {
290
								$this->removeFromFavorite(hex2bin((string) $action["store_entryid"]), $store, PR_WLINK_STORE_ENTRYID, false);
291
							}
292
						}
293
294
						$data = [];
295
						$data["store_entryid"] = bin2hex((string) $entryid);
296
						if (isset($action["folder_type"])) {
297
							$data["folder_type"] = $action["folder_type"];
298
						}
299
300
						$this->addActionData("delete", $data);
301
						$GLOBALS["bus"]->addData($this->getResponseData());
302
						$GLOBALS["bus"]->notify(ADDRESSBOOK_ENTRYID, OBJECT_SAVE);
303
						break;
304
305
					case "opensharedfolder":
306
						$username = strtolower((string) $action["user_name"]);
307
						$store = $GLOBALS["mapisession"]->addUserStore($username);
308
						if (!$store) {
309
							break;
310
						}
311
312
						$options = [$username => [$action["folder_type"] => $action]];
313
						$data = $GLOBALS["operations"]->getHierarchyList($this->list_properties, HIERARCHY_GET_ONE, $store, $options, $username);
314
315
						if (empty($data["item"][0]["folders"]["item"])) {
316
							throw new MAPIException(null, MAPI_E_NO_ACCESS);
317
						}
318
319
						$folders = count($data["item"][0]["folders"]["item"]);
320
						if ($folders === 0) {
321
							throw new MAPIException(null, MAPI_E_NO_ACCESS);
322
						}
323
324
						$noPermissionFolders = array_filter($data['item'][0]['folders']['item'], fn ($item) => $item['props']['access'] === 0);
325
						if (count($noPermissionFolders) >= $folders) {
326
							// Throw an exception that we couldn't open the shared store,
327
							// lets have processException() fill in our error message.
328
							throw new MAPIException(null, MAPI_E_NO_ACCESS);
329
						}
330
331
						$this->addActionData("list", $data);
332
						$GLOBALS["bus"]->addData($this->getResponseData());
333
						$GLOBALS["bus"]->notify(ADDRESSBOOK_ENTRYID, OBJECT_SAVE);
334
						break;
335
336
					case "sharedstoreupdate":
337
						$supported_types = ['inbox' => 1, 'all' => 1];
338
						$users = $GLOBALS["settings"]->get("zarafa/v1/contexts/hierarchy/shared_stores", []);
339
340
						foreach ($users as $username => $data) {
341
							$key = array_keys($data)[0];
342
							$folder_type = $data[$key]['folder_type'];
343
344
							if (!isset($supported_types[$folder_type])) {
345
								continue;
346
							}
347
348
							$GLOBALS["bus"]->notify(REQUEST_ENTRYID, HIERARCHY_UPDATE, [strtolower(hex2bin((string) $username)), $folder_type]);
349
						}
350
351
						$this->sendFeedback(true);
352
						break;
353
354
					default:
355
						$this->handleUnknownActionType($actionType);
356
				}
357
			}
358
			catch (MAPIException $e) {
359
				$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...
360
			}
361
		}
362
	}
363
364
	/**
365
	 * Function does customization of exception based on module data.
366
	 * like, here it will generate display message based on actionType
367
	 * for particular exception, and send feedback to the client.
368
	 *
369
	 * @param object     $e             Exception object
370
	 * @param string     $actionType    the action type, sent by the client
371
	 * @param MAPIobject $store         store object of the folder
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...
372
	 * @param string     $parententryid parent entryid of the message
373
	 * @param string     $entryid       entryid of the folder
374
	 * @param array      $action        the action data, sent by the client
375
	 */
376
	#[Override]
377
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
378
		if (is_null($e->displayMessage)) {
379
			switch ($actionType) {
380
				case "list":
381
					$e->setDisplayMessage(_("Could not load the hierarchy."));
382
					break;
383
384
				case "open":
385
					$e->setDisplayMessage(_("Could not load folder properties."));
386
					break;
387
388
				case "delete":
389
					if (isset($action["message_action"], $action["message_action"]["action_type"]) &&
390
						$action["message_action"]["action_type"] === "removefavorites") {
391
						$e->setDisplayMessage(_("Could not remove folder from favorites."));
392
					}
393
					else {
394
						if ($e->getCode() == MAPI_E_NO_ACCESS) {
395
							$e->setDisplayMessage(_("You have insufficient privileges to delete folder."));
396
						}
397
						else {
398
							$e->setDisplayMessage(_("Could not delete folder."));
399
						}
400
						break;
401
					}
402
403
					// no break
404
				case "save":
405
					if ($entryid) {
406
						if (isset($action["message_action"], $action["message_action"]["action_type"])) {
407
							switch ($action["message_action"]["action_type"]) {
408
								case "copy":
409
									if ($e->getCode() == MAPI_E_NO_ACCESS) {
410
										$e->setDisplayMessage(_("You have insufficient privileges to copy folder."));
411
									}
412
									else {
413
										$e->setDisplayMessage(_("Could not copy folder."));
414
									}
415
									break;
416
417
								case "move":
418
									if ($e->getCode() == MAPI_E_NO_ACCESS) {
419
										$e->setDisplayMessage(_("You have insufficient privileges to move this folder."));
420
									}
421
									else {
422
										$e->setDisplayMessage(_("Could not move folder."));
423
									}
424
									break;
425
426
								case "emptyfolder":
427
									if ($e->getCode() == MAPI_E_NO_ACCESS) {
428
										$e->setDisplayMessage(_("You have insufficient privileges to delete items."));
429
									}
430
									else {
431
										$e->setDisplayMessage(_("Could not empty folder."));
432
									}
433
									break;
434
435
								case "readflags":
436
									$e->setDisplayMessage(_("Could not perform action correctly."));
437
									break;
438
439
								case "addtofavorites":
440
									if ($e->getCode() == MAPI_E_COLLISION) {
441
										$e->setDisplayMessage(_("A favorite folder with this name already exists, please use another name."));
442
									}
443
									else {
444
										$e->setDisplayMessage(_("Could not add folder to favorites."));
445
									}
446
									break;
447
							}
448
						}
449
						else {
450
							// Exception generated while setting folder permissions.
451
							if (isset($action["permissions"])) {
452
								if ($e->getCode() == MAPI_E_NO_ACCESS) {
453
									$e->setDisplayMessage(_("You have insufficient privileges to set permissions for this folder."));
454
								}
455
								else {
456
									$e->setDisplayMessage(_("Could not set folder permissions."));
457
								}
458
							}
459
							else {
460
								// Exception generated while renaming folder.
461
								match ($e->getCode()) {
462
									MAPI_E_NO_ACCESS => $e->setDisplayMessage(_("You have insufficient privileges to rename this folder.")),
463
									MAPI_E_COLLISION => $e->setDisplayMessage(_("A folder with this name already exists. Use another name.")),
464
									default => $e->setDisplayMessage(_("Could not rename folder.")),
465
								};
466
							}
467
						}
468
					}
469
					else {
470
						// Exception generated while creating new folder.
471
						match ($e->getCode()) {
472
							MAPI_E_NO_ACCESS => $e->setDisplayMessage(_("You have insufficient privileges to create this folder.")),
473
							MAPI_E_COLLISION => $e->setDisplayMessage(_("A folder with this name already exists. Use another name.")),
474
							default => $e->setDisplayMessage(_("Could not create folder.")),
475
						};
476
					}
477
					break;
478
479
				case "closesharedfolder":
480
					$e->setDisplayMessage(_("Could not close shared folder."));
481
					break;
482
483
				case "opensharedfolder":
484
					if ($e->getCode() == MAPI_E_NOT_FOUND) {
485
						$e->setDisplayMessage(_("User could not be resolved."));
486
					}
487
					else {
488
						$folderType = $action["folder_type"];
489
						if ($folderType == "all") {
490
							$folderType = 'entire inbox';
491
						}
492
						$e->setDisplayMessage(sprintf(_('You have insufficient privileges to open this %1$s folder. The folder owner can set these using the \'permissions\'-tab of the folder properties (right click the %1$s folder > properties > permissions).'), $folderType));
493
					}
494
					break;
495
			}
496
		}
497
498
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
499
	}
500
501
	/**
502
	 * Generates the hierarchy list. All folders and subfolders are added to response data.
503
	 */
504
	public function hierarchyList() {
505
		$data = $GLOBALS["operations"]->getHierarchyList($this->list_properties);
506
507
		$this->addActionData("list", $data);
508
		$GLOBALS["bus"]->addData($this->getResponseData());
509
	}
510
511
	/**
512
	 * Add folder's properties to response data. This function doesn't add permission details yet.
513
	 *
514
	 * @param resource $store      mapi store of the folder
515
	 * @param string   $entryid    entryid of the folder
516
	 * @param string   $actionType type of action
517
	 */
518
	public function addFolderToResponseData($store, $entryid, $actionType) {
519
		$folder = mapi_msgstore_openentry($store, $entryid);
520
		$folderProps = mapi_getprops($folder, $this->list_properties);
521
522
		$data = $GLOBALS["operations"]->setFolder($folderProps);
523
		$this->addActionData($actionType, $data);
524
	}
525
526
	/**
527
	 * Adds a folder to the hierarchylist.
528
	 *
529
	 * @param object $store         message Store Object
530
	 * @param string $parententryid entryid of the parent folder
531
	 * @param string $name          name of the new folder
532
	 * @param string $type          type of the folder (calendar, mail, ...).
533
	 *
534
	 * @return bool true on success or false on failure
535
	 */
536
	public function addFolder($store, $parententryid, $name, $type) {
537
		$props = [];
538
		$result = $GLOBALS["operations"]->createFolder($store, $parententryid, $name, $type, $props);
539
540
		if ($result && isset($props[PR_ENTRYID])) {
541
			// Notify about this newly created folder
542
			$this->addFolderToResponseData($store, $props[PR_ENTRYID], "update");
0 ignored issues
show
$store of type object is incompatible with the type resource expected by parameter $store of HierarchyModule::addFolderToResponseData(). ( Ignorable by Annotation )

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

542
			$this->addFolderToResponseData(/** @scrutinizer ignore-type */ $store, $props[PR_ENTRYID], "update");
Loading history...
543
544
			// Add all response data to Bus
545
			$GLOBALS["bus"]->addData($this->getResponseData());
546
547
			// Notify parent folder that a folder has been added
548
			$props[PR_ENTRYID] = $parententryid;
549
			$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_SAVE, $props);
550
		}
551
552
		return $result;
553
	}
554
555
	/**
556
	 * returns properties of a folder, used by the properties dialog.
557
	 *
558
	 * @param mixed $store
559
	 * @param mixed $folder
560
	 */
561
	public function getFolderProps($store, $folder) {
562
		$data = $GLOBALS["operations"]->getProps($folder, $this->properties);
563
564
		// adding container_class if missing
565
		if (!isset($data["props"]["container_class"])) {
566
			$data["props"]["container_class"] = "IPF.Note";
567
		}
568
569
		// replace "IPM_SUBTREE" with the display name of the store, and use the store message size
570
		$store_props = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID]);
571
		if ($data["entryid"] == bin2hex((string) $store_props[PR_IPM_SUBTREE_ENTRYID])) {
572
			$store_props = mapi_getprops($store, [PR_DISPLAY_NAME, PR_MESSAGE_SIZE_EXTENDED,
573
				PR_CONTENT_COUNT, PR_QUOTA_WARNING_THRESHOLD, PR_PROHIBIT_SEND_QUOTA, PR_PROHIBIT_RECEIVE_QUOTA, ]);
574
			$data["props"]["display_name"] = $store_props[PR_DISPLAY_NAME];
575
			$data["props"]["message_size"] = round($store_props[PR_MESSAGE_SIZE_EXTENDED]);
576
			$data["props"]["content_count"] = $store_props[PR_CONTENT_COUNT];
577
			$data["props"]["store_size"] = round($store_props[PR_MESSAGE_SIZE_EXTENDED]);
578
579
			if (isset($store_props[PR_QUOTA_WARNING_THRESHOLD])) {
580
				$data["props"]["quota_warning"] = round($store_props[PR_QUOTA_WARNING_THRESHOLD]);
581
			}
582
			if (isset($store_props[PR_PROHIBIT_SEND_QUOTA])) {
583
				$data["props"]["quota_soft"] = round($store_props[PR_PROHIBIT_SEND_QUOTA]);
584
			}
585
			if (isset($store_props[PR_PROHIBIT_RECEIVE_QUOTA])) {
586
				$data["props"]["quota_hard"] = round($store_props[PR_PROHIBIT_RECEIVE_QUOTA]);
587
			}
588
		}
589
590
		// calculating missing message_size
591
		if (!isset($data["props"]["message_size"])) {
592
			$data["props"]["message_size"] = round($GLOBALS["operations"]->calcFolderMessageSize($folder, false));
593
		}
594
595
		// retrieving folder permissions
596
		$data["permissions"] = [
597
			"item" => $this->getFolderPermissions($folder),
598
		];
599
600
		return $data;
601
	}
602
603
	/**
604
	 * Returns the size and total_size of the given folder.
605
	 *
606
	 * @param mapistore  $store       The store to which the folder belongs
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...
607
	 * @param mapifolder $folder      The folder for which the size must be calculated
0 ignored issues
show
The type mapifolder 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...
608
	 * @param string     $pathname    The path of the current folder
609
	 * @param array      &$subfolders The array in which all information for the subfolders are stored
610
	 * @param bool       $hidden      True to prevent the subfolders to be stored into the $subfolders argument
611
	 *
612
	 * @return array The response data
613
	 */
614
	public function getFolderSize($store, $folder, $pathname, &$subfolders, $hidden = false) {
615
		$columns = [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID, PR_OBJECT_TYPE, PR_DISPLAY_NAME, PR_ATTR_HIDDEN];
616
		$size = $GLOBALS["operations"]->calcFolderMessageSize($folder, false);
617
		$total_size = $size;
618
619
		$table = mapi_folder_gethierarchytable($folder, MAPI_DEFERRED_ERRORS);
620
621
		mapi_table_setcolumns($table, $columns);
622
		$columns = null;
623
624
		$rows = mapi_table_queryrows($table, $columns, 0, 0x7FFFFFF);
625
		foreach ($rows as $row) {
626
			$subfolder = mapi_msgstore_openentry($store, $row[PR_ENTRYID]);
627
			$subpath = (!empty($pathname) ? ($pathname . '\\') : '') . $row[PR_DISPLAY_NAME];
628
629
			/**
630
			 * Don't add  hidden folders, folders with PR_ATTR_HIDDEN property set
631
			 * should not be shown to the client.
632
			 */
633
			$hide = $hidden === true || (isset($row[PR_ATTR_HIDDEN]) && $row[PR_ATTR_HIDDEN] === true);
634
			$info = $this->getFolderSize($store, $subfolder, $subpath, $subfolders, $hide);
635
636
			if ($hide !== true) {
637
				array_push($subfolders, [
638
					"entryid" => bin2hex((string) $row[PR_ENTRYID]),
639
					"parent_entryid" => bin2hex((string) $row[PR_PARENT_ENTRYID]),
640
					"store_entryid" => bin2hex((string) $row[PR_STORE_ENTRYID]),
641
					"props" => [
642
						"folder_pathname" => $subpath, // This equals PR_FOLDER_PATHNAME, which is not supported by Gromox
643
						"display_name" => $row[PR_DISPLAY_NAME],
644
						"object_type" => $row[PR_OBJECT_TYPE],
645
						"message_size" => $info["size"],
646
						"total_message_size" => $info["total_size"],
647
					],
648
				]);
649
			}
650
651
			$total_size += $info["total_size"];
652
		}
653
654
		return ["size" => $size, "total_size" => $total_size];
655
	}
656
657
	/**
658
	 * Function which saves changed properties to a folder.
659
	 *
660
	 * @param object $store  MAPI object of the store
661
	 * @param object $folder MAPI object of the folder
662
	 * @param mixed  $action
663
	 */
664
	public function save($store, $folder, $action) {
665
		// Rename folder
666
		if (isset($action["props"]["display_name"])) {
667
			$this->modifyFolder($store, hex2bin((string) $action["entryid"]), $action["props"]["display_name"]);
668
		}
669
670
		if (isset($action["props"]["comment"])) {
671
			mapi_setprops($folder, [PR_COMMENT => $action["props"]["comment"]]);
672
		}
673
674
		if (isset($action["permissions"])) {
675
			$this->setFolderPermissions($folder, $action["permissions"]);
676
			if (isset($action['props']['recursive'])) {
677
				$hierarchyTable = mapi_folder_gethierarchytable($folder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
678
				$subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID]);
679
				foreach ($subfolders as $subfolder) {
680
					$folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]);
681
					$this->setFolderPermissions($folderObject, $action["permissions"]);
682
					mapi_savechanges($folderObject);
683
				}
684
			}
685
		}
686
687
		mapi_savechanges($folder);
688
	}
689
690
	public function getFolderPermissions($folder) {
691
		$eidObj = $GLOBALS["entryid"]->createMsgStoreEntryIdObj(hex2bin((string) $this->store_entryid));
692
		$cnUserPos = strrpos((string) $eidObj['MailboxDN'], '/cn=');
693
		$cnUserBase = ($cnUserPos !== false) ? substr((string) $eidObj['MailboxDN'], 0, $cnUserPos) : '';
694
		$grants = mapi_zarafa_getpermissionrules($folder, ACCESS_TYPE_GRANT);
695
		foreach ($grants as $id => $grant) {
696
			// The mapi_zarafa_getpermissionrules returns the entryid in the userid key
697
			$userinfo = $this->getUserInfo($grant, $cnUserBase);
698
699
			$rights = [];
700
			$rights["entryid"] = $userinfo["entryid"];
701
			$rights["props"] = [];
702
			$rights["props"]["type"] = ACCESS_TYPE_GRANT;
703
			$rights["props"]["display_name"] = $userinfo["fullname"];
704
			$rights["props"]["object_type"] = $userinfo["type"];
705
			$rights["props"]["entryid"] = $userinfo["entryid"];
706
			$rights["props"]["rights"] = $grant["rights"];
707
708
			$grants[$id] = $rights;
709
		}
710
711
		return $grants;
712
	}
713
714
	public function setFolderPermissions($folder, $permissions) {
715
		$folderProps = mapi_getprops($folder, [PR_DISPLAY_NAME, PR_STORE_ENTRYID, PR_ENTRYID]);
716
		$store = $GLOBALS["mapisession"]->openMessageStore($folderProps[PR_STORE_ENTRYID]);
717
		$currentPermissions = $this->getFolderPermissions($folder);
718
719
		// check if the folder is the default calendar, if so we also need to set the same permissions on the freebusy folder
720
		$root = mapi_msgstore_openentry($store);
721
		if ($root) {
722
			$rootProps = mapi_getprops($root, [PR_IPM_APPOINTMENT_ENTRYID]);
723
			if ($folderProps[PR_ENTRYID] == $rootProps[PR_IPM_APPOINTMENT_ENTRYID]) {
724
				$freebusy = FreeBusy::getLocalFreeBusyFolder($store);
725
			}
726
		}
727
728
		// first, get the current permissions because we need to delete all current acl's
729
		$curAcls = mapi_zarafa_getpermissionrules($folder, ACCESS_TYPE_GRANT);
730
		$eidObj = $GLOBALS["entryid"]->createMsgStoreEntryIdObj(hex2bin((string) $this->store_entryid));
731
		$cnUserPos = strrpos((string) $eidObj['MailboxDN'], '/cn=');
732
		$cnUserBase = ($cnUserPos !== false) ? substr((string) $eidObj['MailboxDN'], 0, $cnUserPos) : '';
733
		foreach ($curAcls as &$curAcl) {
734
			$curAcl = $this->getUserInfo($curAcl, $cnUserBase);
735
		}
736
737
		// First check which permissions should be removed from the existing list
738
		if (isset($permissions['remove']) && !empty($permissions['remove'])) {
739
			foreach ($permissions['remove'] as $i => &$delAcl) {
740
				$userid = hex2bin((string) $delAcl['entryid']);
741
				foreach ($curAcls as $aclIndex => &$curAcl) {
742
					// do not remove default and anonymous grants
743
					if ($curAcl['userid'] === $userid && $curAcl['memberid'] != 0 && $curAcl['memberid'] != 0xFFFFFFFF) {
744
						$curAcl['rights'] = ecRightsNone;
745
						$curAcl['state'] = RIGHT_DELETED | RIGHT_AUTOUPDATE_DENIED;
746
					}
747
				}
748
				unset($curAcl);
749
			}
750
			unset($delAcl);
751
		}
752
753
		// Then we check which permissions must be updated in the existing list
754
		if (isset($permissions['modify']) && !empty($permissions['modify'])) {
755
			foreach ($permissions['modify'] as $i => &$modAcl) {
756
				$entryid = $modAcl['entryid'];
757
				// No permission for this user
758
				// This is necessary for recursive folder permissions.
759
				// If a subfolder does not have any permissions for the user yet,
760
				// they need to be added instead of modified.
761
				if (!$this->idInCurrentPermissions($currentPermissions, $entryid)) {
762
					if (!isset($permissions['add'])) {
763
						$permissions['add'] = [];
764
					}
765
					array_push($permissions['add'], $modAcl);
766
				}
767
				else {
768
					$userid = hex2bin((string) $entryid);
769
					foreach ($curAcls as $aclIndex => &$curAcl) {
770
						if ($curAcl['userid'] === $userid) {
771
							$curAcl['rights'] = $modAcl['rights'];
772
							$curAcl['state'] = RIGHT_MODIFY | RIGHT_AUTOUPDATE_DENIED;
773
						}
774
						if (isset($curAcl['memberid']) && ($curAcl['memberid'] == 0 || $curAcl['memberid'] == 0xFFFFFFFF)) {
775
							$curAcl['userid'] = null;
776
						}
777
					}
778
				}
779
				unset($curAcl);
780
			}
781
			unset($modAcl);
782
		}
783
784
		// Finally we check which permissions must be added to the existing list
785
		if (isset($permissions['add']) && !empty($permissions['add'])) {
786
			$cnt = count($curAcls);
787
			foreach ($permissions['add'] as $i => &$addAcl) {
788
				$curAcls[$cnt++] = [
789
					'type' => ACCESS_TYPE_GRANT,
790
					'userid' => hex2bin((string) $addAcl['entryid']),
791
					'rights' => $addAcl['rights'],
792
					'state' => RIGHT_NEW | RIGHT_AUTOUPDATE_DENIED,
793
					'memberid' => 0, // for new permissions memberid may be any number
794
				];
795
			}
796
			unset($addAcl);
797
		}
798
799
		if (!empty($curAcls)) {
800
			mapi_zarafa_setpermissionrules($folder, $curAcls);
801
802
			// $freebusy is only set when the calendar folder permissions is updated
803
			if (isset($freebusy) && $freebusy !== false) {
804
				// set permissions on free/busy message
805
				foreach ($curAcls as $key => &$acl) {
806
					if ($acl['type'] == ACCESS_TYPE_GRANT && ($acl['rights'] & ecRightsEditOwned)) {
807
						$acl['rights'] |= ecRightsEditAny;
808
					}
809
				}
810
				unset($acl);
811
				mapi_zarafa_setpermissionrules($freebusy, $curAcls);
812
			}
813
		}
814
	}
815
816
	public function idInCurrentPermissions($currentPermissions, $entryid) {
817
		foreach ($currentPermissions as $permission) {
818
			if ($permission['entryid'] === $entryid) {
819
				return true;
820
			}
821
		}
822
823
		return false;
824
	}
825
826
	public function getUserInfo($grant, $cnUserBase) {
827
		// Create fake entryids for default and anonymous permissions
828
		if ($grant["memberid"] == 0) {
829
			$entryid = $GLOBALS["entryid"]->createMuidemsabEntryid($cnUserBase . '/cn=0000000000000000-default');
830
			$grant["fullname"] = _("default");
831
			$grant["username"] = _("default");
832
			$grant["entryid"] = $entryid;
833
			$grant["userid"] = hex2bin((string) $entryid);
834
835
			return $grant;
836
		}
837
838
		if ($grant["memberid"] == 0xFFFFFFFF) {
839
			$entryid = $GLOBALS["entryid"]->createMuidemsabEntryid($cnUserBase . '/cn=ffffffffffffffff-anonymous');
840
			$grant["fullname"] = _("anonymous");
841
			$grant["username"] = _("anonymous");
842
			$grant["entryid"] = $entryid;
843
			$grant["userid"] = hex2bin((string) $entryid);
844
845
			return $grant;
846
		}
847
848
		// open the addressbook
849
		$ab = $GLOBALS["mapisession"]->getAddressbook();
850
		$user = mapi_ab_openentry($ab, $grant["userid"]);
851
		if ($user) {
852
			$props = mapi_getprops($user, [PR_ACCOUNT, PR_DISPLAY_NAME, PR_OBJECT_TYPE]);
853
854
			$grant["fullname"] = $props[PR_DISPLAY_NAME];
855
			$grant["username"] = $props[PR_ACCOUNT];
856
			$grant["entryid"] = bin2hex((string) $grant["userid"]);
857
858
			return $grant;
859
		}
860
861
		error_log(sprintf("No user with the entryid %s found (memberid: %s)", bin2hex((string) $grant["userid"]), $grant["memberid"]));
862
863
		// default return stuff
864
		return [
865
			"fullname" => _("Unknown user/group"),
866
			"username" => _("unknown"),
867
			"entryid" => null,
868
			"type" => MAPI_MAILUSER,
869
			"id" => $grant["userid"],
870
			"userid" => null,
871
		];
872
	}
873
874
	/**
875
	 * Function is used to get the IPM_COMMON_VIEWS folder from defaults store.
876
	 *
877
	 * @return object MAPI folder object
878
	 */
879
	public function getCommonViewsFolder() {
880
		$defaultStore = $GLOBALS["mapisession"]->getDefaultMessageStore();
881
		$commonViewsFolderEntryid = mapi_getprops($defaultStore, [PR_COMMON_VIEWS_ENTRYID]);
882
		$commonViewsFolder = mapi_msgstore_openentry($defaultStore, $commonViewsFolderEntryid[PR_COMMON_VIEWS_ENTRYID]);
883
884
		return $commonViewsFolder;
885
	}
886
887
	/**
888
	 * Remove favorites link message from associated contains table of IPM_COMMON_VIEWS.
889
	 * It will also remove favorites search folders of given store.
890
	 *
891
	 * @param string $entryid  entryid of the folder
892
	 * @param object $store    MAPI object of the store
893
	 * @param string $prop     property which is used to find record from associated contains table of
894
	 *                         IPM_COMMON_VIEWS folder
895
	 * @param bool   $doNotify true to notify the IPM_COMMO_VIEWS folder on client side
896
	 */
897
	public function removeFromFavorite($entryid, $store = false, $prop = PR_WLINK_ENTRYID, $doNotify = true) {
898
		$commonViewsFolder = $this->getCommonViewsFolder();
899
		$associatedTable = mapi_folder_getcontentstable($commonViewsFolder, MAPI_ASSOCIATED);
900
901
		$restriction = [RES_OR,
902
			[
903
				[RES_PROPERTY,
904
					[
905
						RELOP => RELOP_EQ,
906
						ULPROPTAG => PR_MESSAGE_CLASS,
907
						VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.Link"],
908
					],
909
				],
910
				[RES_PROPERTY,
911
					[
912
						RELOP => RELOP_EQ,
913
						ULPROPTAG => PR_MESSAGE_CLASS,
914
						VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
915
					],
916
				],
917
			],
918
		];
919
		$finderHierarchyTables = [];
920
		if (!empty($store)) {
921
			$props = mapi_getprops($store, [PR_FINDER_ENTRYID]);
922
923
			try {
924
				$finderFolder = mapi_msgstore_openentry($store, $props[PR_FINDER_ENTRYID]);
925
				$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
926
				$finderHierarchyTables[$props[PR_FINDER_ENTRYID]] = $hierarchyTable;
927
			}
928
			catch (Exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
929
			}
930
		}
931
932
		$messages = mapi_table_queryallrows($associatedTable, [PR_ENTRYID, PR_MESSAGE_CLASS, PR_WB_SF_ID, PR_WLINK_ENTRYID, PR_WLINK_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID], $restriction);
933
934
		if (!empty($messages)) {
935
			foreach ($messages as $message) {
936
				if ($message[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.SFInfo" && !empty($finderHierarchyTables)) {
937
					$props = $GLOBALS["operations"]->getFavoritesLinkedSearchFolderProps($message[PR_WB_SF_ID], $finderHierarchyTables);
938
					if (!empty($props)) {
939
						$this->deleteSearchFolder($store, $props[PR_PARENT_ENTRYID], $props[PR_ENTRYID], []);
0 ignored issues
show
It seems like $store can also be of type false; however, parameter $store of HierarchyModule::deleteSearchFolder() does only seem to accept object, 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

939
						$this->deleteSearchFolder(/** @scrutinizer ignore-type */ $store, $props[PR_PARENT_ENTRYID], $props[PR_ENTRYID], []);
Loading history...
940
					}
941
				}
942
				elseif ($message[PR_MESSAGE_CLASS] === "IPM.Microsoft.WunderBar.Link") {
943
					if (isset($message[$prop]) && $GLOBALS['entryid']->compareEntryIds($message[$prop], $entryid)) {
944
						mapi_folder_deletemessages($commonViewsFolder, [$message[PR_ENTRYID]]);
945
						if ($doNotify) {
946
							$GLOBALS["bus"]->notify(bin2hex((string) $message[PR_ENTRYID]), OBJECT_SAVE, $message);
947
						}
948
					}
949
					elseif (isset($message[PR_WLINK_STORE_ENTRYID])) {
950
						$storeObj = $GLOBALS["mapisession"]->openMessageStore($message[PR_WLINK_STORE_ENTRYID]);
951
						$storeProps = mapi_getprops($storeObj, [PR_ENTRYID]);
952
						if ($GLOBALS['entryid']->compareEntryIds($message[PR_WLINK_ENTRYID], $storeProps[PR_ENTRYID])) {
953
							mapi_folder_deletemessages($commonViewsFolder, [$message[PR_ENTRYID]]);
954
							$this->sendFeedback(true);
955
						}
956
					}
957
				}
958
			}
959
		}
960
	}
961
962
	/**
963
	 * Function which is used to remove the search link message(IPM.Microsoft.WunderBar.SFInfo)
964
	 * from associated contains table of IPM_COMMON_VIEWS folder.
965
	 *
966
	 * @param string $searchFolderId GUID that identifies the search folder
967
	 */
968
	public function removeSearchLinkMessage($searchFolderId) {
969
		$commonViewsFolder = $this->getCommonViewsFolder();
970
		$associatedTable = mapi_folder_getcontentstable($commonViewsFolder, MAPI_ASSOCIATED);
971
972
		$restriction = [RES_AND,
973
			[
974
				[RES_PROPERTY,
975
					[
976
						RELOP => RELOP_EQ,
977
						ULPROPTAG => PR_MESSAGE_CLASS,
978
						VALUE => [PR_MESSAGE_CLASS => "IPM.Microsoft.WunderBar.SFInfo"],
979
					],
980
				],
981
				[RES_PROPERTY,
982
					[
983
						RELOP => RELOP_EQ,
984
						ULPROPTAG => PR_WB_SF_ID,
985
						VALUE => [PR_WB_SF_ID => hex2bin($searchFolderId)],
986
					],
987
				],
988
			],
989
		];
990
991
		$messages = mapi_table_queryallrows($associatedTable, [PR_WB_SF_ID, PR_ENTRYID], $restriction);
992
993
		if (!empty($messages)) {
994
			foreach ($messages as $message) {
995
				if (bin2hex((string) $message[PR_WB_SF_ID]) === $searchFolderId) {
996
					mapi_folder_deletemessages($commonViewsFolder, [$message[PR_ENTRYID]]);
997
				}
998
			}
999
		}
1000
	}
1001
1002
	/**
1003
	 * Function is used to create link message for the selected folder
1004
	 * in associated contains of IPM_COMMON_VIEWS folder.
1005
	 *
1006
	 * @param string $store   $store entryid of the store
1007
	 * @param string $entryid $entryid entryid of the MAPI folder
1008
	 */
1009
	public function addToFavorite($store, $entryid) {
1010
		$commonViewsFolder = $this->getCommonViewsFolder();
1011
1012
		// In Favorites list all folders are must be sibling of other folders.
1013
		// whether it is sub folder of any other folders.
1014
		// So unset "subfolders" property as we don't required to show sub folders in favorites list.
1015
		unset($this->properties["subfolders"]);
1016
		$folder = mapi_msgstore_openentry($store, $entryid);
1017
		$folderProps = mapi_getprops($folder, $this->properties);
1018
		$GLOBALS["operations"]->createFavoritesLink($commonViewsFolder, $folderProps);
1019
1020
		$this->addActionData("update", $GLOBALS["operations"]->setFavoritesFolder($folderProps));
1021
		$GLOBALS["bus"]->addData($this->getResponseData());
1022
	}
1023
1024
	/**
1025
	 * Function which is used delete the search folder from respective store.
1026
	 *
1027
	 * @param object $store         $store $store MAPI store in which search folder is belongs
1028
	 * @param array  $parententryid $parententryid parent folder to search folder it is FIND_ROOT folder which
1029
	 *                              treated as search root folder
1030
	 * @param string $entryid       $entryid search folder entryid which is going to remove
1031
	 * @param array  $action        the action data, sent by the client
1032
	 */
1033
	public function deleteSearchFolder($store, $parententryid, $entryid, $action) {
1034
		$folder = mapi_msgstore_openentry($store, $entryid);
1035
		$props = mapi_getprops($folder, [PR_EXTENDED_FOLDER_FLAGS]);
1036
		// for more information about PR_EXTENDED_FOLDER_FLAGS go through this link
1037
		// https://msdn.microsoft.com/en-us/library/ee203919(v=exchg.80).aspx
1038
		$flags = unpack("H2ExtendedFlags-Id/H2ExtendedFlags-Cb/H8ExtendedFlags-Data/H2SearchFolderTag-Id/H2SearchFolderTag-Cb/H8SearchFolderTag-Data/H2SearchFolderId-Id/H2SearchFolderId-Cb/H32SearchFolderId-Data", (string) $props[PR_EXTENDED_FOLDER_FLAGS]);
1039
		$searchFolderId = $flags["SearchFolderId-Data"];
1040
		$this->removeSearchLinkMessage($searchFolderId);
1041
1042
		// Do not remove the search folder when the 'keepSearchFolder' flag is set.
1043
		// This flag indicates there is currently an open search tab which uses this search folder.
1044
		if (!isset($action["message_action"]["keepSearchFolder"])) {
1045
			$finderFolder = mapi_msgstore_openentry($store, $parententryid);
1046
1047
			return mapi_folder_deletefolder($finderFolder, $entryid, DEL_FOLDERS | DEL_MESSAGES | DELETE_HARD_DELETE);
1048
		}
1049
		// Rename search folder to default search folder name otherwise,
1050
		// It will not be picked up by our search folder cleanup logic.
1051
		$storeProps = mapi_getprops($store, [PR_FINDER_ENTRYID]);
1052
		$props = [];
1053
		$folder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
1054
		$folderName = $GLOBALS["operations"]->checkFolderNameConflict($store, $folder, "grommunio Web Search Folder");
1055
1056
		return $GLOBALS["operations"]->renameFolder($store, $entryid, $folderName, $props);
1057
	}
1058
1059
	/**
1060
	 * Function which is used create link message for the created search folder.
1061
	 * in associated contains table of IPM_COMMON_VIEWS folder.
1062
	 *
1063
	 * @param object $folder MAPI search folder for which link message needs to
1064
	 *                       create in associated contains table of IPM_COMMON_VIEWS folder
1065
	 */
1066
	public function createLinkedSearchFolder($folder) {
1067
		$searchFolderTag = openssl_random_pseudo_bytes(4);
1068
		$searchFolderId = openssl_random_pseudo_bytes(16);
1069
1070
		// PR_EXTENDED_FOLDER_FLAGS used to create permanent/persistent search folder in MAPI.
1071
		// also it used to identify/get the linked search folder from FINDER_ROOT folder.
1072
		// PR_EXTENDED_FOLDER_FLAGS contains at least the SearchFolderTag, SearchFolderID,
1073
		// and ExtendedFlags subproperties.
1074
		// For more information about PR_EXTENDED_FOLDER_FLAGS go through this link
1075
		// https://msdn.microsoft.com/en-us/library/ee203919(v=exchg.80).aspx
1076
		$extendedFolderFlags = "0104000000010304" . bin2hex($searchFolderTag) . "0210" . bin2hex($searchFolderId);
1077
1078
		mapi_setprops($folder, [
1079
			PR_EXTENDED_FOLDER_FLAGS => hex2bin($extendedFolderFlags),
1080
		]);
1081
		mapi_savechanges($folder);
1082
1083
		$folderProps = mapi_getprops($folder, $this->properties);
1084
		$commonViewsFolder = $this->getCommonViewsFolder();
1085
		$GLOBALS["operations"]->createFavoritesLink($commonViewsFolder, $folderProps, $searchFolderId);
1086
1087
		$folderProps = mapi_getprops($folder, $this->properties);
1088
		$this->addActionData("update", $GLOBALS["operations"]->setFavoritesFolder($folderProps));
1089
		$GLOBALS["bus"]->addData($this->getResponseData());
1090
	}
1091
1092
	/**
1093
	 * Modifies a folder off the hierarchylist.
1094
	 *
1095
	 * @param object $store   message Store Object
1096
	 * @param string $entryid entryid of the folder
1097
	 * @param string $name    name of the folder
1098
	 */
1099
	public function modifyFolder($store, $entryid, $name) {
1100
		$props = [];
1101
		$result = $GLOBALS["operations"]->renameFolder($store, $entryid, $name, $props);
1102
1103
		if ($result && isset($props[PR_ENTRYID])) {
1104
			$GLOBALS["bus"]->notify(bin2hex($props[PR_ENTRYID]), OBJECT_SAVE, $props);
1105
		}
1106
	}
1107
1108
	/**
1109
	 * Deletes a folder in the hierarchylist.
1110
	 *
1111
	 * @param object $store         message Store Object
1112
	 * @param string $parententryid entryid of the parent folder
1113
	 * @param string $entryid       entryid of the folder
1114
	 * @param array  $action        the action data, sent by the client
1115
	 */
1116
	public function deleteFolder($store, $parententryid, $entryid, $action) {
1117
		$props = [];
1118
		$pubStore = $GLOBALS["mapisession"]->isPublicStore($action['store_entryid'] ?? '');
1119
		$result = $GLOBALS["operations"]->deleteFolder($store, $parententryid, $entryid, $props, $action['soft_delete'] ?? false, $pubStore ? true : false);
1120
1121
		// Indicate if the delete succeedded
1122
		$this->sendFeedback($result);
1123
1124
		if (isset($props[PR_ENTRYID])) {
1125
			if ($result) {
1126
				$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_SAVE, $props);
1127
1128
				$props = [];
1129
				$props[PR_PARENT_ENTRYID] = $parententryid;
1130
1131
				$storeprops = mapi_getprops($store, [PR_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID]);
1132
				$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
1133
				$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_SAVE, $props);
1134
1135
				$props[PR_PARENT_ENTRYID] = $storeprops[PR_IPM_WASTEBASKET_ENTRYID];
1136
				$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_SAVE, $props);
1137
			}
1138
		}
1139
		else {
1140
			$props[PR_ENTRYID] = $entryid;
1141
			$props[PR_PARENT_ENTRYID] = $parententryid;
1142
1143
			if ($result) {
1144
				$this->removeFromFavorite($props[PR_ENTRYID]);
1145
1146
				$storeprops = mapi_getprops($store, [PR_ENTRYID, PR_IPM_FAVORITES_ENTRYID]);
1147
				$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
1148
1149
				// Notify about that folder is deleted
1150
				$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_DELETE, $props);
1151
1152
				// Notify its parent about the delete
1153
				$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_SAVE, $props);
1154
1155
				// Notifying corresponding folder in 'Favorites'
1156
				if (isset($storeprops[PR_IPM_FAVORITES_ENTRYID])) {
1157
					$folderEntryID = "00000001" . substr(bin2hex($entryid), 8);
1158
					$props[PR_ENTRYID] = hex2bin($folderEntryID);
1159
					$props[PR_PARENT_ENTRYID] = $storeprops[PR_IPM_FAVORITES_ENTRYID];
1160
					$GLOBALS["bus"]->notify(bin2hex($parententryid), OBJECT_DELETE, $props);
1161
				}
1162
			}
1163
		}
1164
	}
1165
1166
	/**
1167
	 * Deletes all messages in a folder.
1168
	 *
1169
	 * @param object $store   message Store Object
1170
	 * @param string $entryid entryid of the folder
1171
	 */
1172
	public function emptyFolder($store, $entryid) {
1173
		$props = [];
1174
1175
		$result = false;
0 ignored issues
show
The assignment to $result is dead and can be removed.
Loading history...
1176
1177
		// False will only remove the message of
1178
		// selected folder only and can't remove the
1179
		// child folders.
1180
		$emptySubFolders = false;
1181
		$storeProps = mapi_getprops($store, [PR_IPM_WASTEBASKET_ENTRYID]);
1182
		// Check that selected folder is Waste basket or Junk folder then empty folder by removing
1183
		// the child folders.
1184
		if (isset($storeProps[PR_IPM_WASTEBASKET_ENTRYID]) && $storeProps[PR_IPM_WASTEBASKET_ENTRYID] === $entryid) {
1185
			$emptySubFolders = true;
1186
		}
1187
		else {
1188
			$root = mapi_msgstore_openentry($store);
1189
			$rootProps = mapi_getprops($root, [PR_ADDITIONAL_REN_ENTRYIDS]);
1190
			// check if selected folder is junk folder then make junk folder empty with
1191
			// it's child folder and it's contains.
1192
			if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS]) && is_array($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
1193
				// Checking if folder is junk folder or not.
1194
				$emptySubFolders = $GLOBALS['entryid']->compareEntryIds($rootProps[PR_ADDITIONAL_REN_ENTRYIDS][4], $entryid);
1195
			}
1196
1197
			if ($emptySubFolders === false) {
1198
				$folder = mapi_msgstore_openentry($store, $entryid);
1199
				$folderProps = mapi_getprops($folder, [PR_SUBFOLDERS]);
1200
				$emptySubFolders = $folderProps[PR_SUBFOLDERS] === false;
1201
			}
1202
		}
1203
1204
		$result = $GLOBALS["operations"]->emptyFolder($store, $entryid, $props, false, $emptySubFolders);
1205
1206
		if ($result && isset($props[PR_ENTRYID])) {
1207
			$this->addFolderToResponseData($store, $entryid, "folders");
0 ignored issues
show
$store of type object is incompatible with the type resource expected by parameter $store of HierarchyModule::addFolderToResponseData(). ( Ignorable by Annotation )

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

1207
			$this->addFolderToResponseData(/** @scrutinizer ignore-type */ $store, $entryid, "folders");
Loading history...
1208
1209
			// Add all response data to Bus
1210
			$GLOBALS["bus"]->addData($this->getResponseData());
1211
		}
1212
	}
1213
1214
	/**
1215
	 * Copies of moves a folder in the hierarchylist.
1216
	 *
1217
	 * @param object $store               message Store Object
1218
	 * @param string $parententryid       entryid of the parent folder
1219
	 * @param string $sourcefolderentryid entryid of the folder to be copied of moved
1220
	 * @param string $destfolderentryid   entryid of the destination folder
1221
	 * @param mixed  $deststore
1222
	 * @param mixed  $moveFolder
1223
	 */
1224
	public function copyFolder($store, $parententryid, $sourcefolderentryid, $destfolderentryid, $deststore, $moveFolder) {
1225
		$props = [];
1226
		$result = $GLOBALS["operations"]->copyFolder($store, $parententryid, $sourcefolderentryid, $destfolderentryid, $deststore, $moveFolder, $props);
1227
1228
		if ($result) {
1229
			if ($moveFolder) {
1230
				try {
1231
					// If destination folder is wastebasket then remove source folder from favorites list if
1232
					// it is present in it.
1233
					$defaultStore = $GLOBALS["mapisession"]->getDefaultMessageStore();
1234
					$wastebasketFolderEntryid = mapi_getprops($defaultStore, [PR_IPM_WASTEBASKET_ENTRYID]);
1235
					if ($GLOBALS["entryid"]->compareEntryIds($wastebasketFolderEntryid[PR_IPM_WASTEBASKET_ENTRYID], $destfolderentryid)) {
1236
						$this->removeFromFavorite($sourcefolderentryid);
1237
					}
1238
1239
					/*
1240
					 * Normally it works well within same store,
1241
					 * but entryid gets changed when different stores so we can't use old entryid anymore.
1242
					 * When moving to different store send delete notification.
1243
					 */
1244
					$this->addFolderToResponseData($deststore, $sourcefolderentryid, "folders");
1245
1246
					// Add all response data to Bus
1247
					$GLOBALS["bus"]->addData($this->getResponseData());
1248
				}
1249
				catch (MAPIException $e) {
1250
					$exCode = $e->getCode();
1251
					// gromox throws MAPI_E_INVALID_PARAMETER if it's not able to open an entry
1252
					if ($exCode == MAPI_E_INVALID_ENTRYID || $exCode == MAPI_E_INVALID_PARAMETER) {
1253
						// Entryid of the folder might be change after move, so send delete notification for folder.
1254
						$GLOBALS["bus"]->notify(bin2hex($props[PR_ENTRYID]), OBJECT_DELETE, $props);
1255
					}
1256
				}
1257
1258
				// if move folder then refresh parent of source folder
1259
				$sourcefolder = mapi_msgstore_openentry($store, $parententryid);
1260
				$folderProps = mapi_getprops($sourcefolder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1261
				$GLOBALS["bus"]->notify(bin2hex((string) $folderProps[PR_ENTRYID]), OBJECT_SAVE, $folderProps);
1262
			}
1263
			else {
1264
				$this->sendFeedback(true);
1265
			}
1266
1267
			// Update subfolders of copy/move folder
1268
			$folder = mapi_msgstore_openentry($deststore, $destfolderentryid);
1269
			$hierarchyTable = mapi_folder_gethierarchytable($folder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
1270
			mapi_table_sort($hierarchyTable, [PR_DISPLAY_NAME => TABLE_SORT_ASCEND], TBL_BATCH);
1271
1272
			/**
1273
			 * remove hidden folders, folders with PR_ATTR_HIDDEN property set
1274
			 * should not be shown to the client.
1275
			 */
1276
			$restriction = [RES_OR,
1277
				[
1278
					[RES_PROPERTY,
1279
						[
1280
							RELOP => RELOP_EQ,
1281
							ULPROPTAG => PR_ATTR_HIDDEN,
1282
							VALUE => [PR_ATTR_HIDDEN => false],
1283
						],
1284
					],
1285
					[RES_NOT,
1286
						[
1287
							[RES_EXIST,
1288
								[
1289
									ULPROPTAG => PR_ATTR_HIDDEN,
1290
								],
1291
							],
1292
						],
1293
					],
1294
				],
1295
			];
1296
1297
			$subfolders = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID], $restriction);
1298
1299
			if (is_array($subfolders)) {
1300
				foreach ($subfolders as $subfolder) {
1301
					$folderObject = mapi_msgstore_openentry($deststore, $subfolder[PR_ENTRYID]);
1302
					$folderProps = mapi_getprops($folderObject, [PR_ENTRYID, PR_STORE_ENTRYID]);
1303
					$GLOBALS["bus"]->notify(bin2hex((string) $subfolder[PR_ENTRYID]), OBJECT_SAVE, $folderProps);
1304
				}
1305
			}
1306
1307
			// Now update destination folder
1308
			$folder = mapi_msgstore_openentry($deststore, $destfolderentryid);
1309
			$folderProps = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1310
			$GLOBALS["bus"]->notify(bin2hex((string) $folderProps[PR_ENTRYID]), OBJECT_SAVE, $folderProps);
1311
		}
1312
		else {
1313
			if ($moveFolder) {
1314
				$this->sendFeedback(false, _('Could not move folder'));
0 ignored issues
show
_('Could not move folder') of type string is incompatible with the type array expected by parameter $data of Module::sendFeedback(). ( Ignorable by Annotation )

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

1314
				$this->sendFeedback(false, /** @scrutinizer ignore-type */ _('Could not move folder'));
Loading history...
1315
			}
1316
			else {
1317
				$this->sendFeedback(false, _('Could not copy folder'));
1318
			}
1319
		}
1320
	}
1321
1322
	/**
1323
	 * Set all messages read.
1324
	 *
1325
	 * @param object $store   message Store Object
1326
	 * @param string $entryid entryid of the folder
1327
	 */
1328
	public function setReadFlags($store, $entryid) {
1329
		$props = [];
0 ignored issues
show
The assignment to $props is dead and can be removed.
Loading history...
1330
		$folder = mapi_msgstore_openentry($store, $entryid);
1331
1332
		if (!$folder) {
1333
			return;
1334
		}
1335
1336
		if (mapi_folder_setreadflags($folder, [], SUPPRESS_RECEIPT)) {
1337
			$props = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1338
1339
			if (!isset($props[PR_ENTRYID])) {
1340
				return;
1341
			}
1342
1343
			$this->addFolderToResponseData($store, $props[PR_ENTRYID], "folders");
0 ignored issues
show
$store of type object is incompatible with the type resource expected by parameter $store of HierarchyModule::addFolderToResponseData(). ( Ignorable by Annotation )

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

1343
			$this->addFolderToResponseData(/** @scrutinizer ignore-type */ $store, $props[PR_ENTRYID], "folders");
Loading history...
1344
1345
			// Add all response data to Bus
1346
			$GLOBALS["bus"]->addData($this->getResponseData());
1347
		}
1348
	}
1349
}
1350