ListModule::stopSearch()   A
last analyzed

Complexity

Conditions 6
Paths 12

Size

Total Lines 32
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 12
nop 3
dl 0
loc 32
rs 9.2222
c 0
b 0
f 0
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);
176
			$data["folder"] = [];
177
178
			// Obtain some statistics from the folder contents
179
			$contentcount = mapi_getprops($folder, [PR_CONTENT_COUNT, PR_CONTENT_UNREAD]);
180
			if (isset($contentcount[PR_CONTENT_COUNT])) {
181
				$data["folder"]["content_count"] = $contentcount[PR_CONTENT_COUNT];
182
			}
183
184
			if (isset($contentcount[PR_CONTENT_UNREAD])) {
185
				$data["folder"]["content_unread"] = $contentcount[PR_CONTENT_UNREAD];
186
			}
187
		}
188
189
		$data = $this->filterPrivateItems($data);
190
191
		// Allowing to hook in just before the data sent away to be sent to the client
192
		$GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', [
193
			'moduleObject' => &$this,
194
			'store' => $store,
195
			'entryid' => $entryid,
196
			'action' => $action,
197
			'data' => &$data,
198
		]);
199
200
		// unset will remove the value but will not regenerate array keys, so we need to
201
		// do it here
202
		$data["item"] = array_values($data["item"]);
203
204
		if (isset($action['use_searchfolder']) && $action['use_searchfolder'] === true) {
205
			$data["search_meta"] = [];
206
			$data["search_meta"]["search_store_entryid"] = $action["store_entryid"];
207
			$data["search_meta"]["results"] = count($data["item"]);
208
		}
209
210
		$this->addActionData($actionType, $data);
211
		$GLOBALS["bus"]->addData($this->getResponseData());
212
	}
213
214
	/**
215
	 *	Function will set search restrictions on search folder and start search process
216
	 *	and it will also parse visible columns and sorting data when sending results to client.
217
	 *
218
	 * @param object $store      MAPI Message Store Object
219
	 * @param string $entryid    entryid of the folder
220
	 * @param object $action     the action data, sent by the client
221
	 * @param string $actionType the action type, sent by the client
222
	 */
223
	public function search($store, $entryid, $action, $actionType) {
224
		$useSearchFolder = $action["use_searchfolder"] ?? false;
225
		if (!$useSearchFolder) {
226
			/*
227
			 * store doesn't support search folders so we can't use this
228
			 * method instead we will pass restriction to messageList and
229
			 * it will give us the restricted results
230
			 */
231
			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

231
			return $this->messageList($store, $entryid, /** @scrutinizer ignore-type */ $action, "list");
Loading history...
232
		}
233
234
		$this->searchFolderList = true; // Set to indicate this is not the normal folder, but a search folder
235
		$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...
236
		$searchInTodoList = $GLOBALS['entryid']->compareEntryIds(bin2hex($entryid), bin2hex(TodoList::getEntryId()));
237
238
		// Parse Restriction
239
		$this->parseRestriction($action);
240
		if ($this->restriction == false) {
241
			// if error in creating restriction then send error to client
242
			$errorInfo = [];
243
			$errorInfo["error_message"] = _("Error in search, please try again") . ".";
244
			$errorInfo["original_error_message"] = "Error in parsing restrictions.";
245
246
			return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo);
0 ignored issues
show
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

246
			return $this->sendSearchErrorToClient($store, /** @scrutinizer ignore-type */ $entryid, $action, $errorInfo);
Loading history...
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

246
			return $this->sendSearchErrorToClient($store, $entryid, $action, /** @scrutinizer ignore-type */ $errorInfo);
Loading history...
247
		}
248
249
		if ($searchInTodoList) {
250
			// Since we cannot search in a search folder, we will combine the search restriction
251
			// with the search restriction of the to-do list to mimic searching in the To-do list
252
			$this->restriction = [
253
				RES_AND,
254
				[
255
					$this->restriction,
256
					TodoList::_createRestriction(),
257
				],
258
			];
259
260
			// When searching in the To-do list we will actually always search in the IPM subtree, so
261
			// set the entryid to that.
262
			$userStore = WebAppAuthentication::getMAPISession()->getDefaultMessageStore();
263
			$props = mapi_getprops($userStore, [PR_IPM_SUBTREE_ENTRYID]);
264
			$entryid = $props[PR_IPM_SUBTREE_ENTRYID];
265
		}
