ListModule::search()   F
last analyzed

Complexity

Conditions 26
Paths 9226

Size

Total Lines 186
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 96
c 0
b 0
f 0
nc 9226
nop 4
dl 0
loc 186
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * ListModule
5
 * Superclass of every module, which retrieves a MAPI message list. It
6
 * extends the Module class.
7
 */
8
class ListModule extends Module {
9
	/**
10
	 * @var array list of columns which are selected in the previous request
11
	 */
12
	public $properties;
13
14
	/**
15
	 * @var array sort
16
	 */
17
	public $sort;
18
19
	/**
20
	 * @var int startrow in the table
21
	 */
22
	public $start;
23
24
	/**
25
	 * @var array contains (when needed) a restriction used when searching and filtering the records
26
	 */
27
	public $restriction;
28
29
	/**
30
	 * @var bool contains check whether a search result is listed or just the contents of a normal folder
31
	 */
32
	public $searchFolderList;
33
34
	/**
35
	 * @var array stores entryids and last modification time of
36
	 *            messages that are already sent to the server
37
	 */
38
	public $searchResults;
39
40
	/**
41
	 * @var MAPIMessage resource of the freebusy message which holds
0 ignored issues
show
Bug introduced by
The type MAPIMessage was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
42
	 *                  information regarding delegation details, this variable will
43
	 *                  only be populated when user is a delegate
44
	 */
45
	public $localFreeBusyMessage;
46
47
	/**
48
	 * @var BinString binary string of PR_MDB_PROVIDER property
0 ignored issues
show
Bug introduced by
The type BinString 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...
49
	 *                of a store, this variable will only be populated when user is a delegate
50
	 */
51
	public $storeProviderGuid;
52
53
	/**
54
	 * Constructor.
55
	 *
56
	 * @param int   $id     unique id
57
	 * @param array $data   list of all actions
58
	 * @param mixed $events
59
	 */
60
	public function __construct($id, $data, $events = false) {
61
		$this->start = 0;
62
63
		$this->restriction = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array of property $restriction.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
64
		$this->searchFolderList = false;
65
		$this->localFreeBusyMessage = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type MAPIMessage of property $localFreeBusyMessage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
66
		$this->storeProviderGuid = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type BinString of property $storeProviderGuid.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
67
68
		$this->sort = [
69
			PR_IMPORTANCE => TABLE_SORT_DESCEND,
70
			PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND,
71
		];
72
73
		parent::__construct($id, $data);
74
	}
75
76
	/**
77
	 * Executes all the actions in the $data variable.
78
	 */
79
	#[Override]
80
	public function execute() {
81
		foreach ($this->data as $actionType => $action) {
82
			if (isset($actionType)) {
83
				try {
84
					$store = $this->getActionStore($action);
85
					$parententryid = $this->getActionParentEntryID($action);
86
					$entryid = $this->getActionEntryID($action);
87
88
					switch ($actionType) {
89
						case "list":
90
							$this->getDelegateFolderInfo($store);
0 ignored issues
show
Bug introduced by
$store of type object is incompatible with the type resource expected by parameter $store of ListModule::getDelegateFolderInfo(). ( Ignorable by Annotation )

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

90
							$this->getDelegateFolderInfo(/** @scrutinizer ignore-type */ $store);
Loading history...
91
							$this->messageList($store, $entryid, $action, $actionType);
92
							break;
93
94
						default:
95
							$this->handleUnknownActionType($actionType);
96
					}
97
				}
98
				catch (MAPIException|SearchException $e) {
99
					$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...
100
				}
101
			}
102
		}
103
	}
104
105
	/**
106
	 * Function does customization of MAPIException based on module data.
107
	 * like, here it will generate display message based on actionType
108
	 * for particular exception.
109
	 *
110
	 * @param object     $e             Exception object
111
	 * @param string     $actionType    the action type, sent by the client
112
	 * @param MAPIobject $store         store object of the current user
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...
113
	 * @param string     $parententryid parent entryid of the message
114
	 * @param string     $entryid       entryid of the message/folder
115
	 * @param array      $action        the action data, sent by the client
116
	 */
117
	#[Override]
118
	public function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) {
119
		if (is_null($e->displayMessage)) {
120
			$hexEntryid = $entryid != null ? bin2hex($entryid) : 'null';
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $entryid of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
121
122
			switch ($actionType) {
123
				case "list":
124
					if ($e->getCode() == MAPI_E_NO_ACCESS) {
125
						$e->setDisplayMessage(_("You have insufficient privileges to see the contents of this folder.") . " ({$hexEntryid})");
126
					}
127
					else {
128
						$e->setDisplayMessage(_("Could not load the contents of this folder.") . " ({$hexEntryid})");
129
					}
130
					break;
131
			}
132
		}
133
134
		parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action);
135
	}
136
137
	/**
138
	 * Function which retrieves a list of messages in a folder.
139
	 *
140
	 * @param object $store      MAPI Message Store Object
141
	 * @param string $entryid    entryid of the folder
142
	 * @param array  $action     the action data, sent by the client
143
	 * @param string $actionType the action type, sent by the client
144
	 */
