Issues (752)

plugins/mdm/php/class.pluginmdmmodule.php (6 issues)

1
<?php
2
3
require_once BASE_PATH . 'server/includes/core/class.encryptionstore.php';
4
require_once 'zpushprops.php';
5
6
/**
7
 * PluginMDMModule Module.
8
 */
9
class PluginMDMModule extends Module {
10
	// content data
11
	public const FOLDERUUID = 1;
12
	public const FOLDERTYPE = 2;
13
	public const FOLDERBACKENDID = 5;
14
15
	private $stateFolder;
16
	private $deviceStates;
17
	private $devices;
18
19
	/**
20
	 * Constructor.
21
	 *
22
	 * @param int   $id   unique id
23
	 * @param array $data list of all actions
24
	 */
25
	public function __construct($id, $data) {
26
		parent::__construct($id, $data);
27
		$this->stateFolder = null;
28
		$this->deviceStates = [];
29
		$this->devices = [];
30
		$this->setupDevices();
31
	}
32
33
	/**
34
	 * Function sets up the array with the user's devices.
35
	 */
36
	public function setupDevices() {
37
		$devices = [];
38
		$stateFolder = $this->getStoreStateFolder();
39
		if ($stateFolder) {
0 ignored issues
show
$stateFolder is of type MAPIFolder, thus it always evaluated to true.
Loading history...
40
			$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
41
			$username = $GLOBALS["mapisession"]->getUserName();
42
			$hierarchyTable = mapi_folder_gethierarchytable($stateFolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
43
			$rows = mapi_table_queryallrows($hierarchyTable, [PR_ENTRYID, PR_DISPLAY_NAME]);
44
			foreach ($rows as $row) {
45
				$deviceStateFolder = mapi_msgstore_openentry($store, $row[PR_ENTRYID]);
46
				if (mapi_last_hresult() == 0) {
47
					$this->deviceStates[$row[PR_DISPLAY_NAME]] = $deviceStateFolder;
48
49
					$deviceStateFolderContents = mapi_folder_getcontentstable($deviceStateFolder, MAPI_ASSOCIATED);
50
					$restriction = $this->getStateMessageRestriction("devicedata");
51
					mapi_table_restrict($deviceStateFolderContents, $restriction);
52
					if (mapi_table_getrowcount($deviceStateFolderContents) == 1) {
53
						$rows = mapi_table_queryrows($deviceStateFolderContents, [PR_ENTRYID], 0, 1);
54
						$message = mapi_msgstore_openentry($store, $rows[0][PR_ENTRYID]);
55
						$state = base64_decode(streamProperty($message, PR_BODY));
56
						$unserializedState = json_decode($state);
57
						// fallback for "old-style" states
58
						if (isset($unserializedState->data->devices)) {
59
							$devices[$unserializedState->data->devices->{$username}->data->deviceid] = $unserializedState->data->devices->{$username}->data;
60
						}
61
						else {
62
							$devices[$unserializedState->data->deviceid] = $unserializedState->data;
63
						}
64
					}
65
				}
66
			}
67
		}
68
		$this->devices = $devices;
69
	}
70
71
	/**
72
	 * Function which triggers full resync of a device.
73
	 *
74
	 * @param string $deviceid of phone which has to be resynced
75
	 *
76
	 * @return bool $response true if removing states succeeded or false on failure
77
	 */
78
	public function resyncDevice($deviceid) {
79
		$deviceStateFolder = $this->deviceStates[$deviceid];
80
		if ($deviceStateFolder) {
81
			try {
82
				// find all messages that are not 'devicedata' and remove them
83
				$deviceStateFolderContents = mapi_folder_getcontentstable($deviceStateFolder, MAPI_ASSOCIATED);
84
				$restriction = $this->getStateMessageRestriction("devicedata", RELOP_NE);
85
				mapi_table_restrict($deviceStateFolderContents, $restriction);
86
87
				$rows = mapi_table_queryallrows($deviceStateFolderContents, [PR_ENTRYID, PR_DISPLAY_NAME]);
88
				$messages = [];
89
				foreach ($rows as $row) {
90
					$messages[] = $row[PR_ENTRYID];
91
				}
92
				mapi_folder_deletemessages($deviceStateFolder, $messages, DEL_ASSOCIATED | DELETE_HARD_DELETE);
93
				if (mapi_last_hresult() == NOERROR) {
94
					return true;
95
				}
96
			}
97
			catch (Exception $e) {
98
				error_log(sprintf("mdm plugin resyncDevice Exception: %s", $e));
99
100
				return false;
101
			}
102
		}
103
		error_log(sprintf("mdm plugin resyncDevice device state folder %s", $deviceStateFolder));
104
105
		return false;
106
	}
107
108
	/**
109
	 * Function which triggers remote wipe of a device.
110
	 *
111
	 * @param string $deviceid of phone which has to be wiped
112
	 * @param string $password user password
113
	 * @param int    $wipeType remove account only or all data
114
	 *
115
	 * @return bool true if the request was successful, false otherwise
116
	 */
117
	public function wipeDevice($deviceid, $password, $wipeType = SYNC_PROVISION_RWSTATUS_PENDING) {
118
		$opts = ['http' => [
119
			'method' => 'POST',
120
			'header' => 'Content-Type: application/json',
121
			'ignore_errors' => true,
122
			'content' => json_encode(
123
				[
124
					'password' => $password,
125
					'remoteIP' => '[::1]',
126
					'status' => $wipeType,
127
					'time' => time(),
128
				]
129
			),
130
		],
131
		];
132
		$ret = file_get_contents(PLUGIN_MDM_ADMIN_API_WIPE_ENDPOINT . $GLOBALS["mapisession"]->getUserName() . "?devices=" . $deviceid, false, stream_context_create($opts));
133
		$ret = json_decode($ret, true);
134
135
		return strncasecmp('success', (string) $ret['message'], 7) === 0;
136
	}
137
138
	/**
139
	 * Function which triggers removal of a device.
140
	 *
141
	 * @param string $deviceid of phone which has to be removed
142
	 * @param string $password user password
143
	 *
144
	 * @return bool|string $response object contains the response of the soap request from grommunio-sync or false on failure
145
	 */
146
	public function removeDevice($deviceid, $password) {
147
		// TODO remove the device from device / user list
148
		$deviceStateFolder = $this->deviceStates[$deviceid];
149
		$stateFolder = $this->getStoreStateFolder();
150
		if ($stateFolder && $deviceStateFolder) {
151
			$props = mapi_getprops($deviceStateFolder, [PR_ENTRYID]);
152
153
			try {
154
				mapi_folder_deletefolder($stateFolder, $props[PR_ENTRYID], DEL_MESSAGES);
155
				$opts = ['http' => [
156
					'method' => 'POST',
157
					'header' => 'Content-Type: application/json',
158
					'ignore_errors' => true,
159
					'content' => json_encode(
160
						[
161
							'password' => $password,
162
							'remoteIP' => '[::1]',
163
							'status' => SYNC_PROVISION_RWSTATUS_NA,
164
							'time' => time(),
165
						]
166
					),
167
				],
168
				];
169
				$ret = file_get_contents(PLUGIN_MDM_ADMIN_API_WIPE_ENDPOINT . $GLOBALS["mapisession"]->getUserName() . "?devices=" . $deviceid, false, stream_context_create($opts));
170
171
				return $ret;
172
			}
173
			catch (Exception $e) {
174
				error_log(sprintf("mdm plugin removeDevice Exception: %s", $e));
175
176
				return false;
177
			}
178
		}
179
		error_log(sprintf(
180
			"mdm plugin removeDevice state folder %s device state folder %s",
181
			$stateFolder,
182
			$deviceStateFolder
183
		));
184
185
		return false;
186
	}
187
188
	/**
189
	 * Function to get details of the given device.
190
	 *
191
	 * @param string $deviceid id of device
192
	 *
193
	 * @return array contains device props
194
	 */
195
	public function getDeviceDetails($deviceid) {
196
		$device = [];
197
		$device['props'] = $this->getDeviceProps($this->devices[$deviceid]);
198
		$device['sharedfolders'] = ['item' => $this->getAdditionalFolderList($deviceid)];
199
200
		return $device;
201
	}
202
203
	/**
204
	 * Executes all the actions in the $data variable.
205
	 *
206
	 * @return bool true on success or false on failure
207
	 */
208
	#[Override]
209
	public function execute() {
210
		foreach ($this->data as $actionType => $actionData) {
211
			if (isset($actionType)) {
212
				try {
213
					switch ($actionType) {
214
						case 'wipe':
215
							$this->addActionData('wipe', [
216
								'type' => 3,
217
								'wipe' => $this->wipeDevice($actionData['deviceid'], $actionData['password'], $actionData['wipetype']),
218
							]);
219
							$GLOBALS['bus']->addData($this->getResponseData());
220
							break;
221
222
						case 'resync':
223
							$this->addActionData('resync', [
224
								'type' => 3,
225
								'resync' => $this->resyncDevice($actionData['deviceid']),
226
							]);
227
							$GLOBALS['bus']->addData($this->getResponseData());
228
							break;
229
230
						case 'remove':
231
							$this->addActionData('remove', [
232
								'type' => 3,
233
								'remove' => $this->removeDevice($actionData['deviceid'], $actionData['password']),
234
							]);
235
							$GLOBALS['bus']->addData($this->getResponseData());
236
							break;
237
238
						case 'list':
239
							$items = [];
240
							$data['page'] = [];
241
242
							foreach ($this->devices as $device) {
243
								array_push($items, ['props' => $this->getDeviceProps($device)]);
244
							}
245
							$data['page']['start'] = 0;
246
							$data['page']['rowcount'] = count($this->devices);
247
							$data['page']['totalrowcount'] = $data['page']['rowcount'];
248
							$data = array_merge($data, ['item' => $items]);
249
							$this->addActionData('list', $data);
250
							$GLOBALS['bus']->addData($this->getResponseData());
251
							break;
252
253
						case 'open':
254
							$device = $this->getDeviceDetails($actionData["entryid"]);
255
							$item = ["item" => $device];
256
							$this->addActionData('item', $item);
257
							$GLOBALS['bus']->addData($this->getResponseData());
258
							break;
259
260
						case 'save':
261
							$this->saveDevice($actionData);
262
							$device = $this->getDeviceDetails($actionData["entryid"]);
263
							$item = ["item" => $device];
264
							$this->addActionData('update', $item);
265
							$GLOBALS['bus']->addData($this->getResponseData());
266
							break;
267
268
						default:
269
							$this->handleUnknownActionType($actionType);
270
					}
271
				}
272
				catch (Exception $e) {
273
					$title = _('Mobile device management plugin');
274
					$display_message = sprintf(_('Unexpected error occurred. Please contact your system administrator. Error code: %s'), $e->getMessage());
275
					$this->sendFeedback(true, ["type" => ERROR_GENERAL, "info" => ['title' => $title, 'display_message' => $display_message]]);
276
				}
277
			}
278
		}
279
	}
280
281
	/**
282
	 * Function which is use to get device properties.
283
	 *
284
	 * @param object $device array of device properties
285
	 *
286
	 * @return array
287
	 */
288
	public function getDeviceProps($device) {
289
		$item = [];
290
		$propsList = ['devicetype', 'deviceos', 'devicefriendlyname', 'useragent', 'asversion', 'firstsynctime',
291
			'lastsynctime', 'lastupdatetime', 'policyname', ];
292
293
		$item['entryid'] = $device->deviceid;
294
		$item['message_class'] = "IPM.MDM";
295
		foreach ($propsList as $prop) {
296
			if (isset($device->{$prop})) {
297
				$item[$prop] = $device->{$prop};
298
			}
299
		}
300
		$item['wipestatus'] = $this->getProvisioningWipeStatus($device->deviceid);
301
302
		return array_merge($item, $this->getSyncFoldersProps($device));
0 ignored issues
show
$device of type object is incompatible with the type array expected by parameter $device of PluginMDMModule::getSyncFoldersProps(). ( Ignorable by Annotation )

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

302
		return array_merge($item, $this->getSyncFoldersProps(/** @scrutinizer ignore-type */ $device));
Loading history...
303
	}
304
305
	/**
306
	 * Function which is use to gather some statistics about synchronized folders.
307
	 *
308
	 * @param array $device array of device props
309
	 *
310
	 * @return array $syncFoldersProps has list of properties related to synchronized folders
311
	 */
312
	public function getSyncFoldersProps($device) {
313
		$synchedFolderTypes = [];
314
		$synchronizedFolders = 0;
315
316
		foreach ($device->contentdata as $folderid => $folderdata) {
317
			if (isset($folderdata->{self::FOLDERUUID})) {
318
				$type = $folderdata->{self::FOLDERTYPE};
319
320
				$folderType = $this->getSyncFolderType($type);
321
				if (isset($synchedFolderTypes[$folderType])) {
322
					++$synchedFolderTypes[$folderType];
323
				}
324
				else {
325
					$synchedFolderTypes[$folderType] = 1;
326
				}
327
			}
328
		}
329
		$syncFoldersProps = [];
330
		foreach ($synchedFolderTypes as $key => $value) {
331
			$synchronizedFolders += $value;
332
			$syncFoldersProps[strtolower($key) . 'folder'] = $value;
333
		}
334
		/*
335
		TODO getAdditionalFolderList
336
		$client = $this->getSoapClient();
337
		$items = $client->AdditionalFolderList($device['deviceid']);
338
		$syncFoldersProps['sharedfolders'] = count($items);
339
		$syncFoldersProps["shortfolderids"] = $device['hasfolderidmapping'] ? _("Yes") : _("No");
340
		$syncFoldersProps['synchronizedfolders'] = $synchronizedFolders + count($items);
341
		*/
342
		$syncFoldersProps['synchronizedfolders'] = $synchronizedFolders;
343
344
		return $syncFoldersProps;
345
	}
346
347
	/**
348
	 * Function which is use to get general type like Mail,Calendar,Contacts,etc. from folder type.
349
	 *
350
	 * @param int $type foldertype for a folder already known to the mobile
351
	 *
352
	 * @return string general folder type
353
	 */
354
	public function getSyncFolderType($type) {
355
		return match ($type) {
356
			SYNC_FOLDER_TYPE_APPOINTMENT, SYNC_FOLDER_TYPE_USER_APPOINTMENT => "Calendars",
357
			SYNC_FOLDER_TYPE_CONTACT, SYNC_FOLDER_TYPE_USER_CONTACT => "Contacts",
358
			SYNC_FOLDER_TYPE_TASK, SYNC_FOLDER_TYPE_USER_TASK => "Tasks",
359
			SYNC_FOLDER_TYPE_NOTE, SYNC_FOLDER_TYPE_USER_NOTE => "Notes",
360
			default => "Emails",
361
		};
362
	}
363
364
	/**
365
	 * Function which is use to get list of additional folders which was shared with given device.
366
	 *
367
	 * @param string $devid device id
368
	 *
369
	 * @return array has list of properties related to shared folders
370
	 */
371
	public function getAdditionalFolderList($devid) {
372
		return [];
373
		// TODO implement
374
		$stores = $GLOBALS["mapisession"]->getAllMessageStores();
0 ignored issues
show
$stores = $GLOBALS['mapi...->getAllMessageStores() is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
375
		$client = $this->getSoapClient();
376
		$items = $client->AdditionalFolderList($devid);
377
		$data = [];
378
		foreach ($items as $item) {
379
			foreach ($stores as $store) {
380
				try {
381
					$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin((string) $item->folderid));
382
				}
383
				catch (MAPIException) {
384
					continue;
385
				}
386
			}
387
			if (isset($entryid)) {
388
				$item->entryid = bin2hex($entryid);
389
			}
390
			array_push($data, ["props" => $item]);
391
		}
392
393
		return $data;
394
	}
395
396
	/**
397
	 * Function which is use to remove additional folder which was shared with given device.
398
	 *
399
	 * @param string $entryId  id of device
400
	 * @param string $folderid id of folder which will remove from device
401
	 */
402
	public function additionalFolderRemove($entryId, $folderid) {
403
		$client = $this->getSoapClient();
0 ignored issues
show
The method getSoapClient() does not exist on PluginMDMModule. ( Ignorable by Annotation )

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

403
		/** @scrutinizer ignore-call */ 
404
  $client = $this->getSoapClient();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
404
		$client->AdditionalFolderRemove($entryId, $folderid);
405
	}
406
407
	/**
408
	 * Function which is use to add additional folder which will share with given device.
409
	 *
410
	 * @param string $entryId id of device
411
	 * @param array  $folder  folder which will share with device
412
	 */
413
	public function additionalFolderAdd($entryId, $folder) {
414
		$client = $this->getSoapClient();
415
		$containerClass = $folder[PR_CONTAINER_CLASS] ?? "IPF.Note";
416
		$folderId = bin2hex((string) $folder[PR_SOURCE_KEY]);
417
		$userName = $folder["user"];
418
		$folderName = $userName === "SYSTEM" ? $folder[PR_DISPLAY_NAME] : $folder[PR_DISPLAY_NAME] . " - " . $userName;
419
		$folderType = $this->getFolderTypeFromContainerClass($containerClass);
420
		$client->AdditionalFolderAdd($entryId, $userName, $folderId, $folderName, $folderType, FLD_FLAGS_REPLYASUSER);
421
	}
422
423
	/**
424
	 * Function which use to save the device.
425
	 * It will use to add or remove folders in the device.
426
	 *
427
	 * @param array $data array of added and removed folders
428
	 */
429
	public function saveDevice($data) {
430
		$entryid = $data["entryid"];
431
		if (isset($data['sharedfolders'])) {
432
			if (isset($data['sharedfolders']['remove'])) {
433
				$deletedFolders = $data['sharedfolders']['remove'];
434
				foreach ($deletedFolders as $folder) {
435
					$this->additionalFolderRemove($entryid, $folder["folderid"]);
436
				}
437
			}
438
			if (isset($data['sharedfolders']['add'])) {
439
				$addFolders = $data['sharedfolders']['add'];
440
				$hierarchyFolders = $this->getHierarchyList();
441
				foreach ($addFolders as $folder) {
442
					foreach ($hierarchyFolders as $hierarchyFolder) {
443
						$folderEntryid = bin2hex((string) $hierarchyFolder[PR_ENTRYID]);
444
						if ($folderEntryid === $folder["entryid"]) {
445
							$this->additionalFolderAdd($entryid, $hierarchyFolder);
446
447
							continue 2;
448
						}
449
					}
450
				}
451
			}
452
		}
453
	}
454
455
	/**
456
	 * Gets the hierarchy list of all required stores.
457
	 * Function which is use to get the hierarchy list with PR_SOURCE_KEY.
458
	 *
459
	 * @return array the array of all hierarchy folders
460
	 */
461
	public function getHierarchyList() {
462
		$storeList = $GLOBALS["mapisession"]->getAllMessageStores();
463
		$properties = $GLOBALS["properties"]->getFolderListProperties();
464
		$otherUsers = $GLOBALS["mapisession"]->retrieveOtherUsersFromSettings();
465
		$properties["source_key"] = PR_SOURCE_KEY;
466
		$openWholeStore = true;
467
		$storeData = [];
468
469
		foreach ($storeList as $store) {
470
			$msgstore_props = mapi_getprops($store, [PR_MDB_PROVIDER, PR_ENTRYID, PR_IPM_SUBTREE_ENTRYID, PR_USER_NAME]);
471
			$storeType = $msgstore_props[PR_MDB_PROVIDER];
472
473
			if ($storeType == ZARAFA_SERVICE_GUID) {
474
				continue;
475
			}
476
			if ($storeType == ZARAFA_STORE_DELEGATE_GUID) {
477
				$storeUserName = $GLOBALS["mapisession"]->getUserNameOfStore($msgstore_props[PR_ENTRYID]);
478
			}
479
			elseif ($storeType == ZARAFA_STORE_PUBLIC_GUID) {
480
				$storeUserName = "SYSTEM";
481
			}
482
			else {
483
				$storeUserName = $msgstore_props[PR_USER_NAME];
484
			}
485
486
			if (is_array($otherUsers)) {
487
				if (isset($otherUsers[$storeUserName])) {
488
					$sharedFolders = $otherUsers[$storeUserName];
489
					if (!isset($otherUsers[$storeUserName]['all'])) {
490
						$openWholeStore = false;
491
						$a = $this->getSharedFolderList($store, $sharedFolders, $properties, $storeUserName);
492
						$storeData = array_merge($storeData, $a);
493
					}
494
				}
495
			}
496
497
			if ($openWholeStore) {
498
				if (isset($msgstore_props[PR_IPM_SUBTREE_ENTRYID])) {
499
					$subtreeFolderEntryID = $msgstore_props[PR_IPM_SUBTREE_ENTRYID];
500
501
					try {
502
						$subtreeFolder = mapi_msgstore_openentry($store, $subtreeFolderEntryID);
503
					}
504
					catch (MAPIException $e) {
505
						// We've handled the event
506
						$e->setHandled();
507
					}
508
509
					$this->getSubFolders($subtreeFolder, $store, $properties, $storeData, $storeUserName);
510
				}
511
			}
512
		}
513
514
		return $storeData;
515
	}
516
517
	/**
518
	 * Helper function to get the shared folder list.
519
	 *
520
	 * @param object $store         message Store Object
521
	 * @param object $sharedFolders mapi Folder Object
522
	 * @param array  $properties    MAPI property mappings for folders
523
	 * @param string $storeUserName owner name of store
524
	 *
525
	 * @return array shared folders list
526
	 */
527
	public function getSharedFolderList($store, $sharedFolders, $properties, $storeUserName) {
528
		$msgstore_props = mapi_getprops($store, [PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_OBJECT_TYPE, PR_STORE_SUPPORT_MASK, PR_MAILBOX_OWNER_ENTRYID, PR_MAILBOX_OWNER_NAME, PR_USER_ENTRYID, PR_USER_NAME, PR_QUOTA_WARNING_THRESHOLD, PR_QUOTA_SEND_THRESHOLD, PR_QUOTA_RECEIVE_THRESHOLD, PR_MESSAGE_SIZE_EXTENDED, PR_MAPPING_SIGNATURE, PR_COMMON_VIEWS_ENTRYID, PR_FINDER_ENTRYID]);
529
		$storeData = [];
530
		$folders = [];
531
532
		try {
533
			$inbox = mapi_msgstore_getreceivefolder($store);
534
			$inboxProps = mapi_getprops($inbox, [PR_ENTRYID]);
535
		}
536
		catch (MAPIException $e) {
537
			// don't propagate this error to parent handlers, if store doesn't support it
538
			if ($e->getCode() === MAPI_E_NO_SUPPORT) {
539
				$e->setHandled();
540
			}
541
		}
542
543
		$root = mapi_msgstore_openentry($store);
544
		$rootProps = mapi_getprops($root, [PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID, PR_ADDITIONAL_REN_ENTRYIDS]);
545
546
		$additional_ren_entryids = [];
547
		if (isset($rootProps[PR_ADDITIONAL_REN_ENTRYIDS])) {
548
			$additional_ren_entryids = $rootProps[PR_ADDITIONAL_REN_ENTRYIDS];
549
		}
550
551
		$defaultfolders = [
552
			"default_folder_inbox" => ["inbox" => PR_ENTRYID],
553
			"default_folder_outbox" => ["store" => PR_IPM_OUTBOX_ENTRYID],
554
			"default_folder_sent" => ["store" => PR_IPM_SENTMAIL_ENTRYID],
555
			"default_folder_wastebasket" => ["store" => PR_IPM_WASTEBASKET_ENTRYID],
556
			"default_folder_favorites" => ["store" => PR_IPM_FAVORITES_ENTRYID],
557
			"default_folder_publicfolders" => ["store" => PR_IPM_PUBLIC_FOLDERS_ENTRYID],
558
			"default_folder_calendar" => ["root" => PR_IPM_APPOINTMENT_ENTRYID],
559
			"default_folder_contact" => ["root" => PR_IPM_CONTACT_ENTRYID],
560
			"default_folder_drafts" => ["root" => PR_IPM_DRAFTS_ENTRYID],
561
			"default_folder_journal" => ["root" => PR_IPM_JOURNAL_ENTRYID],
562
			"default_folder_note" => ["root" => PR_IPM_NOTE_ENTRYID],
563
			"default_folder_task" => ["root" => PR_IPM_TASK_ENTRYID],
564
			"default_folder_junk" => ["additional" => 4],
565
			"default_folder_syncissues" => ["additional" => 1],
566
			"default_folder_conflicts" => ["additional" => 0],
567
			"default_folder_localfailures" => ["additional" => 2],
568
			"default_folder_serverfailures" => ["additional" => 3],
569
		];
570
571
		foreach ($defaultfolders as $key => $prop) {
572
			$tag = reset($prop);
573
			$from = key($prop);
574
575
			switch ($from) {
576
				case "inbox":
577
					if (isset($inboxProps[$tag])) {
578
						$storeData["props"][$key] = bin2hex((string) $inboxProps[$tag]);
579
					}
580
					break;
581
582
				case "store":
583
					if (isset($msgstore_props[$tag])) {
584
						$storeData["props"][$key] = bin2hex((string) $msgstore_props[$tag]);
585
					}
586
					break;
587
588
				case "root":
589
					if (isset($rootProps[$tag])) {
590
						$storeData["props"][$key] = bin2hex((string) $rootProps[$tag]);
591
					}
592
					break;
593
594
				case "additional":
595
					if (isset($additional_ren_entryids[$tag])) {
596
						$storeData["props"][$key] = bin2hex((string) $additional_ren_entryids[$tag]);
597
					}
598
					break;
599
			}
600
		}
601
602
		$store_access = true;
603
		$openSubFolders = false;
604
		foreach ($sharedFolders as $type => $sharedFolder) {
605
			$openSubFolders = ($sharedFolder["show_subfolders"] == true);
606
			$folderEntryID = hex2bin($storeData["props"]["default_folder_" . $sharedFolder["folder_type"]]);
607
608
			try {
609
				// load folder props
610
				$folder = mapi_msgstore_openentry($store, $folderEntryID);
611
			}
612
			catch (MAPIException $e) {
613
				// Indicate that we don't have access to the store,
614
				// so no more attempts to read properties or open entries.
615
				$store_access = false;
616
617
				// We've handled the event
618
				$e->setHandled();
619
			}
620
		}
621
622
		if ($store_access === true) {
623
			$folderProps = mapi_getprops($folder, $properties);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $folder seems to be defined by a foreach iteration on line 604. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
624
			$folderProps["user"] = $storeUserName;
625
			array_push($folders, $folderProps);
626
627
			// If folder has sub folders then add its.
628
			if ($openSubFolders === true) {
629
				if ($folderProps[PR_SUBFOLDERS] != false) {
630
					$subFoldersData = [];
631
					$this->getSubFolders($folder, $store, $properties, $subFoldersData, $storeUserName);
632
					$folders = array_merge($folders, $subFoldersData);
633
				}
634
			}
635
		}
636
637
		return $folders;
638
	}
639
640
	/**
641
	 * Helper function to get the sub folders of a given folder.
642
	 *
643
	 * @param object $folder        mapi Folder Object
644
	 * @param object $store         Message Store Object
645
	 * @param array  $properties    MAPI property mappings for folders
646
	 * @param array  $storeData     Reference to an array. The folder properties are added to this array.
647
	 * @param string $storeUserName owner name of store
648
	 */
649
	public function getSubFolders($folder, $store, $properties, &$storeData, $storeUserName) {
650
		/**
651
		 * remove hidden folders, folders with PR_ATTR_HIDDEN property set
652
		 * should not be shown to the client.
653
		 */
654
		$restriction = [RES_OR, [
655
			[RES_PROPERTY,
656
				[
657
					RELOP => RELOP_EQ,
658
					ULPROPTAG => PR_ATTR_HIDDEN,
659
					VALUE => [PR_ATTR_HIDDEN => false],
660
				],
661
			],
662
			[RES_NOT,
663
				[
664
					[RES_EXIST,
665
						[
666
							ULPROPTAG => PR_ATTR_HIDDEN,
667
						],
668
					],
669
				],
670
			],
671
		]];
672
673
		$expand = [
674
			[
675
				'folder' => $folder,
676
				'props' => mapi_getprops($folder, [PR_ENTRYID, PR_SUBFOLDERS]),
677
			],
678
		];
679
680
		// Start looping through the $expand array, during each loop we grab the first item in
681
		// the array and obtain the hierarchy table for that particular folder. If one of those
682
		// subfolders has subfolders of its own, it will be appended to $expand again to ensure
683
		// it will be expanded later.
684
		while (!empty($expand)) {
685
			$item = array_shift($expand);
686
			$columns = $properties;
687
688
			$hierarchyTable = mapi_folder_gethierarchytable($item['folder'], MAPI_DEFERRED_ERRORS);
689
			mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH);
690
691
			mapi_table_setcolumns($hierarchyTable, $columns);
692
			$columns = null;
693
694
			// Load the hierarchy in small batches
695
			$batchcount = 100;
696
			do {
697
				$rows = mapi_table_queryrows($hierarchyTable, $columns, 0, $batchcount);
698
699
				foreach ($rows as $subfolder) {
700
					// If the subfolders has subfolders of its own, append the folder
701
					// to the $expand array, so it can be expanded in the next loop.
702
					if ($subfolder[PR_SUBFOLDERS]) {
703
						$folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]);
704
						array_push($expand, ['folder' => $folderObject, 'props' => $subfolder]);
705
					}
706
					$subfolder["user"] = $storeUserName;
707
					// Add the folder to the return list.
708
					array_push($storeData, $subfolder);
709
				}
710
711
				// When the server returned a different number of rows then was requested,
712
				// we have reached the end of the table and we should exit the loop.
713
			}
714
			while (count($rows) === $batchcount);
715
		}