266
267
		$isSetSearchFolderEntryId = isset($action['search_folder_entryid']);
268
		if ($isSetSearchFolderEntryId) {
269
			$this->sessionData['searchFolderEntryId'] = $action['search_folder_entryid'];
270
		}
271
272
		if (isset($action['forceCreateSearchFolder']) && $action['forceCreateSearchFolder']) {
273
			$isSetSearchFolderEntryId = false;
274
		}
275
276
		// create or open search folder
277
		$searchFolder = $this->createSearchFolder($store, $isSetSearchFolderEntryId);
278
		if ($searchFolder === false) {
279
			// if error in creating search folder then send error to client
280
			$errorInfo = [];
281
282
			$errorInfo["error_message"] = match (mapi_last_hresult()) {
283
				MAPI_E_NO_ACCESS => _("Unable to perform search query, no permissions to create search folder."),
284
				MAPI_E_NOT_FOUND => _("Unable to perform search query, search folder not found."),
285
				default => _("Unable to perform search query, store might not support searching."),
286
			};
287
288
			$errorInfo["original_error_message"] = _("Error in creating search folder.");
289
290
			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

290
			return $this->sendSearchErrorToClient($store, $entryid, $action, /** @scrutinizer ignore-type */ $errorInfo);
Loading history...
291
		}
292
293
		$subfolder_flag = 0;
294
		if ($searchInTodoList || (isset($action["subfolders"]) && $action["subfolders"] == "true")) {
295
			$subfolder_flag = RECURSIVE_SEARCH;
296
		}
297
298
		if (!is_array($entryid)) {
299
			$entryids = [$entryid];
300
		}
301
		else {
302
			$entryids = $entryid;
303
		}
304
305
		$searchFolderEntryId = $this->sessionData['searchFolderEntryId'];
306
307
		// check if searchcriteria has changed
308
		$restrictionCheck = md5(serialize($this->restriction) . $searchFolderEntryId . $subfolder_flag);
309
310
		// check if there is need to set searchcriteria again
311
		if (!isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck != $this->sessionData['searchCriteriaCheck']) {
312
			if (!empty($this->sessionData['searchOriginalEntryids'])) {
313
				// get entryids of original folders, and use it to set new search criteria
314
				$entryids = [];
315
				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...
316
					$entryids[] = hex2bin((string) $this->sessionData['searchOriginalEntryids'][$index]);
317
				}
318
			}
319
			else {
320
				// store entryids of original folders, so that can be used for re-setting the search criteria if needed
321
				$this->sessionData['searchOriginalEntryids'] = [];
322
				for ($index = 0, $len = count($entryids); $index < $len; ++$index) {
323
					$this->sessionData['searchOriginalEntryids'][] = bin2hex((string) $entryids[$index]);
324
				}
325
			}
326
327
			mapi_folder_setsearchcriteria($searchFolder, $this->restriction, $entryids, $subfolder_flag);
328
			$this->sessionData['searchCriteriaCheck'] = $restrictionCheck;
329
		}
330
331
		if (isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck == $this->sessionData['searchCriteriaCheck']) {
332
			$folderEntryid = bin2hex((string) $entryid);
333
			if ($this->sessionData['searchOriginalEntryids'][0] !== $folderEntryid) {
334
				$this->sessionData['searchOriginalEntryids'][0] = $folderEntryid;
335
				mapi_folder_setsearchcriteria($searchFolder, $this->restriction, [$entryid], $subfolder_flag);
336
			}
337
		}
338
339
		unset($action["restriction"]);
340
341
		// Sort
342
		$this->parseSortOrder($action);
343
344
		// Create the data array, which will be send back to the client
345
		$data = [];
346
347
		// Wait until we have some data, no point in returning before we have data. Stop waiting after 10 seconds
348
		$start = time();
