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