1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 contains the request manager class.
 27  */
 28 
 29 /**
 30  * Creates a request manager.
 31  * @class
 32  * This class manages the sending of requests to the server, and handles the
 33  * responses, including refresh blocks and notifications.
 34  *
 35  * @author Conrad Damon
 36  * 
 37  * @param {ZmController}	controller	the main controller
 38  */
 39 ZmRequestMgr = function(controller) {
 40 
 41 	this._controller = controller;
 42 	
 43 	appCtxt.setRequestMgr(this);
 44 
 45 	ZmCsfeCommand.setServerUri(appCtxt.get(ZmSetting.CSFE_SERVER_URI));
 46 	var cv = appCtxt.get(ZmSetting.CLIENT_VERSION);
 47 	ZmCsfeCommand.clientVersion = (!cv || cv.indexOf('@') == 0) ? "dev build" : cv;
 48 	
 49 	this._shell = appCtxt.getShell();
 50 
 51     this._highestNotifySeen = 0;
 52 
 53 	this._cancelActionId = {};
 54 	this._pendingRequests = {};
 55 
 56 	this._useXml = appCtxt.get(ZmSetting.USE_XML);
 57 	this._logRequest = appCtxt.get(ZmSetting.LOG_REQUEST);
 58 	this._stdTimeout = appCtxt.get(ZmSetting.TIMEOUT);
 59 
 60 	this._unreadListener = new AjxListener(this, this._unreadChangeListener);
 61 };
 62 
 63 ZmRequestMgr.prototype.isZmRequestMgr = true;
 64 ZmRequestMgr.prototype.toString = function() { return "ZmRequestMgr"; };
 65 
 66 // request states
 67 ZmRequestMgr._SENT		= 1;
 68 ZmRequestMgr._RESPONSE	= 2;
 69 ZmRequestMgr._CANCEL	= 3;
 70 
 71 // retry settings
 72 ZmRequestMgr.RETRY_MAX			= 2;	// number of times to retry before throwing exception
 73 ZmRequestMgr.RETRY_DELAY		= 5;	// seconds to delay between retries
 74 ZmRequestMgr.RETRY_ON_EXCEPTION = {};	// which exceptions to retry on
 75 ZmRequestMgr.RETRY_ON_EXCEPTION[ZmCsfeException.EMPTY_RESPONSE] = true;
 76 
 77 ZmRequestMgr._nextReqId = 1;
 78 
 79 ZmRequestMgr.OFFLINE_HEAP_DUMP          = "heapdump_upload";
 80 ZmRequestMgr.OFFLINE_MUST_RESYNC        = "resync";
 81 ZmRequestMgr.OFFLINE_MUST_GAL_RESYNC    = "gal_resync";
 82 ZmRequestMgr.OFFLINE_FOLDER_MOVE_FAILED = "foldermove_failed";
 83 
 84 // ms to delay after a response to make sure focus is in sync
 85 ZmRequestMgr.FOCUS_CHECK_DELAY = 500;
 86 
 87 /**
 88  * Sends a request to the CSFE and processes the response. Notifications and
 89  * refresh blocks that come in the response header are handled. Also handles
 90  * exceptions by default, though the caller can pass in a special callback to
 91  * run for exceptions. The error callback should return true if it has
 92  * handled the exception, and false if standard exception handling should still
 93  * be performed.
 94  *
 95  * @param {Hash}		params				a hash of parameters
 96  * @param {AjxSoapDoc}	soapDoc				the SOAP document that represents the request
 97  * @param {Object}		jsonObj				the JSON object that represents the request (alternative to soapDoc)
 98  * @param {Boolean}		asyncMode			if <code>true</code>, request will be made asynchronously
 99  * @param {AjxCallback}	callback			the next callback in chain for async request
100  * @param {AjxCallback}	errorCallback		the callback to run if there is an exception
101  * @param {AjxCallback}	continueCallback	the callback to run after user re-auths
102  * @param {AjxCallback}	offlineCallback	    the callback to run if the user is offline
103  * @param {int}			timeout				the timeout value (in seconds)
104  * @param {Boolean}		noBusyOverlay		if <code>true</code>, don't use the busy overlay
105  * @param {String}		accountName			the name of account to execute on behalf of
106  * @param {Object}		response			the pre-determined response (no request will be made)
107  * @param {Boolean}		skipAuthCheck		if <code>true</code>, do not check if auth token has changed
108  * @param {constant}	resend				the reason for resending request
109  * @param {Boolean}		sensitive			if <code>true</code>, attempt to use secure conn to protect data
110  * @param {Boolean}		noSession			if <code>true</code>, no session info is included
111  * @param {String}		restUri				the REST URI to send the request to
112  * @param {boolean}		emptyResponseOkay	if true, empty or no response from server is not an erro
113  * @param {boolean}		offlineRequest	    if true, request will not be send to server
114  * @param {boolean}		useChangeToken	    if true, request will try to use change token in header
115  */
116 ZmRequestMgr.prototype.sendRequest =
117 function(params) {
118 	var response = params.response;
119 	if (response) {
120 		if (params.reqId) {
121 			params = this._pendingRequests[params.reqId] || params;
122 			params.response = response;
123 		}
124 		params.asyncMode = true;	// canned response set up async style
125 		return this._handleResponseSendRequest(params, new ZmCsfeResult(response));
126 	}
127 	if (params.offlineRequest || appCtxt.isWebClientOffline()) {
128 		if (params.offlineCallback) {
129 			params.offlineCallback.run(params);
130 		}
131 		return;
132 	}
133 	
134 	var reqId = params.reqId = ("Req_"+ZmRequestMgr._nextReqId++);
135 	DBG.println("req", "assign req ID: " + reqId);
136 	var timeout = params.timeout = (params.timeout != null) ? params.timeout : this._stdTimeout;
137 	if (timeout) {
138 		timeout = timeout * 1000; // convert seconds to ms
139 	}
140 	var asyncCallback = params.asyncMode ? new AjxCallback(this, this._handleResponseSendRequest, [params]) : null;
141 
142 	if (params.sensitive) {
143 		DBG.println(AjxDebug.DBG2, "request contains sensitive data");
144 		// NOTE: If only http mode is available, there's nothing we can
145 		//       do. And if we're already using https mode, then there's
146 		//       nothing we need to do. We only attempt to send the
147 		//       request securely if mixed mode is enabled and the app
148 		//       was loaded using http.
149 		var isHttp = document.location.protocol == ZmSetting.PROTO_HTTP;
150 		var isMixedMode = appCtxt.get(ZmSetting.PROTOCOL_MODE) == ZmSetting.PROTO_MIXED;
151 		if(isHttp && isMixedMode) {
152 			return this._sensitiveRequest(params, reqId);
153 		}
154 	}
155 
156 	var command = new ZmCsfeCommand();
157 	// bug fix #10652, 82704 - dont set change token if accountName is not main account or is not specified
158 	// (since we're executing on someone else's mbox)
159 	var accountName = params.accountName;
160 	if (!accountName) {
161 		var acct = appCtxt.getActiveAccount();
162 		accountName = (acct && acct.id != ZmAccountList.DEFAULT_ID) ? acct.name : null;
163 	}
164 	var changeToken = null;
165 	if (params.useChangeToken && (!accountName || (accountName === appCtxt.accountList.mainAccount.name))) {
166 		changeToken = this._changeToken;
167 	}
168 	var cmdParams, methodName;
169 
170 	if (params.restUri) {
171 		cmdParams =	{	restUri:			params.restUri,
172 						asyncMode:			params.asyncMode,
173 						callback:			asyncCallback
174 					};
175 	} else {
176 		cmdParams = {	jsonObj:			params.jsonObj,
177 						soapDoc:			params.soapDoc,
178 						accountName:		accountName,
179 						useXml:				this._useXml,
180 						changeToken:		changeToken,
181 						asyncMode:			params.asyncMode,
182 						callback:			asyncCallback,
183 						logRequest:			this._logRequest,
184 						highestNotifySeen:	this._highestNotifySeen,
185 						noAuthToken:		true,	// browser will handle auth token cookie
186 						skipAuthCheck:		params.skipAuthCheck,
187 						resend:				params.resend,
188 						noSession:			params.noSession,
189 						useStringify1:		(AjxEnv.isIE || AjxEnv.isModernIE) && params.fromChildWindow,
190 						emptyResponseOkay:	params.emptyResponseOkay
191 					};
192 		methodName = params.methodName = ZmCsfeCommand.getMethodName(cmdParams.jsonObj || cmdParams.soapDoc);
193 	}
194 
195 	appCtxt.currentRequestParams = params;
196 	DBG.println("req", "send request " + reqId + ": " + methodName);
197 	var cancelParams = timeout ? [reqId, params.errorCallback, params.noBusyOverlay] : null;
198 	if (!params.noBusyOverlay) {
199 		var cancelCallback = null;
200 		var showBusyDialog = false;
201 		if (timeout) {
202 			DBG.println("req", "ZmRequestMgr.sendRequest: timeout for " + reqId + " is " + timeout);
203 			cancelCallback = new AjxCallback(this, this.cancelRequest, cancelParams);
204 			this._shell.setBusyDialogText(ZmMsg.askCancel);
205 			showBusyDialog = true;
206 		}
207 		// put up busy overlay to block user input
208 		this._shell.setBusy(true, reqId, showBusyDialog, timeout, cancelCallback);
209 	} else if (timeout) {
210 		var action = new AjxTimedAction(this, this.cancelRequest, cancelParams);
211 		this._cancelActionId[reqId] = AjxTimedAction.scheduleAction(action, timeout);
212 		DBG.println("req", "schedule cancel action for reqId " + reqId + ": " + this._cancelActionId[reqId]);
213 	}
214 
215 	this._pendingRequests[reqId] = command;
216 
217 	try {
218 		DBG.println("req", "invoke req: " + params.reqId);
219 		var response = params.restUri ? command.invokeRest(cmdParams) : command.invoke(cmdParams);
220 		command.state = ZmRequestMgr._SENT;
221 	} catch (ex) {
222 		DBG.println("req", "caught exception on invoke of req: " + params.reqId);
223 		this._handleResponseSendRequest(params, new ZmCsfeResult(ex, true));
224 		return;
225 	}
226 
227 	return (params.asyncMode) ? reqId : (this._handleResponseSendRequest(params, response));
228 };
229 
230 /**
231  * @private
232  * @param {Array}	params.ignoreErrs	list of error codes that can be ignored, when params.errorCallback does not exists.
233  */
234 ZmRequestMgr.prototype._handleResponseSendRequest =
235 function(params, result) {
236 	DBG.println("req", "ZmRequestMgr.handleResponseSendRequest for req: " + params.reqId);
237 	var isCannedResponse = (params.response != null);
238 	if (!isCannedResponse && !appCtxt.isWebClientOffline()) {
239 		if (!this._pendingRequests[params.reqId]) {
240 			DBG.println("req", "ZmRequestMgr.handleResponseSendRequest no pending request for " + params.reqId);
241 			return;
242 		}
243 		if (this._pendingRequests[params.reqId].state == ZmRequestMgr._CANCEL) {
244 			DBG.println("req", "ZmRequestMgr.handleResponseSendRequest state=CANCEL for " + params.reqId);
245 			return;
246 		}
247 	
248 		this._pendingRequests[params.reqId].state = ZmRequestMgr._RESPONSE;
249 	
250 		if (!params.noBusyOverlay) {
251 			this._shell.setBusy(false, params.reqId); // remove busy overlay
252 		}
253 	}
254 
255 	var response, refreshBlock;
256 	try {
257 		if (params.asyncMode && !params.restUri) {
258 			response = result.getResponse(); // may throw exception
259 		} else {
260 			// for sync responses, manually throw exception if necessary
261 			if (result._isException) {
262 				throw result._data;
263 			} else {
264 				response = result;
265 			}
266 		}
267 		if (response.Header) {
268 			refreshBlock = this._handleHeader(response.Header);
269 		}
270 	} catch (ex) {
271 		DBG.println("req", "Request " + params.reqId + " got an exception");
272 		var ecb = params.errorCallback;
273 		if (ecb) {
274             var handled = ecb.run(ex, params);
275 			if (!handled) {
276 				this._handleException(ex, params);
277 			}
278 		} else {
279 			var ignore = function(ignoreErrs, errCode){
280 			/*
281 				Checks errCode exits in ignoreErrs
282 			*/
283 				if (ignoreErrs && (ignoreErrs.length > 0)){
284 					for (var val in ignoreErrs)
285 						if (ignoreErrs[val] == errCode) 
286 							return true;
287 				}
288 				return false;
289 			}(params.ignoreErrs, ex.code)
290 
291             if (ex.code === ZmCsfeException.EMPTY_RESPONSE && params.offlineCallback) {
292                 params.offlineCallback(params);
293 				if (appCtxt.isWebClientOffline() && !params.noBusyOverlay) {
294 					this._shell.setBusy(false, params.reqId); // remove busy overlay
295 				}
296                 ignore = true;
297             }
298 			if (!ignore)
299 				this._handleException(ex, params);
300 		}
301 		var hdr = result.getHeader();
302 		if (hdr) {
303 			this._handleHeader(hdr);
304 			this._handleNotifications(hdr);
305 		}
306 		this._clearPendingRequest(params.reqId);
307 		return;
308 	}
309 
310     if (params.asyncMode && !params.restUri) {
311 	    result.set(response.Body);
312 	}
313 
314     // if we didn't get an exception, then we should make sure that the
315     // poll timer is running (just in case it got an exception and stopped)
316 	if (!appCtxt.isOffline && !isCannedResponse) {
317 		this._controller._kickPolling(true);
318 	}
319 
320 	var methodName = ZmCsfeCommand.getMethodName(params.jsonObj || params.soapDoc);
321 	if (params.asyncMode && params.callback) {
322 		DBG.println(AjxDebug.DBG1, "------------------------- Running response callback for " + methodName);
323 		params.callback.run(result);
324 	}
325 
326 	DBG.println(AjxDebug.DBG1, "------------------------- Processing notifications for " + methodName);
327 	this._handleNotifications(response.Header, methodName);
328 
329 	this._clearPendingRequest(params.reqId);
330 
331 	if (refreshBlock && (!appCtxt.isOffline || !appCtxt.multiAccounts) && !params.more) {
332 		this._refreshHandler(refreshBlock);
333 	}
334 	
335 	if (!params.asyncMode) {
336 		return response.Body;
337 	}
338 	
339 	var ctlr = this._controller;
340 	if (ctlr._evtMgr && ctlr._evtMgr.isListenerRegistered(ZmAppEvent.RESPONSE)) {
341 		ctlr._evt.request = methodName;
342 		ctlr.notify(ZmAppEvent.RESPONSE);
343 	}
344 };
345 
346 /**
347  * Cancels the request.
348  * 
349  * @param	{String}	reqId		the request id
350  * @param	{AjxCallback}	errorCallback		the callback
351  * @param	{Boolean}	noBusyOverlay	if <code>true</code>, do not show busy overlay
352  */
353 ZmRequestMgr.prototype.cancelRequest =
354 function(reqId, errorCallback, noBusyOverlay) {
355 	DBG.println("req", "ZmRequestMgr.cancelRequest: " + reqId);
356 	if (!this._pendingRequests[reqId]) { return; }
357 	if (this._pendingRequests[reqId].state == ZmRequestMgr._RESPONSE) { return; }
358 
359 	this._pendingRequests[reqId].state = ZmRequestMgr._CANCEL;
360 	if (!noBusyOverlay) {
361 		this._shell.setBusy(false, reqId);
362 	}
363 	DBG.println("req", "canceling the XHR");
364 	this._pendingRequests[reqId].cancel();
365 	if (errorCallback) {
366 		DBG.println("req", "calling the error callback");
367 		var ex = new AjxException("Request canceled", AjxException.CANCELED, "ZmRequestMgr.prototype.cancelRequest");
368 		errorCallback.isAjxCallback ? errorCallback.run(ex) : errorCallback(ex);
369 	}
370 	this._clearPendingRequest(reqId);
371 };
372 
373 /**
374  * @private
375  */
376 ZmRequestMgr.prototype._clearPendingRequest =
377 function(reqId) {
378 	var request = this._pendingRequests[reqId];
379 	if (request) {
380 		if (request.iframeId) {
381 			var iframe = document.getElementById(request.iframeId);
382 			if (iframe) {
383 				iframe.parentNode.removeChild(iframe);
384 			}
385 		}
386 		delete this._pendingRequests[reqId];
387 	}
388 	var cancelId = this._cancelActionId[reqId];
389 	if (cancelId && cancelId != -1) {
390 		DBG.println("req", "unschedule cancel action for reqId " + reqId + ": " + cancelId);
391 		AjxTimedAction.cancelAction(cancelId);
392 		this._cancelActionId[reqId] = -1;
393 	}
394 };
395 
396 /**
397  * Handles a response's SOAP header, except for notifications. Updates our
398  * change token, and processes a <code><refresh></code> block if there is one (happens
399  * when a new session is created on the server).
400  *
401  * @param {Object}	hdr	a SOAP header
402  * 
403  * @private
404  */
405 ZmRequestMgr.prototype._handleHeader =
406 function(hdr) {
407 
408 	var ctxt = hdr && hdr.context;
409 	if (!ctxt) { return; }
410 
411 	// update change token if we got one
412 	if (ctxt.change) {
413 		this._changeToken = ctxt.change.token;
414 	}
415 
416 	// offline/zdesktop only
417 	if (ctxt.zdsync && ctxt.zdsync.account) {
418 		var acctList = ctxt.zdsync.account;
419 		for (var i = 0; i < acctList.length; i++) {
420             var acct = appCtxt.accountList.getAccount(acctList[i].id);
421 			if (acct) {
422                 //server is sending info to get user's consent on something.
423                 var dialog = acctList[i].dialog;
424                 if(dialog) {
425                     this._handleOfflineInfoDialog(dialog[0], acct)
426                 }
427 				acct.updateState(acctList[i]);
428 			}
429 		}
430 	}
431 
432 	if (ctxt.refresh) {
433 		this._controller.runAppFunction("_clearDeferredFolders");
434 		this._loadTrees(ctxt.refresh);
435 		this._controller.runAppFunction("_createVirtualFolders");
436 		this._highestNotifySeen = 0;
437 	}
438 
439 	return ctxt.refresh;
440 };
441 /**
442  * Handles server's notification to get user's consent on something
443  *
444  * @param {Object}	dlg is json object
445  * @param {Object}	account object
446  *
447  * @private
448  */
449 ZmRequestMgr.prototype._handleOfflineInfoDialog =
450 function(dlg, acct) {
451 
452     if(!dlg.type) {
453         return;
454     }
455     var cont;
456     switch(dlg.type) {
457         case ZmRequestMgr.OFFLINE_HEAP_DUMP: {
458             cont = ZmMsg.offlineHeapDump;
459             break;
460         }
461         case ZmRequestMgr.OFFLINE_MUST_RESYNC: {
462             cont = AjxMessageFormat.format(ZmMsg.offlineMustReSync, acct.name);
463             break;
464         }
465 		case ZmRequestMgr.OFFLINE_MUST_GAL_RESYNC: {
466 			cont = AjxMessageFormat.format(ZmMsg.offlineMustGalReSync, acct.name);
467 			break;
468 		}
469         case ZmRequestMgr.OFFLINE_FOLDER_MOVE_FAILED: {
470             appCtxt.setStatusMsg(ZmMsg.offlineMoveFolderError);
471             break;
472         }
473         default:
474     }
475     if (!cont) {
476         return;
477     }
478     var dialog = appCtxt.getOkCancelMsgDialog();
479     dialog.setMessage(cont);
480     dialog.registerCallback(DwtDialog.OK_BUTTON, this._handleOfflineDialogAction, this, [dialog, dlg.type, acct.id, true]);
481     dialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._handleOfflineDialogAction, this, [dialog, dlg.type, acct.id, false]);
482     dialog.popup();
483 };
484 /**
485  * Sends DialogActionRequest with user's consent YES/NO
486  * @param {object} dlg is getOkCancelMsgDialog
487  * @param {string} type
488  * @param {string} acctId Account ID
489  * @param {boolean} action
490  */
491 ZmRequestMgr.prototype._handleOfflineDialogAction =
492 function(dlg, type, acctId, action) {
493     var args = {
494 			jsonObj: { DialogActionRequest: { _jsns: "urn:zimbraOffline", type: type, id:acctId, action: action ? "yes" : "no" } },
495             callback: new AjxCallback(this, this._handleOfflineDialogActionResp, dlg),
496 			errorCallback: new AjxCallback(this, this._handleOfflineDialogActionResp, dlg),
497 			asyncMode: true
498 		};
499     this.sendRequest(args);
500 };
501 /**
502  * callback to hide dialog
503  *
504  * @param dlg
505  * @param resp
506  */
507 ZmRequestMgr.prototype._handleOfflineDialogActionResp =
508 function(dlg, resp) {
509       if(dlg.isPoppedUp()){
510         dlg.popdown();
511     }
512 };
513 
514 /**
515  * For transient network exceptions, retry the request after a small delay.
516  * We will only retry a limited number of times.
517  * 
518  * @param {AjxException}	ex			the exception
519  * @param {Hash}	params		a hash of the original request params
520  */
521 ZmRequestMgr.prototype._handleException =
522 function(ex, params) {
523 	var handled = false;
524 	if (ZmRequestMgr.RETRY_ON_EXCEPTION[ex.code]) {
525 		params.retryCount = params.retryCount || 0;
526 		if (params.retryCount < ZmRequestMgr.RETRY_MAX) {
527 			DBG.println(AjxDebug.DBG1, "RETRY " + ex.method + " due to " + ex.code);
528 			params.resend = ZmCsfeCommand.RETRY;
529 			params.retryCount++;
530 			AjxTimedAction.scheduleAction(new AjxTimedAction(this, 
531 				function() {
532 					this.sendRequest(params);
533 				}), ZmRequestMgr.RETRY_DELAY * 1000);
534 			handled = true;
535 		}
536 	}
537 	
538 	if (!handled) {
539 		this._controller._handleException(ex, params);
540 	}
541 };
542 
543 /**
544  * Handles the <code><notify></code> block of a response's SOAP header.
545  *
546  * @param {Object}	hdr	a SOAP header
547  * 
548  * @private
549  */
550 ZmRequestMgr.prototype._handleNotifications =
551 function(hdr, methodName) {
552 
553 	if (hdr && hdr.context && hdr.context.notify) {
554         for (var i = 0; i < hdr.context.notify.length; i++) {
555         	var notify = hdr.context.notify[i];
556         	var seq = notify.seq;
557             // BUG?  What if the array isn't in sequence-order?  Could we miss notifications?
558 			var sid = hdr.context && ZmCsfeCommand.extractSessionId(hdr.context.session);
559             if (notify.seq > this._highestNotifySeen && !(sid && ZmCsfeCommand._staleSession[sid])) {
560                 DBG.println(AjxDebug.DBG1, "Handling notification[" + i + "] seq=" + seq);
561                 this._highestNotifySeen = seq;
562                 this._notifyHandler(notify, methodName);
563             } else {
564             	DBG.println(AjxDebug.DBG1, "SKIPPING notification[" + i + "] seq=" + seq + " highestNotifySeen=" + this._highestNotifySeen);
565 	      	}
566     	}
567 	}
568 };
569 
570 /**
571  * A <code><refresh></code> block is returned in a SOAP response any time the session ID has 
572  * changed. It always happens on the first SOAP command (GetInfoRequest).
573  * After that, it happens after a session timeout. 
574  * 
575  * @param {Object}	refresh	the refresh block (JSON)
576  * @private
577  */
578 ZmRequestMgr.prototype._refreshHandler =
579 function(refresh) {
580 
581 	DBG.println(AjxDebug.DBG1, "Handling REFRESH");
582 	AjxDebug.println(AjxDebug.NOTIFY, "REFRESH block received");
583 	if (!appCtxt.inStartup) {
584 		this._controller._execPoll();
585 	}
586 
587 	if (refresh.version) {
588 		if (!this._canceledReload) {
589 			var curVersion = appCtxt.get(ZmSetting.SERVER_VERSION);
590 			if (curVersion != refresh.version) {
591 				appCtxt.set(ZmSetting.SERVER_VERSION, refresh.version);
592 				if (curVersion) {
593 					var dlg = appCtxt.getYesNoMsgDialog();
594 					dlg.reset();
595 					dlg.registerCallback(DwtDialog.YES_BUTTON, this._reloadYesCallback, this, [dlg, curVersion, refresh.version]);
596 					dlg.registerCallback(DwtDialog.NO_BUTTON, this._reloadNoCallback, this, [dlg, refresh]);
597 					var msg = AjxMessageFormat.format(ZmMsg.versionChangeRestart, [curVersion, refresh.version]);
598 					dlg.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
599 					dlg.popup();
600 					appCtxt.reloadAppCache(true);
601 					return;
602 				}
603 			}
604 		}
605 	}
606 
607 	if (!this._recentlyRefreshed) {
608 		// Run any app-requested refresh routines
609 		this._controller.runAppFunction("refresh", false, refresh);
610 		this._recentlyRefreshed = true;
611 		this._lastSkippedRefresh = null;
612 	} else {
613 		this._lastSkippedRefresh = refresh;
614 	}
615 
616 	if (!this._refreshTimer)
617 		this._refreshTimer = new AjxTimedAction(this, this._refreshTimeout);
618 
619 	AjxTimedAction.scheduleAction(this._refreshTimer, 5000);
620 };
621 
622 ZmRequestMgr.prototype._refreshTimeout =
623 function() {
624 	if (this._lastSkippedRefresh) {
625 		this._controller.runAppFunction("refresh", false, this._lastSkippedRefresh);
626 		this._lastSkippedRefresh = null;
627 	}
628 	this._recentlyRefreshed = false;
629 };
630 
631 ZmRequestMgr.prototype._loadTrees =
632 function(refresh) {
633 	var unread = {};
634 	var main = appCtxt.multiAccounts ? appCtxt.accountList.mainAccount : null;
635 	this._loadTree(ZmOrganizer.TAG, unread, refresh.tags, null, main);
636 	this._loadTree(ZmOrganizer.FOLDER, unread, refresh.folder[0], "folder", main);
637 };
638 
639 /**
640  * User has accepted reload due to change in server version.
641  * 
642  * @private
643  */
644 ZmRequestMgr.prototype._reloadYesCallback =
645 function(dialog) {
646 	dialog.popdown();
647     window.onbeforeunload = null;
648     var url = AjxUtil.formatUrl();
649 	DBG.println(AjxDebug.DBG1, "SERVER_VERSION changed!");
650     ZmZimbraMail.sendRedirect(url); // redirect to self to force reload
651 };
652 
653 /**
654  * User has canceled reload due to change in server version.
655  * 
656  * @private
657  */
658 ZmRequestMgr.prototype._reloadNoCallback =
659 function(dialog, refresh) {
660 	dialog.popdown();
661 	this._canceledReload = true;
662 	this._refreshHandler(refresh);
663 };
664 
665 /**
666  * @private
667  */
668 ZmRequestMgr.prototype._loadTree =
669 function(type, unread, obj, objType, account) {
670 	var isTag = (type == ZmOrganizer.TAG);
671 	var tree = appCtxt.getTree(type, account);
672 	if (tree) {
673 		tree.reset();
674 	} else {
675 		tree = isTag ? new ZmTagTree(account) : new ZmFolderTree();
676 	}
677 	appCtxt.setTree(type, tree, account);
678 	tree.addChangeListener(this._unreadListener);
679 	tree.getUnreadHash(unread);
680 	tree.loadFromJs(obj, objType, account);
681 };
682 
683 /**
684  * To handle notifications, we keep track of all the models in use. A model could
685  * be an item, a list of items, or an organizer tree. Currently we never get an
686  * organizer by itself.
687  * 
688  * @private
689  */
690 ZmRequestMgr.prototype._notifyHandler =
691 function(notify, methodName) {
692 	DBG.println(AjxDebug.DBG1, "Handling NOTIFY");
693 	AjxDebug.println(AjxDebug.NOTIFY, "Notification block:");
694 	AjxDebug.dumpObj(AjxDebug.NOTIFY, notify);
695 	this._controller.runAppFunction("preNotify", false, notify);
696 	if (notify.deleted && notify.deleted.id) {
697 		this._handleDeletes(notify.deleted);
698 	}
699 	if (notify.created) {
700 		this._handleCreates(notify.created);
701 	}
702 	if (notify.modified) {
703 		this._handleModifies(notify.modified);
704 	}
705 
706 	if (ZmOffline.isOnlineMode() && (notify.deleted || notify.created || notify.modified)) {
707 		appCtxt.webClientOfflineHandler.scheduleSyncRequest(notify, methodName);
708 	}
709 	this._controller.runAppFunction("postNotify", false, notify);
710 };
711 
712 /**
713  * A delete notification hands us a list of IDs which could be anything. First, we
714  * run any app delete handlers. Any IDs which have been handled by an app will
715  * be nulled out. The generic handling here will be applied to the rest - the item is
716  * retrieved from the item cache and told it has been deleted.
717  *
718  * @param {Object}	deletes	the node containing all 'deleted' notifications
719  * 
720  * @private
721  */
722 ZmRequestMgr.prototype._handleDeletes =
723 function(deletes) {
724 	var ids = deletes.id.split(",");
725 	this._controller.runAppFunction("deleteNotify", false, ids);
726 
727 	for (var i = 0; i < ids.length; i++) {
728 		var id = ids[i];
729 		if (!id) { continue; }
730 		var item = appCtxt.cacheGet(id);
731 		DBG.println(AjxDebug.DBG2, "ZmRequestMgr: handling delete notif for ID " + id);
732 		if (item && item.notifyDelete) {
733 			item.notifyDelete();
734 			appCtxt.cacheRemove(id);
735 			item = null;
736 		}
737 	}
738 };
739 
740 /**
741  * Create notifications hand us full XML nodes. First, we run any app
742  * create handlers, which will mark any create nodes that they handle. Remaining
743  * creates are handled here.
744  * 
745  * @param {Object}	creates	the node containing all 'created' notifications
746  * 
747  * @private
748  */
749 ZmRequestMgr.prototype._handleCreates =
750 function(creates) {
751 	this._controller.runAppFunction("createNotify", false, creates);
752 
753 	for (var name in creates) {
754         if (creates.hasOwnProperty(name)) {
755             var list = creates[name];
756             for (var i = 0; i < list.length; i++) {
757                 var create = list[i];
758                 if (create._handled) { continue; }
759                 // ignore create notif for item we already have (except tags, which can reuse IDs)
760                 if (appCtxt.cacheGet(create.id) && name != "tag") { continue; }
761 
762                 DBG.println(AjxDebug.DBG1, "ZmRequestMgr: handling CREATE for node: " + name);
763                 if (name == "tag") {
764                     var account = appCtxt.multiAccounts && ZmOrganizer.parseId(create.id).account;
765                     var tagTree = appCtxt.getTagTree(account);
766                     if (tagTree) {
767                         tagTree.root.notifyCreate(create);
768                     }
769                 } else if (name == "folder" || name == "search" || name == "link") {
770                     var parentId = create.l;
771                     var parent = appCtxt.getById(parentId);
772                     if (parent && parent.notifyCreate && parent.type != ZmOrganizer.TAG) { // bug #37148
773                         parent.notifyCreate(create, name);
774                     }
775                 }
776             }
777         }
778 	}
779 };
780 
781 /**
782  * First, we run any app modify handlers, which will mark any nodes that
783  * they handle. Remaining modify notifications are handled here.
784  * 
785  * @param {Object}	modifies	the node containing all 'modified' notifications
786  * 
787  * @private
788  */
789 ZmRequestMgr.prototype._handleModifies =
790 function(modifies) {
791 
792 	this._controller.runAppFunction("modifyNotify", false, modifies);
793 
794 	for (var name in modifies) {
795 		if (name == "mbx") {
796 			var mboxes = modifies[name];
797 			for (var i = 0; i < mboxes.length; i++) {
798 				var mbox = mboxes[i];
799 				var acctId = mbox.acct;
800 				var account = acctId && appCtxt.accountList.getAccount(acctId);
801 				var setting = appCtxt.getSettings(account).getSetting(ZmSetting.QUOTA_USED);
802 				setting.notifyModify({_name:name, s:mbox.s, account:account});
803 			}
804 			continue;
805 		}
806 
807 		var list = modifies[name];
808 		for (var i = 0; i < list.length; i++) {
809 			var mod = list[i];
810 			if (mod._handled) { continue; }
811 			DBG.println(AjxDebug.DBG2, "ZmRequestMgr: handling modified notif for ID " + mod.id + ", node type = " + name);
812 			var item = appCtxt.cacheGet(mod.id);
813 
814 			// bug fix #31991 - for contact modifies, check the contact list
815 			// Since we lazily create ZmContact items, it wont be in the global cache.
816 			// TODO: move to contacts app
817 			if (!item && name == "cn" && AjxDispatcher.loaded("ContactsCore")) {
818 				var capp = appCtxt.getApp(ZmApp.CONTACTS);
819 				if (capp.isContactListLoaded()) {
820 					item = capp.getContactList().getById(mod.id);
821 				}
822 			}
823 
824 			if (item && item.notifyModify) {
825 				mod._isRemote = (name == "folder" && item.link);	// remote subfolder
826 				item.notifyModify(mod);
827 			}
828 		}
829 	}
830 };
831 
832 /**
833  * Changes browser title if it's a folder or tag whose unread count has changed.
834  *
835  * @param ev	the event
836  * 
837  * @private
838  */
839 ZmRequestMgr.prototype._unreadChangeListener =
840 function(ev) {
841 	if (ev.event == ZmEvent.E_MODIFY) {
842 		var fields = ev.getDetail("fields");
843 		if (fields && fields[ZmOrganizer.F_UNREAD]) {
844 			var organizers = ev.getDetail("organizers");
845 			var organizer = organizers ? organizers[0] : null;
846 			var id = organizer ? (organizer.isSystem() ? organizer.nId : organizer.id) : null;
847 			var search = appCtxt.getCurrentSearch();
848 			if (search) {
849 				var searchFolder = appCtxt.multiAccounts && appCtxt.getById(search.folderId);
850 				var searchFolderId = (searchFolder && searchFolder.getAccount() == appCtxt.getActiveAccount())
851 					? searchFolder.nId : search.folderId;
852 
853 				if (id && (id == searchFolderId || id == search.tagId)) {
854 					Dwt.setTitle(search.getTitle());
855 				}
856 			}
857 			var mailApp = appCtxt.getApp(ZmApp.MAIL);
858 			if (mailApp) {
859 				mailApp.setNewMailNotice(organizer);
860 			}
861 		}
862 	}
863 };
864 
865 ZmRequestMgr.prototype._sensitiveRequest =
866 function(params, reqId) {
867 	DBG.println(AjxDebug.DBG2, "sending request securely");
868 	// adjust command parameters
869 	// TODO: Because of timing issues, should we not use session info?
870 	// TODO: But for batch commands, some updates would not be seen immediately.
871 	// TODO: To avoid security warning, send response in URL; so limit length
872 	params.noSession = true;
873 	params.noAuthToken = true;
874 
875 	// information
876 	var requestStr = ZmCsfeCommand.getRequestStr(params);
877 	var loc = document.location;
878 	var port = appCtxt.get(ZmSetting.HTTPS_PORT);
879 	if (port && port != ZmSetting.DEFAULT_HTTPS_PORT) {
880 		port = ":"+port;
881 	}
882 
883 	// create iframe
884 	var iframe = document.createElement("IFRAME");
885 	iframe.style.display = "none";
886 	iframe.id = Dwt.getNextId();
887 	document.body.appendChild(iframe);
888 
889 	// set contents
890 	var iframeDoc = Dwt.getIframeDoc(iframe);
891 	iframeDoc.write(
892 		"<form ",
893 			"id=",iframe.id,"-form ",
894 			"target=",iframe.id,"-iframe ",
895 			"method=POST ",
896 			"action='https://",loc.hostname,port,appContextPath,"/public/secureRequest.jsp'",
897 		">",
898 			"<input type=hidden name=reqId value='",reqId,"'>",
899 			"<textarea name=data>",
900 				AjxStringUtil.htmlEncode(requestStr),
901 			"</textarea>",
902 		"</form>",
903 		"<iframe name=",iframe.id,"-iframe></iframe>"
904 	);
905 	iframeDoc.close();
906 
907 	// save the params for the response
908 	params.iframeId = iframe.id;
909 	this._pendingRequests[reqId] = params;
910 
911 	// submit form
912 	var form = iframeDoc.getElementById(iframe.id+"-form");
913 	form.submit();
914 };
915