349
		$table = mapi_folder_getcontentstable($searchFolder, MAPI_DEFERRED_ERRORS);
350
351
		sleep(1);
352
353
		while (time() - $start < 10) {
354
			$count = mapi_table_getrowcount($table);
355
			$result = mapi_folder_getsearchcriteria($searchFolder);
356
357
			// Stop looping if we have data or the search is finished
358
			if ($count > 0) {
359
				break;
360
			}
361
362
			if (($result["searchstate"] & SEARCH_REBUILD) == 0) {
363
				break;
364
			} // Search is done
365
366
			sleep(1);
367
		}
368
369
		// Get the table and merge the arrays
370
		$table = $GLOBALS["operations"]->getTable($store, hex2bin((string) $searchFolderEntryId), $this->properties, $this->sort, $this->start);
371
		$data = array_merge($data, $table);
372
373
		$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

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

561
		$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

561
		$this->deleteSearchFolder($store, $entryid, /** @scrutinizer ignore-type */ $action);
Loading history...
562
563
		// send success message to client
564
		$this->addActionData("stopsearch", ['success' => true]);
565
		$GLOBALS["bus"]->addData($this->getResponseData());
566
	}
567
568
	/**
569
	 * Function will delete search folder.
570
	 *
571
	 * @param object    $store   MAPI Message Store Object
572
	 * @param hexString $entryid entryid of the folder
573
	 * @param array     $action  the action data, sent by the client
574
	 *
575
	 * @return bool true on success or false on failure
576
	 */
577
	public function deleteSearchFolder($store, $entryid, $action) {
578
		if ($entryid && $store) {
0 ignored issues
show
introduced by
$store is of type object, thus it always evaluated to true.
Loading history...
579
			$storeProps = mapi_getprops($store, [PR_FINDER_ENTRYID]);
580
581
			$finderFolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
582
583
			if (mapi_last_hresult() != NOERROR) {
584
				return;
585
			}
586
587
			$hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS);
588
589
			$restriction = [RES_CONTENT,
590
				[
591
					FUZZYLEVEL => FL_FULLSTRING,
592
					ULPROPTAG => PR_ENTRYID,
593
					VALUE => [PR_ENTRYID => $entryid],
594
				],
595
			];
596
597
			mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH);
598
599
			// entryids are unique so there would be only one matching row,
600
			// so only fetch first row
601
			$folders = mapi_table_queryrows($hierarchyTable, [PR_ENTRYID], 0, 1);
602
603
			// delete search folder
604
			if (is_array($folders) && is_array($folders[0])) {
605
				mapi_folder_deletefolder($finderFolder, $folders[0][PR_ENTRYID]);
606
			}
607
608
			return true;
609
		}
610
611
		return false;
612
	}
613
614
	/**
615
	 *	Function will create a search folder in FINDER_ROOT folder
616
	 *	if folder exists then it will open it.
617
	 *
618
	 * @param object $store        MAPI Message Store Object
619
	 * @param bool   $openIfExists open if folder exists
620
	 *
621
	 * @return bool|resource $folder created search folder
622
	 */