145
	public function messageList($store, $entryid, $action, $actionType) {
146
		$this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content
147
148
		if (!$store || !$entryid) {
0 ignored issues
show
introduced by
$store is of type object, thus it always evaluated to true.
Loading history...
149
			return;
150
		}
151
152
		// Restriction
153
		$this->parseRestriction($action);
0 ignored issues
show
Bug introduced by
$action of type array is incompatible with the type object expected by parameter $action of ListModule::parseRestriction(). ( Ignorable by Annotation )

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

153
		$this->parseRestriction(/** @scrutinizer ignore-type */ $action);
Loading history...
154
155
		// Sort
156
		$this->parseSortOrder($action, null, true);
157
158
		$limit = false;
159
		if (isset($action['restriction']['limit'])) {
160
			$limit = $action['restriction']['limit'];
161
		}
162
		else {
163
			$limit = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
164
		}
165
166
		$isSearchFolder = isset($action['search_folder_entryid']);
167
		$entryid = $isSearchFolder ? hex2bin((string) $action['search_folder_entryid']) : $entryid;
168
169
		// Get the table and merge the arrays
170
		$data = $GLOBALS["operations"]->getTable($store, $entryid, $this->properties, $this->sort, $this->start, $limit, $this->restriction);
171
172
		// If the request come from search folder then no need to send folder information
173
		if (!$isSearchFolder) {
174
			// Open the folder.
175
			$folder = mapi_msgstore_openentry($store, $entryid);
0 ignored issues
show
Unused Code introduced by
The assignment to $folder is dead and can be removed.
Loading history...
176
			$data["folder"] = [];
177
178
			// Skip content statistics to avoid expensive counts
179
		}
180
181
		$data = $this->filterPrivateItems($data);
182
183
		// Allowing to hook in just before the data sent away to be sent to the client
184
		$GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', [
185
			'moduleObject' => &$this,
186
			'store' => $store,
187
			'entryid' => $entryid,
188
			'action' => $action,
189
			'data' => &$data,
190
		]);
191
192
		// unset will remove the value but will not regenerate array keys, so we need to
193
		// do it here
194
		$data["item"] = array_values($data["item"]);
195
196
		if (isset($action['use_searchfolder']) && $action['use_searchfolder'] === true) {
197
			$data["search_meta"] = [];
198
			$data["search_meta"]["search_store_entryid"] = $action["store_entryid"];
199
			$data["search_meta"]["results"] = count($data["item"]);
200
		}
201
202
		$this->addActionData($actionType, $data);
203
		$GLOBALS["bus"]->addData($this->getResponseData());
204
	}
205
206
	/**
207
	 *	Function will set search restrictions on search folder and start search process
208
	 *	and it will also parse visible columns and sorting data when sending results to client.
209
	 *
210
	 * @param object $store      MAPI Message Store Object
211
	 * @param string $entryid    entryid of the folder
212
	 * @param object $action     the action data, sent by the client
213
	 * @param string $actionType the action type, sent by the client
214
	 */
215
	public function search($store, $entryid, $action, $actionType) {
216
		$useSearchFolder = $action["use_searchfolder"] ?? false;
217
		if (!$useSearchFolder) {
218
			/*
219
			 * store doesn't support search folders so we can't use this
220
			 * method instead we will pass restriction to messageList and
221
			 * it will give us the restricted results
222
			 */
223
			return $this->messageList($store, $entryid, $action, "list");
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->messageList($stor...tryid, $action, 'list') targeting ListModule::messageList() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
$action of type object is incompatible with the type array expected by parameter $action of ListModule::messageList(). ( Ignorable by Annotation )

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

223
			return $this->messageList($store, $entryid, /** @scrutinizer ignore-type */ $action, "list");
Loading history...
224
		}
225
226
		$this->searchFolderList = true; // Set to indicate this is not the normal folder, but a search folder
227
		$this->restriction = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array of property $restriction.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
228
		$searchInTodoList = $GLOBALS['entryid']->compareEntryIds(bin2hex($entryid), bin2hex(TodoList::getEntryId()));
229
230
		// Parse Restriction
231
		$this->parseRestriction($action);
232
		if ($this->restriction == false) {
233
			// if error in creating restriction then send error to client
234
			$errorInfo = [];
235
			$errorInfo["error_message"] = _("Error in search, please try again") . ".";
236
			$errorInfo["original_error_message"] = "Error in parsing restrictions.";
237
238
			return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo);
0 ignored issues
show
Bug introduced by
$errorInfo of type array is incompatible with the type object expected by parameter $errorInfo of ListModule::sendSearchErrorToClient(). ( Ignorable by Annotation )

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

238
			return $this->sendSearchErrorToClient($store, $entryid, $action, /** @scrutinizer ignore-type */ $errorInfo);
Loading history...
Bug introduced by
$entryid of type string is incompatible with the type hexString expected by parameter $entryid of ListModule::sendSearchErrorToClient(). ( Ignorable by Annotation )

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

238
			return $this->sendSearchErrorToClient($store, /** @scrutinizer ignore-type */ $entryid, $action, $errorInfo);
Loading history...
239
		}
