1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 ZmApptCache = function(calViewController) { 25 this._calViewController = calViewController; 26 this.clearCache(); 27 }; 28 29 ZmApptCache.prototype.toString = 30 function() { 31 return "ZmApptCache"; 32 }; 33 34 ZmApptCache.prototype.clearCache = 35 function(folderId) { 36 if (!folderId) { 37 this._cachedApptSummaries = {}; 38 this._cachedApptVectors = {}; 39 this._cachedIds = {}; 40 } else { 41 var cacheEntries = this._cachedApptVectors[folderId]; 42 if (cacheEntries) { 43 for (var j in cacheEntries) { 44 var cachedVec = cacheEntries[j]; 45 var len = cachedVec.size(); 46 for (var i = 0; i < len; i++) { 47 var appt = cachedVec.get(i); 48 if (appt.folderId == folderId) { 49 delete this._cachedIds[appt.id]; 50 } 51 } 52 } 53 54 } 55 delete this._cachedApptSummaries[folderId]; 56 delete this._cachedApptVectors[folderId]; 57 58 } 59 60 this._cachedMergedApptVectors = {}; 61 var miniCalCache = this._calViewController.getMiniCalCache(); 62 miniCalCache.clearCache(); 63 }; 64 65 ZmApptCache._sortFolderId = 66 function (a,b) { 67 return a-b; 68 }; 69 70 ZmApptCache.prototype._getCachedMergedKey = 71 function(params) { 72 var sortedFolderIds = []; 73 sortedFolderIds = sortedFolderIds.concat(params.folderIds); 74 sortedFolderIds.sort(); 75 76 // add query to cache key since user searches should not be cached 77 var query = params.query && params.query.length > 0 78 ? (params.query + ":") : ""; 79 80 return (params.start + ":" + params.end + ":" + params.fanoutAllDay + ":" + query + sortedFolderIds.join(":")); 81 }; 82 83 ZmApptCache.prototype._getCachedVector = 84 function(start, end, fanoutAllDay, folderId, query) { 85 var folderCache = this._cachedApptVectors[folderId]; 86 if (folderCache == null) 87 folderCache = this._cachedApptVectors[folderId] = {}; 88 89 var q = query ? (":" + query) : ""; 90 var cacheKey = start + ":" + end + ":" + fanoutAllDay + q; 91 92 var vec = folderCache[cacheKey]; 93 if (vec == null) { 94 // try to find it in the appt summaries results 95 var apptList = this._getCachedApptSummaries(start, end, folderId, query); 96 if (apptList != null) { 97 vec = folderCache[cacheKey] = ZmApptList.toVector(apptList, start, end, fanoutAllDay); 98 } 99 } 100 return vec; 101 }; 102 103 ZmApptCache.prototype._cacheVector = 104 function(vector, start, end, fanoutAllDay, folderId, query) { 105 var folderCache = this._cachedApptVectors[folderId]; 106 if (folderCache == null) 107 folderCache = this._cachedApptVectors[folderId] = {}; 108 109 var q = query ? (":" + query) : ""; 110 var cacheKey = start + ":" + end + ":" + fanoutAllDay + q; 111 folderCache[cacheKey] = vector; 112 }; 113 114 ZmApptCache.prototype._cacheApptSummaries = 115 function(apptList, start, end, folderId, query) { 116 var folderCache = this._cachedApptSummaries[folderId]; 117 if (folderCache == null) 118 folderCache = this._cachedApptSummaries[folderId] = {}; 119 120 var q = query ? (":" + query) : ""; 121 var cacheKey = start + ":" + end + q; 122 folderCache[cacheKey] = {start:start, end:end, list:apptList}; 123 }; 124 125 ZmApptCache.prototype._getCachedApptSummaries = 126 function(start, end, folderId, query) { 127 var found = false; 128 129 var folderCache = this._cachedApptSummaries[folderId]; 130 if (folderCache == null) 131 folderCache = this._cachedApptSummaries[folderId] = {}; 132 133 var q = query ? (":" + query) : ""; 134 var cacheKey = start + ":" + end + q; 135 136 // see if this particular range is cached 137 var entry = this._cachedApptSummaries[cacheKey]; 138 if (entry != null) { return entry.list; } 139 140 // look through all cache results. typically if we are asking for a week/day, 141 // the month range will already be in the cache 142 for (var key in folderCache) { 143 entry = folderCache[key]; 144 if (start >= entry.start && end <= entry.end) { 145 found = true; 146 break; 147 } 148 } 149 if (!found) { return null; } 150 151 // hum. should this ever happen? 152 if (entry.start == start && entry.end == end) { 153 return entry.list; 154 } 155 156 // get subset, and cache it for future use (mainly if someone pages back and forth) 157 var apptList = entry.list.getSubset(start, end); 158 folderCache[cacheKey] = {start:start, end:end, list:apptList}; 159 return apptList; 160 }; 161 162 ZmApptCache.prototype._updateCachedIds = 163 function(apptList) { 164 var list = apptList.getVector(); 165 var size = list.size(); 166 for (var i=0; i < size; i++) { 167 var ao = list.get(i); 168 this._cachedIds[ao.id] = 1; 169 } 170 }; 171 172 /** 173 * Returns a vector of appt summaries for the specified time range across the 174 * specified folders. 175 * @param start [long] start time in MS 176 * @param end [long] end time in MS 177 * @param fanoutAllDay [Boolean]* 178 * @param folderIds [Array]* list of calendar folder Id's (null means use checked calendars in overview) 179 * @param callback [AjxCallback]* callback to call once search results are returned 180 * @param noBusyOverlay [Boolean]* don't show veil during search 181 */ 182 ZmApptCache.prototype.getApptSummaries = 183 function(params) { 184 185 var apptVec = this.setSearchParams(params); 186 187 if (apptVec) { 188 if (params.callback) { 189 params.callback.run(apptVec); 190 } 191 return apptVec; 192 } 193 194 // this array will hold a list of appts as we collect them from the server 195 this._rawAppts = []; 196 197 if (params.callback) { 198 this._search(params); 199 } else { 200 return this._search(params); 201 } 202 }; 203 204 ZmApptCache.prototype.setSearchParams = 205 function(params) { 206 if (params.folderIds && (!(params.folderIds instanceof Array))) { 207 params.folderIds = [params.folderIds]; 208 } 209 else if (!params.folderIds || (params.folderIds && params.folderIds.length == 0)) { 210 return (new AjxVector()); 211 } 212 213 params.mergeKey = this._getCachedMergedKey(params); 214 var list = this._cachedMergedApptVectors[params.mergeKey]; 215 if (list != null) { 216 return list.clone(); 217 } 218 219 params.needToFetch = []; 220 params.resultList = []; 221 222 for (var i = 0; i < params.folderIds.length; i++) { 223 var fid = params.folderIds[i]; 224 225 // bug #46296/#47041 - skip shared folders if account is offline 226 var calFolder = appCtxt.isOffline && appCtxt.getById(fid); 227 if (calFolder && calFolder.isRemote() && calFolder.getAccount().status == ZmZimbraAccount.STATUS_OFFLINE) { 228 continue; 229 } 230 231 // check vector cache first 232 list = this._getCachedVector(params.start, params.end, params.fanoutAllDay, fid); 233 if (list != null) { 234 params.resultList.push(list); 235 } else { 236 params.needToFetch.push(fid); // need to make soap call 237 } 238 } 239 240 // if already cached, return from cache 241 if (params.needToFetch.length == 0) { 242 var newList = ZmApptList.mergeVectors(params.resultList); 243 this._cachedMergedApptVectors[params.mergeKey] = newList.clone(); 244 return newList; 245 } 246 247 this.setFolderSearchParams(params.needToFetch, params); 248 params.offset = 0; 249 250 return null; 251 }; 252 253 ZmApptCache.prototype.setFolderSearchParams = 254 function (foldersToFetch, params) { 255 var folderIdMapper = {}; 256 var query = ""; 257 for (var i = 0; i < foldersToFetch.length; i++) { 258 var fid = foldersToFetch[i]; 259 260 // map remote folder ids into local ones while processing search since 261 // server wont do it for us (see bug 7083) 262 var folder = appCtxt.getById(fid); 263 var rid = folder ? folder.getRemoteId() : fid; 264 folderIdMapper[rid] = fid; 265 266 if (query.length) { 267 query += " OR "; 268 } 269 query += "inid:" + ['"', fid, '"'].join(""); 270 271 } 272 params.queryHint = query; 273 params.folderIdMapper = folderIdMapper; 274 } 275 276 ZmApptCache.prototype._search = 277 function(params) { 278 var jsonObj = {SearchRequest:{_jsns:"urn:zimbraMail"}}; 279 var request = jsonObj.SearchRequest; 280 281 this._setSoapParams(request, params); 282 283 var accountName = params.accountName || (appCtxt.multiAccounts ? appCtxt.accountList.mainAccount.name : null); 284 if (params.callback) { 285 appCtxt.getAppController().sendRequest({ 286 jsonObj: jsonObj, 287 asyncMode: true, 288 callback: (new AjxCallback(this, this._getApptSummariesResponse, [params])), 289 errorCallback: (new AjxCallback(this, this._getApptSummariesError, [params])), 290 offlineCallback: this.offlineSearchAppts.bind(this, null, null, params), 291 noBusyOverlay: params.noBusyOverlay, 292 accountName: accountName 293 }); 294 } 295 else { 296 var response = appCtxt.getAppController().sendRequest({jsonObj: jsonObj, accountName: accountName, ignoreErrs: ["mail.NO_SUCH_MOUNTPOINT"]}); 297 var result = new ZmCsfeResult(response, false); 298 return this._getApptSummariesResponse(params, result); 299 } 300 }; 301 302 ZmApptCache.prototype._initAccountLists = 303 function(){ 304 if(!this._accountsSearchList){ 305 this._accountsSearchList = new AjxVector(); 306 this._accountsMiniCalList = []; 307 } 308 }; 309 310 ZmApptCache.prototype.batchRequest = 311 function(searchParams, miniCalParams, reminderSearchParams) { 312 // *always* recreate the accounts list, otherwise we dispose its contents 313 // before the view has a chance to remove the corresponding elements 314 this._accountsSearchList = new AjxVector(); 315 this._accountsMiniCalList = []; 316 317 this._doBatchRequest(searchParams, miniCalParams, reminderSearchParams); 318 }; 319 320 ZmApptCache.prototype._doBatchRequest = 321 function(searchParams, miniCalParams, reminderSearchParams) { 322 this._cachedVec = null; 323 var caledarIds = searchParams.accountFolderIds.shift(); 324 if (searchParams) { 325 searchParams.folderIds = caledarIds; 326 } 327 if (miniCalParams) { 328 miniCalParams.folderIds = caledarIds; 329 } 330 331 var apptVec; 332 var jsonObj = {BatchRequest:{_jsns:"urn:zimbra", onerror:"continue"}}; 333 var request = jsonObj.BatchRequest; 334 335 if (searchParams) { 336 if (!searchParams.folderIds && !appCtxt.multiAccounts) { 337 searchParams.folderIds = this._calViewController.getCheckedCalendarFolderIds(); 338 } 339 searchParams.query = this._calViewController._userQuery; 340 apptVec = this.setSearchParams(searchParams); 341 DBG.println(AjxDebug.DBG1, "_doBatchRequest searchParams key: " + searchParams.mergeKey + " , size = " + (apptVec ? apptVec.size().toString() : "null")); 342 343 // search data in cache 344 if (apptVec) { 345 this._cachedVec = apptVec; 346 this._accountsSearchList.addList(apptVec); 347 } else { 348 var searchRequest = request.SearchRequest = {_jsns:"urn:zimbraMail"}; 349 this._setSoapParams(searchRequest, searchParams); 350 } 351 } 352 353 if (reminderSearchParams) { 354 if (!reminderSearchParams.folderIds) { 355 reminderSearchParams.folderIds = this._calViewController.getCheckedCalendarFolderIds(true); 356 } 357 358 // reminder search params is only for grouping reminder related srch 359 apptVec = this.setSearchParams(reminderSearchParams); 360 361 if (!apptVec) { 362 var searchRequest ={_jsns:"urn:zimbraMail"}; 363 request.SearchRequest = request.SearchRequest ? [request.SearchRequest, searchRequest] : searchRequest; 364 this._setSoapParams(searchRequest, reminderSearchParams); 365 } 366 else if (reminderSearchParams.callback) { 367 reminderSearchParams.callback.run(apptVec); 368 } 369 } 370 371 if (miniCalParams) { 372 var miniCalCache = this._calViewController.getMiniCalCache(); 373 var cacheData = miniCalCache.getCacheData(miniCalParams); 374 //DBG.println(AjxDebug.DBG1, "_doBatchRequest minical key: " + miniCalCache._getCacheKey(miniCalParams) + " , size = " + (cacheData ? cacheData.length.toString() : "null")); 375 376 // mini cal data in cache 377 if (cacheData && cacheData.length > 0) { 378 miniCalCache.highlightMiniCal(cacheData); 379 if (miniCalParams.callback) { 380 miniCalParams.callback.run(cacheData); 381 } 382 } else { 383 var miniCalRequest = request.GetMiniCalRequest = {_jsns:"urn:zimbraMail"}; 384 miniCalCache._setSoapParams(miniCalRequest, miniCalParams); 385 } 386 } 387 388 // both mini cal and search data is in cache, no need to send request 389 if (searchParams && !request.SearchRequest && !request.GetMiniCalRequest) { 390 391 // process the next account 392 if (searchParams.accountFolderIds.length > 0) { 393 this._doBatchRequest(searchParams, miniCalParams); 394 } 395 else if (searchParams.callback) { 396 searchParams.callback.run(this._accountsSearchList); 397 } 398 DBG.println(AjxDebug.DBG1, "ZmApptCache._doBatchCommand, Search and Minical data cached, EXIT"); 399 return; 400 } 401 402 if ((searchParams && searchParams.callback) || miniCalParams.callback) { 403 //re-init the account search list to avoid the duplication 404 if (searchParams && request.SearchRequest) { 405 this._accountsSearchList = new AjxVector(); 406 } 407 var accountName = (appCtxt.multiAccounts && searchParams.folderIds && (searchParams.folderIds.length > 0)) 408 ? appCtxt.getById(searchParams.folderIds[0]).getAccount().name : null; 409 410 var params = { 411 jsonObj: jsonObj, 412 asyncMode: true, 413 callback: (new AjxCallback(this, this.handleBatchResponse, [searchParams, miniCalParams, reminderSearchParams])), 414 errorCallback: (new AjxCallback(this, this.handleBatchResponseError, [searchParams, miniCalParams, reminderSearchParams])), 415 offlineCallback: this.offlineSearchAppts.bind(this, searchParams, miniCalParams, reminderSearchParams), 416 noBusyOverlay: true, 417 accountName: accountName 418 }; 419 DBG.println(AjxDebug.DBG1, "ZmApptCache._doBatchCommand, Send Async Request"); 420 appCtxt.getAppController().sendRequest(params); 421 } else { 422 DBG.println(AjxDebug.DBG1, "ZmApptCache._doBatchCommand, Send Sync Request"); 423 var response = appCtxt.getAppController().sendRequest({jsonObj:jsonObj}); 424 var batchResp = (response && response.BatchResponse) ? response.BatchResponse : null; 425 return this.processBatchResponse(batchResp, searchParams, miniCalParams); 426 } 427 }; 428 429 ZmApptCache.prototype.processBatchResponse = 430 function(batchResp, searchParams, miniCalParams, reminderSearchParams) { 431 432 //loading the client with app=calendar will directly process the inline batch response 433 if(!this._accountsSearchList) this._initAccountLists(); 434 435 var accountList = this._accountsSearchList.clone(); 436 var miniCalCache = this._calViewController.getMiniCalCache(); 437 var miniCalResp = batchResp && batchResp.GetMiniCalResponse; 438 var searchResp = batchResp && batchResp.SearchResponse; 439 440 if (batchResp && batchResp.Fault) { 441 if (this._processErrorCode(batchResp)) { 442 if (searchParams.accountFolderIds.length > 0) { 443 this._doBatchRequest(searchParams, miniCalParams); 444 } 445 return; 446 } 447 } 448 449 if (miniCalResp) { 450 var data = []; 451 miniCalCache.processBatchResponse(miniCalResp, data); 452 if (!appCtxt.multiAccounts) { 453 miniCalCache.highlightMiniCal(data); 454 miniCalCache.updateCache(miniCalParams, data); 455 456 if (miniCalParams.callback) { 457 miniCalParams.callback.run(data); 458 } 459 } else { 460 this._accountsMiniCalList = this._accountsMiniCalList.concat(data); 461 } 462 } 463 464 if (!searchResp || !searchParams) { 465 if (searchParams) { 466 if (searchParams.accountFolderIds && searchParams.accountFolderIds.length > 0) { 467 this._doBatchRequest(searchParams, miniCalParams); 468 } else if (searchParams.callback) { 469 searchParams.callback.run(accountList); 470 } 471 } 472 473 if (appCtxt.multiAccounts && miniCalParams) { 474 this._highliteMiniCal(miniCalCache, miniCalParams); 475 } 476 return; 477 } 478 479 // currently we send only one search request in batch 480 if (!(searchResp instanceof Array)) { 481 searchResp = [searchResp]; 482 } 483 484 if (searchResp.length > 1) { 485 // process reminder list 486 this.processSearchResponse(searchResp[1], reminderSearchParams); 487 } 488 489 var list = this.processSearchResponse(searchResp[0], searchParams); 490 accountList.addList(list); 491 this._accountsSearchList = accountList.clone(); 492 493 if (searchParams.accountFolderIds && searchParams.accountFolderIds.length > 0) { 494 this._doBatchRequest(searchParams, miniCalParams); 495 } 496 else { 497 if (appCtxt.multiAccounts && miniCalParams) { 498 this._highliteMiniCal(miniCalCache, miniCalParams); 499 } 500 501 if (searchParams.callback) { 502 searchParams.callback.run(accountList, null, searchParams.query); 503 } else { 504 return accountList; 505 } 506 } 507 }; 508 509 ZmApptCache.prototype._highliteMiniCal = 510 function(miniCalCache, miniCalParams) { 511 miniCalCache.highlightMiniCal(this._accountsMiniCalList); 512 miniCalCache.updateCache(miniCalParams, this._accountsMiniCalList); 513 514 if (miniCalParams.callback) { 515 miniCalParams.callback.run(this._accountsMiniCalList); 516 } 517 }; 518 519 ZmApptCache.prototype.handleBatchResponseError = 520 function(searchParams, miniCalParams, reminderSearchParams, response) { 521 var resp = response && response._data && response._data.BatchResponse; 522 this._calViewController.resetSearchFlags(); 523 this._processErrorCode(resp); 524 }; 525 526 ZmApptCache.prototype._processErrorCode = 527 function(resp) { 528 if (resp && resp.Fault && (resp.Fault.length > 0)) { 529 530 if (this._calViewController) { 531 this._calViewController.searchInProgress = false; 532 } 533 534 var errors = []; 535 var ids = {}; 536 var invalidAccountMarker = {}; 537 for (var i = 0; i < resp.Fault.length; i++) { 538 var fault = resp.Fault[i]; 539 var error = (fault && fault.Detail) ? fault.Detail.Error : null; 540 var code = error ? error.Code : null; 541 var attrs = error ? error.a : null; 542 if (code == ZmCsfeException.ACCT_NO_SUCH_ACCOUNT || code == ZmCsfeException.MAIL_NO_SUCH_MOUNTPOINT) { 543 for (var j in attrs) { 544 var attr = attrs[j]; 545 if (attr && (attr.t == "IID") && (attr.n == "itemId")) { 546 var id = attr._content; 547 ids[id] = true; 548 if (code == ZmCsfeException.ACCT_NO_SUCH_ACCOUNT) { 549 invalidAccountMarker[id] = true; 550 } 551 } 552 } 553 554 } else { 555 DBG.println("Unknown error occurred: "+code); 556 errors[fault.requestId] = fault; 557 } 558 } 559 560 var deleteHandled = false; 561 var zidsMap = {}; 562 for (var id in ids) { 563 if (id && appCtxt.getById(id)) { 564 var folder = appCtxt.getById(id); 565 folder.noSuchFolder = true; 566 this.handleDeleteMountpoint(folder); 567 deleteHandled = true; 568 if (invalidAccountMarker[id] && folder.zid) { 569 zidsMap[folder.zid] = true; 570 } 571 } 572 } 573 574 // no such mount point error - mark all folders owned by same account as invalid 575 this.markAllInvalidAccounts(zidsMap); 576 577 if (deleteHandled) { 578 this.runErrorRecovery(); 579 } 580 581 return deleteHandled; 582 } 583 584 return false; 585 }; 586 587 588 //remove this after server sends fault for all removed accounts instead of no such mount point 589 ZmApptCache.prototype.markAllInvalidAccounts = 590 function(zidsMap) { 591 if (this._calViewController) { 592 var folderIds = this._calViewController.getCheckedCalendarFolderIds(); 593 for (var i = 0; i < folderIds.length; i++) { 594 var folder = appCtxt.getById(folderIds[i]); 595 if (folder) { 596 if (folder.zid && zidsMap[folder.zid]) { 597 folder.noSuchFolder = true; 598 this.handleDeleteMountpoint(folder); 599 } 600 } 601 } 602 this._calViewController._updateCheckedCalendars(); 603 } 604 }; 605 606 ZmApptCache.prototype.handleDeleteMountpoint = 607 function(organizer) { 608 // Change its appearance in the tree. 609 var tc = appCtxt.getOverviewController().getTreeController(ZmOrganizer.CALENDAR); 610 var treeView = tc.getTreeView(appCtxt.getCurrentApp().getOverviewId()); 611 var node = treeView && treeView.getTreeItemById(organizer.id); 612 if (organizer && node) { 613 node.setText(organizer.getName(true)); 614 } 615 }; 616 617 ZmApptCache.prototype.runErrorRecovery = 618 function() { 619 if (this._calViewController) { 620 this._calViewController.searchInProgress = false; 621 this._calViewController._updateCheckedCalendars(); 622 if (this._calViewController.onErrorRecovery) { 623 this._calViewController.onErrorRecovery.run(); 624 } 625 } 626 }; 627 628 ZmApptCache.prototype.handleBatchResponse = 629 function(searchParams, miniCalParams, reminderSearchParams, response) { 630 var batchResp = response && response._data && response._data.BatchResponse; 631 return this.processBatchResponse(batchResp, searchParams, miniCalParams, reminderSearchParams); 632 }; 633 634 ZmApptCache.prototype._setSoapParams = 635 function(request, params) { 636 request.sortBy = "none"; 637 request.limit = "500"; 638 // AjxEnv.DEFAULT_LOCALE is set to the browser's locale setting in the case 639 640 // when the user's (or their COS) locale is not set. 641 request.locale = { _content: AjxEnv.DEFAULT_LOCALE }; 642 request.calExpandInstStart = params.start; 643 request.calExpandInstEnd = params.end; 644 request.types = ZmSearch.TYPE[ZmItem.APPT]; 645 request.offset = params.offset; 646 647 var query = params.query; 648 649 if((query && query.indexOf("date:")!=-1)){ 650 var dtArray = query.split(":"); 651 query = null; 652 var curDate = new Date(parseInt(dtArray[1])); 653 curDate.setHours(0,0,0,0); 654 var endDate = new Date(curDate.getTime()); 655 AjxDateUtil.rollToNextDay(endDate); 656 request.calExpandInstStart = curDate.getTime(); 657 request.calExpandInstEnd = endDate.getTime(); 658 } 659 660 661 if (params.queryHint) { 662 query = (query != null) 663 ? (query + " (" + params.queryHint + ")") 664 : params.queryHint; 665 } 666 request.query = {_content:query}; 667 }; 668 669 ZmApptCache.prototype._getApptSummariesResponse = 670 function(params, result) { 671 // TODO: mark both as needing refresh? 672 if (!result) { return; } 673 674 var callback = params.callback; 675 var resp; 676 try { 677 resp = result.getResponse(); 678 } catch (ex) { 679 if (callback) { 680 callback.run(result); 681 } 682 return; 683 } 684 685 var newList = this.processSearchResponse(resp.SearchResponse, params); 686 687 if (callback && newList) { 688 callback.run(newList, params.query, result); 689 } else { 690 return newList; 691 } 692 }; 693 694 ZmApptCache.prototype._getApptSummariesError = 695 function(params, ex) { 696 var code = ex ? ex.code : null; 697 if (params.errorCallback) { 698 //if there is a error callback handler then call it else do default handling 699 params.errorCallback.run(ex); 700 if (code !== ZmCsfeException.ACCT_NO_SUCH_ACCOUNT && code !== ZmCsfeException.MAIL_NO_SUCH_MOUNTPOINT) { 701 //additional processing is needed for these codes so do not return yet. 702 return true; 703 } 704 } else { 705 if (code == ZmCsfeException.MAIL_QUERY_PARSE_ERROR) { 706 var d = appCtxt.getMsgDialog(); 707 d.setMessage(ZmMsg.errorCalendarParse); 708 d.popup(); 709 return true; 710 } 711 712 if (code == ZmCsfeException.MAIL_NO_SUCH_TAG) { 713 var msg = ex.getErrorMsg(); 714 appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING); 715 return true; 716 } 717 } 718 719 var ids = {}; 720 var invalidAccountMarker = {}; 721 722 // check for deleted remote mount point or account 723 var itemIds = (ex.data && ex.data.itemId && ex.data.itemId.length) ? ex.data.itemId : []; 724 if (code == ZmCsfeException.ACCT_NO_SUCH_ACCOUNT || code == ZmCsfeException.MAIL_NO_SUCH_MOUNTPOINT) { 725 for(var j = 0; j < itemIds.length; j++) { 726 var id = itemIds[j]; 727 ids[id] = true; 728 if (code == ZmCsfeException.ACCT_NO_SUCH_ACCOUNT) { 729 invalidAccountMarker[id] = true; 730 } 731 } 732 } 733 734 var deleteHandled = this.handleDeletedFolderIds(ids, invalidAccountMarker); 735 736 if (deleteHandled) { 737 var newFolderIds = []; 738 739 // filter out invalid folder ids 740 for (var i = 0; i < params.folderIds.length; i++) { 741 var folderId = params.folderIds[i]; 742 var isDeleted = (folderId && ids[folderId]); 743 if (!isDeleted) { 744 newFolderIds.push(folderId); 745 } 746 } 747 748 // search again if some of the folders are marked for deletion 749 if (params.folderIds.length != newFolderIds.length) { 750 params.folderIds = newFolderIds; 751 // handle the case where all checked folders are invalid 752 if (params.folderIds.length == 0) { 753 params.callback.run(new AjxVector(), ""); 754 return true; 755 } 756 DBG.println('Appt Summaries Search Failed - Error Recovery Search'); 757 this.getApptSummaries(params); 758 } 759 } 760 761 return deleteHandled; 762 }; 763 764 ZmApptCache.prototype.handleDeletedFolderIds = 765 function(ids, invalidAccountMarker) { 766 var deleteHandled = false; 767 var zidsMap = {}; 768 for (var id in ids) { 769 if (id && appCtxt.getById(id)) { 770 var folder = appCtxt.getById(id); 771 folder.noSuchFolder = true; 772 this.handleDeleteMountpoint(folder); 773 deleteHandled = true; 774 if (invalidAccountMarker[id] && folder.zid) { 775 zidsMap[folder.zid] = true; 776 } 777 } 778 } 779 780 //no such mount point error - mark all folders owned by same account as invalid 781 this.markAllInvalidAccounts(zidsMap); 782 return deleteHandled; 783 }; 784 785 ZmApptCache.prototype.processSearchResponse = 786 function(searchResp, params) { 787 if (!searchResp) { 788 if (this._cachedVec) { 789 var resultList = this._cachedVec.clone(); 790 this._cachedVec = null; 791 return resultList; 792 } 793 return; 794 } 795 796 if (searchResp && searchResp.appt && searchResp.appt.length) { 797 this._rawAppts = this._rawAppts != null 798 ? this._rawAppts.concat(searchResp.appt) 799 : searchResp.appt; 800 801 // if "more" flag set, keep requesting more appts 802 if (searchResp.more) { 803 var lastAppt = searchResp.appt[searchResp.appt.length-1]; 804 if (lastAppt) { 805 params.offset += 500; 806 this._search(params); 807 return; 808 } 809 } 810 } 811 812 if (this._rawAppts && this._rawAppts.length) { 813 var fanoutAllDay = params.fanoutAllDay; 814 var folderIds = params.needToFetch; 815 var start = params.start; 816 var end = params.end; 817 var query = params.query; 818 819 // create a list of appts for each folder returned 820 var folder2List = this.createFolder2ListMap(this._rawAppts, "l", params.folderIdMapper); 821 822 if (folderIds && folderIds.length) { 823 for (var i = 0; i < folderIds.length; i++) { 824 var folderId = folderIds[i]; 825 var apptList = new ZmApptList(); 826 apptList.loadFromSummaryJs(folder2List[folderId]); 827 list = this.createCaches(apptList, params, folderId); 828 params.resultList.push(list); 829 } 830 } 831 } 832 // merge all the data and return 833 var newList = ZmApptList.mergeVectors(params.resultList); 834 this._cachedMergedApptVectors[params.mergeKey] = newList.clone(); 835 836 this._rawAppts = null; 837 return newList; 838 }; 839 840 841 ZmApptCache.prototype.createFolder2ListMap = 842 function(items, folderFieldName, folderIdMapper) { 843 var folder2List = {}; 844 var item; 845 for (var j = 0; j < items.length; j++) { 846 item = items[j]; 847 var fid = folderIdMapper && folderIdMapper[item[folderFieldName]]; 848 if (!folder2List[fid]) { 849 folder2List[fid] = []; 850 } 851 folder2List[fid].push(item); 852 } 853 return folder2List; 854 } 855 856 ZmApptCache.prototype.createCaches = 857 function(apptList, params, folderId) { 858 this._updateCachedIds(apptList); 859 this._cacheApptSummaries(apptList, params.start, params.end, folderId, params.query); 860 861 // convert to sorted vector 862 var list = ZmApptList.toVector(apptList, params.start, params.end, params.fanoutAllDay, params.includeReminders); 863 this._cacheVector(list, params.start, params.end, params.fanoutAllDay, folderId, params.query); 864 865 return list; 866 } 867 868 // return true if the cache contains the specified id(s) 869 // id can be an array or a single id. 870 ZmApptCache.prototype.containsAnyId = 871 function(ids) { 872 if (!ids) { return false; } 873 if (ids instanceof Array) { 874 for (var i=0; i < ids.length; i++) { 875 if (this._cachedIds[ids[i]]) 876 return true; 877 } 878 } else { 879 if (this._cachedIds[ids]) 880 return true; 881 } 882 return false; 883 }; 884 885 // similar to containsAnyId, though deals with an object 886 // (or array of objects) that have the id property 887 ZmApptCache.prototype.containsAnyItem = 888 function(items) { 889 if (!items) { return false; } 890 if (items instanceof Array) { 891 for (var i=0; i < items.length; i++) { 892 if (items[i].id && this._cachedIds[items[i].id]) { 893 return true; 894 } 895 } 896 } else { 897 if (items.id && this._cachedIds[items.id]) { 898 return true; 899 } 900 } 901 return false; 902 }; 903 904 // This will be invoked from ZmApptCache.getApptSummaries (via _search) 905 // and _doBatchCommand, and the ZmMiniCalCache offline callback. 906 // Search and Reminder Params (if both are passed) will use the 907 // same date range. 908 ZmApptCache.prototype.offlineSearchAppts = 909 function(searchParams, miniCalParams, reminderParams) { 910 // MiniCal search called with searchParams set 911 var params = null; 912 if (searchParams) { 913 params = searchParams; 914 } else { 915 params = reminderParams; 916 917 } 918 if (!params || !params.start || !params.end) { 919 if (params && params.errorCallback) { 920 params.errorCallback.run(); 921 } 922 return; 923 } 924 925 var search = [params.start, params.end]; 926 var offlineSearchAppts2 = this._offlineSearchAppts2.bind( 927 this, searchParams, miniCalParams, reminderParams, params.errorCallback, search); 928 // Find the appointments whose startDate falls within the specified range 929 ZmOfflineDB.doIndexSearch( 930 search, ZmApp.CALENDAR, null, offlineSearchAppts2, params.errorCallback, "startDate"); 931 } 932 933 ZmApptCache.prototype._offlineSearchAppts2 = 934 function(searchParams, miniCalParams, reminderParams, errorCallback, search, apptContainers) { 935 var apptContainer; 936 var apptSet = {}; 937 for (var i = 0; i < apptContainers.length; i++) { 938 apptContainer = apptContainers[i]; 939 apptSet[apptContainer.instanceId] = apptContainer.appt; 940 } 941 942 var offlineSearchAppts3 = this._offlineSearchAppts3.bind( 943 this, searchParams, miniCalParams, reminderParams, apptSet); 944 // Find the appointments whose endDate falls within the specified range 945 ZmOfflineDB.doIndexSearch( 946 search, ZmApp.CALENDAR, null, offlineSearchAppts3, errorCallback, "endDate"); 947 } 948 949 ZmApptCache.prototype._offlineSearchAppts3 = 950 function(searchParams, miniCalParams, reminderParams, apptSet, apptContainers) { 951 var apptContainer; 952 var reminderList; 953 var calendarList; 954 955 for (var i = 0; i < apptContainers.length; i++) { 956 apptContainer = apptContainers[i]; 957 // Just drop them in - new entries are added, duplicate entries just written again 958 apptSet[apptContainer.instanceId] = apptContainer.appt; 959 } 960 // For the moment, just create an array 961 var appts = []; 962 var appt; 963 for (var instanceId in apptSet) { 964 appt = apptSet[instanceId]; 965 appts.push(appt); 966 } 967 var cachedVec = this._cachedVec; 968 this._cachedVec = null; 969 970 if (reminderParams) { 971 reminderList = this._cacheOfflineSearch(reminderParams, appts); 972 973 // For getApptSummaries, searchParams == null, so its OK to invoke the reminder 974 // callback, and return with the reminderList. 975 if (!searchParams) { 976 if (reminderParams.callback && reminderList) { 977 // Last param == raw SOAP result. The only usage seems to be from: 978 // ZmSearchController.doSearch -> ZmCalViewController._handleUserSearch ...-> ZmApptCache.getApptSummaries 979 // The callbacks return this to ZmSearchController._handleResponseDoSearch. 980 // In order to support that param, we would need to have the rawAppts also 981 // stored in a separate ObjectStore, and apply the search params to it 982 // *** NOT DONE, But not supporting Calendar search right now *** 983 reminderParams.callback.run(reminderList, reminderParams.query, null); 984 } else { 985 // Seems like the only way to get here is from 986 // ZmFreeBusySchedulerView.popupFreeBusyToolTop -> 987 // ZmCalViewController.getUserStatusToolTipText ...-> ZmApptCache.getApptSummaries, 988 // where getUserStatusToolTipText does not provide a callback (it may be expecting 989 // the appt to be cached). For offline, we are not providing FreeBusy, so should never hit here 990 DBG.println(AjxDebug.DBG1, "ZmApptCache._offlineSearchAppts3 called with no reminderParam.callback"); 991 return reminderList; 992 } 993 } 994 } 995 996 if (searchParams) { 997 if (cachedVec) { 998 // Cache hit in _doBatchResponse for a calendar search. Access and use in-memory cache 999 calendarList = cachedVec.clone(); 1000 } else { 1001 // _doBatchCommand: Search params provided - whether or not there are reminder results, the 1002 // search callback is executed, not the reminder. 1003 calendarList = this._cacheOfflineSearch(searchParams, appts); 1004 } 1005 if (searchParams.callback) { 1006 searchParams.callback.run(calendarList, null, searchParams.query); 1007 } else { 1008 // This should never occur offline 1009 } 1010 } 1011 1012 1013 if (miniCalParams && calendarList) { 1014 // Base the miniCal off of the checked calendar appt data 1015 this.processOfflineMiniCal(miniCalParams, calendarList); 1016 } 1017 1018 } 1019 1020 ZmApptCache.prototype.processOfflineMiniCal = 1021 function(miniCalParams, apptList) { 1022 // Base the minical off of the checked calendar appt data 1023 var dates = {}; 1024 var dateList = []; 1025 var date; 1026 var appt; 1027 for (var i = 0; i < apptList.size(); i++) { 1028 appt = apptList.get(i); 1029 date = this._formatMiniCalEntry(appt.startDate); 1030 dates[date] = true; 1031 } 1032 for (date in dates) { 1033 dateList.push(date); 1034 } 1035 var miniCalCache = this._calViewController.getMiniCalCache(); 1036 miniCalCache.highlightMiniCal(dateList); 1037 miniCalCache.updateCache(miniCalParams, dateList); 1038 //DBG.println(AjxDebug.DBG1, "Cache miniCal key: " + miniCalCache._getCacheKey(miniCalParams) + " , size = " + dateList.length); 1039 if (miniCalParams.callback) { 1040 miniCalParams.callback.run(dateList); 1041 } 1042 } 1043 1044 ZmApptCache.prototype._formatMiniCalEntry = 1045 function(date) { 1046 return date.getFullYear().toString() + AjxDateUtil._getMonth(date, true).toString() + 1047 AjxDateUtil._getDate(date,true).toString(); 1048 } 1049 1050 ZmApptCache.prototype._cacheOfflineSearch = 1051 function(params, appts) { 1052 var resultList = params.resultList || []; 1053 var apptList; 1054 var appt; 1055 var folderList; 1056 var list; 1057 var folderId; 1058 1059 var folder2List = this.createFolder2ListMap(appts, "folderId", params.folderIdMapper); 1060 1061 // The offline db returns all entries within the specified date range. 1062 // Prune the entries by folder id here - Only process those in the params.needToFetch list 1063 var folderIds = params.needToFetch; 1064 if (folderIds && folderIds.length) { 1065 for (var i = 0; i < folderIds.length; i++) { 1066 folderId = folderIds[i]; 1067 folderList = folder2List[folderId]; 1068 if (folderList) { 1069 apptList = new ZmApptList(); 1070 for (var j = 0; j < folderList.length; j++) { 1071 // Assuming appts are a new instance, i.e. changes (like list) are not persisted 1072 // SO, just set this appts list 1073 appt = ZmAppt.loadOfflineData(folderList[j], apptList); 1074 apptList.add(appt); 1075 } 1076 list = this.createCaches(apptList, params, folderId); 1077 resultList.push(list); 1078 } 1079 } 1080 } 1081 //} 1082 // merge all the data and return 1083 var newList = ZmApptList.mergeVectors(resultList); 1084 this._cachedMergedApptVectors[params.mergeKey] = newList.clone(); 1085 //DBG.println(AjxDebug.DBG1, "Cache appts: " + params.mergeKey + " , size = " + newList.size()); 1086 return newList; 1087 } 1088 1089 // Update a field in a ZmAppt. This will also trigger a clearCache call, since 1090 // the cache entries will have an out-of-date field. This is essentially what the online 1091 // mode does - on a notification that modified or deletes an appt, it clears the in-memory cache. 1092 ZmApptCache.prototype.updateOfflineAppt = 1093 function(id, field, value, nullData, callback) { 1094 1095 var search = [id]; 1096 var errorCallback = this.updateErrorCallback.bind(this, field, value); 1097 var updateOfflineAppt2 = this._updateOfflineAppt2.bind(this, field, value, nullData, errorCallback, callback); 1098 // Find the appointments that match the specified id, update appt[field] = value, 1099 // and write it back into the db 1100 ZmOfflineDB.doIndexSearch([id], ZmApp.CALENDAR, null, updateOfflineAppt2, errorCallback, "invId"); 1101 } 1102 1103 ZmApptCache.prototype.updateErrorCallback = 1104 function(field, value, e) { 1105 DBG.println(AjxDebug.DBG1, "Error while updating appt['" + field + "'] = '" + value + "' in indexedDB. Error = " + e); 1106 } 1107 1108 ZmApptCache.prototype._updateOfflineAppt2 = 1109 function(fieldName, value, nullData, errorCallback, callback, apptContainers) { 1110 if (apptContainers.length > 0) { 1111 //this.clearCache(); 1112 var appt; 1113 var fieldNames = fieldName.split("."); 1114 var firstFieldName = fieldNames[0]; 1115 var lastFieldName = fieldNames[fieldNames.length-1]; 1116 var field; 1117 for (var i = 0; i < apptContainers.length; i++) { 1118 appt = apptContainers[i].appt; 1119 field = appt; 1120 if (!appt[firstFieldName]) { 1121 appt[firstFieldName] = nullData; 1122 } else { 1123 for (var j = 0; j < fieldNames.length-1; j++) { 1124 field = field[fieldNames[j]]; 1125 if (!field) break; 1126 } 1127 field[lastFieldName] = value; 1128 } 1129 } 1130 var errorCallback = this.updateErrorCallback.bind(this, field, value); 1131 var updateOfflineAppt3 = this._updateOfflineAppt3.bind(this, field, value, callback); 1132 ZmOfflineDB.setItem(apptContainers, ZmApp.CALENDAR, updateOfflineAppt3, errorCallback); 1133 } 1134 } 1135 1136 ZmApptCache.prototype._updateOfflineAppt3 = 1137 function(field, value, callback) { 1138 // Final step - Do a grand refresh. We've modified the indexedDB entry, but appts 1139 // are used in the various caches, and in the display lists. 1140 this.clearCache(); 1141 this._calViewController.refreshCurrentView(); 1142 1143 if (callback) { 1144 callback.run(field, value); 1145 } 1146 } 1147