1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 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) 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file contains the batch command class. 27 */ 28 29 /** 30 * Creates an empty batch command. Use the {@link #add} method to add commands to it, 31 * and {@link #run} to invoke it. 32 * @class 33 * This class represent a batch command, which is a collection of separate 34 * requests. Each command is a callback with a method, arguments, and (usually) an 35 * object on which to call the method. Normally, when the command is run, it creates 36 * a SOAP document or JSON object which it hands to the app controller's <code>sendRequest()</code> 37 * method. It may also pass a response callback and/or an error callback. 38 * <p> 39 * Instead of calling sendRequest(), the command should hand the batch command its SOAP 40 * document or JSON object, response callback, and error callback. The last argument that 41 * the command receives is a reference to the batch command; that's how it knows it's in batch mode. 42 * </p> 43 * <p> 44 * After all commands have been added to the batch command, call its run() method. That will 45 * create a BatchRequest out of the individual commands' requests and send it to the 46 * server. Each subrequest gets an ID. When the BatchResponse comes back, it is broken into 47 * individual responses. If a response indicates success (it is a <code>*Response</code>), the corresponding 48 * response callback is called with the result. If the response is a fault, the corresponding 49 * error callback is called with the exception. 50 * </p> 51 * <p> 52 * A command does not have to be the method that generates a SOAP document or JSON object. 53 * It can be higher-level. Just make sure that the reference to the batch command gets passed down to it. 54 * </p> 55 * @author Conrad Damon 56 * 57 * @param {Boolean} continueOnError if <code>true</code>, the batch request continues processing when a subrequest fails (defaults to <code>true</code>) 58 * @param {String} accountName the account name to run this batch command as. 59 * @param {Boolean} useJson if <code>true</code>, send JSON rather than XML 60 */ 61 ZmBatchCommand = function(continueOnError, accountName, useJson) { 62 63 this._onError = (continueOnError === false) ? ZmBatchCommand.STOP : ZmBatchCommand.CONTINUE; 64 this._accountName = accountName; 65 this._useJson = useJson; 66 this._requestBody = null; 67 68 this.curId = 0; 69 this._cmds = []; 70 this._requests = []; 71 this._respCallbacks = []; 72 this._errorCallbacks = []; 73 }; 74 75 /** 76 * Returns a string representation of the object. 77 * 78 * @return {String} a string representation of the object 79 */ 80 ZmBatchCommand.prototype.toString = 81 function() { 82 return "ZmBatchCommand"; 83 }; 84 85 // 86 // Data 87 // 88 89 ZmBatchCommand.prototype._sensitive = false; 90 ZmBatchCommand.prototype._noAuthToken = false; 91 92 // 93 // Constants 94 // 95 ZmBatchCommand.STOP = "stop"; 96 ZmBatchCommand.CONTINUE = "continue"; 97 98 // 99 // Public methods 100 // 101 102 /** 103 * Sets the sensitive flag. This indicates that this batch command 104 * contains a request with sensitive data. Note: There is no way to unset 105 * this value for the batch command. 106 * 107 * @param {Boolean} sensitive <code>true</code> to set command as sensitive 108 */ 109 ZmBatchCommand.prototype.setSensitive = function(sensitive) { 110 this._sensitive = this._sensitive || sensitive; 111 }; 112 113 /** 114 * Sets the noAuthToken flag. 115 * 116 * @param {Boolean} noAuthToken <code>true</code> to send command with noAuthToken 117 */ 118 ZmBatchCommand.prototype.setNoAuthToken = function(noAuthToken) { 119 this._noAuthToken = noAuthToken; 120 }; 121 122 /** 123 * Checks if the command is sensitive. 124 * 125 * @return {Boolean} <code>true</code> if the command is sensitive 126 */ 127 ZmBatchCommand.prototype.isSensitive = function() { 128 return this._sensitive; 129 }; 130 131 /** 132 * Adds a command to the list of commands to run as part of this batch request. 133 * 134 * @param {AjxCallback} cmd the command 135 */ 136 ZmBatchCommand.prototype.add = 137 function(cmd) { 138 this._cmds.push(cmd); 139 }; 140 141 /** 142 * Gets the number of commands that are part of this batch request. 143 * 144 * @return {int} the size 145 */ 146 ZmBatchCommand.prototype.size = 147 function() { 148 return this.curId || this._cmds.length; 149 }; 150 151 /** 152 * Runs the batch request. For each individual request, either a response or an 153 * error callback will be called. 154 * 155 * @param {AjxCallback} callback the callback to run after entire batch request has completed 156 * @param {AjxCallback} errorCallback the error callback called if anything fails. 157 * The error callbacks arguments are all 158 * of the exceptions that occurred. Note: 159 * only the first exception is passed if 160 * this batch command's onError is set to 161 * stop. 162 */ 163 ZmBatchCommand.prototype.run = 164 function(callback, errorCallback, offlineCallback) { 165 166 // Invoke each command so that it hands us its SOAP doc, response callback, 167 // and error callback 168 for (var i = 0; i < this._cmds.length; i++) { 169 var cmd = this._cmds[i]; 170 cmd.run(this); 171 this.curId++; 172 } 173 174 var params = { 175 sensitive: this._sensitive, 176 noAuthToken: this._noAuthToken, 177 asyncMode: true, 178 callback: new AjxCallback(this, this._handleResponseRun, [callback, errorCallback]), 179 errorCallback: errorCallback, 180 offlineCallback: offlineCallback, 181 accountName: this._accountName 182 }; 183 184 // Create the BatchRequest 185 if (this._useJson) { 186 var jsonObj = {BatchRequest:{_jsns:"urn:zimbra", onerror:this._onError}}; 187 var batchRequest = jsonObj.BatchRequest; 188 var size = this.size(); 189 if (size && this._requests.length) { 190 for (var i = 0; i < size; i++) { 191 var request = this._requests[i]; 192 //Bug fix # 67110 the request object is sometimes undefined 193 if(request) { 194 request.requestId = i; 195 var methodName = ZmCsfeCommand.getMethodName(request); 196 if (!batchRequest[methodName]) { 197 batchRequest[methodName] = []; 198 } 199 request[methodName].requestId = i; 200 batchRequest[methodName].push(request[methodName]); 201 } 202 } 203 params.jsonObj = jsonObj; 204 this._requestBody = jsonObj; 205 } 206 } 207 else { 208 var batchSoapDoc = AjxSoapDoc.create("BatchRequest", "urn:zimbra"); 209 batchSoapDoc.setMethodAttribute("onerror", this._onError); 210 // Add each command's request element to the BatchRequest, and set its ID 211 var size = this.size(); 212 if (size > 0) { 213 for (var i = 0; i < size; i++) { 214 var soapDoc = this._requests[i]; 215 var reqEl = soapDoc.getMethod(); 216 reqEl.setAttribute("requestId", i); 217 var node = batchSoapDoc.adoptNode(reqEl); 218 batchSoapDoc.getMethod().appendChild(node); 219 } 220 params.soapDoc = batchSoapDoc; 221 this._requestBody = batchSoapDoc; 222 } 223 } 224 225 // Issue the BatchRequest *but* only when there's something to request 226 if (params.jsonObj || params.soapDoc) { 227 appCtxt.getAppController().sendRequest(params); 228 } 229 else if (callback) { 230 callback.run(); 231 } 232 }; 233 234 ZmBatchCommand.prototype.getRequestBody = 235 function() { 236 return this._requestBody; 237 } 238 239 /** 240 * @private 241 */ 242 ZmBatchCommand.prototype._handleResponseRun = 243 function(callback, errorCallback, result) { 244 var batchResponse = result.getResponse(); 245 if (!batchResponse.BatchResponse) { 246 DBG.println(AjxDebug.DBG1, "Missing batch response!"); 247 return; 248 } 249 // NOTE: In case the order of the requests is significant, we process 250 // the responses in the same order. 251 var responses = []; 252 for (var method in batchResponse.BatchResponse) { 253 if (method.match(/^_/)) continue; 254 255 var methodResponses = batchResponse.BatchResponse[method]; 256 for (var i = 0; i < methodResponses.length; i++) { 257 responses[methodResponses[i].requestId] = { method: method, resp: methodResponses[i] }; 258 } 259 } 260 var exceptions = []; 261 for (var i = 0; i < responses.length; i++) { 262 var response = responses[i]; 263 try { 264 this._processResponse(response.method, response.resp); 265 } 266 catch (ex) { 267 exceptions.push(ex); 268 if (this._onError == ZmBatchCommand.STOP) { 269 break; 270 } 271 } 272 } 273 if (exceptions.length > 0 && errorCallback) { 274 errorCallback.run.apply(errorCallback, exceptions); 275 } 276 else if (callback) { 277 callback.run(result); 278 } 279 }; 280 281 /** 282 * Adds the given command parameters to the batch command, as part of a command's 283 * invocation. Should be called by a function that was added via {@link #add} earlier; that 284 * function should pass the request object. 285 * 286 * @param {AjxSoapDoc|Object} request a SOAP document or JSON object with the command's request 287 * @param {AjxCallback} respCallback the next callback in chain for async request 288 * @param {AjxCallback} errorCallback the callback to run if there is an exception 289 * 290 * @see #add 291 */ 292 ZmBatchCommand.prototype.addRequestParams = 293 function(request, respCallback, errorCallback) { 294 this._requests[this.curId] = request; 295 this._respCallbacks[this.curId] = respCallback; 296 this._errorCallbacks[this.curId] = errorCallback; 297 }; 298 299 /** 300 * Adds the given command parameters to the batch command, as part of a command's 301 * invocation. Should be called without a previous {@link #add} command, when the request 302 * object can immediately generate its request object. 303 * 304 * @param {AjxSoapDoc|object} request a SOAP document or JSON object with the command's request 305 * @param {AjxCallback} respCallback the next callback in chain for async request 306 * @param {AjxCallback} errorCallback the callback to run if there is an exception 307 * 308 * @see #add 309 */ 310 ZmBatchCommand.prototype.addNewRequestParams = 311 function(request, respCallback, errorCallback) { 312 this.addRequestParams(request, respCallback, errorCallback); 313 this.curId++; 314 }; 315 316 /** 317 * Each type of request will return an array of <code>*Response</code> elements. There may also be 318 * an array of Fault elements. Each element has an ID, so we can match it to its 319 * response or error callback, and run whichever is appropriate. 320 * 321 * @private 322 */ 323 ZmBatchCommand.prototype._processResponse = 324 function(method, resp) { 325 var id = resp.requestId; 326 327 // handle error 328 if (method == "Fault") { 329 var ex = ZmCsfeCommand.faultToEx(resp, "ZmBatchCommand.prototype.run"); 330 if (this._errorCallbacks[id]) { 331 var handled = this._errorCallbacks[id].run(ex); 332 if (!handled) { 333 appCtxt.getAppController()._handleException(ex); 334 } 335 } 336 throw ex; 337 } 338 339 // process response callback 340 if (this._respCallbacks[id]) { 341 var data = {}; 342 data[method] = resp; 343 var result = new ZmCsfeResult(data); 344 this._respCallbacks[id].run(result, resp); 345 } 346 }; 347