240
241
		if ($searchInTodoList) {
242
			// Since we cannot search in a search folder, we will combine the search restriction
243
			// with the search restriction of the to-do list to mimic searching in the To-do list
244
			$this->restriction = [
245
				RES_AND,
246
				[
247
					$this->restriction,
248
					TodoList::_createRestriction(),
249
				],
250
			];
251
252
			// When searching in the To-do list we will actually always search in the IPM subtree, so
253
			// set the entryid to that.
254
			$userStore = WebAppAuthentication::getMAPISession()->getDefaultMessageStore();
255
			$props = mapi_getprops($userStore, [PR_IPM_SUBTREE_ENTRYID]);
256
			$entryid = $props[PR_IPM_SUBTREE_ENTRYID];
257
		}
258
259
		$isSetSearchFolderEntryId = isset($action['search_folder_entryid']);
260
		if ($isSetSearchFolderEntryId) {
261
			$this->sessionData['searchFolderEntryId'] = $action['search_folder_entryid'];
262
		}
263
264
		if (isset($action['forceCreateSearchFolder']) && $action['forceCreateSearchFolder']) {
265
			$isSetSearchFolderEntryId = false;
266
		}
267
268
		// create or open search folder
269
		$searchFolder = $this->createSearchFolder($store, $isSetSearchFolderEntryId);
270
		if ($searchFolder === false) {
271
			// if error in creating search folder then send error to client
272
			$errorInfo = [];
273
274
			$errorInfo["error_message"] = match (mapi_last_hresult()) {
275
				MAPI_E_NO_ACCESS => _("Unable to perform search query, no permissions to create search folder."),
276
				MAPI_E_NOT_FOUND => _("Unable to perform search query, search folder not found."),
277
				default => _("Unable to perform search query, store might not support searching."),
278
			};
279
280
			$errorInfo["original_error_message"] = _("Error in creating search folder.");
281
282
			return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo);
0 ignored issues
show
Bug introduced by
$errorInfo of type array is incompatible with the type object expected by parameter $errorInfo of ListModule::sendSearchErrorToClient(). ( Ignorable by Annotation )

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

282
			return $this->sendSearchErrorToClient($store, $entryid, $action, /** @scrutinizer ignore-type */ $errorInfo);
Loading history...
283
		}
284
285
		$subfolder_flag = 0;
286
		if ($searchInTodoList || (isset($action["subfolders"]) && $action["subfolders"] == "true")) {
287
			$subfolder_flag = RECURSIVE_SEARCH;
288
		}
289
290
		if (!is_array($entryid)) {
291
			$entryids = [$entryid];
292
		}
293
		else {
294
			$entryids = $entryid;
295
		}
296
297
		$searchFolderEntryId = $this->sessionData['searchFolderEntryId'];
298
299
		// check if searchcriteria has changed
300
		$restrictionCheck = md5(serialize($this->restriction) . $searchFolderEntryId . $subfolder_flag);
301
302
		// check if there is need to set searchcriteria again
303
		if (!isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck != $this->sessionData['searchCriteriaCheck']) {
304
			if (!empty($this->sessionData['searchOriginalEntryids'])) {
305
				// get entryids of original folders, and use it to set new search criteria
306
				$entryids = [];
307
				for ($index = 0; $index < count($this->sessionData['searchOriginalEntryids']); ++$index) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
308
					$entryids[] = hex2bin((string) $this->sessionData['searchOriginalEntryids'][$index]);
309
				}
310
			}
311
			else {
312
				// store entryids of original folders, so that can be used for re-setting the search criteria if needed
313
				$this->sessionData['searchOriginalEntryids'] = [];
314
				for ($index = 0, $len = count($entryids); $index < $len; ++$index) {
315
					$this->sessionData['searchOriginalEntryids'][] = bin2hex((string) $entryids[$index]);
316
				}
317
			}
318
319
			mapi_folder_setsearchcriteria($searchFolder, $this->restriction, $entryids, $subfolder_flag);
320
			$this->sessionData['searchCriteriaCheck'] = $restrictionCheck;
321
		}
322
323
		if (isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck == $this->sessionData['searchCriteriaCheck']) {
324
			$folderEntryid = bin2hex((string) $entryid);
325
			if ($this->sessionData['searchOriginalEntryids'][0] !== $folderEntryid) {
326
				$this->sessionData['searchOriginalEntryids'][0] = $folderEntryid;
327
				mapi_folder_setsearchcriteria($searchFolder, $this->restriction, [$entryid], $subfolder_flag);
328
			}
329
		}
330
331
		unset($action["restriction"]);
332
333
		// Sort
334
		$this->parseSortOrder($action);
335
336
		// Create the data array, which will be send back to the client
337
		$data = [];
338
339
		// Wait until we have some data, no point in returning before we have data. Stop waiting after 10 seconds
340
		$start = time();
341
		$table = mapi_folder_getcontentstable($searchFolder, MAPI_DEFERRED_ERRORS);
342
343
		sleep(1);
