1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file defines the search controller. 27 * 28 */ 29 30 /** 31 * Creates a search controller. 32 * @class 33 * This class represents the search controller. 34 * 35 * @param {DwtControl} container the top-level container 36 * @extends ZmController 37 */ 38 ZmSearchController = function(container) { 39 40 ZmController.call(this, container); 41 42 this._inited = false; 43 this._contactSource = ZmItem.CONTACT; 44 this._results = null; 45 46 if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) { 47 this._setView(); 48 } 49 }; 50 51 ZmSearchController.prototype = new ZmController; 52 ZmSearchController.prototype.constructor = ZmSearchController; 53 54 ZmSearchController.prototype.isZmSearchController = true; 55 ZmSearchController.prototype.toString = function() { return "ZmSearchController"; }; 56 57 // Consts 58 ZmSearchController.QUERY_ISREMOTE = "is:remote OR is:local"; 59 60 61 /** 62 * Gets the search tool bar. 63 * 64 * @return {ZmButtonToolBar} the tool bar 65 */ 66 ZmSearchController.prototype.getSearchToolbar = 67 function() { 68 return this._searchToolBar; 69 }; 70 71 /** 72 * Performs a search by date. 73 * 74 * @param {Date} d the date or <code>d</code> for now 75 * @param {String} searchFor the search for string 76 */ 77 ZmSearchController.prototype.dateSearch = 78 function(d, searchFor) { 79 d = d || new Date(); 80 var formatter = AjxDateFormat.getDateInstance(AjxDateFormat.SHORT); 81 var date = formatter.format(d); 82 var groupBy = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy(); 83 var query = "date:" + date; 84 this.search({ 85 query: query, 86 types: [groupBy], 87 searchFor: searchFor, 88 origin: ZmId.SEARCH, 89 userInitiated: true 90 }); 91 }; 92 93 /** 94 * Performs a search by from address. 95 * 96 * @param {String} address the from address 97 */ 98 ZmSearchController.prototype.fromSearch = 99 function(address) { 100 101 // always search for mail when doing a "from: <address>" search 102 var groupBy = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy(); 103 var terms = AjxUtil.map(AjxUtil.toArray(address), function(addr) { 104 return "from:" + ((addr && addr.isAjxEmailAddress) ? addr.getAddress() : addr); 105 }); 106 107 this.search({ 108 query: terms.join(" OR "), 109 types: [groupBy], 110 origin: ZmId.SEARCH, 111 userInitiated: true 112 }); 113 }; 114 115 116 /** 117 * Performs a search by to address. 118 * 119 * @param {String} address the to address 120 */ 121 ZmSearchController.prototype.toSearch = 122 function(address) { 123 124 // always search for mail when doing a "tocc: <address>" search 125 var groupBy = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy(); 126 var terms = AjxUtil.map(AjxUtil.toArray(address), function(addr) { 127 return "tocc:" + ((addr && addr.isAjxEmailAddress) ? addr.getAddress() : addr); 128 }); 129 130 var params = { 131 types: [groupBy], 132 origin: ZmId.SEARCH, 133 userInitiated: true, 134 query: terms.join(" OR ") 135 } 136 if (this.currentSearch && this.currentSearch.folderId == ZmFolder.ID_SENT) { 137 if (terms.length > 1) { 138 params.query = "(" + params.query + ")"; 139 } 140 params.query = "in:sent AND " + params.query; 141 } 142 this.search(params); 143 }; 144 145 /** 146 * Sets the search field. 147 * 148 * @param {String} searchString the search string 149 */ 150 ZmSearchController.prototype.setSearchField = 151 function(searchString) { 152 if (appCtxt.get(ZmSetting.SHOW_SEARCH_STRING) && this._searchToolBar) { 153 this._searchToolBar.setSearchFieldValue(searchString); 154 } else { 155 this._currentQuery = searchString; 156 } 157 }; 158 159 /** 160 * Gets the search field value. 161 * 162 * @return {String} the search field value or an empty string 163 */ 164 ZmSearchController.prototype.getSearchFieldValue = 165 function() { 166 return this._searchToolBar ? this._searchToolBar.getSearchFieldValue() : ""; 167 }; 168 169 ZmSearchController.prototype.setEnabled = 170 function(enabled) { 171 if (this._searchToolBar) { 172 this._searchToolBar.setEnabled(enabled); 173 } 174 }; 175 176 /** 177 * Sets the default type. This method provides a programmatic way to set the search type. 178 * 179 * @param {Object} type the search type to set as the default 180 */ 181 ZmSearchController.prototype.setDefaultSearchType = 182 function(type) { 183 if (!this._searchToolBar) { 184 return; 185 } 186 var menu = this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON).getMenu(); 187 menu.checkItem(ZmOperation.MENUITEM_ID, type); 188 this._searchMenuListener(null, type, true); 189 }; 190 191 /** 192 * @private 193 */ 194 ZmSearchController.prototype._setView = 195 function() { 196 197 // Create search panel - a composite is needed because the search builder 198 // element (ZmBrowseView) is added to it (can't add it to the toolbar) 199 this.searchPanel = new DwtComposite({ 200 parent: this._container, 201 className: "SearchPanel", 202 posStyle: Dwt.ABSOLUTE_STYLE 203 }); 204 205 this._searchToolBar = new ZmMainSearchToolBar({ 206 parent: this.searchPanel, 207 id: ZmId.SEARCH_TOOLBAR 208 }); 209 210 this._createTabGroup(); 211 this._tabGroup.addMember(this._searchToolBar.getChildren()); 212 213 // Register keyboard callback for search field 214 this._searchToolBar.registerEnterCallback(this._toolbarSearch.bind(this)); 215 216 // Button listeners 217 this._searchToolBar.addSelectionListener(ZmSearchToolBar.SEARCH_BUTTON, this._searchButtonListener.bind(this)); 218 }; 219 220 /** 221 * @private 222 */ 223 ZmSearchController.prototype._addMenuListeners = 224 function(menu) { 225 // Menu listeners 226 var searchMenuListener = this._searchMenuListener.bind(this); 227 var items = menu.getItems(); 228 for (var i = 0; i < items.length; i++) { 229 var item = items[i]; 230 item.addSelectionListener(searchMenuListener); 231 var mi = item.getData(ZmOperation.MENUITEM_ID); 232 // set mail as default search 233 if (mi == ZmId.SEARCH_MAIL) { 234 item.setChecked(true, true); 235 } 236 } 237 }; 238 239 /** 240 * Performs a search and displays the results. 241 * 242 * @param {Hash} params a hash of parameters: 243 * 244 * @param {String} query the search string 245 * @param {constant} searchFor the semantic type to search for 246 * @param {Array} types the item types to search for 247 * @param {constant} sortBy the sort constraint 248 * @param {int} offset the starting point in list of matching items 249 * @param {int} limit the maximum number of items to return 250 * @param {int} searchId the ID of owning search folder (if any) 251 * @param {Boolean} noRender if <code>true</code>, results will not be passed to controller 252 * @param {Boolean} userText if <code>true</code>, text was typed by user into search box 253 * @param {AjxCallback} callback the async callback 254 * @param {AjxCallback} errorCallback the async callback to run if there is an exception 255 * @param {Object} response the canned JSON response (no request will be made) 256 * @param {boolean} skipUpdateSearchToolbar don't update the search toolbar (e.g. from the ZmDumpsterDialog where the search is called from its own search toolbar 257 * @param {string} origin indicates what initiated the search 258 * @param {string} sessionId session ID of search results tab (if search came from one) 259 * @param {Boolean} noGal if true, don't search GAL. This is to override the this._contactSource value in contacts search, specifically for clicking on TAGS. 260 * 261 */ 262 ZmSearchController.prototype.search = 263 function(params) { 264 265 // if the search string starts with "$set:" then it is a command to the client 266 if (params.query && (params.query.indexOf("$set:") == 0 || params.query.indexOf("$cmd:") == 0)) { 267 appCtxt.getClientCmdHandler().execute((params.query.substr(5)), this); 268 return; 269 } 270 271 params.searchAllAccounts = this.searchAllAccounts; 272 var respCallback = this._handleResponseSearch.bind(this, params.callback); 273 this._doSearch(params, params.noRender, respCallback, params.errorCallback); 274 }; 275 276 /** 277 * @private 278 */ 279 ZmSearchController.prototype._handleResponseSearch = 280 function(callback, result) { 281 if (callback) { 282 callback.run(result); 283 } 284 }; 285 286 /** 287 * Performs the given search. It takes a ZmSearch, rather than constructing one out of the currently selected menu 288 * choices. Aside from re-executing a search, it can be used to perform a canned search. 289 * 290 * @param {ZmSearch} search the search object 291 * @param {Boolean} noRender if <code>true</code>, results will not be passed to controller 292 * @param {Object} changes the hash of changes to make to search 293 * @param {AjxCallback} callback the async callback 294 * @param {AjxCallback} errorCallback the async callback to run if there is an exception 295 */ 296 ZmSearchController.prototype.redoSearch = 297 function(search, noRender, changes, callback, errorCallback) { 298 299 var params = {}; 300 params.query = search.query; 301 params.queryHint = search.queryHint; 302 params.types = search.types; 303 params.forceTypes = search.forceTypes; 304 params.sortBy = search.sortBy; 305 params.offset = search.offset; 306 params.limit = search.limit; 307 params.fetch = search.fetch; 308 params.searchId = search.searchId; 309 params.lastSortVal = search.lastSortVal; 310 params.endSortVal = search.endSortVal; 311 params.lastId = search.lastId; 312 params.soapInfo = search.soapInfo; 313 params.accountName = search.accountName; 314 params.searchFor = this._searchFor; 315 params.idsOnly = search.idsOnly; 316 params.inDumpster = search.inDumpster; 317 params.userInitiated = search.userInitiated; 318 params.sessionId = search.sessionId; 319 params.isEmpty = search.isEmpty; 320 params.markRead = search.markRead; 321 322 if (changes) { 323 for (var key in changes) { 324 params[key] = changes[key]; 325 } 326 } 327 328 this._doSearch(params, noRender, callback, errorCallback); 329 }; 330 331 /** 332 * Resets search for all accounts. 333 * 334 */ 335 ZmSearchController.prototype.resetSearchAllAccounts = 336 function() { 337 var button = this.searchAllAccounts && this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON); 338 var menu = button && button.getMenu(); 339 var allAccountsMI = menu && menu.getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_ALL_ACCOUNTS); 340 341 if (allAccountsMI) { 342 allAccountsMI.setChecked(false, true); 343 344 var selItem = menu.getSelectedItem(); 345 var icon = this._inclSharedItems 346 ? this._getSharedImage(selItem) : selItem.getImage(); 347 button.setImage(icon); 348 349 this.searchAllAccounts = false; 350 } 351 }; 352 353 /** 354 * Resets the search toolbar. This is used by the offline client to "reset" the toolbar whenever user 355 * switches between accounts. 356 * 357 */ 358 ZmSearchController.prototype.resetSearchToolbar = 359 function() { 360 var smb = this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON); 361 var mi = smb ? smb.getMenu().getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_GAL) : null; 362 if (mi) { 363 mi.setVisible(appCtxt.getActiveAccount().isZimbraAccount); 364 } 365 }; 366 367 /** 368 * Gets the item type, based on searchFor. The type is the same as the searchFor, except for mail in which the type is either msg or conv based on view. 369 * 370 * @param {String} searchFor general description of what to search for 371 * @param {Boolean} userInitiated true if using a search tab 372 * @return {String} type 373 * 374 * @see #search 375 */ 376 ZmSearchController.prototype.getTypeFromSearchFor = 377 function(searchFor, userInitiated) { 378 379 var type = searchFor; 380 381 if (searchFor === ZmId.SEARCH_MAIL) { 382 var ac = window.parentAppCtxt || window.appCtxt, 383 app = ac.getApp(userInitiated ? ZmApp.SEARCH : ZmApp.MAIL); 384 type = app ? app.getGroupMailBy() : ZmItem.MSG; 385 } 386 387 return type; 388 }; 389 390 /** 391 * Get the searchFor var which is the same as type except for mail, in which case the type is either msg or conv but searchFor is mail. 392 * 393 * @param {String} type type of items to search for 394 * @return {String} searchFor 395 * 396 * @see #search 397 */ 398 ZmSearchController.prototype.getSearchForFromType = 399 function(type) { 400 return (type === ZmItem.MSG || type === ZmItem.CONV) ? ZmId.SEARCH_MAIL : type; 401 }; 402 403 /** 404 * Selects the appropriate item in the overview based on the search. Selection only happens 405 * if the search was a simple search for a folder, tag, or saved search. A check is done to 406 * make sure that item is not already selected, so selection should only occur for a query 407 * manually run by the user. 408 * 409 * @param {ZmSearch} searchObj the current search 410 */ 411 ZmSearchController.prototype.updateOverview = function(searchObj) { 412 413 var search = searchObj || appCtxt.getCurrentSearch(); 414 if (!search) { 415 return; 416 } 417 418 var id, type; 419 if (search.isSimple() || search.searchId) { 420 if (search.searchId) { 421 id = this._getNormalizedId(search.searchId); 422 type = ZmOrganizer.SEARCH; 423 } 424 else if (search.folderId) { 425 id = this._getNormalizedId(search.folderId); 426 var folderTree = appCtxt.getFolderTree(), 427 folder = folderTree && folderTree.getById(id); 428 429 type = ZmOrganizer.ITEM_ORGANIZER[search.searchFor] || (folder && folder.type) || ZmOrganizer.FOLDER; 430 } 431 else if (search.tagId) { 432 id = this._getNormalizedId(search.tagId); 433 type = ZmOrganizer.TAG; 434 } 435 436 if (type) { 437 var app = appCtxt.getCurrentApp(); 438 var overview = app && app.getOverview(); 439 if (overview) { 440 overview.setSelected(id, type); 441 } 442 } 443 } 444 }; 445 446 /** 447 * @private 448 */ 449 ZmSearchController.prototype._getSuitableSortBy = 450 function(type) { 451 var sortBy; 452 453 var viewType; 454 switch (type) { 455 case ZmItem.CONV: viewType = ZmId.VIEW_CONVLIST; break; 456 case ZmItem.MSG: viewType = ZmId.VIEW_TRAD; break; 457 case ZmItem.CONTACT: viewType = ZmId.VIEW_CONTACT_SIMPLE; break; 458 case ZmItem.APPT: viewType = ZmId.VIEW_CAL; break; 459 case ZmItem.TASK: viewType = ZmId.VIEW_TASKLIST; break; 460 case ZmId.SEARCH_GAL: viewType = ZmId.VIEW_CONTACT_SIMPLE; break; 461 case ZmItem.BRIEFCASE_ITEM: viewType = ZmId.VIEW_BRIEFCASE_DETAIL; break; 462 // more types go here as they are suported... 463 } 464 465 if (viewType) { 466 sortBy = appCtxt.get(ZmSetting.SORTING_PREF, viewType); 467 } 468 //bug:1108 & 43789#c19 (changelist 290073) since sort-by-[RCPT|ATTACHMENT|FLAG|PRIORITY] gives exception with querystring. 469 // Avoided [RCPT|ATTACHMENT|FLAG|PRIORITY] sorting with querysting instead used date sorting 470 var queryString = this._searchToolBar.getSearchFieldValue(); 471 if (queryString && queryString.length > 0) { 472 if (sortBy === ZmSearch.RCPT_ASC || sortBy === ZmSearch.RCPT_DESC) { 473 sortBy = sortBy === ZmSearch.RCPT_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC; 474 } 475 else if (sortBy === ZmSearch.FLAG_ASC || sortBy === ZmSearch.FLAG_DESC) { 476 sortBy = sortBy === ZmSearch.FLAG_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC; 477 } 478 else if (sortBy === ZmSearch.ATTACH_ASC || sortBy === ZmSearch.ATTACH_DESC) { 479 sortBy = sortBy === ZmSearch.ATTACH_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC; 480 } 481 else if (sortBy === ZmSearch.PRIORITY_ASC || sortBy === ZmSearch.PRIORITY_DESC) { 482 sortBy = sortBy === ZmSearch.PRIORITY_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC; 483 } 484 } 485 486 return sortBy; 487 }; 488 489 /** 490 * Performs the search. 491 * 492 * @param {Hash} params a hash of params for the search 493 * @param {String} params.searchFor the search for 494 * @param {String} params.query the search query 495 * @param {String} params.userText the user text 496 * @param {Array} params.type an array of types 497 * @param {Boolean} params.forceTypes use the types we pass, do not override (in case of mail) to the current user's view pref (MSG vs. CONV). 498 * @param {boolean} params.inclSharedItems overrides this._inclSharedItems - see ZmTagsHelper._tagClick 499 * @param {boolean} params.forceSearch Ignores special processing and just executes the search. 500 * @param {Boolean} noRender if <code>true</code>, the search results will not be rendered 501 * @param {AjxCallback} callback the callback 502 * @param {AjxCallback} errorCallback the error callback 503 * @param {boolean} params.skipUpdateSearchToolbar don't update the search toolbar (e.g. from the ZmDumpsterDialog where the search is called from its own search toolbar 504 * @param {Boolean} noGal if true, don't search GAL. This is to override the this._contactSource value in contacts search, specifically for clicking on TAGS. 505 * 506 * @see #search 507 * 508 * @private 509 */ 510 ZmSearchController.prototype._doSearch = 511 function(params, noRender, callback, errorCallback) { 512 513 var searchFor = this._searchFor = params.searchFor || this._searchFor || ZmSearchToolBar.MENU_ITEMS[0]; 514 appCtxt.notifyZimlets("onSearch", [params.query]); 515 516 if (!params.skipUpdateSearchToolbar && this._searchToolBar) { 517 var value = (appCtxt.get(ZmSetting.SHOW_SEARCH_STRING) || params.userText) 518 ? params.query : null; 519 this._searchToolBar.setSearchFieldValue(value || ""); 520 521 // bug: 42512 - deselect global inbox if searching via search toolbar 522 if (appCtxt.multiAccounts && params.userText && this.searchAllAccounts) { 523 appCtxt.getCurrentApp().getOverviewContainer().deselectAll(); 524 } 525 } 526 527 // get types from search type if not passed in explicitly 528 // Note - types is now always one value (used to allow all types case, but not anymore). 529 var types = params.types; 530 // Support calling it with null, scalar, array or vector, to make sure different clients of this method work. 531 var type = !types ? searchFor : AjxUtil.toArray(types)[0]; 532 533 //now make sure the searchFor matches the type (searchFor can be taken from the toolbar, but it's not always what we want, for example 534 //in the case of saved search) 535 searchFor = this.getSearchForFromType(type); 536 537 //this makes sure for mail we get the type from the user's setting (CONV/MSG). 538 if (!params.forceTypes) { 539 type = this.getTypeFromSearchFor(searchFor, params.userInitiated); 540 } 541 542 var types = AjxVector.fromArray([type]); //need this Vector (one item) only for couple more usages below that I'm afraid to change now. 543 544 if (searchFor == ZmId.SEARCH_MAIL) { 545 params = appCtxt.getApp(ZmApp.MAIL).getSearchParams(params); 546 } 547 548 if (searchFor == ZmItem.TASK) { 549 var tlc = AjxDispatcher.run("GetTaskListController"); 550 params.allowableTaskStatus = tlc && tlc.getAllowableTaskStatus(); 551 } 552 553 if (params.searchAllAccounts && !params.queryHint) { 554 params.queryHint = appCtxt.accountList.generateQuery(null, types); 555 params.accountName = appCtxt.accountList.mainAccount.name; 556 } 557 else if (params.inclSharedItems || this._inclSharedItems) { 558 // a query hint is part of the query that the user does not see 559 params.queryHint = ZmSearchController.generateQueryForShares(type); 560 } 561 562 // only set contact source if we are searching for contacts 563 params.contactSource = !params.noGal && (type === ZmItem.CONTACT || type === ZmId.SEARCH_GAL) 564 ? this._contactSource : null; 565 if (params.contactSource == ZmId.SEARCH_GAL) { 566 params.expandDL = true; 567 } 568 569 // find suitable sort by value if not given one (and if applicable) 570 params.sortBy = params.sortBy || this._getSuitableSortBy(type); 571 params.types = types; 572 var search = new ZmSearch(params); 573 574 // force drafts folder into msg view 575 //Also force dumpster search into msg view 576 if (searchFor === ZmId.SEARCH_MAIL && (params.inDumpster || (!params.isViewSwitch && search.folderId && search.folderId == ZmFolder.ID_DRAFTS))) { 577 search.types = AjxVector.fromArray([ZmItem.MSG]); 578 search.isDefaultToMessageView = true; 579 } 580 581 var respCallback = this._handleResponseDoSearch.bind(this, search, noRender, callback, params.noUpdateOverview); 582 var offlineCallback = this._handleOfflineDoSearch.bind(this, search, respCallback); 583 if (search.folderId == ZmFolder.ID_OUTBOX) { 584 var offlineRequest = true; 585 } 586 if (!errorCallback) { 587 errorCallback = this._handleErrorDoSearch.bind(this, search); 588 if (!params.errorCallback) { 589 params.errorCallback = errorCallback; 590 } 591 } 592 593 // calendar searching is special so hand it off if necessary 594 search.calController = null; 595 if (searchFor == ZmItem.APPT && !params.forceSearch && !params.inDumpster) { 596 var searchResultsController, sessionId; 597 if (search.userInitiated && ZmApp.SEARCH_RESULTS_TAB[ZmApp.CALENDAR]) { 598 searchResultsController = appCtxt.getApp(ZmApp.SEARCH).getSearchResultsController(search.sessionId, ZmApp.CALENDAR); 599 sessionId = searchResultsController.getCurrentViewId(); 600 } 601 var controller = AjxDispatcher.run("GetCalController", sessionId, searchResultsController); 602 if (controller && type === ZmItem.APPT) { 603 search.calController = controller; 604 controller.handleUserSearch(params, respCallback); 605 } else { 606 search.execute({offlineCache:params && params.offlineCache, callback:respCallback, errorCallback:errorCallback, offlineCallback:offlineCallback, offlineRequest:offlineRequest}); 607 } 608 } else { 609 search.execute({offlineCache:params && params.offlineCache, callback:respCallback, errorCallback:errorCallback, offlineCallback:offlineCallback, offlineRequest:offlineRequest}); 610 } 611 }; 612 613 /** 614 * Takes the search result and hands it to the appropriate controller for display. 615 * 616 * @param {ZmSearch} search contains search info used to run search against server 617 * @param {Boolean} noRender <code>true</code> to skip rendering results 618 * @param {AjxCallback} callback the callback to run after processing search response 619 * @param {Boolean} noUpdateOverview <code>true</code> to skip updating the overview 620 * @param {ZmCsfeResult} result the search results 621 */ 622 ZmSearchController.prototype._handleResponseDoSearch = 623 function(search, noRender, callback, noUpdateOverview, result) { 624 625 DBG.println("s", "SEARCH was user initiated: " + Boolean(search.userInitiated)); 626 var results = result && result.getResponse(); 627 if (!results) { return; } 628 629 if (!results.type) { 630 results.type = search.types.get(0); 631 } 632 633 this.currentSearch = search; 634 DBG.timePt("execute search", true); 635 636 if (!noRender) { 637 this._showResults(results, search, noUpdateOverview); 638 } 639 640 if (callback) { 641 callback.run(result); 642 } 643 }; 644 645 /** 646 * Takes the search result and hands it to the appropriate controller for display. 647 * 648 * @param {ZmSearch} search contains search info used to run search against server 649 * @param {AjxCallback} callback the callback to run after generating offline result 650 */ 651 ZmSearchController.prototype._handleOfflineDoSearch = 652 function(search, callback) { 653 //force webclient offline mode into msg view for mail search 654 if (search.types && search.types.replaceObject(ZmItem.CONV, ZmItem.MSG)) { 655 search.isDefaultToMessageView = true; 656 } 657 var respCallback = this._handleOfflineResponseDoSearch.bind(this, search, callback); 658 ZmOfflineDB.search(search, respCallback); 659 }; 660 661 /** 662 * @private 663 */ 664 ZmSearchController.prototype._showResults = 665 function(results, search, noUpdateOverview) { 666 667 this._results = results = (results && results.isZmSearchResult) ? results : new ZmSearchResult(search); 668 669 DBG.timePt("handle search results"); 670 671 var ac = window.parentAppCtxt || window.appCtxt; 672 if (ac.get(ZmSetting.SAVED_SEARCHES_ENABLED)) { 673 var saveBtn = this._searchToolBar && this._searchToolBar.getButton(ZmSearchToolBar.SAVE_BUTTON); 674 if (saveBtn) { 675 saveBtn.setEnabled(this._contactSource != ZmId.SEARCH_GAL); 676 } 677 } 678 679 var app = search.calController ? ac.getApp(ZmApp.CALENDAR) : ac.getApp(ZmItem.APP[results.type]) || ac.getCurrentApp(); 680 var appName = app.getName(); 681 if (search.userInitiated && ZmApp.SEARCH_RESULTS_TAB[appName]) { 682 var ctlr = (search.calController && search.calController.searchResultsController) || 683 ac.getApp(ZmApp.SEARCH).getSearchResultsController(search.sessionId, appName); 684 DBG.println("sr", "New search results controller: " + ctlr.viewId); 685 ctlr.show(results, search.calController); 686 this._searchToolBar.setSearchFieldValue(""); 687 } 688 else if (app.showSearchResults) { 689 // show results based on type - may invoke package load 690 var loadCallback = this._handleLoadShowResults.bind(this, results, search, noUpdateOverview); 691 app.currentSearch = search; 692 app.currentQuery = search.query; 693 app.showSearchResults(results, loadCallback); 694 } 695 }; 696 697 // Opens a new, empty search tab 698 ZmSearchController.prototype.openNewSearchTab = function() { 699 this._toolbarSearch({ 700 isEmpty: true, 701 origin: ZmId.SEARCH 702 }); 703 }; 704 705 /** 706 * @private 707 */ 708 ZmSearchController.prototype._handleLoadShowResults = 709 function(results, search, noUpdateOverview) { 710 appCtxt.setCurrentList(results.getResults(results.type)); 711 if (!noUpdateOverview) { 712 this.updateOverview(search); 713 } 714 DBG.timePt("render search results"); 715 }; 716 717 /** 718 * Handle a few minor errors where we show an empty result set and issue a 719 * status message to indicate why the query failed. Those errors are: no such 720 * folder, no such tag, and bad query. If it's a "no such folder" error caused 721 * by the deletion of a folder backing a mountpoint, we pass it along for 722 * special handling by ZmZimbraMail. 723 * 724 * @private 725 */ 726 ZmSearchController.prototype._handleErrorDoSearch = 727 function(search, ex) { 728 DBG.println(AjxDebug.DBG1, "Search exception: " + ex.code); 729 if (ex.code == ZmCsfeException.MAIL_NO_SUCH_TAG || 730 ex.code == ZmCsfeException.MAIL_QUERY_PARSE_ERROR || 731 ex.code == ZmCsfeException.MAIL_TOO_MANY_TERMS || 732 (ex.code == ZmCsfeException.MAIL_NO_SUCH_FOLDER && !(ex.data.itemId && ex.data.itemId.length))) 733 { 734 var msg = ex.getErrorMsg(); 735 appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING); 736 return true; 737 } 738 return false; 739 }; 740 741 /** 742 * Provides a string to add to the query when the search includes shared items. 743 * 744 * @param {String} type item type 745 * 746 * @private 747 */ 748 ZmSearchController.generateQueryForShares = 749 function(type, account) { 750 var ac = window.parentAppCtxt || window.appCtxt; 751 var list = []; 752 var app = ac.getApp(ZmItem.APP[type]); 753 if (!app) { 754 return null; 755 } 756 var ids = app.getRemoteFolderIds(account); 757 for (var i = 0; i < ids.length; i++) { 758 var id = ids[i]; 759 var idText = AjxUtil.isNumeric(id) ? id : ['"', id, '"'].join(""); 760 list.push("inid:" + idText); 761 } 762 763 if (list.length > 0) { 764 list.push("is:local"); 765 return list.join(" OR "); 766 } 767 768 return null; 769 }; 770 771 // called when the search button has been pressed 772 ZmSearchController.prototype._searchButtonListener = 773 function(ev) { 774 this._toolbarSearch({ 775 ev: ev, 776 zimletEvent: "onSearchButtonClick", 777 origin: ZmId.SEARCH 778 }); 779 }; 780 781 /** 782 * Runs a search based on the state of the toolbar. 783 * 784 * @param {Hash} params a hash of parameters: 785 * 786 * @param {Event} ev browser event 787 * @param {string} zimletEvent type of notification to send zimlets 788 * @param {string} query search string (optional, overrides input field) 789 * @param {Boolean} isEmpty force a search for "" 790 * @param {string} origin indicates what initiated the search 791 * @param {string} sessionId session ID of search results tab (if search came from one) 792 * @param {boolean} skipUpdateSearchToolbar don't update the search toolbar (e.g. from the ZmDumpsterDialog where the search is called from its own search toolbar 793 * @param {String} sortBy 794 * 795 * @private 796 */ 797 ZmSearchController.prototype._toolbarSearch = 798 function(params) { 799 800 // find out if the custom search menu item is selected and pass it the event 801 var result = params.searchFor || this._searchToolBar.getSearchType(); 802 if (result && result.listener) { 803 result.listener.run(params.ev); 804 } else { 805 var queryString = !params.isEmpty ? params.query || this._searchToolBar.getSearchFieldValue() : ""; 806 var userText = (queryString.length > 0); 807 if (queryString) { 808 this._currentQuery = null; 809 } else { 810 queryString = this._currentQuery || ""; 811 } 812 813 appCtxt.notifyZimlets(params.zimletEvent, [queryString]); 814 var searchParams = { 815 query: queryString, 816 userText: userText, 817 userInitiated: true, 818 getHtml: appCtxt.get(ZmSetting.VIEW_AS_HTML), 819 searchFor: result, 820 skipUpdateSearchToolbar: params.skipUpdateSearchToolbar, 821 origin: params.origin, 822 sessionId: params.sessionId, 823 errorCallback: params.errorCallback, 824 sortBy: params.sortBy, 825 isEmpty: params.isEmpty || !queryString 826 }; 827 this.search(searchParams); 828 } 829 }; 830 831 /** 832 * @private 833 */ 834 ZmSearchController.prototype._searchMenuListener = 835 function(ev, id, noFocus) { 836 var btn = this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON); 837 if (!btn) { return; } 838 839 var menu = btn.getMenu(); 840 var item = ev ? ev.item : (menu.getItemById(ZmOperation.MENUITEM_ID, id)); 841 842 if (!item || (!!(item._style & DwtMenuItem.SEPARATOR_STYLE))) { return; } 843 id = item.getData(ZmOperation.MENUITEM_ID); 844 845 var selItem = menu.getSelectedItem(); 846 var sharedMI = menu.getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_SHARED); 847 848 // enable shared menu item if not a gal search 849 if (id == ZmId.SEARCH_GAL) { 850 this._contactSource = ZmId.SEARCH_GAL; 851 if (sharedMI) { 852 sharedMI.setChecked(false, true); 853 sharedMI.setEnabled(false); 854 } 855 } else { 856 if (sharedMI) { 857 // we allow user to check "Shared Items" for appointments since it 858 // is based on whats checked in their tree view 859 if (id == ZmItem.APPT || id == ZmId.SEARCH_CUSTOM) { 860 if (this._sharedMenuItemChecked == null) { 861 this._sharedMenuItemChecked = sharedMI.getChecked(); 862 } 863 sharedMI.setChecked(false, true); 864 sharedMI.setEnabled(false); 865 } else { 866 sharedMI.setEnabled(true); 867 if (this._sharedMenuItemChecked) { 868 sharedMI.setChecked(true, true); 869 } 870 this._sharedMenuItemChecked = null; 871 } 872 } 873 this._contactSource = ZmItem.CONTACT; 874 } 875 this._inclSharedItems = sharedMI && sharedMI.getChecked(); 876 877 // search all accounts? Only applies to multi-account mbox 878 var allAccountsMI = menu.getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_ALL_ACCOUNTS); 879 if (allAccountsMI) { 880 if (id == ZmItem.APPT) { 881 this.resetSearchAllAccounts(); 882 allAccountsMI.setEnabled(false); 883 } else { 884 allAccountsMI.setEnabled(true); 885 this.searchAllAccounts = allAccountsMI && allAccountsMI.getChecked(); 886 } 887 } 888 889 if (id == ZmId.SEARCH_SHARED) { 890 var icon = this.searchAllAccounts 891 ? allAccountsMI.getImage() : selItem.getImage(); 892 893 if (this._inclSharedItems) { 894 icon = this._getSharedImage(selItem); 895 } 896 897 btn.setImage(icon); 898 } 899 else if (id == ZmId.SEARCH_ALL_ACCOUNTS) { 900 var icon = (this.searchAllAccounts && !this._inclSharedItems) 901 ? item.getImage() 902 : (this._inclSharedItems) ? this._getSharedImage(selItem) : selItem.getImage(); 903 btn.setImage(icon); 904 } 905 else { 906 // only set search for if a "real" search-type menu item was clicked 907 this._searchFor = id; 908 var icon = item.getImage(); 909 910 if (this._inclSharedItems) { 911 icon = this._getSharedImage(selItem); 912 } 913 else if (this.searchAllAccounts) { 914 icon = allAccountsMI.getImage(); 915 } 916 917 btn.setImage(icon); 918 } 919 920 // set button tooltip 921 var tooltip = ZmMsg[ZmSearchToolBar.TT_MSG_KEY[id]]; 922 if (id != ZmId.SEARCH_SHARED && id != ZmId.SEARCH_ALL_ACCOUNTS) { 923 btn.setToolTipContent(tooltip); 924 btn.setAttribute('aria-label', tooltip); 925 } 926 927 if (!noFocus) { 928 // restore focus to INPUT if user changed type 929 setTimeout(this._searchToolBar.focus.bind(this._searchToolBar), 10); 930 } 931 }; 932 933 /** 934 * @private 935 */ 936 ZmSearchController.prototype._getSharedImage = 937 function(selItem) { 938 var selItemId = selItem && selItem.getData(ZmOperation.MENUITEM_ID); 939 return (selItemId && ZmSearchToolBar.SHARE_ICON[selItemId]) 940 ? ZmSearchToolBar.SHARE_ICON[selItemId] 941 : ZmSearchToolBar.ICON[selItemId]; //use regular icon if no share icon 942 }; 943 944 /** 945 * @private 946 */ 947 ZmSearchController.prototype._getNormalizedId = 948 function(id) { 949 var nid = id; 950 951 var acct = appCtxt.getActiveAccount(); 952 if (!acct.isMain && id.indexOf(":") == -1) { 953 nid = acct.id + ":" + id; 954 } 955 956 return nid; 957 }; 958 959 /** 960 * Takes the search result and hands it to the appropriate controller for display. 961 * 962 * @param {ZmSearch} search contains search info used to run search against server 963 * @param {AjxCallback} callback online response callback to run after generating search response 964 * @param {Object} result the object stored in indexedDB 965 * 966 * @private 967 */ 968 ZmSearchController.prototype._handleOfflineResponseDoSearch = 969 function(search, callback, result) { 970 971 if (search.sortBy === ZmSearch.DATE_DESC) { 972 //Sort by received date descending 973 result.sort(function(a, b) { 974 return b.d - a.d; 975 }); 976 } 977 else if (search.sortBy === ZmSearch.DATE_ASC) { 978 //Sort by received date ascending 979 result.sort(function(a, b) { 980 return a.d - b.d; 981 }); 982 } 983 984 var searchResult = new ZmSearchResult(search); 985 if (search.searchFor === ZmId.SEARCH_MAIL || search.parsedSearchFor === ZmId.SEARCH_MAIL) { 986 search.types = new AjxVector([ZmItem.MSG]); 987 searchResult.set({m : result}); 988 } 989 else if (search.searchFor === ZmItem.CONTACT || search.contactSource === ZmItem.CONTACT) { 990 searchResult.set({cn : result}); 991 } 992 var zmCsfeResult = new ZmCsfeResult(searchResult); 993 callback(zmCsfeResult); 994 995 if (search.folderId == ZmFolder.ID_OUTBOX || search.folderId == ZmFolder.ID_DRAFTS) { 996 ZmOffline.updateFolderCountCallback(search.folderId, result.length); 997 } 998 }; 999 1000 ZmSearchController.prototype._addOfflineDrafts = 1001 function(search, result) { 1002 var callback = this._addOfflineDraftsCallback.bind(this, search, result); 1003 var key = {methodName : "SaveDraftRequest"}; 1004 ZmOfflineDB.getItemInRequestQueue(key, callback, callback); 1005 }; 1006 1007 ZmSearchController.prototype._addOfflineDraftsCallback = 1008 function(search, result, newResult) { 1009 var respEl = ZmOffline.generateMsgResponse(newResult); 1010 this._handleResponseDoIndexedDBSearch(search, [].concat(result).concat(respEl)); 1011 }; 1012