716
	}
717
718
	/**
719
	 * Function which is use get folder types from the container class.
720
	 *
721
	 * @param string $containerClass container class of folder
722
	 *
723
	 * @return int folder type
724
	 */
725
	public function getFolderTypeFromContainerClass($containerClass) {
726
		return match ($containerClass) {
727
			"IPF.Note" => SYNC_FOLDER_TYPE_USER_MAIL,
728
			"IPF.Appointment" => SYNC_FOLDER_TYPE_USER_APPOINTMENT,
729
			"IPF.Contact" => SYNC_FOLDER_TYPE_USER_CONTACT,
730
			"IPF.StickyNote" => SYNC_FOLDER_TYPE_USER_NOTE,
731
			"IPF.Task" => SYNC_FOLDER_TYPE_USER_TASK,
732
			"IPF.Journal" => SYNC_FOLDER_TYPE_USER_JOURNAL,
733
			default => SYNC_FOLDER_TYPE_UNKNOWN,
734
		};
735
	}
736
737
	/**
738
	 * Returns MAPIFolder object which contains the state information.
739
	 * Creates this folder if it is not available yet.
740
	 *
741
	 * @return MAPIFolder
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...
742
	 */
743
	public function getStoreStateFolder() {
744
		if (!$this->stateFolder) {
745
			$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
746
			$rootFolder = mapi_msgstore_openentry($store);
747
			$hierarchy = mapi_folder_gethierarchytable($rootFolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
748
			$restriction = $this->getStateFolderRestriction(PLUGIN_MDM_STORE_STATE_FOLDER);
749
			mapi_table_restrict($hierarchy, $restriction);
750
			if (mapi_table_getrowcount($hierarchy) == 1) {
751
				$rows = mapi_table_queryrows($hierarchy, [PR_ENTRYID], 0, 1);
752
				$this->stateFolder = mapi_msgstore_openentry($store, $rows[0][PR_ENTRYID]);
753
			}
754
		}
755
756
		return $this->stateFolder;
757
	}
758
759
	/**
760
	 * Returns the restriction for the state folder name.
761
	 *
762
	 * @param string $folderName the state folder name
763
	 *
764
	 * @return array
765
	 */
766
	public function getStateFolderRestriction($folderName) {
767
		return [RES_AND, [
768
			[RES_PROPERTY,
769
				[RELOP => RELOP_EQ,
770
					ULPROPTAG => PR_DISPLAY_NAME,
771
					VALUE => $folderName,
772
				],
773
			],
774
			[RES_PROPERTY,
775
				[RELOP => RELOP_EQ,
776
					ULPROPTAG => PR_ATTR_HIDDEN,
777
					VALUE => true,
778
				],
779
			],
780
		]];
781
	}
782
783
	/**
784
	 * Returns the restriction for the associated message in the state folder.
785
	 *
786
	 * @param string $messageName the message name
787
	 * @param int    $op          comparison operation
788
	 *
789
	 * @return array
790
	 */
791
	public function getStateMessageRestriction($messageName, $op = RELOP_EQ) {
792
		return [RES_AND, [
793
			[RES_PROPERTY,
794
				[RELOP => $op,
795
					ULPROPTAG => PR_DISPLAY_NAME,
796
					VALUE => $messageName,
797
				],
798
			],
799
			[RES_PROPERTY,
800
				[RELOP => RELOP_EQ,
801
					ULPROPTAG => PR_MESSAGE_CLASS,
802
					VALUE => 'IPM.Note.GrommunioState',
803
				],
804
			],
805
		]];
806
	}
807
808
	/**
809
	 * Returns the status of the remote wipe policy.
810
	 *
811
	 * @param mixed $deviceid
812
	 *
813
	 * @return int returns the current status of the device - SYNC_PROVISION_RWSTATUS_*
814
	 */
815
	public function getProvisioningWipeStatus($deviceid) {
816
		// retrieve the WIPE STATUS from the Admin API
817
		$api_response = file_get_contents(PLUGIN_MDM_ADMIN_API_WIPE_ENDPOINT . $GLOBALS["mapisession"]->getUserName() . "?devices=" . $deviceid);
818
		if ($api_response) {
819
			$data = json_decode($api_response, true);
820
			if (isset($data['data'][$deviceid]["status"])) {
821
				return $data['data'][$deviceid]["status"];
822
			}
823
		}
824
825
		return SYNC_PROVISION_RWSTATUS_NA;
826
	}
827
}
828