344
345
		while (time() - $start < 10) {
346
			$count = mapi_table_getrowcount($table);
347
			$result = mapi_folder_getsearchcriteria($searchFolder);
348
349
			// Stop looping if we have data or the search is finished
350
			if ($count > 0) {
351
				break;
352
			}
353
354
			if (($result["searchstate"] & SEARCH_REBUILD) == 0) {
355
				break;
356
			} // Search is done
357
358
			sleep(1);
359
		}
360
361
		// Get the table and merge the arrays
362
		$table = $GLOBALS["operations"]->getTable($store, hex2bin((string) $searchFolderEntryId), $this->properties, $this->sort, $this->start);
363
		$data = array_merge($data, $table);
364
365
		$this->getDelegateFolderInfo($store);
0 ignored issues
show
Bug introduced by
$store of type object is incompatible with the type resource expected by parameter $store of ListModule::getDelegateFolderInfo(). ( Ignorable by Annotation )

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

365
		$this->getDelegateFolderInfo(/** @scrutinizer ignore-type */ $store);
Loading history...
366
		$data = $this->filterPrivateItems($data);
367
368
		// remember which entryid's are send to the client
369
		$searchResults = [];
370
		foreach ($table["item"] as $item) {
371
			// store entryid => last_modification_time mapping
372
			$searchResults[$item["entryid"]] = $item["props"]["last_modification_time"];
373
		}
374
375
		// store search results into session data
376
		if (!isset($this->sessionData['searchResults'])) {
377
			$this->sessionData['searchResults'] = [];
378
		}
379
		$this->sessionData['searchResults'][$searchFolderEntryId] = $searchResults;
380
381
		$result = mapi_folder_getsearchcriteria($searchFolder);
382
383
		$data["search_meta"] = [];
384
		$data["search_meta"]["searchfolder_entryid"] = $searchFolderEntryId;
385
		$data["search_meta"]["search_store_entryid"] = $action["store_entryid"];
386
		$data["search_meta"]["searchstate"] = $result["searchstate"];
387
		$data["search_meta"]["results"] = count($searchResults);
388
389
		// Reopen the search folder, because otherwise the suggestion property will
390
		// not have been updated
391
		$searchFolder = $this->createSearchFolder($store, true);
392
		$storeProps = mapi_getprops($searchFolder, [PR_EC_SUGGESTION]);
393
		if (isset($storeProps[PR_EC_SUGGESTION])) {
394
			$data["search_meta"]["suggestion"] = $storeProps[PR_EC_SUGGESTION];
395
		}
396
397
		$this->addActionData("search", $data);
398
		$GLOBALS["bus"]->addData($this->getResponseData());
399
400
		return true;
401
	}
402
403
	/**
404
	 *	Function will check for the status of the search on server
405
	 *	and it will also send intermediate results of search, so we don't have to wait
406
	 *	until search is finished on server to send results.
407
	 *
408
	 * @param object    $store   MAPI Message Store Object
409
	 * @param hexString $entryid entryid of the folder
0 ignored issues
show
Bug introduced by
The type hexString 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...
410
	 * @param object    $action  the action data, sent by the client
411
	 */
412
	public function updatesearch($store, $entryid, $action) {
413
		if (!isset($entryid) || !$entryid) {
414
			// if no entryid is present then we can't do anything here
415
			return;
416
		}
417
418
		$listData = [];
419
		if (isset($action['search_folder_entryid'])) {
420
			$entryid = hex2bin($action['search_folder_entryid']);
421
		}
422
		$searchFolder = mapi_msgstore_openentry($store, $entryid);
423
		$searchResult = mapi_folder_getsearchcriteria($searchFolder);
424
		$searchState = $searchResult["searchstate"];
425
		$table = mapi_folder_getcontentstable($searchFolder, MAPI_DEFERRED_ERRORS);
426
427
		if (is_array($this->sort) && !empty($this->sort)) {
428
			// this sorting will be done on currently fetched results, not all results
429
			// @TODO find a way to do sorting on all search results
430
			mapi_table_sort($table, $this->sort, TBL_BATCH);
431
		}
432
433
		$rowCount = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50);
434
435
		$searchResults = [];
436
		$entryid = bin2hex($entryid);
437
		if (isset($this->sessionData['searchResults'][$entryid])) {
438
			$searchResults = $this->sessionData['searchResults'][$entryid];
439
		}
440
441
		// searchResults contains entryids of messages
442
		// that are already sent to the server
443
		$numberOfResults = count($searchResults);