623
	public function createSearchFolder($store, $openIfExists = true) {
624
		if (isset($this->sessionData['searchFolderEntryId']) && $openIfExists) {
625
			try {
626
				$searchFolder = mapi_msgstore_openentry($store, hex2bin($this->sessionData['searchFolderEntryId']));
627
628
				if ($searchFolder !== false) {
629
					// search folder exists, don't create new search folder
630
					return $searchFolder;
631
				}
632
			}
633
			catch (MAPIException) {
634
				// ignore error and continue creation of search folder
635
				unset($this->sessionData['searchFolderEntryId']);
636
			}
637
		}
638
639
		// create new search folder
640
		$searchFolderRoot = $this->getSearchFoldersRoot($store);
641
		if ($searchFolderRoot === false) {
642
			// error in finding search root folder
643
			// or store doesn't support search folders
644
			return false;
645
		}
646
647
		// check for folder name, if exists then delete it
648
		$folderName = "grommunio Web Search Folder";
649
650
		try {
651
			$table = mapi_folder_gethierarchytable($searchFolderRoot, 0);
652
			$rows = mapi_table_queryrows($table, [PR_DISPLAY_NAME, PR_ENTRYID], 0, 0xFFFF);
653
			foreach ($rows as $row) {
654
				if (strcasecmp($folderName, (string) $row[PR_DISPLAY_NAME]) == 0) {
655
					mapi_folder_deletefolder($searchFolderRoot, $row[PR_ENTRYID], DEL_FOLDERS | DEL_MESSAGES | DELETE_HARD_DELETE);
656
					break;
657
				}
658
			}
659
			$searchFolder = mapi_folder_createfolder($searchFolderRoot, $folderName, '', OPEN_IF_EXISTS, FOLDER_SEARCH);
660
661
			$props = mapi_getprops($searchFolder, [PR_ENTRYID]);
662
			$this->sessionData['searchFolderEntryId'] = bin2hex((string) $props[PR_ENTRYID]);
663
664
			// we have created new search folder so search criteria check should be removed
665
			unset($this->sessionData['searchCriteriaCheck']);
666
667
			return $searchFolder;
668
		}
669
		catch (MAPIException $e) {
670
			// don't propagate the event to higher level exception handlers
671
			$e->setHandled();
672
		}
673
674
		return false;
675
	}
676
677
	/**
678
	 *	Function will open FINDER_ROOT folder in root container
679
	 *	public folder's don't have FINDER_ROOT folder.
680
	 *
681
	 *	@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...
682
	 * @param mixed $store
683
	 *
684
	 * @return bool|resource finder root folder for search folders
685
	 */
686
	public function getSearchFoldersRoot($store) {
687
		$searchRootFolder = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $searchRootFolder is dead and can be removed.
Loading history...
688
689
		// check if we can create search folders
690
		$storeProps = mapi_getprops($store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID, PR_DISPLAY_NAME]);
691
		if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) !== STORE_SEARCH_OK) {
692
			// store doesn't support search folders, public store don't have FINDER_ROOT folder
693
			return false;
694
		}
695
696
		try {
697
			$searchRootFolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
698
		}
699
		catch (MAPIException $e) {
700
			$msg = "Unable to open FINDER_ROOT for store: %s.";
701
			error_log(sprintf($msg, $storeProps[PR_DISPLAY_NAME]));
702
			// don't propagate the event to higher level exception handlers
703
			$e->setHandled();
704
		}
705
706
		return $searchRootFolder;
707
	}
708
709
	/**
710
	 *	Function will send error message to client if any error has occurred in search.
711
	 *
712
	 * @param object    $store     MAPI Message Store Object
713
	 * @param hexString $entryid   entryid of the folder
714
	 * @param object    $action    the action data, sent by the client
715
	 * @param object    $errorInfo the error information object
716
	 */
717
	public function sendSearchErrorToClient($store, $entryid, $action, $errorInfo) {
718
		if ($errorInfo) {
0 ignored issues
show
introduced by
$errorInfo is of type object, thus it always evaluated to true.
Loading history...
719
			$exception = new SearchException($errorInfo["original_error_message"] ?? $errorInfo['error_message'], mapi_last_hresult());
720
			$exception->setDisplayMessage($errorInfo['error_message']);
721
722
			// after sending error, remove error data
723
			$errorInfo = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $errorInfo is dead and can be removed.
Loading history...
724
725
			throw $exception;
726
		}
727
728
		return false;
729
	}
730
731
	/**
732
	 *	Function will create restriction based on restriction array.
733
	 *
734
	 * @param object $action the action data, sent by the client
735
	 */
736
	public function parseRestriction($action) {
737
		if (isset($action["restriction"]) && is_array($action['restriction'])) {
738
			if (isset($action["restriction"]["start"])) {
739
				// Set start variable
740
				$this->start = $action["restriction"]["start"];
741
			}
742
			foreach ($action["restriction"] as $key => $value) {
743
				$props = $this->properties;
744
				if (!empty($value)) {
745
					switch ($key) {
746
						case "search":
747
							$props = array_merge($this->properties, ['body' => PR_BODY]);
748
749
							// no break
750
						case "task":
751
						case "note":
752
						case "filter":
753
							$this->restriction = Conversion::json2restriction($props, $value);
754
							break;
755
					}
756
				}
757
			}
758
		}
759
	}
