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