444
445
		if ($numberOfResults < $rowCount) {
446
			$items = mapi_table_queryallrows($table, [PR_ENTRYID, PR_LAST_MODIFICATION_TIME]);
447
448
			foreach ($items as $props) {
449
				$sendItemToClient = false;
450
451
				if (!array_key_exists(bin2hex((string) $props[PR_ENTRYID]), $searchResults)) {
452
					$sendItemToClient = true;
453
				}
454
				else {
455
					/*
456
					 * it could happen that an item in search folder has been changed
457
					 * after we have sent it to client, so we have to again send it
458
					 * so we will have to use last_modification_time of item to check
459
					 * that item has been modified since we have sent it to client
460
					 */
461
					// TODO if any item is deleted from search folder it will be not notified to client
462
					if ($searchResults[bin2hex((string) $props[PR_ENTRYID])] < $props[PR_LAST_MODIFICATION_TIME]) {
463
						$sendItemToClient = true;
464
					}
465
				}
466
467
				if ($sendItemToClient) {
468
					// only get primitive properties, no need to get body, attachments or recipient information
469
					$message = $GLOBALS["operations"]->openMessage($store, $props[PR_ENTRYID]);
470
					array_push($listData, $GLOBALS["operations"]->getProps($message, $this->properties));
471
472
					// store entryid => last_modification_time mapping
473
					$searchResults[bin2hex((string) $props[PR_ENTRYID])] = $props[PR_LAST_MODIFICATION_TIME];
474
				}
475
476
				// when we have more results then fit in the client, we break here,
477
				// we only need to update the counters from this point
478
				$numberOfResults = count($searchResults);
479
				if ($numberOfResults >= $rowCount) {
480
					break;
481
				}
482
			}
483
		}
484
485
		$totalRowCount = mapi_table_getrowcount($table);
486
487
		$data = [];
488
		$data["search_meta"] = [];
489
		$data["search_meta"]["searchfolder_entryid"] = $entryid;
490
		$data["search_meta"]["search_store_entryid"] = $action["store_entryid"];
491
		$data["search_meta"]["searchstate"] = $searchState;
492
		$data["search_meta"]["results"] = $numberOfResults;		// actual number of items that we are sending to client
493
494
		$data["page"] = [];
495
		$data["page"]["start"] = 0;
496
		$data["page"]["rowcount"] = $rowCount;
497
		$data["page"]["totalrowcount"] = $totalRowCount;	// total number of items
498
499
		if (!empty($listData)) {
500
			$data["item"] = array_merge([], $listData);
501
		}
502
503
		// search is finished so we no more need entryids of search results so clear it up
504
		if ($searchState & SEARCH_REBUILD === 0) {
505
			// remove search result entryids stored in session
506
			unset($this->sessionData['searchResults'][$entryid]);
507
		}
508
		else {
509
			// store data for next request
510
			$this->sessionData['searchResults'][$entryid] = $searchResults;
511
		}
512
513
		$this->addActionData("updatesearch", $data);
514
		$GLOBALS["bus"]->addData($this->getResponseData());
515
516
		return true;
517
	}
518
519
	/**
520
	 *	Function will stop search on the server if search folder exists.
521
	 *
522
	 * @param object    $store   MAPI Message Store Object
523
	 * @param hexString $entryid entryid of the folder
524
	 * @param object    $action  the action data, sent by the client
525
	 */
526
	public function stopSearch($store, $entryid, $action) {
527
		// if no entryid is present in the request then get the search folder entryid from session data
528
		$entryid = !empty($entryid) ? $entryid : (!empty($action['search_folder_entryid']) ? hex2bin((string) $action['search_folder_entryid']) : null);
529
530
		if (empty($entryid)) {
531
			// still no entryid? sorry i can't help you anymore
532
			$this->addActionData("stopsearch", ['success' => false]);
533
			$GLOBALS["bus"]->addData($this->getResponseData());
534
535
			return;
536
		}
537
538
		// remove search result entryids stored in session
539
		unset($this->sessionData['searchResults'][bin2hex($entryid)], $this->sessionData['searchCriteriaCheck'], $this->sessionData['searchFolderEntryId'], $this->sessionData['searchOriginalEntryids']);
540
541
		$searchFolder = mapi_msgstore_openentry($store, $entryid);
542
		$searchResult = mapi_folder_getsearchcriteria($searchFolder);
543
544
		// check if search folder exists and search is in progress
545
		if ($searchResult !== false && ($searchResult["searchstate"] & SEARCH_REBUILD !== 0)) {
546
			mapi_folder_setsearchcriteria($searchFolder, $searchResult['restriction'], $searchResult['folderlist'], STOP_SEARCH);
547
		}
548
549
		/*
550
		 * when stopping search process, we have to remove search folder also,
551
		 * so next search request with same restriction will not get uncompleted results
552
		 */
553
		$this->deleteSearchFolder($store, $entryid, $action);
0 ignored issues
show
Bug introduced by
It seems like $entryid can also be of type string; however, parameter $entryid of ListModule::deleteSearchFolder() does only seem to accept hexString, 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

553
		$this->deleteSearchFolder($store, /** @scrutinizer ignore-type */ $entryid, $action);
Loading history...
Bug introduced by
$action of type object is incompatible with the type array expected by parameter $action of ListModule::deleteSearchFolder(). ( Ignorable by Annotation )

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

553
		$this->deleteSearchFolder($store, $entryid, /** @scrutinizer ignore-type */ $action);
Loading history...
554
555
		// send success message to client
556
		$this->addActionData("stopsearch", ['success' => true]);
557
		$GLOBALS["bus"]->addData($this->getResponseData());
558
	}
559
560
	/**
561
	 * Function will delete search folder.
562
	 *
563
	 * @param object    $store   MAPI Message Store Object
564
	 * @param hexString $entryid entryid of the folder
565
	 * @param array     $action  the action data, sent by the client
566
	 *
567
	 * @return bool true on success or false on failure
568
	 */