760
761
	/**
762
	 * Parses the incoming sort request and builds a MAPI sort order. Normally
763
	 * properties are mapped from the XML to MAPI by the standard $this->properties mapping. However,
764
	 * if you want other mappings, you can specify them in the optional $map mapping.
765
	 *
766
	 * $allow_multi_instance is used for creating multiple instance of MV property related items.
767
	 * $properties is used for using a custom set of properties instead of properties stored in module
768
	 *
769
	 * @param mixed $action
770
	 * @param mixed $map
771
	 * @param mixed $allow_multi_instance
772
	 * @param mixed $properties
773
	 */
774
	public function parseSortOrder($action, $map = false, $allow_multi_instance = false, $properties = false) {
775
		if (isset($action["sort"])) {
776
			$this->sort = [];
777
778
			if (!$properties) {
779
				$properties = $this->properties;
780
			}
781
782
			// Unshift MVI_FLAG of MV properties. So the table is not sort on it anymore.
783
			// Otherwise the server would generate multiple rows for one item (categories).
784
			foreach ($properties as $id => $property) {
785
				switch (mapi_prop_type($property)) {
786
					case PT_MV_STRING8 | MVI_FLAG:
787
					case PT_MV_LONG | MVI_FLAG:
788
						$properties[$id] = $properties[$id] & ~MV_INSTANCE;
789
						break;
790
				}
791
			}
792
793
			// Loop through the sort columns
794
			foreach ($action["sort"] as $column) {
795
				if (isset($column["direction"])) {
796
					if (isset($properties[$column["field"]]) || ($map && isset($map[$column["field"]]))) {
797
						if ($map && isset($map[$column["field"]])) {
798
							$property = $map[$column["field"]];
799
						}
800
						else {
801
							$property = $properties[$column["field"]];
802
						}
803
804
						// Check if column is a MV property
805
						switch (mapi_prop_type($property)) {
806
							case PT_MV_STRING8:
807
							case PT_MV_LONG:
808
								// Set MVI_FLAG.
809
								// The server will generate multiple rows for one item (for example: categories)
810
								if ($allow_multi_instance) {
811
									$properties[$column["field"]] = $properties[$column["field"]] | MVI_FLAG;
812
								}
813
								$property = $properties[$column["field"]];
814
								break;
815
						}
816
817
						// Set sort direction
818
						switch (strtolower($column["direction"])) {
819
							default:
820
							case "asc":
821
								$this->sort[$property] = TABLE_SORT_ASCEND;
822
								break;
823
824
							case "desc":
825
								$this->sort[$property] = TABLE_SORT_DESCEND;
826
								break;
827
						}
828
					}
829
				}
830
			}
831
		}
832
	}
833
834
	/**
835
	 * Function which gets the delegation details from localfreebusy message to use in
836
	 * processPrivateItems function.
837
	 *
838
	 * @param resource $store MAPI Message Store Object
839
	 */
840
	public function getDelegateFolderInfo($store) {
841
		$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...
842
		$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...
843
844
		try {
845
			$this->storeProviderGuid = mapi_getprops($store, [PR_MDB_PROVIDER]);
846
			$this->storeProviderGuid = $this->storeProviderGuid[PR_MDB_PROVIDER];
847
848
			if ($this->storeProviderGuid !== ZARAFA_STORE_DELEGATE_GUID) {
849
				// user is not a delegate, so no point of processing further
850
				return;
851
			}
852
853
			// get localfreebusy message
854
			$this->localFreeBusyMessage = FreeBusy::getLocalFreeBusyMessage($store);
855
		}
856
		catch (MAPIException $e) {
857
			// we got some error, but we don't care about that error instead just continue
858
			$e->setHandled();
859
860
			$this->localFreeBusyMessage = false;
861
			$this->storeProviderGuid = false;
862
		}
863
	}
864
865
	/**
866
	 * Helper function which loop through each item and filter out
867
	 * private items, if any.
868
	 *
869
	 * @param array array structure with row search data
870
	 * @param mixed $data
871
	 *
872
	 * @return array array structure with row search data
873
	 */
