Total Complexity | 141 |
Total Lines | 962 |
Duplicated Lines | 0 % |
Changes | 5 | ||
Bugs | 2 | Features | 0 |
Complex classes like ListModule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ListModule, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 |
||
|
|||
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 |
||
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) { |
||
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); |
||
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); |
||
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 |
||
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'; |
||
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) { |
||
149 | return; |
||
150 | } |
||
151 | |||
152 | // Restriction |
||
153 | $this->parseRestriction($action); |
||
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"); |
||
232 | } |
||
233 | |||
234 | $this->searchFolderList = true; // Set to indicate this is not the normal folder, but a search folder |
||
235 | $this->restriction = false; |
||
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); |
||
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); |
||
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) { |
||
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); |
||
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 |
||
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); |
||
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) { |
||
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 |
||
682 | * @param mixed $store |
||
683 | * |
||
684 | * @return bool|resource finder root folder for search folders |
||
685 | */ |
||
686 | public function getSearchFoldersRoot($store) { |
||
687 | $searchRootFolder = false; |
||
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) { |
||
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; |
||
842 | $this->storeProviderGuid = false; |
||
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) { |
||
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 []; |
||
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) { |
||
970 | } |
||
971 | } |
||
972 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths