Passed
Push — master ( 82c7e6...367bbb )
by
unknown
09:42 queued 03:21
created

PluginMDMModule::getLastConnectionTime()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 11
rs 10
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
introduced by
$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
		$item['lastconnecttime'] = $this->getLastConnectionTime($device->deviceid, $item['lastupdatetime']);
302
303
		return array_merge($item, $this->getSyncFoldersProps($device));
0 ignored issues
show
Bug introduced by
$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

303
		return array_merge($item, $this->getSyncFoldersProps(/** @scrutinizer ignore-type */ $device));
Loading history...
304
	}
305
306
	/**
307
	 * Function which is use to gather some statistics about synchronized folders.
308
	 *
309
	 * @param array $device array of device props
310
	 *
311
	 * @return array $syncFoldersProps has list of properties related to synchronized folders
312
	 */
313
	public function getSyncFoldersProps($device) {
314
		$synchedFolderTypes = [];
315
		$synchronizedFolders = 0;
316
317
		foreach ($device->contentdata as $folderid => $folderdata) {
318
			if (isset($folderdata->{self::FOLDERUUID})) {
319
				$type = $folderdata->{self::FOLDERTYPE};
320
321
				$folderType = $this->getSyncFolderType($type);
322
				if (isset($synchedFolderTypes[$folderType])) {
323
					++$synchedFolderTypes[$folderType];
324
				}
325
				else {
326
					$synchedFolderTypes[$folderType] = 1;
327
				}
328
			}
329
		}
330
		$syncFoldersProps = [];
331
		foreach ($synchedFolderTypes as $key => $value) {
332
			$synchronizedFolders += $value;
333
			$syncFoldersProps[strtolower($key) . 'folder'] = $value;
334
		}
335
		/*
336
		TODO getAdditionalFolderList
337
		$client = $this->getSoapClient();
338
		$items = $client->AdditionalFolderList($device['deviceid']);
339
		$syncFoldersProps['sharedfolders'] = count($items);
340
		$syncFoldersProps["shortfolderids"] = $device['hasfolderidmapping'] ? _("Yes") : _("No");
341
		$syncFoldersProps['synchronizedfolders'] = $synchronizedFolders + count($items);
342
		*/
343
		$syncFoldersProps['synchronizedfolders'] = $synchronizedFolders;
344
345
		return $syncFoldersProps;
346
	}
347
348
	/**
349
	 * Function which is use to get general type like Mail,Calendar,Contacts,etc. from folder type.
350
	 *
351
	 * @param int $type foldertype for a folder already known to the mobile
352
	 *
353
	 * @return string general folder type
354
	 */
355
	public function getSyncFolderType($type) {
356
		return match ($type) {
357
			SYNC_FOLDER_TYPE_APPOINTMENT, SYNC_FOLDER_TYPE_USER_APPOINTMENT => "Calendars",
358
			SYNC_FOLDER_TYPE_CONTACT, SYNC_FOLDER_TYPE_USER_CONTACT => "Contacts",
359
			SYNC_FOLDER_TYPE_TASK, SYNC_FOLDER_TYPE_USER_TASK => "Tasks",
360
			SYNC_FOLDER_TYPE_NOTE, SYNC_FOLDER_TYPE_USER_NOTE => "Notes",
361
			default => "Emails",
362
		};
363
	}
364
365
	/**
366
	 * Function which is use to get list of additional folders which was shared with given device.
367
	 *
368
	 * @param string $devid device id
369
	 *
370
	 * @return array has list of properties related to shared folders
371
	 */
372
	public function getAdditionalFolderList($devid) {
373
		return [];
374
		// TODO implement
375
		$stores = $GLOBALS["mapisession"]->getAllMessageStores();
0 ignored issues
show
Unused Code introduced by
$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...
376
		$client = $this->getSoapClient();
377
		$items = $client->AdditionalFolderList($devid);
378
		$data = [];
379
		foreach ($items as $item) {
380
			foreach ($stores as $store) {
381
				try {
382
					$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin((string) $item->folderid));
383
				}
384
				catch (MAPIException) {
385
					continue;
386
				}
387
			}
388
			if (isset($entryid)) {
389
				$item->entryid = bin2hex($entryid);
390
			}
391
			array_push($data, ["props" => $item]);
392
		}
393
394
		return $data;
395
	}
396
397
	/**
398
	 * Function which is use to remove additional folder which was shared with given device.
399
	 *
400
	 * @param string $entryId  id of device
401
	 * @param string $folderid id of folder which will remove from device
402
	 */
403
	public function additionalFolderRemove($entryId, $folderid) {
404
		$client = $this->getSoapClient();
0 ignored issues
show
Bug introduced by
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

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