874
	public function filterPrivateItems($data) {
875
		// Disable private items
876
		if (isset($data["item"]) && is_array($data["item"])) {
877
			for ($index = 0, $len = count($data["item"]); $index < $len; ++$index) {
878
				$data["item"][$index] = $this->processPrivateItem($data["item"][$index]);
879
880
				if (empty($data["item"][$index])) {
881
					// remove empty results from data
882
					unset($data["item"][$index]);
883
				}
884
			}
885
		}
886
887
		return $data;
888
	}
889
890
	/**
891
	 * Function will be used to process private items in a list response, modules can
892
	 * can decide what to do with the private items, remove the entire row or just
893
	 * hide the data. This function will entirely remove the private message but
894
	 * if any child class needs different behavior then this can be overridden.
895
	 *
896
	 * @param object $item item properties
897
	 *
898
	 * @return object item properties if its non private item otherwise empty array
899
	 */
900
	public function processPrivateItem($item) {
901
		if ($this->checkPrivateItem($item)) {
902
			// hide the item by returning empty array, that can be removed from response
903
			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...
904
		}
905
906
		return $item;
907
	}
908
909
	/**
910
	 * Function will be used check if any item is private or not and if it private then
911
	 * we should process it as private, because you don't want to process private items in
912
	 * user's default store.
913
	 * This function will check we are dealing with delegate stores or not if it is then
914
	 * the delegator has permission to see private items of delegate.
915
	 *
916
	 * @param object $item item properties
917
	 *
918
	 * @return bool true if items should be processed as private else false
919
	 */
920
	public function checkPrivateItem($item) {
921
		// flag to indicate that item should be considered as private
922
		$private = false;
923
924
		$isPrivate = (isset($item['props']['private']) && $item['props']['private'] === true);
925
		$isSensitive = (isset($item['props']['sensitivity']) && $item['props']['sensitivity'] === SENSITIVITY_PRIVATE);
926
927
		if ($isPrivate || $isSensitive) {
928
			// check for delegate permissions for delegate store
929
			if ($this->storeProviderGuid !== false && $this->storeProviderGuid === ZARAFA_STORE_DELEGATE_GUID) {
930
				// by default we should always hide the item if we are in delegate store
931
				$private = true;
932
933
				// find delegate properties
934
				if ($this->localFreeBusyMessage !== false) {
0 ignored issues
show
introduced by
The condition $this->localFreeBusyMessage !== false is always true.
Loading history...
935
					try {
936
						$localFreeBusyMessageProps = mapi_getprops($this->localFreeBusyMessage, [PR_SCHDINFO_DELEGATE_ENTRYIDS, PR_DELEGATE_FLAGS]);
937
938
						if (isset($localFreeBusyMessageProps[PR_SCHDINFO_DELEGATE_ENTRYIDS], $localFreeBusyMessageProps[PR_DELEGATE_FLAGS])) {
939
							// if more then one delegates info is stored then find index of
940
							// current user
941
							$userEntryId = $GLOBALS['mapisession']->getUserEntryID();
942
943
							$userFound = false;
944
							$seePrivate = false;
945
							foreach ($localFreeBusyMessageProps[PR_SCHDINFO_DELEGATE_ENTRYIDS] as $key => $entryId) {
946
								if ($GLOBALS['entryid']->compareEntryIds(bin2hex((string) $userEntryId), bin2hex((string) $entryId))) {
947
									$userFound = true;
948
									$seePrivate = $localFreeBusyMessageProps[PR_DELEGATE_FLAGS][$key];
949
									break;
950
								}
951
							}
952
953
							if ($userFound !== false && $seePrivate === 1) {
954
								// if delegate has permission then don't hide the item
955
								$private = false;
956
							}
957
						}
958
					}
959
					catch (MAPIException $e) {
960
						if ($e->getCode() === MAPI_E_NOT_FOUND) {
961
							// no information available for delegates, ignore error
962
							$e->setHandled();
963
						}
964
					}
965
				}
966
			}
967
		}
968
969
		return $private;
970
	}
971
}
972