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