grommunio /
grommunio-web
| 1 | <?php |
||
| 2 | require_once(BASE_PATH . 'server/includes/core/class.indexsqlite.php'); |
||
| 3 | |||
| 4 | class AdvancedSearchListModule extends ListModule |
||
| 5 | { |
||
| 6 | /** |
||
| 7 | * Constructor |
||
| 8 | * @param int $id unique id. |
||
| 9 | * @param array $data list of all actions. |
||
| 10 | */ |
||
| 11 | function __construct($id, $data) |
||
| 12 | { |
||
| 13 | parent::__construct($id, $data); |
||
| 14 | // TODO: create a new method in Properties class that will return only the properties we |
||
| 15 | // need for search list (and perhaps for preview???) |
||
| 16 | $this->properties = $GLOBALS["properties"]->getMailListProperties(); |
||
| 17 | $this->properties = array_merge($this->properties, $GLOBALS["properties"]->getAppointmentListProperties()); |
||
| 18 | $this->properties = array_merge($this->properties, $GLOBALS["properties"]->getContactListProperties()); |
||
| 19 | $this->properties = array_merge($this->properties, $GLOBALS["properties"]->getStickyNoteListProperties()); |
||
| 20 | $this->properties = array_merge($this->properties, $GLOBALS["properties"]->getTaskListProperties()); |
||
| 21 | $this->properties = array_merge($this->properties, array( |
||
| 22 | 'body' => PR_BODY, |
||
| 23 | 'html_body' => PR_HTML, |
||
| 24 | 'startdate' => "PT_SYSTIME:PSETID_Appointment:0x820d", |
||
| 25 | 'duedate' => "PT_SYSTIME:PSETID_Appointment:0x820e", |
||
| 26 | 'creation_time' => PR_CREATION_TIME, |
||
| 27 | "task_duedate" => "PT_SYSTIME:PSETID_Task:0x8105")); |
||
| 28 | $this->properties = getPropIdsFromStrings($GLOBALS["mapisession"]->getDefaultMessageStore(), $this->properties); |
||
| 29 | |||
| 30 | |||
| 31 | } |
||
| 32 | |||
| 33 | /** |
||
| 34 | * Executes all the actions in the $data variable. |
||
| 35 | * @return boolean true on success or false on failure. |
||
| 36 | */ |
||
| 37 | function execute() |
||
| 38 | { |
||
| 39 | foreach($this->data as $actionType => $action) |
||
| 40 | { |
||
| 41 | if(isset($actionType)) { |
||
| 42 | try { |
||
| 43 | $store = $this->getActionStore($action); |
||
| 44 | $parententryid = $this->getActionParentEntryID($action); |
||
| 45 | $entryid = $this->getActionEntryID($action); |
||
| 46 | |||
| 47 | switch($actionType) |
||
| 48 | { |
||
| 49 | case "list": |
||
| 50 | case "updatelist": |
||
| 51 | $this->getDelegateFolderInfo($store); |
||
| 52 | $this->messageList($store, $entryid, $action, $actionType); |
||
| 53 | break; |
||
| 54 | case "search": |
||
| 55 | $this->search($store, $entryid, $action, $actionType); |
||
| 56 | break; |
||
| 57 | case "updatesearch": |
||
| 58 | $this->updatesearch($store, $entryid, $action); |
||
| 59 | break; |
||
| 60 | case "stopsearch": |
||
| 61 | $this->stopSearch($store, $entryid, $action); |
||
| 62 | break; |
||
| 63 | case "delete_searchfolder": |
||
| 64 | $this->deleteSearchFolder($store, $entryid, $action); |
||
| 65 | break; |
||
| 66 | } |
||
| 67 | } catch (MAPIException $e) { |
||
| 68 | // This is a very nasty hack that makes sure that grommunio Web doesn't show an error message when |
||
| 69 | // search wants to throw an error. This is only done because a proper fix for this bug has not |
||
| 70 | // been found yet. When WA-9161 is really solved, this should be removed again. |
||
| 71 | if ( $actionType !== 'search' && $actionType !== 'updatesearch' && $actionType !== 'stopsearch' ){ |
||
| 72 | $this->processException($e, $actionType); |
||
| 73 | } else { |
||
| 74 | if ( DEBUG_LOADER === 'LOAD_SOURCE' ){ |
||
| 75 | // Log all info we can get about this error to the error log of the web server |
||
| 76 | error_log("Error in search: \n" . var_export($e, true) . "\n\n" . var_export(debug_backtrace(), true)); |
||
| 77 | } |
||
| 78 | // Send success feedback without data, as if nothing strange happened... |
||
| 79 | $this->sendFeedback(true); |
||
| 80 | } |
||
| 81 | } |
||
| 82 | } |
||
| 83 | } |
||
| 84 | } |
||
| 85 | |||
| 86 | /** |
||
| 87 | * Function which retrieves a list of messages in a folder |
||
| 88 | * @param object $store MAPI Message Store Object |
||
| 89 | * @param string $entryid entryid of the folder |
||
| 90 | * @param array $action the action data, sent by the client |
||
| 91 | * @param string $actionType the action type, sent by the client |
||
| 92 | * @return boolean true on success or false on failure |
||
| 93 | */ |
||
| 94 | function messageList($store, $entryid, $action, $actionType) |
||
| 95 | { |
||
| 96 | $this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content |
||
| 97 | |||
| 98 | if($store && $entryid) { |
||
| 99 | // Restriction |
||
| 100 | $this->parseRestriction($action); |
||
| 101 | |||
| 102 | // Sort |
||
| 103 | $this->parseSortOrder($action, null, true); |
||
| 104 | |||
| 105 | $limit = false; |
||
| 106 | if(isset($action['restriction']['limit'])){ |
||
| 107 | $limit = $action['restriction']['limit']; |
||
| 108 | } |
||
| 109 | |||
| 110 | $isSearchFolder = isset($action['search_folder_entryid']); |
||
| 111 | $entryid = $isSearchFolder ? hex2bin($action['search_folder_entryid']) : $entryid; |
||
| 112 | |||
| 113 | // Get the table and merge the arrays |
||
| 114 | $data = $GLOBALS["operations"]->getTable($store, $entryid, $this->properties, $this->sort, $this->start, $limit, $this->restriction); |
||
| 115 | |||
| 116 | // If the request come from search folder then no need to send folder information |
||
| 117 | if (!$isSearchFolder) { |
||
| 118 | // Open the folder. |
||
| 119 | $folder = mapi_msgstore_openentry($store, $entryid); |
||
| 120 | $data["folder"] = array(); |
||
| 121 | |||
| 122 | // Obtain some statistics from the folder contents |
||
| 123 | $contentcount = mapi_getprops($folder, array(PR_CONTENT_COUNT, PR_CONTENT_UNREAD)); |
||
| 124 | if (isset($contentcount[PR_CONTENT_COUNT])) { |
||
| 125 | $data["folder"]["content_count"] = $contentcount[PR_CONTENT_COUNT]; |
||
| 126 | } |
||
| 127 | |||
| 128 | if (isset($contentcount[PR_CONTENT_UNREAD])) { |
||
| 129 | $data["folder"]["content_unread"] = $contentcount[PR_CONTENT_UNREAD]; |
||
| 130 | } |
||
| 131 | } |
||
| 132 | |||
| 133 | $data = $this->filterPrivateItems($data); |
||
| 134 | |||
| 135 | // Allowing to hook in just before the data sent away to be sent to the client |
||
| 136 | $GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', array( |
||
| 137 | 'moduleObject' =>& $this, |
||
| 138 | 'store' => $store, |
||
| 139 | 'entryid' => $entryid, |
||
| 140 | 'action' => $action, |
||
| 141 | 'data' =>& $data |
||
| 142 | )); |
||
| 143 | |||
| 144 | // unset will remove the value but will not regenerate array keys, so we need to |
||
| 145 | // do it here |
||
| 146 | $data["item"] = array_values($data["item"]); |
||
| 147 | $this->addActionData($actionType, $data); |
||
| 148 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 149 | } |
||
| 150 | } |
||
| 151 | |||
| 152 | private static function parsePatterns($restriction, &$patterns) { |
||
| 153 | if (empty($restriction)) { |
||
| 154 | return; |
||
| 155 | } |
||
| 156 | $type = $restriction[0]; |
||
| 157 | if ($type == RES_CONTENT) { |
||
| 158 | $subres = $restriction[1]; |
||
| 159 | switch ($subres[ULPROPTAG]) { |
||
| 160 | case PR_SUBJECT: |
||
| 161 | $patterns['subject'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 162 | break; |
||
| 163 | case PR_BODY: |
||
| 164 | $patterns['content'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 165 | $patterns['attachments'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 166 | break; |
||
| 167 | case PR_SENDER_NAME: |
||
| 168 | $patterns['sender'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 169 | break; |
||
| 170 | case PR_SENT_REPRESENTING_NAME: |
||
| 171 | $patterns['from'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 172 | break; |
||
| 173 | case PR_DISPLAY_TO: |
||
| 174 | case PR_DISPLAY_CC: |
||
| 175 | $patterns['recipients'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 176 | break; |
||
| 177 | case PR_MESSAGE_CLASS: |
||
| 178 | if (empty($patterns['message_classes'])) { |
||
| 179 | $patterns['message_classes'] = array(); |
||
| 180 | } |
||
| 181 | $patterns['message_classes'][] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 182 | break; |
||
| 183 | case PR_DISPLAY_NAME: |
||
| 184 | $patterns['others'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 185 | break; |
||
| 186 | } |
||
| 187 | } else if ($type == RES_AND || $type == RES_OR) { |
||
| 188 | foreach ($restriction[1] as $subres) { |
||
| 189 | AdvancedSearchListModule::parsePatterns($subres, $patterns); |
||
| 190 | } |
||
| 191 | } else if ($type == RES_BITMASK) { |
||
| 192 | $subres = $restriction[1]; |
||
| 193 | if ($subres[ULPROPTAG] == PR_MESSAGE_FLAGS && $subres[ULTYPE] == BMR_EQZ) { |
||
| 194 | if (MSGFLAG_READ & $subres[ULMASK]) { |
||
| 195 | $patterns['unread'] = true; |
||
| 196 | } |
||
| 197 | } |
||
| 198 | } else if ($type == RES_PROPERTY) { |
||
| 199 | $subres = $restriction[1]; |
||
| 200 | if (($subres[ULPROPTAG] == PR_MESSAGE_DELIVERY_TIME || |
||
| 201 | $subres[ULPROPTAG] == PR_LAST_MODIFICATION_TIME)) { |
||
| 202 | if ($subres[RELOP] == RELOP_LT || |
||
| 203 | $subres[RELOP] == RELOP_LE) { |
||
| 204 | $patterns['date_end'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 205 | } else if ($subres[RELOP] == RELOP_GT || |
||
| 206 | $subres[RELOP] == RELOP_GE) { |
||
| 207 | $patterns['date_start'] = $subres[VALUE][$subres[ULPROPTAG]]; |
||
| 208 | } |
||
| 209 | } |
||
| 210 | } else if ($type == RES_SUBRESTRICTION) { |
||
| 211 | $subres = $restriction[1]; |
||
| 212 | if (($subres[ULPROPTAG] == PR_MESSAGE_ATTACHMENTS)) { |
||
| 213 | $patterns['has_attachments'] = true; |
||
| 214 | } |
||
| 215 | } |
||
| 216 | } |
||
| 217 | |||
| 218 | /** |
||
| 219 | * Function will set search restrictions on search folder and start search process |
||
| 220 | * and it will also parse visible columns and sorting data when sending results to client |
||
| 221 | * @param object $store MAPI Message Store Object |
||
| 222 | * @param hexString $entryid entryid of the folder |
||
| 223 | * @param object $action the action data, sent by the client |
||
| 224 | * @param string $actionType the action type, sent by the client |
||
| 225 | */ |
||
| 226 | function search($store, $entryid, $action, $actionType) |
||
| 227 | { |
||
| 228 | $useSearchFolder = isset($action["use_searchfolder"]) ? $action["use_searchfolder"] : false; |
||
| 229 | if (!$useSearchFolder) { |
||
| 230 | /** |
||
| 231 | * store doesn't support search folders so we can't use this |
||
| 232 | * method instead we will pass restriction to messageList and |
||
| 233 | * it will give us the restricted results |
||
| 234 | */ |
||
| 235 | return parent::messageList($store, $entryid, $action, "list"); |
||
| 236 | } |
||
| 237 | $store_props = mapi_getprops($store, array(PR_MDB_PROVIDER, PR_DEFAULT_STORE)); |
||
| 238 | if ($store_props[PR_MDB_PROVIDER] == ZARAFA_STORE_PUBLIC_GUID || |
||
| 239 | empty($store_props[PR_DEFAULT_STORE]) || !$store_props[PR_DEFAULT_STORE]){ |
||
| 240 | // public store or share store do not support search folders |
||
| 241 | return parent::messageList($store, $entryid, $action, "list"); |
||
| 242 | } |
||
| 243 | if ($GLOBALS['entryid']->compareEntryIds(bin2hex($entryid), bin2hex(TodoList::getEntryId()))) { |
||
| 244 | // todo list do not need to perform full text index search |
||
| 245 | return parent::messageList($store, $entryid, $action, "list"); |
||
| 246 | } |
||
| 247 | |||
| 248 | |||
| 249 | $this->searchFolderList = true; // Set to indicate this is not the normal folder, but a search folder |
||
| 250 | $this->restriction = false; |
||
| 251 | |||
| 252 | |||
| 253 | // Parse Restriction |
||
| 254 | $this->parseRestriction($action); |
||
| 255 | if($this->restriction == false) { |
||
| 256 | // if error in creating restriction then send error to client |
||
| 257 | $errorInfo = array(); |
||
| 258 | $errorInfo["error_message"] = _("Error in search, please try again") . "."; |
||
| 259 | $errorInfo["original_error_message"] = "Error in parsing restrictions."; |
||
| 260 | |||
| 261 | return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo); |
||
| 262 | } |
||
| 263 | |||
| 264 | $isSetSearchFolderEntryId = isset($action['search_folder_entryid']); |
||
| 265 | if($isSetSearchFolderEntryId) { |
||
| 266 | $this->sessionData['searchFolderEntryId'] = $action['search_folder_entryid']; |
||
| 267 | } |
||
| 268 | |||
| 269 | if (isset($action['forceCreateSearchFolder']) && $action['forceCreateSearchFolder']) { |
||
| 270 | $isSetSearchFolderEntryId = false; |
||
| 271 | } |
||
| 272 | |||
| 273 | // create or open search folder |
||
| 274 | $searchFolder = $this->createSearchFolder($store, $isSetSearchFolderEntryId); |
||
| 275 | if ($searchFolder === false) { |
||
| 276 | // if error in creating search folder then send error to client |
||
| 277 | $errorInfo = array(); |
||
| 278 | switch(mapi_last_hresult()) { |
||
| 279 | case MAPI_E_NO_ACCESS: |
||
| 280 | $errorInfo["error_message"] = _("Unable to perform search query, no permissions to create search folder."); |
||
| 281 | break; |
||
| 282 | case MAPI_E_NOT_FOUND: |
||
| 283 | $errorInfo["error_message"] = _("Unable to perform search query, search folder not found."); |
||
| 284 | break; |
||
| 285 | default: |
||
| 286 | $errorInfo["error_message"] = _("Unable to perform search query, store might not support searching."); |
||
| 287 | } |
||
| 288 | |||
| 289 | $errorInfo["original_error_message"] = _("Error in creating search folder."); |
||
| 290 | |||
| 291 | return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo); |
||
| 292 | } |
||
| 293 | |||
| 294 | $subfolder_flag = 0; |
||
| 295 | $recursive = false; |
||
| 296 | if (isset($action["subfolders"]) && $action["subfolders"] == "true") { |
||
| 297 | $recursive = true; |
||
| 298 | $subfolder_flag = RECURSIVE_SEARCH; |
||
| 299 | } |
||
| 300 | |||
| 301 | if(!is_array($entryid)) { |
||
| 302 | $entryids = array($entryid); |
||
| 303 | } else { |
||
| 304 | $entryids = $entryid; |
||
| 305 | } |
||
| 306 | |||
| 307 | $searchFolderEntryId = $this->sessionData['searchFolderEntryId']; |
||
| 308 | |||
| 309 | // check if searchcriteria has changed |
||
| 310 | $restrictionCheck = md5(serialize($this->restriction) . $searchFolderEntryId . $subfolder_flag); |
||
| 311 | |||
| 312 | // check if there is need to set searchcriteria again |
||
| 313 | if(!isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck != $this->sessionData['searchCriteriaCheck']) { |
||
| 314 | if (!empty($this->sessionData['searchOriginalEntryids'])) { |
||
| 315 | // get entryids of original folders, and use it to set new search criteria |
||
| 316 | $entryids = Array(); |
||
| 317 | for($index = 0; $index < count($this->sessionData['searchOriginalEntryids']); $index++) { |
||
| 318 | $entryids[] = hex2bin($this->sessionData['searchOriginalEntryids'][$index]); |
||
| 319 | } |
||
| 320 | } else { |
||
| 321 | // store entryids of original folders, so that can be used for re-setting the search criteria if needed |
||
| 322 | $this->sessionData['searchOriginalEntryids'] = Array(); |
||
| 323 | for($index = 0, $len = count($entryids); $index < $len; $index++) { |
||
| 324 | $this->sessionData['searchOriginalEntryids'][] = bin2hex($entryids[$index]); |
||
| 325 | } |
||
| 326 | } |
||
| 327 | // we never start the search folder because we will populate the search folder by ourselves |
||
| 328 | mapi_folder_setsearchcriteria($searchFolder, $this->restriction, $entryids, $subfolder_flag|STOP_SEARCH); |
||
| 329 | $this->sessionData['searchCriteriaCheck'] = $restrictionCheck; |
||
| 330 | } |
||
| 331 | |||
| 332 | if(isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck == $this->sessionData['searchCriteriaCheck']) { |
||
| 333 | $folderEntryid = bin2hex($entryid); |
||
| 334 | if($this->sessionData['searchOriginalEntryids'][0] !== $folderEntryid) { |
||
| 335 | $this->sessionData['searchOriginalEntryids'][0] = $folderEntryid; |
||
| 336 | // we never start the search folder because we will populate the search folder by ourselves |
||
| 337 | mapi_folder_setsearchcriteria($searchFolder, $this->restriction, array($entryid), $subfolder_flag|STOP_SEARCH); |
||
| 338 | } |
||
| 339 | } |
||
| 340 | |||
| 341 | unset($action["restriction"]); |
||
| 342 | |||
| 343 | // Sort |
||
| 344 | $this->parseSortOrder($action); |
||
| 345 | |||
| 346 | $search_patterns = array(); |
||
| 347 | AdvancedSearchListModule::parsePatterns($this->restriction, $search_patterns); |
||
| 348 | if (isset($search_patterns['message_classes']) && |
||
| 349 | count($search_patterns['message_classes']) >= 7) { |
||
| 350 | unset($search_patterns['message_classes']); |
||
| 351 | } |
||
| 352 | |||
| 353 | $indexDB = new IndexSqlite(); |
||
| 354 | if (!$indexDB->load()) { |
||
| 355 | // if error in creating search folder then send error to client |
||
| 356 | $errorInfo = array(); |
||
| 357 | $errorInfo["error_message"] = _("Unable to perform search query, store might not support searching."); |
||
| 358 | $errorInfo["original_error_message"] = _("Error in creating search folder."); |
||
| 359 | return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo); |
||
| 360 | } |
||
| 361 | $search_result = $indexDB->search(hex2bin($searchFolderEntryId), $search_patterns['sender'], $search_patterns['from'], |
||
| 362 | $search_patterns['recipients'], $search_patterns['subject'], $search_patterns['content'], |
||
| 363 | $search_patterns['attachments'], $search_patterns['others'], $entryid, $recursive, |
||
| 364 | $search_patterns['message_classes'], $search_patterns['date_start'], $search_patterns['date_end'], |
||
| 365 | $search_patterns['unread'], $search_patterns['has_attachments']); |
||
| 366 | if (false == $search_result) { |
||
| 367 | // if error in creating search folder then send error to client |
||
| 368 | $errorInfo = array(); |
||
| 369 | $errorInfo["error_message"] = _("Unable to perform search query, search folder not found."); |
||
| 370 | $errorInfo["original_error_message"] = _("Error in creating search folder."); |
||
| 371 | return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo); |
||
| 372 | } |
||
| 373 | |||
| 374 | // Get the table and merge the arrays |
||
| 375 | $table = $GLOBALS["operations"]->getTable($store, hex2bin($searchFolderEntryId), $this->properties, $this->sort, $this->start); |
||
| 376 | // Create the data array, which will be send back to the client |
||
| 377 | $data = array(); |
||
| 378 | $data = array_merge($data, $table); |
||
| 379 | |||
| 380 | $this->getDelegateFolderInfo($store); |
||
| 381 | $data = $this->filterPrivateItems($data); |
||
| 382 | |||
| 383 | // remember which entryid's are send to the client |
||
| 384 | $searchResults = array(); |
||
| 385 | foreach($table["item"] as $item) { |
||
| 386 | // store entryid => last_modification_time mapping |
||
| 387 | $searchResults[$item["entryid"]] = $item["props"]["last_modification_time"]; |
||
| 388 | } |
||
| 389 | |||
| 390 | // store search results into session data |
||
| 391 | if(!isset($this->sessionData['searchResults'])) { |
||
| 392 | $this->sessionData['searchResults'] = array(); |
||
| 393 | } |
||
| 394 | $this->sessionData['searchResults'][$searchFolderEntryId] = $searchResults; |
||
| 395 | |||
| 396 | $result = mapi_folder_getsearchcriteria($searchFolder); |
||
| 397 | |||
| 398 | $data["search_meta"] = array(); |
||
| 399 | $data["search_meta"]["searchfolder_entryid"] = $searchFolderEntryId; |
||
| 400 | $data["search_meta"]["search_store_entryid"] = $action["store_entryid"]; |
||
| 401 | $data["search_meta"]["searchstate"] = $result["searchstate"]; |
||
| 402 | $data["search_meta"]["results"] = count($searchResults); |
||
| 403 | |||
| 404 | // Reopen the search folder, because otherwise the suggestion property will |
||
| 405 | // not have been updated |
||
| 406 | $searchFolder = $this->createSearchFolder($store, true); |
||
| 407 | $storeProps = mapi_getprops($searchFolder, array(PR_EC_SUGGESTION)); |
||
| 408 | if ( isset($storeProps[PR_EC_SUGGESTION]) ){ |
||
| 409 | $data["search_meta"]["suggestion"] = $storeProps[PR_EC_SUGGESTION]; |
||
| 410 | } |
||
| 411 | |||
| 412 | $this->addActionData("search", $data); |
||
| 413 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 414 | |||
| 415 | return true; |
||
| 416 | |||
| 417 | } |
||
| 418 | |||
| 419 | } |
||
| 420 | ?> |
||
|
0 ignored issues
–
show
|
|||
| 421 |
Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.
A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.