569
	public function deleteSearchFolder($store, $entryid, $action) {
570
		if ($entryid && $store) {
0 ignored issues
show
introduced by
$store is of type object, thus it always evaluated to true.
Loading history...
571
			$storeProps = mapi_getprops($store, [PR_FINDER_ENTRYID]);
572
573
			$finderFolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
574
575
			if (mapi_last_hresult() != NOERROR) {
576
				return;
577
			}
578
579
			$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
580
581
			$restriction = [RES_CONTENT,
582
				[
583
					FUZZYLEVEL => FL_FULLSTRING,
584
					ULPROPTAG => PR_ENTRYID,
585
					VALUE => [PR_ENTRYID => $entryid],
586
				],
587
			];
588
589
			mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH);
590
591
			// entryids are unique so there would be only one matching row,
592
			// so only fetch first row
593
			$folders = mapi_table_queryrows($hierarchyTable, [PR_ENTRYID], 0, 1);
594
595
			// delete search folder
596
			if (is_array($folders) && is_array($folders[0])) {
597
				mapi_folder_deletefolder($finderFolder, $folders[0][PR_ENTRYID]);
598
			}
599
600
			return true;
601
		}
602
603
		return false;
604
	}
605
606
	/**
607
	 *	Function will create a search folder in FINDER_ROOT folder
608
	 *	if folder exists then it will open it.
609
	 *
610
	 * @param object $store        MAPI Message Store Object
611
	 * @param bool   $openIfExists open if folder exists
612
	 *
613
	 * @return bool|resource $folder created search folder
614
	 */
615
	public function createSearchFolder($store, $openIfExists = true) {
616
		if (isset($this->sessionData['searchFolderEntryId']) && $openIfExists) {
617
			try {
618
				$searchFolder = mapi_msgstore_openentry($store, hex2bin($this->sessionData['searchFolderEntryId']));
619
620
				if ($searchFolder !== false) {
621
					// search folder exists, don't create new search folder
622
					return $searchFolder;
623
				}
624
			}
625
			catch (MAPIException) {
626
				// ignore error and continue creation of search folder
627
				unset($this->sessionData['searchFolderEntryId']);
628
			}
629
		}
630
631
		// create new search folder
632
		$searchFolderRoot = $this->getSearchFoldersRoot($store);
633
		if ($searchFolderRoot === false) {
634
			// error in finding search root folder
635
			// or store doesn't support search folders
636
			return false;
637
		}
638
639
		// check for folder name, if exists then delete it
640
		$folderName = "grommunio Web Search Folder";
641
642
		try {
643
			$table = mapi_folder_gethierarchytable($searchFolderRoot, 0);
644
			$rows = mapi_table_queryrows($table, [PR_DISPLAY_NAME, PR_ENTRYID], 0, 0xFFFF);
645
			foreach ($rows as $row) {
646
				if (strcasecmp($folderName, (string) $row[PR_DISPLAY_NAME]) == 0) {
647
					mapi_folder_deletefolder($searchFolderRoot, $row[PR_ENTRYID], DEL_FOLDERS | DEL_MESSAGES | DELETE_HARD_DELETE);
648
					break;
649
				}
650
			}
651
			$searchFolder = mapi_folder_createfolder($searchFolderRoot, $folderName, '', OPEN_IF_EXISTS, FOLDER_SEARCH);
652
653
			$props = mapi_getprops($searchFolder, [PR_ENTRYID]);
654
			$this->sessionData['searchFolderEntryId'] = bin2hex((string) $props[PR_ENTRYID]);
655
656
			// we have created new search folder so search criteria check should be removed
657
			unset($this->sessionData['searchCriteriaCheck']);
658
659
			return $searchFolder;
660
		}
661
		catch (MAPIException $e) {
662
			// don't propagate the event to higher level exception handlers
663
			$e->setHandled();
664
		}
665
666
		return false;
667
	}
668
669
	/**
670
	 *	Function will open FINDER_ROOT folder in root container
671
	 *	public folder's don't have FINDER_ROOT folder.
672
	 *
673
	 *	@param		object			store MAPI message store object
0 ignored issues
show
Bug introduced by
The type store 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...
674
	 * @param mixed $store
675
	 *
676
	 * @return bool|resource finder root folder for search folders
677
	 */
678
	public function getSearchFoldersRoot($store) {
679
		$searchRootFolder = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $searchRootFolder is dead and can be removed.
Loading history...
680
681
		// check if we can create search folders
682
		$storeProps = mapi_getprops($store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID, PR_DISPLAY_NAME]);
683
		if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) !== STORE_SEARCH_OK) {
684
			// store doesn't support search folders, public store don't have FINDER_ROOT folder
685
			return false;
686
		}
687
688
		try {
689
			$searchRootFolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
690
		}
691
		catch (MAPIException $e) {
692
			$msg = "Unable to open FINDER_ROOT for store: %s.";
693
			error_log(sprintf($msg, $storeProps[PR_DISPLAY_NAME]));
694
			// don't propagate the event to higher level exception handlers
695
			$e->setHandled();
696
		}
697
698
		return $searchRootFolder;
699
	}
700
701
	/**
702
	 *	Function will send error message to client if any error has occurred in search.
703
	 *
704
	 * @param object    $store     MAPI Message Store Object
705
	 * @param hexString $entryid   entryid of the folder
706
	 * @param object    $action    the action data, sent by the client
707
	 * @param object    $errorInfo the error information object
708
	 */
709
	public function sendSearchErrorToClient($store, $entryid, $action, $errorInfo) {
710
		if ($errorInfo) {
0 ignored issues
show
introduced by
$errorInfo is of type object, thus it always evaluated to true.
Loading history...
711
			$exception = new SearchException($errorInfo["original_error_message"] ?? $errorInfo['error_message'], mapi_last_hresult());
712
			$exception->setDisplayMessage($errorInfo['error_message']);
713
714
			// after sending error, remove error data
715
			$errorInfo = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $errorInfo is dead and can be removed.
Loading history...
716
717
			throw $exception;
718
		}
719
720
		return false;
721
	}
722
723
	/**
724
	 *	Function will create restriction based on restriction array.
725
	 *
726
	 * @param object $action the action data, sent by the client
727
	 */
728
	public function parseRestriction($action) {
729
		if (isset($action["restriction"]) && is_array($action['restriction'])) {
730
			if (isset($action["restriction"]["start"])) {
731
				// Set start variable
732
				$this->start = $action["restriction"]["start"];
733
			}
734
			foreach ($action["restriction"] as $key => $value) {
735
				$props = $this->properties;
736
				if (!empty($value)) {
737
					switch ($key) {
738
						case "search":
739
							$props = array_merge($this->properties, ['body' => PR_BODY]);
740
741
							// no break
742
						case "task":
743
						case "note":
744
						case "filter":
745
							$this->restriction = Conversion::json2restriction($props, $value);
746
							break;
747
					}
748
				}
749
			}
750
		}
751
	}
752
753
	/**
754
	 * Parses the incoming sort request and builds a MAPI sort order. Normally
755
	 * properties are mapped from the XML to MAPI by the standard $this->properties mapping. However,
756
	 * if you want other mappings, you can specify them in the optional $map mapping.
757
	 *
758
	 * $allow_multi_instance is used for creating multiple instance of MV property related items.
759
	 * $properties is used for using a custom set of properties instead of properties stored in module
760
	 *
761
	 * @param mixed $action
762
	 * @param mixed $map
763
	 * @param mixed $allow_multi_instance
764
	 * @param mixed $properties
765
	 */
766
	public function parseSortOrder($action, $map = false, $allow_multi_instance = false, $properties = false) {
767
		if (isset($action["sort"])) {
768
			$this->sort = [];
769
770
			if (!$properties) {
771
				$properties = $this->properties;
772
			}
773
774
			// Unshift MVI_FLAG of MV properties. So the table is not sort on it anymore.
775
			// Otherwise the server would generate multiple rows for one item (categories).
776
			foreach ($properties as $id => $property) {
777
				switch (mapi_prop_type($property)) {
778
					case PT_MV_STRING8 | MVI_FLAG:
779
					case PT_MV_LONG | MVI_FLAG:
780
						$properties[$id] = $properties[$id] & ~MV_INSTANCE;
781
						break;
782
				}
783
			}
784
785
			// Loop through the sort columns
786
			foreach ($action["sort"] as $column) {
787
				if (isset($column["direction"])) {
788
					if (isset($properties[$column["field"]]) || ($map && isset($map[$column["field"]]))) {
789
						if ($map && isset($map[$column["field"]])) {
790
							$property = $map[$column["field"]];
791
						}
792
						else {
793
							$property = $properties[$column["field"]];
794
						}
795
796
						// Check if column is a MV property
797
						switch (mapi_prop_type($property)) {
798
							case PT_MV_STRING8:
799
							case PT_MV_LONG:
800
								// Set MVI_FLAG.
801
								// The server will generate multiple rows for one item (for example: categories)
802
								if ($allow_multi_instance) {
803
									$properties[$column["field"]] = $properties[$column["field"]] | MVI_FLAG;
804
								}
805
								$property = $properties[$column["field"]];
806
								break;
807
						}
808
809
						// Set sort direction
810
						switch (strtolower($column["direction"])) {
811
							default:
812
							case "asc":
813
								$this->sort[$property] = TABLE_SORT_ASCEND;
814
								break;
815
816
							case "desc":
817
								$this->sort[$property] = TABLE_SORT_DESCEND;
818
								break;
819
						}
820
					}
821
				}
822
			}
823
		}
824
	}
825
826
	/**
827
	 * Function which gets the delegation details from localfreebusy message to use in
828
	 * processPrivateItems function.
829
	 *
830
	 * @param resource $store MAPI Message Store Object
831
	 */
