grommunio /
grommunio-web
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * ListModule |
||
| 4 | * Superclass of every module, which retrieves a MAPI message list. It |
||
| 5 | * extends the Module class. |
||
| 6 | */ |
||
| 7 | class ListModule extends Module |
||
| 8 | { |
||
| 9 | /** |
||
| 10 | * @var array list of columns which are selected in the previous request. |
||
| 11 | */ |
||
| 12 | var $properties; |
||
| 13 | |||
| 14 | /** |
||
| 15 | * @var array sort. |
||
| 16 | */ |
||
| 17 | var $sort; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * @var int startrow in the table. |
||
| 21 | */ |
||
| 22 | var $start; |
||
| 23 | |||
| 24 | /** |
||
| 25 | * @var array contains (when needed) a restriction used when searching and filtering the records |
||
| 26 | */ |
||
| 27 | var $restriction; |
||
| 28 | |||
| 29 | /** |
||
| 30 | * @var bool contains check whether a search result is listed or just the contents of a normal folder |
||
| 31 | */ |
||
| 32 | var $searchFolderList; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * @var array stores entryids and last modification time of |
||
| 36 | * messages that are already sent to the server |
||
| 37 | */ |
||
| 38 | var $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 | var $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 | var $storeProviderGuid; |
||
| 52 | |||
| 53 | /** |
||
| 54 | * Constructor |
||
| 55 | * @param int $id unique id. |
||
| 56 | * @param array $data list of all actions. |
||
| 57 | */ |
||
| 58 | function __construct($id, $data, $events = false) |
||
| 59 | { |
||
| 60 | $this->start = 0; |
||
| 61 | |||
| 62 | $this->restriction = false; |
||
| 63 | $this->searchFolderList = false; |
||
| 64 | $this->localFreeBusyMessage = false; |
||
| 65 | $this->storeProviderGuid = false; |
||
| 66 | |||
| 67 | $this->sort = array(); |
||
| 68 | |||
| 69 | parent::__construct($id, $data); |
||
| 70 | } |
||
| 71 | |||
| 72 | /** |
||
| 73 | * Executes all the actions in the $data variable. |
||
| 74 | * @return boolean true on success of false on fialure. |
||
| 75 | */ |
||
| 76 | function execute() |
||
| 77 | { |
||
| 78 | foreach($this->data as $actionType => $action) |
||
| 79 | { |
||
| 80 | if(isset($actionType)) { |
||
| 81 | try { |
||
| 82 | $store = $this->getActionStore($action); |
||
| 83 | $parententryid = $this->getActionParentEntryID($action); |
||
| 84 | $entryid = $this->getActionEntryID($action); |
||
| 85 | |||
| 86 | switch($actionType) |
||
| 87 | { |
||
| 88 | case "list": |
||
| 89 | $this->getDelegateFolderInfo($store); |
||
| 90 | $this->messageList($store, $entryid, $action, $actionType); |
||
| 91 | break; |
||
| 92 | default: |
||
| 93 | $this->handleUnknownActionType($actionType); |
||
| 94 | } |
||
| 95 | } catch (MAPIException $e) { |
||
| 96 | $this->processException($e, $actionType, $store, $parententryid, $entryid, $action); |
||
| 97 | } catch (SearchException $e) { |
||
| 98 | $this->processException($e, $actionType, $store, $parententryid, $entryid, $action); |
||
| 99 | } |
||
| 100 | } |
||
| 101 | } |
||
| 102 | } |
||
| 103 | |||
| 104 | /** |
||
| 105 | * Function does customization of MAPIException based on module data. |
||
| 106 | * like, here it will generate display message based on actionType |
||
| 107 | * for particular exception. |
||
| 108 | * |
||
| 109 | * @param object $e Exception object |
||
| 110 | * @param string $actionType the action type, sent by the client |
||
| 111 | * @param MAPIobject $store Store object of the current user. |
||
| 112 | * @param string $parententryid parent entryid of the message. |
||
| 113 | * @param string $entryid entryid of the message/folder. |
||
| 114 | * @param array $action the action data, sent by the client |
||
| 115 | */ |
||
| 116 | function handleException(&$e, $actionType = null, $store = null, $parententryid = null, $entryid = null, $action = null) |
||
| 117 | { |
||
| 118 | if (is_null($e->displayMessage)) { |
||
| 119 | switch($actionType) { |
||
| 120 | case "list": |
||
| 121 | if ($e->getCode() == MAPI_E_NO_ACCESS) { |
||
| 122 | $e->setDisplayMessage(_("You have insufficient privileges to see the contents of this folder.")); |
||
| 123 | } else { |
||
| 124 | $e->setDisplayMessage(_("Could not load the contents of this folder.")); |
||
| 125 | } |
||
| 126 | break; |
||
| 127 | } |
||
| 128 | } |
||
| 129 | |||
| 130 | parent::handleException($e, $actionType, $store, $parententryid, $entryid, $action); |
||
| 131 | } |
||
| 132 | |||
| 133 | /** |
||
| 134 | * Function which retrieves a list of messages in a folder |
||
| 135 | * @param object $store MAPI Message Store Object |
||
| 136 | * @param string $entryid entryid of the folder |
||
| 137 | * @param array $action the action data, sent by the client |
||
| 138 | * @param string $actionType the action type, sent by the client |
||
| 139 | * @return boolean true on success or false on failure |
||
| 140 | */ |
||
| 141 | function messageList($store, $entryid, $action, $actionType) |
||
| 142 | { |
||
| 143 | $this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content |
||
| 144 | |||
| 145 | if($store && $entryid) { |
||
| 146 | // Restriction |
||
| 147 | $this->parseRestriction($action); |
||
| 148 | |||
| 149 | // Sort |
||
| 150 | $this->parseSortOrder($action, null, true); |
||
| 151 | |||
| 152 | $limit = false; |
||
| 153 | if(isset($action['restriction']['limit'])){ |
||
| 154 | $limit = $action['restriction']['limit']; |
||
| 155 | } else { |
||
| 156 | $limit = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50); |
||
| 157 | } |
||
| 158 | |||
| 159 | $isSearchFolder = isset($action['search_folder_entryid']); |
||
| 160 | $entryid = $isSearchFolder ? hex2bin($action['search_folder_entryid']) : $entryid; |
||
| 161 | |||
| 162 | // Get the table and merge the arrays |
||
| 163 | $data = $GLOBALS["operations"]->getTable($store, $entryid, $this->properties, $this->sort, $this->start, $limit, $this->restriction); |
||
| 164 | |||
| 165 | // If the request come from search folder then no need to send folder information |
||
| 166 | if (!$isSearchFolder) { |
||
| 167 | // Open the folder. |
||
| 168 | $folder = mapi_msgstore_openentry($store, $entryid); |
||
| 169 | $data["folder"] = array(); |
||
| 170 | |||
| 171 | // Obtain some statistics from the folder contents |
||
| 172 | $contentcount = mapi_getprops($folder, array(PR_CONTENT_COUNT, PR_CONTENT_UNREAD)); |
||
| 173 | if (isset($contentcount[PR_CONTENT_COUNT])) { |
||
| 174 | $data["folder"]["content_count"] = $contentcount[PR_CONTENT_COUNT]; |
||
| 175 | } |
||
| 176 | |||
| 177 | if (isset($contentcount[PR_CONTENT_UNREAD])) { |
||
| 178 | $data["folder"]["content_unread"] = $contentcount[PR_CONTENT_UNREAD]; |
||
| 179 | } |
||
| 180 | } |
||
| 181 | |||
| 182 | $data = $this->filterPrivateItems($data); |
||
| 183 | |||
| 184 | // Allowing to hook in just before the data sent away to be sent to the client |
||
| 185 | $GLOBALS['PluginManager']->triggerHook('server.module.listmodule.list.after', array( |
||
| 186 | 'moduleObject' =>& $this, |
||
| 187 | 'store' => $store, |
||
| 188 | 'entryid' => $entryid, |
||
| 189 | 'action' => $action, |
||
| 190 | 'data' =>& $data |
||
| 191 | )); |
||
| 192 | |||
| 193 | // unset will remove the value but will not regenerate array keys, so we need to |
||
| 194 | // do it here |
||
| 195 | $data["item"] = array_values($data["item"]); |
||
| 196 | |||
| 197 | $this->addActionData($actionType, $data); |
||
| 198 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 199 | } |
||
| 200 | } |
||
| 201 | |||
| 202 | /** |
||
| 203 | * Function will set search restrictions on search folder and start search process |
||
| 204 | * and it will also parse visible columns and sorting data when sending results to client |
||
| 205 | * @param object $store MAPI Message Store Object |
||
| 206 | * @param hexString $entryid entryid of the folder |
||
| 207 | * @param object $action the action data, sent by the client |
||
| 208 | * @param string $actionType the action type, sent by the client |
||
| 209 | */ |
||
| 210 | function search($store, $entryid, $action, $actionType) |
||
| 211 | { |
||
| 212 | $useSearchFolder = isset($action["use_searchfolder"]) ? $action["use_searchfolder"] : false; |
||
| 213 | if(!$useSearchFolder) { |
||
| 214 | /** |
||
| 215 | * store doesn't support search folders so we can't use this |
||
| 216 | * method instead we will pass restriction to messageList and |
||
| 217 | * it will give us the restricted results |
||
| 218 | */ |
||
| 219 | return $this->messageList($store, $entryid, $action, "list"); |
||
| 220 | } |
||
| 221 | |||
| 222 | $this->searchFolderList = true; // Set to indicate this is not the normal folder, but a search folder |
||
| 223 | $this->restriction = false; |
||
| 224 | $searchInTodoList = $GLOBALS['entryid']->compareEntryIds(bin2hex($entryid), bin2hex(TodoList::getEntryId())); |
||
| 225 | |||
| 226 | // Parse Restriction |
||
| 227 | $this->parseRestriction($action); |
||
| 228 | if($this->restriction == false) { |
||
| 229 | // if error in creating restriction then send error to client |
||
| 230 | $errorInfo = array(); |
||
| 231 | $errorInfo["error_message"] = _("Error in search, please try again") . "."; |
||
| 232 | $errorInfo["original_error_message"] = "Error in parsing restrictions."; |
||
| 233 | |||
| 234 | return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo); |
||
| 235 | } |
||
| 236 | |||
| 237 | if ( $searchInTodoList ){ |
||
| 238 | // Since we cannot search in a search folder, we will combine the search restriction |
||
| 239 | // with the search restriction of the to-do list to mimic searching in the To-do list |
||
| 240 | $this->restriction = array( |
||
| 241 | RES_AND, |
||
| 242 | array( |
||
| 243 | $this->restriction, |
||
| 244 | TodoList::_createRestriction() |
||
| 245 | ) |
||
| 246 | ); |
||
| 247 | |||
| 248 | // When searching in the To-do list we will actually always search in the IPM subtree, so |
||
| 249 | // set the entryid to that. |
||
| 250 | $userStore = WebAppAuthentication::getMapiSession()->getDefaultMessageStore(); |
||
| 251 | $props = mapi_getprops($userStore, array(PR_IPM_SUBTREE_ENTRYID)); |
||
| 252 | $entryid = $props[PR_IPM_SUBTREE_ENTRYID]; |
||
| 253 | } |
||
| 254 | |||
| 255 | $isSetSearchFolderEntryId = isset($action['search_folder_entryid']); |
||
| 256 | if($isSetSearchFolderEntryId) { |
||
| 257 | $this->sessionData['searchFolderEntryId'] = $action['search_folder_entryid']; |
||
| 258 | } |
||
| 259 | |||
| 260 | if (isset($action['forceCreateSearchFolder']) && $action['forceCreateSearchFolder']) { |
||
| 261 | $isSetSearchFolderEntryId = false; |
||
| 262 | } |
||
| 263 | |||
| 264 | // create or open search folder |
||
| 265 | $searchFolder = $this->createSearchFolder($store, $isSetSearchFolderEntryId); |
||
| 266 | if ($searchFolder === false) { |
||
| 267 | // if error in creating search folder then send error to client |
||
| 268 | $errorInfo = array(); |
||
| 269 | switch(mapi_last_hresult()) { |
||
| 270 | case MAPI_E_NO_ACCESS: |
||
| 271 | $errorInfo["error_message"] = _("Unable to perform search query, no permissions to create search folder."); |
||
| 272 | break; |
||
| 273 | case MAPI_E_NOT_FOUND: |
||
| 274 | $errorInfo["error_message"] = _("Unable to perform search query, search folder not found."); |
||
| 275 | break; |
||
| 276 | default: |
||
| 277 | $errorInfo["error_message"] = _("Unable to perform search query, store might not support searching."); |
||
| 278 | } |
||
| 279 | |||
| 280 | $errorInfo["original_error_message"] = _("Error in creating search folder."); |
||
| 281 | |||
| 282 | return $this->sendSearchErrorToClient($store, $entryid, $action, $errorInfo); |
||
| 283 | } |
||
| 284 | |||
| 285 | $subfolder_flag = 0; |
||
| 286 | if ($searchInTodoList || (isset($action["subfolders"]) && $action["subfolders"] == "true")) { |
||
| 287 | $subfolder_flag = RECURSIVE_SEARCH; |
||
| 288 | } |
||
| 289 | |||
| 290 | if(!is_array($entryid)) { |
||
| 291 | $entryids = array($entryid); |
||
| 292 | } else { |
||
| 293 | $entryids = $entryid; |
||
| 294 | } |
||
| 295 | |||
| 296 | $searchFolderEntryId = $this->sessionData['searchFolderEntryId']; |
||
| 297 | |||
| 298 | // check if searchcriteria has changed |
||
| 299 | $restrictionCheck = md5(serialize($this->restriction) . $searchFolderEntryId . $subfolder_flag); |
||
| 300 | |||
| 301 | // check if there is need to set searchcriteria again |
||
| 302 | if(!isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck != $this->sessionData['searchCriteriaCheck']) { |
||
| 303 | if (!empty($this->sessionData['searchOriginalEntryids'])) { |
||
| 304 | // get entryids of original folders, and use it to set new search criteria |
||
| 305 | $entryids = Array(); |
||
| 306 | for($index = 0; $index < count($this->sessionData['searchOriginalEntryids']); $index++) { |
||
| 307 | $entryids[] = hex2bin($this->sessionData['searchOriginalEntryids'][$index]); |
||
| 308 | } |
||
| 309 | } else { |
||
| 310 | // store entryids of original folders, so that can be used for re-setting the search criteria if needed |
||
| 311 | $this->sessionData['searchOriginalEntryids'] = Array(); |
||
| 312 | for($index = 0, $len = count($entryids); $index < $len; $index++) { |
||
| 313 | $this->sessionData['searchOriginalEntryids'][] = bin2hex($entryids[$index]); |
||
| 314 | } |
||
| 315 | } |
||
| 316 | |||
| 317 | mapi_folder_setsearchcriteria($searchFolder, $this->restriction, $entryids, $subfolder_flag); |
||
| 318 | $this->sessionData['searchCriteriaCheck'] = $restrictionCheck; |
||
| 319 | } |
||
| 320 | |||
| 321 | if(isset($this->sessionData['searchCriteriaCheck']) || $restrictionCheck == $this->sessionData['searchCriteriaCheck']) { |
||
| 322 | $folderEntryid = bin2hex($entryid); |
||
| 323 | if($this->sessionData['searchOriginalEntryids'][0] !== $folderEntryid) { |
||
| 324 | $this->sessionData['searchOriginalEntryids'][0] = $folderEntryid; |
||
| 325 | mapi_folder_setsearchcriteria($searchFolder, $this->restriction, array($entryid), $subfolder_flag); |
||
| 326 | } |
||
| 327 | } |
||
| 328 | |||
| 329 | unset($action["restriction"]); |
||
| 330 | |||
| 331 | // Sort |
||
| 332 | $this->parseSortOrder($action); |
||
| 333 | |||
| 334 | // Create the data array, which will be send back to the client |
||
| 335 | $data = array(); |
||
| 336 | |||
| 337 | // Wait until we have some data, no point in returning before we have data. Stop waiting after 10 seconds |
||
| 338 | $start = time(); |
||
| 339 | $table = mapi_folder_getcontentstable($searchFolder, MAPI_DEFERRED_ERRORS); |
||
| 340 | |||
| 341 | sleep(1); |
||
| 342 | |||
| 343 | while(time() - $start < 10) { |
||
| 344 | $count = mapi_table_getrowcount($table); |
||
| 345 | $result = mapi_folder_getsearchcriteria($searchFolder); |
||
| 346 | |||
| 347 | // Stop looping if we have data or the search is finished |
||
| 348 | if($count > 0) |
||
| 349 | break; |
||
| 350 | |||
| 351 | if(($result["searchstate"] & SEARCH_REBUILD) == 0) |
||
| 352 | break; // Search is done |
||
| 353 | |||
| 354 | sleep(1); |
||
| 355 | } |
||
| 356 | |||
| 357 | // Get the table and merge the arrays |
||
| 358 | $table = $GLOBALS["operations"]->getTable($store, hex2bin($searchFolderEntryId), $this->properties, $this->sort, $this->start); |
||
| 359 | $data = array_merge($data, $table); |
||
| 360 | |||
| 361 | $this->getDelegateFolderInfo($store); |
||
| 362 | $data = $this->filterPrivateItems($data); |
||
| 363 | |||
| 364 | // remember which entryid's are send to the client |
||
| 365 | $searchResults = array(); |
||
| 366 | foreach($table["item"] as $item) { |
||
| 367 | // store entryid => last_modification_time mapping |
||
| 368 | $searchResults[$item["entryid"]] = $item["props"]["last_modification_time"]; |
||
| 369 | } |
||
| 370 | |||
| 371 | // store search results into session data |
||
| 372 | if(!isset($this->sessionData['searchResults'])) { |
||
| 373 | $this->sessionData['searchResults'] = array(); |
||
| 374 | } |
||
| 375 | $this->sessionData['searchResults'][$searchFolderEntryId] = $searchResults; |
||
| 376 | |||
| 377 | $result = mapi_folder_getsearchcriteria($searchFolder); |
||
| 378 | |||
| 379 | $data["search_meta"] = array(); |
||
| 380 | $data["search_meta"]["searchfolder_entryid"] = $searchFolderEntryId; |
||
| 381 | $data["search_meta"]["search_store_entryid"] = $action["store_entryid"]; |
||
| 382 | $data["search_meta"]["searchstate"] = $result["searchstate"]; |
||
| 383 | $data["search_meta"]["results"] = count($searchResults); |
||
| 384 | |||
| 385 | // Reopen the search folder, because otherwise the suggestion property will |
||
| 386 | // not have been updated |
||
| 387 | $searchFolder = $this->createSearchFolder($store, true); |
||
| 388 | $storeProps = mapi_getprops($searchFolder, array(PR_EC_SUGGESTION)); |
||
| 389 | if ( isset($storeProps[PR_EC_SUGGESTION]) ){ |
||
| 390 | $data["search_meta"]["suggestion"] = $storeProps[PR_EC_SUGGESTION]; |
||
| 391 | } |
||
| 392 | |||
| 393 | $this->addActionData("search", $data); |
||
| 394 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 395 | |||
| 396 | return true; |
||
| 397 | } |
||
| 398 | |||
| 399 | /** |
||
| 400 | * Function will check for the status of the search on server |
||
| 401 | * and it will also send intermediate results of search, so we don't have to wait |
||
| 402 | * until search is finished on server to send results |
||
| 403 | * @param object $store MAPI Message Store Object |
||
| 404 | * @param hexString $entryid entryid of the folder |
||
| 405 | * @param object $action the action data, sent by the client |
||
| 406 | */ |
||
| 407 | function updatesearch($store, $entryid, $action) |
||
| 408 | { |
||
| 409 | if(!isset($entryid) || !$entryid) { |
||
| 410 | // if no entryid is present then we can't do anything here |
||
| 411 | return; |
||
| 412 | } |
||
| 413 | |||
| 414 | $listData = array(); |
||
| 415 | if(isset($action['search_folder_entryid'])) { |
||
| 416 | $entryid = hex2bin($action['search_folder_entryid']); |
||
| 417 | } |
||
| 418 | $searchFolder = mapi_msgstore_openentry($store, $entryid); |
||
| 419 | $searchResult = mapi_folder_getsearchcriteria($searchFolder); |
||
| 420 | $searchState = $searchResult["searchstate"]; |
||
| 421 | $table = mapi_folder_getcontentstable($searchFolder, MAPI_DEFERRED_ERRORS); |
||
| 422 | |||
| 423 | if(is_array($this->sort) && !empty($this->sort)) { |
||
| 424 | // this sorting will be done on currently fetched results, not all results |
||
| 425 | // @TODO find a way to do sorting on all search results |
||
| 426 | mapi_table_sort($table, $this->sort, TBL_BATCH); |
||
| 427 | } |
||
| 428 | |||
| 429 | $rowCount = $GLOBALS['settings']->get('zarafa/v1/main/page_size', 50); |
||
| 430 | |||
| 431 | $searchResults = array(); |
||
| 432 | $entryid = bin2hex($entryid); |
||
| 433 | if(isset($this->sessionData['searchResults'][$entryid])) { |
||
| 434 | $searchResults = $this->sessionData['searchResults'][$entryid]; |
||
| 435 | } |
||
| 436 | |||
| 437 | // searchResults contains entryids of messages |
||
| 438 | // that are already sent to the server |
||
| 439 | $numberOfResults = count($searchResults); |
||
| 440 | |||
| 441 | if($numberOfResults < $rowCount) { |
||
| 442 | $items = mapi_table_queryallrows($table, array(PR_ENTRYID, PR_LAST_MODIFICATION_TIME)); |
||
| 443 | |||
| 444 | foreach($items as $props) { |
||
| 445 | $sendItemToClient = false; |
||
| 446 | |||
| 447 | if(!array_key_exists(bin2hex($props[PR_ENTRYID]), $searchResults)) { |
||
| 448 | $sendItemToClient = true; |
||
| 449 | } else { |
||
| 450 | /** |
||
| 451 | * it could happen that an item in search folder has been changed |
||
| 452 | * after we have sent it to client, so we have to again send it |
||
| 453 | * so we will have to use last_modification_time of item to check |
||
| 454 | * that item has been modified since we have sent it to client |
||
| 455 | */ |
||
| 456 | // TODO if any item is deleted from search folder it will be not notified to client |
||
| 457 | if($searchResults[bin2hex($props[PR_ENTRYID])] < $props[PR_LAST_MODIFICATION_TIME]) { |
||
| 458 | $sendItemToClient = true; |
||
| 459 | } |
||
| 460 | } |
||
| 461 | |||
| 462 | if($sendItemToClient) { |
||
| 463 | // only get primitive properties, no need to get body, attachments or recipient information |
||
| 464 | $message = $GLOBALS["operations"]->openMessage($store, $props[PR_ENTRYID]); |
||
| 465 | array_push($listData, $GLOBALS["operations"]->getProps($message, $this->properties)); |
||
| 466 | |||
| 467 | // store entryid => last_modification_time mapping |
||
| 468 | $searchResults[bin2hex($props[PR_ENTRYID])] = $props[PR_LAST_MODIFICATION_TIME]; |
||
| 469 | } |
||
| 470 | |||
| 471 | // when we have more results then fit in the client, we break here, |
||
| 472 | // we only need to update the counters from this point |
||
| 473 | $numberOfResults = count($searchResults); |
||
| 474 | if($numberOfResults >= $rowCount) { |
||
| 475 | break; |
||
| 476 | } |
||
| 477 | } |
||
| 478 | } |
||
| 479 | |||
| 480 | $totalRowCount = mapi_table_getrowcount($table); |
||
| 481 | |||
| 482 | $data = array(); |
||
| 483 | $data["search_meta"] = array(); |
||
| 484 | $data["search_meta"]["searchfolder_entryid"] = $entryid; |
||
| 485 | $data["search_meta"]["search_store_entryid"] = $action["store_entryid"]; |
||
| 486 | $data["search_meta"]["searchstate"] = $searchState; |
||
| 487 | $data["search_meta"]["results"] = $numberOfResults; // actual number of items that we are sending to client |
||
| 488 | |||
| 489 | $data["page"] = array(); |
||
| 490 | $data["page"]["start"] = 0; |
||
| 491 | $data["page"]["rowcount"] = $rowCount; |
||
| 492 | $data["page"]["totalrowcount"] = $totalRowCount; // total number of items |
||
| 493 | |||
| 494 | if(!empty($listData)) { |
||
| 495 | $data["item"] = array_merge(array(), $listData); |
||
| 496 | } |
||
| 497 | |||
| 498 | // search is finished so we no more need entryids of search results so clear it up |
||
| 499 | if($searchState & SEARCH_REBUILD === 0) { |
||
| 500 | // remove search result entryids stored in session |
||
| 501 | unset($this->sessionData['searchResults'][$entryid]); |
||
| 502 | } else { |
||
| 503 | // store data for next request |
||
| 504 | $this->sessionData['searchResults'][$entryid] = $searchResults; |
||
| 505 | } |
||
| 506 | |||
| 507 | $this->addActionData("updatesearch", $data); |
||
| 508 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 509 | |||
| 510 | return true; |
||
| 511 | } |
||
| 512 | |||
| 513 | /** |
||
| 514 | * Function will stop search on the server if search folder exists |
||
| 515 | * @param object $store MAPI Message Store Object |
||
| 516 | * @param hexString $entryid entryid of the folder |
||
| 517 | * @param object $action the action data, sent by the client |
||
| 518 | */ |
||
| 519 | function stopSearch($store, $entryid, $action) |
||
| 520 | { |
||
| 521 | // if no entryid is present in the request then get the search folder entryid from session data |
||
| 522 | $entryid = !empty($entryid) ? $entryid : hex2bin($action['search_folder_entryid']); |
||
| 523 | |||
| 524 | if(empty($entryid)) { |
||
| 525 | // still no entryid? sorry i can't help you anymore |
||
| 526 | $this->addActionData("stopsearch", array( 'success' => false )); |
||
| 527 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 528 | return; |
||
| 529 | } |
||
| 530 | |||
| 531 | // remove search result entryids stored in session |
||
| 532 | unset($this->sessionData['searchResults'][bin2hex($entryid)]); |
||
| 533 | unset($this->sessionData['searchCriteriaCheck']); |
||
| 534 | unset($this->sessionData['searchFolderEntryId']); |
||
| 535 | unset($this->sessionData['searchOriginalEntryids']); |
||
| 536 | |||
| 537 | $searchFolder = mapi_msgstore_openentry($store, $entryid); |
||
| 538 | $searchResult = mapi_folder_getsearchcriteria($searchFolder); |
||
| 539 | |||
| 540 | // check if search folder exists and search is in progress |
||
| 541 | if($searchResult !== false && ($searchResult["searchstate"] & SEARCH_REBUILD !== 0)) { |
||
| 542 | mapi_folder_setsearchcriteria($searchFolder, $searchResult['restriction'], $searchResult['folderlist'], STOP_SEARCH); |
||
| 543 | } |
||
| 544 | |||
| 545 | /** |
||
| 546 | * when stopping search process, we have to remove search folder also, |
||
| 547 | * so next search request with same restriction will not get uncompleted results |
||
| 548 | */ |
||
| 549 | $this->deleteSearchFolder($store, $entryid, $action); |
||
| 550 | |||
| 551 | // send success message to client |
||
| 552 | $this->addActionData("stopsearch", array( 'success' => true )); |
||
| 553 | $GLOBALS["bus"]->addData($this->getResponseData()); |
||
| 554 | } |
||
| 555 | |||
| 556 | /** |
||
| 557 | * Function will delete search folder |
||
| 558 | * @param object $store MAPI Message Store Object |
||
| 559 | * @param hexString $entryid entryid of the folder |
||
| 560 | * @param array $action the action data, sent by the client |
||
| 561 | * @return boolean true on success or false on failure |
||
| 562 | */ |
||
| 563 | function deleteSearchFolder($store, $entryid, $action) |
||
| 564 | { |
||
| 565 | if($entryid && $store) { |
||
| 566 | $storeProps = mapi_getprops($store, array(PR_FINDER_ENTRYID)); |
||
| 567 | |||
| 568 | $finderFolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]); |
||
| 569 | |||
| 570 | if(mapi_last_hresult() != NOERROR){ |
||
| 571 | return; |
||
| 572 | } |
||
| 573 | |||
| 574 | $hierarchyTable = mapi_folder_gethierarchytable($finderFolder, MAPI_DEFERRED_ERRORS); |
||
| 575 | |||
| 576 | $restriction = array(RES_CONTENT, |
||
| 577 | array( |
||
| 578 | FUZZYLEVEL => FL_FULLSTRING, |
||
| 579 | ULPROPTAG => PR_ENTRYID, |
||
| 580 | VALUE => array(PR_ENTRYID => $entryid) |
||
| 581 | ) |
||
| 582 | ); |
||
| 583 | |||
| 584 | mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH); |
||
| 585 | |||
| 586 | // entryids are unique so there would be only one matching row, |
||
| 587 | // so only fetch first row |
||
| 588 | $folders = mapi_table_queryrows($hierarchyTable, array(PR_ENTRYID), 0, 1); |
||
| 589 | |||
| 590 | // delete search folder |
||
| 591 | if(is_array($folders) && is_array($folders[0])) { |
||
| 592 | mapi_folder_deletefolder($finderFolder, $folders[0][PR_ENTRYID]); |
||
| 593 | } |
||
| 594 | |||
| 595 | return true; |
||
| 596 | } |
||
| 597 | |||
| 598 | return false; |
||
| 599 | } |
||
| 600 | |||
| 601 | /** |
||
| 602 | * Function will create a search folder in FINDER_ROOT folder |
||
| 603 | * if folder exists then it will open it |
||
| 604 | * @param object $store MAPI Message Store Object |
||
| 605 | * @param boolean $openIfExists open if folder exists |
||
| 606 | * @return resource|boolean $folder created search folder |
||
| 607 | */ |
||
| 608 | function createSearchFolder($store, $openIfExists = true) |
||
| 609 | { |
||
| 610 | if(isset($this->sessionData['searchFolderEntryId']) && $openIfExists) { |
||
| 611 | try { |
||
| 612 | $searchFolder = mapi_msgstore_openentry($store, hex2bin($this->sessionData['searchFolderEntryId'])); |
||
| 613 | |||
| 614 | if($searchFolder !== false) { |
||
| 615 | // search folder exists, don't create new search folder |
||
| 616 | return $searchFolder; |
||
| 617 | } |
||
| 618 | } catch (MAPIException $e) { |
||
| 619 | // ignore error and continue creation of search folder |
||
| 620 | unset($this->sessionData['searchFolderEntryId']); |
||
| 621 | } |
||
| 622 | } |
||
| 623 | |||
| 624 | // create new search folder |
||
| 625 | $searchFolderRoot = $this->getSearchFoldersRoot($store); |
||
| 626 | if($searchFolderRoot === false) { |
||
| 627 | // error in finding search root folder |
||
| 628 | // or store doesn't support search folders |
||
| 629 | return false; |
||
| 630 | } |
||
| 631 | |||
| 632 | // check for folder name, if exists then delete it |
||
| 633 | $folderName = "grommunio Web Search Folder"; |
||
| 634 | try { |
||
| 635 | $table = mapi_folder_gethierarchytable($searchFolderRoot, 0); |
||
| 636 | $rows = mapi_table_queryrows($table, array(PR_DISPLAY_NAME, PR_ENTRYID), 0, 0xFFFF); |
||
| 637 | foreach ($rows as $row) { |
||
| 638 | if (0 == strcasecmp($folderName, $row[PR_DISPLAY_NAME])) { |
||
| 639 | mapi_folder_deletefolder($searchFolderRoot, $row[PR_ENTRYID], DEL_FOLDERS|DEL_MESSAGES); |
||
| 640 | break; |
||
| 641 | } |
||
| 642 | } |
||
| 643 | $searchFolder = mapi_folder_createfolder($searchFolderRoot, $folderName, null, 0, FOLDER_SEARCH); |
||
| 644 | |||
| 645 | $props = mapi_getprops($searchFolder, array(PR_ENTRYID)); |
||
| 646 | $this->sessionData['searchFolderEntryId'] = bin2hex($props[PR_ENTRYID]); |
||
| 647 | |||
| 648 | // we have created new search folder so search criteria check should be removed |
||
| 649 | unset($this->sessionData['searchCriteriaCheck']); |
||
| 650 | |||
| 651 | return $searchFolder; |
||
| 652 | } catch (MAPIException $e) { |
||
| 653 | // don't propagate the event to higher level exception handlers |
||
| 654 | $e->setHandled(); |
||
| 655 | } |
||
| 656 | |||
| 657 | return false; |
||
| 658 | } |
||
| 659 | |||
| 660 | /** |
||
| 661 | * Function will open FINDER_ROOT folder in root container |
||
| 662 | * public folder's don't have FINDER_ROOT folder |
||
| 663 | * @param object store MAPI message store object |
||
| 664 | * @return resource|boolean finder root folder for search folders |
||
| 665 | */ |
||
| 666 | function getSearchFoldersRoot($store) |
||
| 667 | { |
||
| 668 | $searchRootFolder = false; |
||
| 669 | |||
| 670 | // check if we can create search folders |
||
| 671 | $storeProps = mapi_getprops($store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID, PR_DISPLAY_NAME)); |
||
| 672 | if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) !== STORE_SEARCH_OK) { |
||
| 673 | // store doesn't support search folders, public store don't have FINDER_ROOT folder |
||
| 674 | return false; |
||
| 675 | } |
||
| 676 | |||
| 677 | try { |
||
| 678 | $searchRootFolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]); |
||
| 679 | } catch (MAPIException $e) { |
||
| 680 | $msg ="Unable to open FINDER_ROOT for store: %s."; |
||
| 681 | error_log(sprintf($msg, $storeProps[PR_DISPLAY_NAME])); |
||
| 682 | // don't propagate the event to higher level exception handlers |
||
| 683 | $e->setHandled(); |
||
| 684 | } |
||
| 685 | |||
| 686 | return $searchRootFolder; |
||
| 687 | } |
||
| 688 | |||
| 689 | /** |
||
| 690 | * Function will send error message to client if any error has occurred in search |
||
| 691 | * @param object $store MAPI Message Store Object |
||
| 692 | * @param hexString $entryid entryid of the folder |
||
| 693 | * @param object $action the action data, sent by the client |
||
| 694 | * @param object $errorInfo the error information object |
||
| 695 | */ |
||
| 696 | function sendSearchErrorToClient($store, $entryid, $action, $errorInfo) |
||
| 697 | { |
||
| 698 | if($errorInfo) { |
||
| 699 | $exception = new SearchException(isset($errorInfo["original_error_message"]) ? $errorInfo["original_error_message"] : $errorInfo['error_message'], mapi_last_hresult()); |
||
| 700 | $exception->setDisplayMessage($errorInfo['error_message']); |
||
| 701 | |||
| 702 | // after sending error, remove error data |
||
| 703 | $errorInfo = array(); |
||
| 704 | |||
| 705 | throw $exception; |
||
| 706 | } |
||
| 707 | |||
| 708 | return false; |
||
| 709 | } |
||
| 710 | |||
| 711 | /** |
||
| 712 | * Function will create restriction based on restriction array. |
||
| 713 | * @param object $action the action data, sent by the client |
||
| 714 | */ |
||
| 715 | function parseRestriction($action) |
||
| 716 | { |
||
| 717 | if (isset($action["restriction"]) && is_array($action['restriction'])) { |
||
| 718 | if(isset($action["restriction"]["start"])) { |
||
| 719 | // Set start variable |
||
| 720 | $this->start = $action["restriction"]["start"]; |
||
| 721 | } |
||
| 722 | foreach ($action["restriction"] as $key => $value) { |
||
| 723 | $props = $this->properties; |
||
| 724 | if (!empty($value)) { |
||
| 725 | switch ($key) { |
||
| 726 | case "search": |
||
| 727 | $props = array_merge($this->properties, array('body' => PR_BODY)); |
||
| 728 | case "task": |
||
| 729 | case "note": |
||
| 730 | case "filter": |
||
| 731 | $this->restriction = Conversion::json2restriction($props, $value); |
||
| 732 | break; |
||
| 733 | } |
||
| 734 | } |
||
| 735 | } |
||
| 736 | } |
||
| 737 | } |
||
| 738 | |||
| 739 | /** |
||
| 740 | * Parses the incoming sort request and builds a MAPI sort order. Normally |
||
| 741 | * properties are mapped from the XML to MAPI by the standard $this->properties mapping. However, |
||
| 742 | * if you want other mappings, you can specify them in the optional $map mapping. |
||
| 743 | * |
||
| 744 | * $allow_multi_instance is used for creating multiple instance of MV property related items. |
||
| 745 | * $properties is used for using a custom set of properties instead of properties stored in module |
||
| 746 | */ |
||
| 747 | function parseSortOrder($action, $map = false, $allow_multi_instance = false, $properties = false) |
||
| 748 | { |
||
| 749 | if(isset($action["sort"])) { |
||
| 750 | $this->sort = array(); |
||
| 751 | |||
| 752 | if(!$properties) { |
||
| 753 | $properties = $this->properties; |
||
| 754 | } |
||
| 755 | |||
| 756 | // Unshift MVI_FLAG of MV properties. So the table is not sort on it anymore. |
||
| 757 | // Otherwise the server would generate multiple rows for one item (categories). |
||
| 758 | foreach($properties as $id => $property) |
||
| 759 | { |
||
| 760 | switch(mapi_prop_type($property)) |
||
| 761 | { |
||
| 762 | case (PT_MV_STRING8 | MVI_FLAG): |
||
| 763 | case (PT_MV_LONG | MVI_FLAG): |
||
| 764 | $properties[$id] = $properties[$id] &~ MV_INSTANCE; |
||
| 765 | break; |
||
| 766 | } |
||
| 767 | } |
||
| 768 | |||
| 769 | // Loop through the sort columns |
||
| 770 | foreach($action["sort"] as $column) |
||
| 771 | { |
||
| 772 | if(isset($column["direction"])) { |
||
| 773 | if(isset($properties[$column["field"]]) || ($map && isset($map[$column["field"]]))) { |
||
| 774 | if($map && isset($map[$column["field"]])) |
||
| 775 | $property = $map[$column["field"]]; |
||
| 776 | else |
||
| 777 | $property = $properties[$column["field"]]; |
||
| 778 | |||
| 779 | // Check if column is a MV property |
||
| 780 | switch(mapi_prop_type($property)) |
||
| 781 | { |
||
| 782 | case PT_MV_STRING8: |
||
| 783 | case PT_MV_LONG: |
||
| 784 | // Set MVI_FLAG. |
||
| 785 | // The server will generate multiple rows for one item (for example: categories) |
||
| 786 | if($allow_multi_instance){ |
||
| 787 | $properties[$column["field"]] = $properties[$column["field"]] | MVI_FLAG; |
||
| 788 | } |
||
| 789 | $property = $properties[$column["field"]]; |
||
| 790 | break; |
||
| 791 | } |
||
| 792 | |||
| 793 | // Set sort direction |
||
| 794 | switch(strtolower($column["direction"])) |
||
| 795 | { |
||
| 796 | default: |
||
| 797 | case "asc": |
||
| 798 | $this->sort[$property] = TABLE_SORT_ASCEND; |
||
| 799 | break; |
||
| 800 | case "desc": |
||
| 801 | $this->sort[$property] = TABLE_SORT_DESCEND; |
||
| 802 | break; |
||
| 803 | } |
||
| 804 | } |
||
| 805 | } |
||
| 806 | } |
||
| 807 | } |
||
| 808 | } |
||
| 809 | |||
| 810 | /** |
||
| 811 | * Function which gets the delegation details from localfreebusy message to use in |
||
| 812 | * processPrivateItems function. |
||
| 813 | * @param {MAPIStore} $store MAPI Message Store Object |
||
| 814 | */ |
||
| 815 | function getDelegateFolderInfo($store) |
||
| 816 | { |
||
| 817 | $this->localFreeBusyMessage = false; |
||
| 818 | $this->storeProviderGuid = false; |
||
| 819 | |||
| 820 | try { |
||
| 821 | $this->storeProviderGuid = mapi_getprops($store, array(PR_MDB_PROVIDER)); |
||
| 822 | $this->storeProviderGuid = $this->storeProviderGuid[PR_MDB_PROVIDER]; |
||
| 823 | |||
| 824 | if($this->storeProviderGuid !== ZARAFA_STORE_DELEGATE_GUID) { |
||
| 825 | // user is not a delegate, so no point of processing further |
||
| 826 | return; |
||
| 827 | } |
||
| 828 | |||
| 829 | // get localfreebusy message |
||
| 830 | $this->localFreeBusyMessage = freebusy::getLocalFreeBusyMessage($store); |
||
| 831 | } catch(MAPIException $e) { |
||
| 832 | // we got some error, but we don't care about that error instead just continue |
||
| 833 | $e->setHandled(); |
||
| 834 | |||
| 835 | $this->localFreeBusyMessage = false; |
||
| 836 | $this->storeProviderGuid = false; |
||
| 837 | } |
||
| 838 | } |
||
| 839 | |||
| 840 | /** |
||
| 841 | * Helper function which loop through each item and filter out |
||
| 842 | * private items, if any. |
||
| 843 | * @param {array} array structure with row search data |
||
| 844 | * @return {array} array structure with row search data |
||
| 845 | */ |
||
| 846 | function filterPrivateItems($data) |
||
| 847 | { |
||
| 848 | // Disable private items |
||
| 849 | for($index = 0, $len = count($data["item"]); $index < $len; $index++) { |
||
| 850 | $data["item"][$index] = $this->processPrivateItem($data["item"][$index]); |
||
| 851 | |||
| 852 | if(empty($data["item"][$index])) { |
||
| 853 | // remove empty results from data |
||
| 854 | unset($data["item"][$index]); |
||
| 855 | } |
||
| 856 | } |
||
| 857 | |||
| 858 | return $data; |
||
| 859 | } |
||
| 860 | |||
| 861 | /** |
||
| 862 | * Function will be used to process private items in a list response, modules can |
||
| 863 | * can decide what to do with the private items, remove the entire row or just |
||
| 864 | * hide the data. This function will entirely remove the private message but |
||
| 865 | * if any child class needs different behavior then this can be overridden. |
||
| 866 | * @param {Object} $item item properties |
||
| 867 | * @return {Object} item properties if its non private item otherwise empty array |
||
| 868 | */ |
||
| 869 | function processPrivateItem($item) |
||
| 870 | { |
||
| 871 | if($this->checkPrivateItem($item)) { |
||
| 872 | // hide the item by returning empty array, that can be removed from response |
||
| 873 | return array(); |
||
| 874 | } |
||
| 875 | |||
| 876 | return $item; |
||
| 877 | } |
||
| 878 | |||
| 879 | /** |
||
| 880 | * Function will be used check if any item is private or not and if it private then |
||
| 881 | * we should process it as private, because you don't want to process private items in |
||
| 882 | * user's default store. |
||
| 883 | * This function will check we are dealing with delegate stores or not if it is then |
||
| 884 | * the delegator has permission to see private items of delegate. |
||
| 885 | * @param {Object} $item item properties |
||
| 886 | * @return {Boolean} true if items should be processed as private else false. |
||
| 887 | */ |
||
| 888 | function checkPrivateItem($item) |
||
| 889 | { |
||
| 890 | // flag to indicate that item should be considered as private |
||
| 891 | $private = false; |
||
| 892 | |||
| 893 | $isPrivate = (isset($item['props']['private']) && $item['props']['private'] === true); |
||
| 894 | $isSensitive = (isset($item['props']['sensitivity']) && $item['props']['sensitivity'] === SENSITIVITY_PRIVATE); |
||
| 895 | |||
| 896 | if($isPrivate || $isSensitive) { |
||
| 897 | // check for delegate permissions for delegate store |
||
| 898 | if($this->storeProviderGuid !== false && $this->storeProviderGuid === ZARAFA_STORE_DELEGATE_GUID) { |
||
| 899 | // by default we should always hide the item if we are in delegate store |
||
| 900 | $private = true; |
||
| 901 | |||
| 902 | // find delegate properties |
||
| 903 | if($this->localFreeBusyMessage !== false) { |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 904 | try { |
||
| 905 | $localFreeBusyMessageProps = mapi_getprops($this->localFreeBusyMessage, array(PR_SCHDINFO_DELEGATE_ENTRYIDS, PR_DELEGATES_SEE_PRIVATE)); |
||
| 906 | |||
| 907 | if(isset($localFreeBusyMessageProps[PR_SCHDINFO_DELEGATE_ENTRYIDS]) && isset($localFreeBusyMessageProps[PR_DELEGATES_SEE_PRIVATE])) { |
||
| 908 | // if more then one delegates info is stored then find index of |
||
| 909 | // current user |
||
| 910 | $userEntryId = $GLOBALS['mapisession']->getUserEntryID(); |
||
| 911 | |||
| 912 | $userFound = false; |
||
| 913 | $seePrivate = false; |
||
| 914 | foreach($localFreeBusyMessageProps[PR_SCHDINFO_DELEGATE_ENTRYIDS] as $key => $entryId) { |
||
| 915 | if ($GLOBALS['entryid']->compareABEntryIds(bin2hex($userEntryId), bin2hex($entryId))) { |
||
| 916 | $userFound = true; |
||
| 917 | $seePrivate = $localFreeBusyMessageProps[PR_DELEGATES_SEE_PRIVATE][$key]; |
||
| 918 | break; |
||
| 919 | } |
||
| 920 | } |
||
| 921 | |||
| 922 | if($userFound !== false && $seePrivate === 1) { |
||
| 923 | // if delegate has permission then don't hide the item |
||
| 924 | $private = false; |
||
| 925 | } |
||
| 926 | } |
||
| 927 | } catch (MAPIException $e) { |
||
| 928 | if($e->getCode() === MAPI_E_NOT_FOUND) { |
||
| 929 | // no information available for delegates, ignore error |
||
| 930 | $e->setHandled(); |
||
| 931 | } |
||
| 932 | } |
||
| 933 | } |
||
| 934 | } |
||
| 935 | } |
||
| 936 | |||
| 937 | return $private; |
||
| 938 | } |
||
| 939 | } |
||
| 940 | ?> |
||
|
0 ignored issues
–
show
It is not recommended to use PHP's closing tag
?> in files other than templates.
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. Loading history...
|
|||
| 941 |