Passed
Push — master ( a7c928...1e5aa5 )
by
unknown
06:33
created

HierarchyModule::getStoreGrants()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

143
							$this->deleteFolder($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $action);
Loading history...
144
							break;
145
						}
146
						if (!isset($action["message_action"]["isSearchFolder"]) ||
147
							!$action["message_action"]["isSearchFolder"]) {
148
							$this->removeFromFavorite($entryid);
149
							break;
150
						}
151
						$result = $this->deleteSearchFolder($store, $parententryid, $entryid, $action);
0 ignored issues
show
Bug introduced by
$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

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

189
											$this->copyFolder($store, /** @scrutinizer ignore-type */ $parententryid, $entryid, $destentryid, $deststore, $action["message_action"]["action_type"] == "move");
Loading history...
190
										}
191
										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...
192
											$GLOBALS["bus"]->notify(ADDRESSBOOK_ENTRYID, OBJECT_SAVE);
193
										}
194
										break;
195
196
									case "emptyfolder":
197
										$this->emptyFolder($store, $entryid);
198
										break;
199
200
									case "readflags":
201
										$this->setReadFlags($store, $entryid);
202
										break;
203
204
									case "addtofavorites":
205
										if (isset($action["message_action"]["isSearchFolder"]) && $action["message_action"]["isSearchFolder"]) {
206
											$searchStoreEntryId = $action["message_action"]["search_store_entryid"];
207
											// Set display name to search folder.
208
											$searchStore = $GLOBALS["mapisession"]->openMessageStore(hex2bin((string) $searchStoreEntryId));
209
											$searchFolder = mapi_msgstore_openentry($searchStore, $entryid);
210
											mapi_setprops($searchFolder, [
211
												PR_DISPLAY_NAME => $action["props"]["display_name"],
212
											]);
213
											mapi_savechanges($searchFolder);
214
											$this->createLinkedSearchFolder($searchFolder);
215
										}
216
										else {
217
											$this->addToFavorite($store, $entryid);
0 ignored issues
show
Bug introduced by
$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

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

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

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

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

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

1324
				$this->sendFeedback(false, /** @scrutinizer ignore-type */ _('Could not move folder'));
Loading history...
1325
			}
1326
			else {
1327
				$this->sendFeedback(false, _('Could not copy folder'));
1328
			}
1329
		}
1330
	}
1331
1332
	/**
1333
	 * Set all messages read.
1334
	 *
1335
	 * @param object $store   message Store Object
1336
	 * @param string $entryid entryid of the folder
1337
	 */
1338
	public function setReadFlags($store, $entryid) {
1339
		$props = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $props is dead and can be removed.
Loading history...
1340
		$folder = mapi_msgstore_openentry($store, $entryid);
1341
1342
		if (!$folder) {
1343
			return;
1344
		}
1345
1346
		if (mapi_folder_setreadflags($folder, [], SUPPRESS_RECEIPT)) {
1347
			$props = mapi_getprops($folder, [PR_ENTRYID, PR_STORE_ENTRYID]);
1348
1349
			if (!isset($props[PR_ENTRYID])) {
1350
				return;
1351
			}
1352
1353
			$this->addFolderToResponseData($store, $props[PR_ENTRYID], "folders");
0 ignored issues
show
Bug introduced by
$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

1353
			$this->addFolderToResponseData(/** @scrutinizer ignore-type */ $store, $props[PR_ENTRYID], "folders");
Loading history...
1354
1355
			// Add all response data to Bus
1356
			$GLOBALS["bus"]->addData($this->getResponseData());
1357
		}
1358
	}
1359
1360
	/**
1361
	 * Returns the visible permissions of the store for the current user.
1362
	 *
1363
	 * @param array $permissions
1364
	 *
1365
	 * @return array of grants
1366
	 */
1367
	public function getStoreGrants(array $permissions): array {
1368
		$mainUserEntryId = bin2hex($GLOBALS['mapisession']->getUserEntryID());
1369
		$grants = [];
1370
		foreach ($permissions as $grant) {
1371
			if ($grant['entryid'] == $mainUserEntryId) {
1372
				// user has owner rights, return all permissions
1373
				if ($grant['props']['rights'] == ecRightsFolderAccess || $grant['props']['rights'] == ecRightsGromoxStoreOwner) {
1374
					unset($grants);
1375
					return $permissions;
1376
				}
1377
				$grants[] = $grant;
1378
			}
1379
			// anonymous and default permissions are always visible
1380
			elseif ($grant['props']['memberid'] == 0 || $grant['props']['memberid'] == 0xFFFFFFFF) {
1381
				$grants[] = $grant;
1382
			}
1383
		}
1384
		$this->showStoreDetails = false;
1385
1386
		return $grants;
1387
	}
1388
}
1389