832
	public function getDelegateFolderInfo($store) {
833
		$this->localFreeBusyMessage = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type MAPIMessage of property $localFreeBusyMessage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
834
		$this->storeProviderGuid = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type BinString of property $storeProviderGuid.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
835
836
		try {
837
			$this->storeProviderGuid = mapi_getprops($store, [PR_MDB_PROVIDER]);
838
			$this->storeProviderGuid = $this->storeProviderGuid[PR_MDB_PROVIDER];
839
840
			if ($this->storeProviderGuid !== ZARAFA_STORE_DELEGATE_GUID) {
841
				// user is not a delegate, so no point of processing further
842
				return;
843
			}
844
845
			// get localfreebusy message
846
			$this->localFreeBusyMessage = FreeBusy::getLocalFreeBusyMessage($store);
847
		}
848
		catch (MAPIException $e) {
849
			// we got some error, but we don't care about that error instead just continue
850
			$e->setHandled();
851
852
			$this->localFreeBusyMessage = false;
853
			$this->storeProviderGuid = false;
854
		}
855
	}
856
857
	/**
858
	 * Helper function which loop through each item and filter out
859
	 * private items, if any.
860
	 *
861
	 * @param array array structure with row search data
862
	 * @param mixed $data
863
	 *
864
	 * @return array array structure with row search data
865
	 */
866
	public function filterPrivateItems($data) {
867
		// Disable private items
868
		if (isset($data["item"]) && is_array($data["item"])) {
869
			for ($index = 0, $len = count($data["item"]); $index < $len; ++$index) {
870
				$data["item"][$index] = $this->processPrivateItem($data["item"][$index]);
871
872
				if (empty($data["item"][$index])) {
873
					// remove empty results from data
874
					unset($data["item"][$index]);
875
				}
876
			}
877
		}
878
879
		return $data;
880
	}
881
882
	/**
883
	 * Function will be used to process private items in a list response, modules can
884
	 * can decide what to do with the private items, remove the entire row or just
885
	 * hide the data. This function will entirely remove the private message but
886
	 * if any child class needs different behavior then this can be overridden.
887
	 *
888
	 * @param object $item item properties
889
	 *
890
	 * @return object item properties if its non private item otherwise empty array
891
	 */
892
	public function processPrivateItem($item) {
893
		if ($this->checkPrivateItem($item)) {
894
			// hide the item by returning empty array, that can be removed from response
895
			return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type object.
Loading history...
896
		}
897
898
		return $item;
899
	}
900
901
	/**
902
	 * Function will be used check if any item is private or not and if it private then
903
	 * we should process it as private, because you don't want to process private items in
904
	 * user's default store.
905
	 * This function will check we are dealing with delegate stores or not if it is then
906
	 * the delegator has permission to see private items of delegate.
907
	 *
908
	 * @param object $item item properties
909
	 *
910
	 * @return bool true if items should be processed as private else false
911
	 */
912
	public function checkPrivateItem($item) {
913
		// flag to indicate that item should be considered as private
914
		$private = false;
915
916
		$isPrivate = (isset($item['props']['private']) && $item['props']['private'] === true);
917
		$isSensitive = (isset($item['props']['sensitivity']) && $item['props']['sensitivity'] === SENSITIVITY_PRIVATE);
918
919
		if ($isPrivate || $isSensitive) {
920
			// check for delegate permissions for delegate store
921
			if ($this->storeProviderGuid !== false && $this->storeProviderGuid === ZARAFA_STORE_DELEGATE_GUID) {
922
				// by default we should always hide the item if we are in delegate store
923
				$private = true;
924
925
				// find delegate properties
926
				if ($this->localFreeBusyMessage !== false) {
0 ignored issues
show
introduced by
The condition $this->localFreeBusyMessage !== false is always true.
Loading history...
927
					try {
928
						$localFreeBusyMessageProps = mapi_getprops($this->localFreeBusyMessage, [PR_SCHDINFO_DELEGATE_ENTRYIDS, PR_DELEGATE_FLAGS]);
929
930
						if (isset($localFreeBusyMessageProps[PR_SCHDINFO_DELEGATE_ENTRYIDS], $localFreeBusyMessageProps[PR_DELEGATE_FLAGS])) {
931
							// if more then one delegates info is stored then find index of
932
							// current user
933
							$userEntryId = $GLOBALS['mapisession']->getUserEntryID();
934
935
							$userFound = false;
936
							$seePrivate = false;
937
							foreach ($localFreeBusyMessageProps[PR_SCHDINFO_DELEGATE_ENTRYIDS] as $key => $entryId) {
938
								if ($GLOBALS['entryid']->compareEntryIds(bin2hex((string) $userEntryId), bin2hex((string) $entryId))) {
939
									$userFound = true;
940
									$seePrivate = $localFreeBusyMessageProps[PR_DELEGATE_FLAGS][$key];
941
									break;
942
								}
943
							}
944
945
							if ($userFound !== false && $seePrivate === 1) {
946
								// if delegate has permission then don't hide the item
947
								$private = false;
948
							}
949
						}
950
					}
951
					catch (MAPIException $e) {
952
						if ($e->getCode() === MAPI_E_NOT_FOUND) {
953
							// no information available for delegates, ignore error
954
							$e->setHandled();
955
						}
956
					}
957
				}
958
			}
959
		}
960
961
		return $private;
962
	}
963
}
964