1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains the command class.
 27  */
 28 
 29 /**
 30  * Creates a command.
 31  * @class
 32  * This class represents a command.
 33  * 
 34  */
 35 ZmCsfeCommand = function() {
 36 };
 37 
 38 ZmCsfeCommand.prototype.isZmCsfeCommand = true;
 39 ZmCsfeCommand.prototype.toString = function() { return "ZmCsfeCommand"; };
 40 
 41 // Static properties
 42 
 43 // Global settings for each CSFE command
 44 ZmCsfeCommand._COOKIE_NAME = "ZM_AUTH_TOKEN";
 45 ZmCsfeCommand.serverUri = null;
 46 
 47 ZmCsfeCommand._sessionId = null;	// current session ID
 48 ZmCsfeCommand._staleSession = {};	// old sessions
 49 
 50 // Reasons for re-sending a request
 51 ZmCsfeCommand.REAUTH	= "reauth";
 52 ZmCsfeCommand.RETRY		= "retry";
 53 
 54 // Static methods
 55 
 56 /**
 57  * Gets the auth token cookie.
 58  * 
 59  * @return	{String}	the auth token
 60  */
 61 ZmCsfeCommand.getAuthToken =
 62 function() {
 63 	return AjxCookie.getCookie(document, ZmCsfeCommand._COOKIE_NAME);
 64 };
 65 
 66 /**
 67  * Sets the auth token cookie name.
 68  * 
 69  * @param	{String}	cookieName		the cookie name to user
 70  */
 71 ZmCsfeCommand.setCookieName =
 72 function(cookieName) {
 73 	ZmCsfeCommand._COOKIE_NAME = cookieName;
 74 };
 75 
 76 /**
 77  * Sets the server URI.
 78  * 
 79  * @param	{String}	uri		the URI
 80  */
 81 ZmCsfeCommand.setServerUri =
 82 function(uri) {
 83 	ZmCsfeCommand.serverUri = uri;
 84 };
 85 
 86 /**
 87  * Sets the auth token.
 88  * 
 89  * @param	{String}	authToken		the auth token
 90  * @param	{int}		lifetimeMs		the token lifetime in milliseconds
 91  * @param	{String}	sessionId		the session id
 92  * @param	{Boolean}	secure		<code>true</code> for secure
 93  * 
 94  */
 95 ZmCsfeCommand.setAuthToken =
 96 function(authToken, lifetimeMs, sessionId, secure) {
 97 	ZmCsfeCommand._curAuthToken = authToken;
 98 	if (lifetimeMs != null) {
 99 		var exp = null;
100 		if(lifetimeMs > 0) {
101 			exp = new Date();
102 			var lifetime = parseInt(lifetimeMs);
103 			exp.setTime(exp.getTime() + lifetime);
104 		}
105 		AjxCookie.setCookie(document, ZmCsfeCommand._COOKIE_NAME, authToken, exp, "/", null, secure);
106 	} else {
107 		AjxCookie.deleteCookie(document, ZmCsfeCommand._COOKIE_NAME, "/");
108 	}
109 	if (sessionId) {
110 		ZmCsfeCommand.setSessionId(sessionId);
111 	}
112 };
113 
114 /**
115  * Clears the auth token cookie.
116  * 
117  */
118 ZmCsfeCommand.clearAuthToken =
119 function() {
120 	AjxCookie.deleteCookie(document, ZmCsfeCommand._COOKIE_NAME, "/");
121 };
122 
123 /**
124  * Gets the session id.
125  * 
126  * @return	{String}	the session id
127  */
128 ZmCsfeCommand.getSessionId =
129 function() {
130 	return ZmCsfeCommand._sessionId;
131 };
132 
133 /**
134  * Sets the session id and, if the session id is new, designates the previous
135  * session id as stale.
136  * 
137  * @param	{String}	sessionId		the session id
138  * 
139  */
140 ZmCsfeCommand.setSessionId =
141 function(sessionId) {
142     var sid = ZmCsfeCommand.extractSessionId(sessionId);
143     if (sid) {
144         if (sid && !ZmCsfeCommand._staleSession[sid]) {
145             if (sid != ZmCsfeCommand._sessionId) {
146                 if (ZmCsfeCommand._sessionId) {
147                     // Mark the old session as stale...
148                     ZmCsfeCommand._staleSession[ZmCsfeCommand._sessionId] = true;
149                 }
150                 // ...before accepting the new session.
151                 ZmCsfeCommand._sessionId = sid;
152             }
153         }
154     }
155 };
156 
157 ZmCsfeCommand.clearSessionId =
158 function() {
159 	ZmCsfeCommand._sessionId = null;
160 };
161 
162 /**
163  * Isolates the parsing of the various forms of session types that we
164  * might have to handle.
165  *
166  * @param {mixed} session Any valid session object: string, number, object,
167  * or array.
168  * @return {Number|Null} If the input contained a valid session object, the
169  * session number will be returned. If the input is not valid, null will
170  * be returned.
171  */
172 ZmCsfeCommand.extractSessionId =
173 function(session) {
174     var id;
175 
176     if (session instanceof Array) {
177         // Array form
178 	    session = session[0].id;
179     }
180     else if (session && session.id) {
181         // Object form
182         session = session.id;
183     }
184 
185     // We either have extracted the id or were given some primitive form.
186     // Whatever we have at this point, attempt conversion and clean up response.
187     id = parseInt(session, 10);
188     // Normalize response
189     if (isNaN(id)) {
190         id = null;
191     }
192 
193 	return id;
194 };
195 
196 /**
197  * Converts a fault to an exception.
198  * 
199  * @param	{Hash}	fault		the fault
200  * @param	{Hash}	params		a hash of parameters
201  * @return	{ZmCsfeException}	the exception
202  */
203 ZmCsfeCommand.faultToEx =
204 function(fault, params) {
205 	var newParams = {
206 		msg: AjxStringUtil.getAsString(fault.Reason.Text),
207 		code: AjxStringUtil.getAsString(fault.Detail.Error.Code),
208 		method: (params ? params.methodNameStr : null),
209 		detail: AjxStringUtil.getAsString(fault.Code.Value),
210 		data: fault.Detail.Error.a,
211 		trace: (fault.Detail.Error.Trace || "")
212 	};
213 
214 	var request;
215 	if (params) {
216 		if (params.soapDoc) {
217 			// note that we don't pretty-print XML if we get a soapDoc
218 			newParams.request = params.soapDoc.getXml();
219 		} else if (params.jsonRequestObj) {
220 			if (params.jsonRequestObj && params.jsonRequestObj.Header && params.jsonRequestObj.Header.context) {
221 				params.jsonRequestObj.Header.context.authToken = "(removed)";
222 			}
223 			newParams.request = AjxStringUtil.prettyPrint(params.jsonRequestObj, true);
224 		}
225 	}
226 
227 	return new ZmCsfeException(newParams);
228 };
229 
230 /**
231  * Gets the method name of the given request or response.
232  *
233  * @param {AjxSoapDoc|Object}	request	the request
234  * @return	{String}			the method name or "[unknown]"
235  */
236 ZmCsfeCommand.getMethodName =
237 function(request) {
238 
239 	// SOAP request
240 	var methodName = (request && request._methodEl && request._methodEl.tagName)
241 		? request._methodEl.tagName : null;
242 
243 	if (!methodName) {
244 		for (var prop in request) {
245 			if (/Request|Response$/.test(prop)) {
246 				methodName = prop;
247 				break;
248 			}
249 		}
250 	}
251 	return (methodName || "[unknown]");
252 };
253 
254 /**
255  * Sends a SOAP request to the server and processes the response. The request can be in the form
256  * of a SOAP document, or a JSON object.
257  *
258  * @param	{Hash}			params				a hash of parameters:
259  * @param	{AjxSoapDoc}	soapDoc				the SOAP document that represents the request
260  * @param	{Object}		jsonObj				the JSON object that represents the request (alternative to soapDoc)
261  * @param	{Boolean}		noAuthToken			if <code>true</code>, the check for an auth token is skipped
262  * @param	{Boolean}		authToken			authToken to use instead of the local one
263  * @param	{String}		serverUri			the URI to send the request to
264  * @param	{String}		targetServer		the host that services the request
265  * @param	{Boolean}		useXml				if <code>true</code>, an XML response is requested
266  * @param	{Boolean}		noSession			if <code>true</code>, no session info is included
267  * @param	{String}		changeToken			the current change token
268  * @param	{int}			highestNotifySeen 	the sequence # of the highest notification we have processed
269  * @param	{Boolean}		asyncMode			if <code>true</code>, request sent asynchronously
270  * @param	{AjxCallback}	callback			the callback to run when response is received (async mode)
271  * @param	{Boolean}		logRequest			if <code>true</code>, SOAP command name is appended to server URL
272  * @param	{String}		accountId			the ID of account to execute on behalf of
273  * @param	{String}		accountName			the name of account to execute on behalf of
274  * @param	{Boolean}		skipAuthCheck		if <code>true</code> to skip auth check (i.e. do not check if auth token has changed)
275  * @param	{constant}		resend				the reason for resending request
276  * @param	{boolean}		useStringify1		use JSON.stringify1 (gets around IE child win issue with Array)
277  * @param	{boolean}		emptyResponseOkay	if true, empty or no response from server is not an erro
278  */
279 ZmCsfeCommand.prototype.invoke =
280 function(params) {
281 	this.cancelled = false;
282 	if (!(params && (params.soapDoc || params.jsonObj))) { return; }
283 
284 	var requestStr = ZmCsfeCommand.getRequestStr(params);
285 
286 	var rpcCallback;
287 	try {
288 		var uri = (params.serverUri || ZmCsfeCommand.serverUri) + params.methodNameStr;
289 		this._st = new Date();
290 		
291 		var requestHeaders = {"Content-Type": "application/soap+xml; charset=utf-8"};
292 		if (AjxEnv.isIE6 && (location.protocol == "https:")) { //bug 22829
293 			requestHeaders["Connection"] = "Close";
294 		}
295 			
296 		if (params.asyncMode) {
297 			//DBG.println(AjxDebug.DBG1, "set callback for asynchronous response");
298 			rpcCallback = new AjxCallback(this, this._runCallback, [params]);
299 			this._rpcId = AjxRpc.invoke(requestStr, uri, requestHeaders, rpcCallback);
300 		} else {
301 			//DBG.println(AjxDebug.DBG1, "parse response synchronously");
302 			var response = AjxRpc.invoke(requestStr, uri, requestHeaders);
303 			return (!params.returnXml) ? (this._getResponseData(response, params)) : response;
304 		}
305 	} catch (ex) {
306 		this._handleException(ex, params, rpcCallback);
307 	}
308 };
309 
310 /**
311  * Sends a REST request to the server via GET and returns the response.
312  *
313  * @param {Hash}	params			a hash of parameters
314  * @param	{String}       params.restUri			the REST URI to send the request to
315  * @param	{Boolean}       params.asyncMode			if <code>true</code> request sent asynchronously
316  * @param	{AjxCallback}	params.callback			the callback to run when response is received (async mode)
317  */
318 ZmCsfeCommand.prototype.invokeRest =
319 function(params) {
320 
321 	if (!(params && params.restUri)) { return; }
322 
323 	var rpcCallback;
324 	try {
325 		this._st = new Date();
326 		if (params.asyncMode) {
327 			rpcCallback = new AjxCallback(this, this._runCallback, [params]);
328 			this._rpcId = AjxRpc.invoke(null, params.restUri, null, rpcCallback, true);
329 		} else {
330 			var response = AjxRpc.invoke(null, params.restUri, null, null, true);
331 			return response.text;
332 		}
333 	} catch (ex) {
334 		this._handleException(ex, params, rpcCallback);
335 	}
336 };
337 
338 /**
339  * Cancels this request (which must be async).
340  * 
341  */
342 ZmCsfeCommand.prototype.cancel =
343 function() {
344 	DBG.println("req", "CSFE cancel: " + this._rpcId);
345 	if (!this._rpcId) { return; }
346 	this.cancelled = true;
347 	var req = AjxRpc.getRpcRequestById(this._rpcId);
348 	if (req) {
349 		req.cancel();
350 		if (AjxEnv.isFirefox3_5up) {
351 			AjxRpc.removeRpcCtxt(req);
352 		}
353 	}
354 };
355 
356 /**
357  * Gets the request string.
358  * 
359  * @param	{Hash}	params		a hash of parameters
360  * @return	{String}	the request string
361  */
362 ZmCsfeCommand.getRequestStr =
363 function(params) {
364 	return 	params.soapDoc ? ZmCsfeCommand._getSoapRequestStr(params) : ZmCsfeCommand._getJsonRequestStr(params);
365 };
366 
367 /**
368  * @private
369  */
370 ZmCsfeCommand._getJsonRequestStr =
371 function(params) {
372 
373 	var obj = {Header:{}, Body:params.jsonObj};
374 
375 	var context = obj.Header.context = {_jsns:"urn:zimbra"};
376 	var ua_name = ["ZimbraWebClient - ", AjxEnv.browser, " (", AjxEnv.platform, ")"].join("");
377 	context.userAgent = {name:ua_name};
378 	if (ZmCsfeCommand.clientVersion) {
379 		context.userAgent.version = ZmCsfeCommand.clientVersion;
380 	}
381 	if (params.noSession) {
382 		context.nosession = {};
383 	} else {
384 		var sessionId = ZmCsfeCommand.getSessionId();
385 		if (sessionId) {
386 			context.session = {_content:sessionId, id:sessionId};
387 		} else {
388 			context.session = {};
389 		}
390 	}
391 	if (params.targetServer) {
392 		context.targetServer = {_content:params.targetServer};
393 	}
394 	if (params.highestNotifySeen) {
395 		context.notify = {seq:params.highestNotifySeen};
396 	}
397 	if (params.changeToken) {
398 		context.change = {token:params.changeToken, type:"new"};
399 	}
400 
401 	// if we're not checking auth token, we don't want token/acct mismatch	
402 	if (!params.skipAuthCheck) {
403 		if (params.accountId) {
404 			context.account = {_content:params.accountId, by:"id"}
405 		} else if (params.accountName) {
406 			context.account = {_content:params.accountName, by:"name"}
407 		}
408 	}
409 	
410 	// Tell server what kind of response we want
411 	if (params.useXml) {
412 		context.format = {type:"xml"};
413 	}
414 
415 	params.methodNameStr = ZmCsfeCommand.getMethodName(params.jsonObj);
416 
417 	// Get auth token from cookie if required
418 	if (!params.noAuthToken) {
419 		var authToken = params.authToken || ZmCsfeCommand.getAuthToken();
420 		if (!authToken) {
421 			throw new ZmCsfeException(ZMsg.authTokenRequired, ZmCsfeException.NO_AUTH_TOKEN, params.methodNameStr);
422 		}
423 		if (ZmCsfeCommand._curAuthToken && !params.skipAuthCheck && 
424 			(params.resend != ZmCsfeCommand.REAUTH) && (authToken != ZmCsfeCommand._curAuthToken)) {
425 			throw new ZmCsfeException(ZMsg.authTokenChanged, ZmCsfeException.AUTH_TOKEN_CHANGED, params.methodNameStr);
426 		}
427 		context.authToken = ZmCsfeCommand._curAuthToken = authToken;
428 	}
429 	else if (ZmCsfeCommand.noAuth) {
430 		throw new ZmCsfeException(ZMsg.authRequired, ZmCsfeException.NO_AUTH_TOKEN, params.methodNameStr);
431 	}
432 
433 	if (window.csrfToken) {
434 		context.csrfToken = window.csrfToken;
435 	}
436 
437 	AjxDebug.logSoapMessage(params);
438 	DBG.dumpObj(AjxDebug.DBG1, obj);
439 
440 	params.jsonRequestObj = obj;
441 	
442 	var requestStr = (params.useStringify1 ?
443 	                  JSON.stringify1(obj) : JSON.stringify(obj));
444 
445 	// bug 74240: escape non-ASCII characters to prevent the browser from
446 	// combining decomposed characters in paths
447 	return AjxStringUtil.jsEncode(requestStr)
448 };
449 
450 /**
451  * @private
452  */
453 ZmCsfeCommand._getSoapRequestStr =
454 function(params) {
455 
456 	var soapDoc = params.soapDoc;
457 
458 	if (!params.resend) {
459 
460 		// Add the SOAP header and context
461 		var hdr = soapDoc.createHeaderElement();
462 		var context = soapDoc.set("context", null, hdr, "urn:zimbra");
463 	
464 		var ua = soapDoc.set("userAgent", null, context);
465 		var name = ["ZimbraWebClient - ", AjxEnv.browser, " (", AjxEnv.platform, ")"].join("");
466 		ua.setAttribute("name", name);
467 		if (ZmCsfeCommand.clientVersion) {
468 			ua.setAttribute("version", ZmCsfeCommand.clientVersion);
469 		}
470 	
471 		if (params.noSession) {
472 			soapDoc.set("nosession", null, context);
473 		} else {
474 			var sessionId = ZmCsfeCommand.getSessionId();
475 			var si = soapDoc.set("session", null, context);
476 			if (sessionId) {
477 				si.setAttribute("id", sessionId);
478 			}
479 		}
480 		if (params.targetServer) {
481 			soapDoc.set("targetServer", params.targetServer, context);
482 		}
483 		if (params.highestNotifySeen) {
484 		  	var notify = soapDoc.set("notify", null, context);
485 		  	notify.setAttribute("seq", params.highestNotifySeen);
486 		}
487 		if (params.changeToken) {
488 			var ct = soapDoc.set("change", null, context);
489 			ct.setAttribute("token", params.changeToken);
490 			ct.setAttribute("type", "new");
491 		}
492 	
493 		// if we're not checking auth token, we don't want token/acct mismatch	
494 		if (!params.skipAuthCheck) {
495 			if (params.accountId) {
496 				var acc = soapDoc.set("account", params.accountId, context);
497 				acc.setAttribute("by", "id");
498 			} else if (params.accountName) {
499 				var acc = soapDoc.set("account", params.accountName, context);
500 				acc.setAttribute("by", "name");
501 			}
502 		}
503 	
504 		if (params.skipExpiredToken) {
505 			var tokenControl = soapDoc.set("authTokenControl", null, context);
506 			tokenControl.setAttribute("voidOnExpired", "1");
507 		}	
508 		// Tell server what kind of response we want
509 		if (!params.useXml) {
510 			var js = soapDoc.set("format", null, context);
511 			js.setAttribute("type", "js");
512 		}
513 	}
514 
515 	params.methodNameStr = ZmCsfeCommand.getMethodName(soapDoc);
516 
517 	// Get auth token from cookie if required
518 	if (!params.noAuthToken) {
519 		var authToken = params.authToken || ZmCsfeCommand.getAuthToken();
520 		if (!authToken) {
521 			throw new ZmCsfeException(ZMsg.authTokenRequired, ZmCsfeException.NO_AUTH_TOKEN, params.methodNameStr);
522 		}
523 		if (ZmCsfeCommand._curAuthToken && !params.skipAuthCheck && 
524 			(params.resend != ZmCsfeCommand.REAUTH) && (authToken != ZmCsfeCommand._curAuthToken)) {
525 			throw new ZmCsfeException(ZMsg.authTokenChanged, ZmCsfeException.AUTH_TOKEN_CHANGED, params.methodNameStr);
526 		}
527 		ZmCsfeCommand._curAuthToken = authToken;
528 		if (params.resend == ZmCsfeCommand.REAUTH) {
529 			// replace old auth token with current one
530 			var nodes = soapDoc.getDoc().getElementsByTagName("authToken");
531 			if (nodes && nodes.length == 1) {
532 				DBG.println(AjxDebug.DBG1, "Re-auth: replacing auth token");
533 				nodes[0].firstChild.data = authToken;
534 			} else {
535 				// can't find auth token, just add it to context element
536 				nodes = soapDoc.getDoc().getElementsByTagName("context");
537 				if (nodes && nodes.length == 1) {
538 					DBG.println(AjxDebug.DBG1, "Re-auth: re-adding auth token");
539 					soapDoc.set("authToken", authToken, nodes[0]);
540 				} else {
541 					DBG.println(AjxDebug.DBG1, "Re-auth: could not find context!");
542 				}
543 			}
544 		} else if (!params.resend){
545 			soapDoc.set("authToken", authToken, context);
546 		}
547 	}
548 	else if (ZmCsfeCommand.noAuth && !params.ignoreAuthToken) {
549 		throw new ZmCsfeException(ZMsg.authRequired, ZmCsfeException.NO_AUTH_TOKEN, params.methodNameStr);
550 	}
551 
552 	if (window.csrfToken) {
553 		soapDoc.set("csrfToken", window.csrfToken, context);
554 	}
555 
556 	AjxDebug.logSoapMessage(params);
557 	DBG.printXML(AjxDebug.DBG1, soapDoc.getXml());
558 
559 	return soapDoc.getXml();
560 };
561 
562 /**
563  * Runs the callback that was passed to invoke() for an async command.
564  *
565  * @param {AjxCallback}	callback	the callback to run with response data
566  * @param {Hash}	params	a hash of parameters (see method invoke())
567  * 
568  * @private
569  */
570 ZmCsfeCommand.prototype._runCallback =
571 function(params, result) {
572 	if (!result) { return; }
573 	if (this.cancelled && params.skipCallbackIfCancelled) {	return; }
574 
575 	var response;
576 	if (result instanceof ZmCsfeResult) {
577 		response = result; // we already got an exception and packaged it
578 	} else {
579 		response = this._getResponseData(result, params);
580 	}
581 	this._en = new Date();
582 
583 	if (params.callback && response) {
584 		params.callback.run(response);
585 	} else if (!params.emptyResponseOkay) {
586 		DBG.println(AjxDebug.DBG1, "ZmCsfeCommand.prototype._runCallback: Missing callback!");
587 	}
588 };
589 
590 /**
591  * Takes the response to an RPC request and returns a JS object with the response data.
592  *
593  * @param {Object}	response	the RPC response with properties "text" and "xml"
594  * @param {Hash}	params	a hash of parameters (see method invoke())
595  */
596 ZmCsfeCommand.prototype._getResponseData =
597 function(response, params) {
598 	this._en = new Date();
599 	DBG.println(AjxDebug.DBG1, "ROUND TRIP TIME: " + (this._en.getTime() - this._st.getTime()));
600 
601 	var result = new ZmCsfeResult();
602 	var xmlResponse = false;
603 	var restResponse = Boolean(params.restUri);
604 	var respDoc = null;
605 
606 	// check for un-parseable HTML error response from server
607 	if (!response.success && !response.xml && (/<html/i.test(response.text))) {
608 		// bad XML or JS response that had no fault
609 		var ex = new ZmCsfeException(null, ZmCsfeException.CSFE_SVC_ERROR, params.methodNameStr, "HTTP response status " + response.status);
610 		if (params.asyncMode) {
611 			result.set(ex, true);
612 			return result;
613 		} else {
614 			throw ex;
615 		}
616 	}
617 
618 	if (typeof(response.text) == "string" && response.text.indexOf("{") == 0) {
619 		respDoc = response.text;
620 	} else if (!restResponse) {
621 		// an XML response if we requested one, or a fault
622 		try {
623 			xmlResponse = true;
624 			if (!(response.text || (response.xml && (typeof response.xml) == "string"))) {
625 				if (params.emptyResponseOkay) {
626 					return null;
627 				}
628 				else {
629 					// If we can't reach the server, req returns immediately with an empty response rather than waiting and timing out
630 					throw new ZmCsfeException(null, ZmCsfeException.EMPTY_RESPONSE, params.methodNameStr);
631 				}
632 			}
633 			// responseXML is empty under IE
634 			respDoc = (AjxEnv.isIE || response.xml == null) ? AjxSoapDoc.createFromXml(response.text) :
635 															  AjxSoapDoc.createFromDom(response.xml);
636 		} catch (ex) {
637 			DBG.dumpObj(AjxDebug.DBG1, ex);
638 			if (params.asyncMode) {
639 				result.set(ex, true);
640 				return result;
641 			} else {
642 				throw ex;
643 			}
644 		}
645 		if (!respDoc) {
646 			var ex = new ZmCsfeException(null, ZmCsfeException.SOAP_ERROR, params.methodNameStr, "Bad XML response doc");
647 			DBG.dumpObj(AjxDebug.DBG1, ex);
648 			if (params.asyncMode) {
649 				result.set(ex, true);
650 				return result;
651 			} else {
652 				throw ex;
653 			}
654 		}
655 	}
656 
657 	var obj = restResponse ? response.text : {};
658 
659 	if (xmlResponse) {
660 		DBG.printXML(AjxDebug.DBG1, respDoc.getXml());
661 		obj = respDoc._xmlDoc.toJSObject(true, false, true);
662 	} else if (!restResponse) {
663 		try {
664 			obj = JSON.parse(respDoc);
665 		} catch (ex) {
666 			if (ex.name == "SyntaxError") {
667 				ex = new ZmCsfeException(null, ZmCsfeException.BAD_JSON_RESPONSE, params.methodNameStr, respDoc);
668 				AjxDebug.println(AjxDebug.BAD_JSON, "bad json. respDoc=" + respDoc);
669 			}
670 			DBG.dumpObj(AjxDebug.DBG1, ex);
671 			if (params.asyncMode) {
672 				result.set(ex, true);
673 				return result;
674 			} else {
675 				throw ex;
676 			}
677 		}
678 
679 	}
680 
681 	params.methodNameStr = ZmCsfeCommand.getMethodName(obj.Body);
682 	AjxDebug.logSoapMessage(params);
683 	DBG.dumpObj(AjxDebug.DBG1, obj, -1);
684 
685 	var fault = obj && obj.Body && obj.Body.Fault;
686 	if (fault) {
687 		// JS response with fault
688 		if (AjxUtil.isString(fault) && fault.indexOf("<")==0) { // We got an xml string
689 			fault = AjxXmlDoc.createFromXml(fault).toJSObject(true, false, true);
690 		}
691 		var ex = ZmCsfeCommand.faultToEx(fault, params);
692 		if (params.asyncMode) {
693 			result.set(ex, true, obj.Header);
694 			return result;
695 		} else {
696 			throw ex;
697 		}
698 	} else if (!response.success) {
699 		// bad XML or JS response that had no fault
700 		var ex = new ZmCsfeException(null, ZmCsfeException.CSFE_SVC_ERROR, params.methodNameStr, "HTTP response status " + response.status);
701 		if (params.asyncMode) {
702 			result.set(ex, true);
703 			return result;
704 		} else {
705 			throw ex;
706 		}
707 	} else {
708 		// good response
709 		if (params.asyncMode) {
710 			result.set(obj);
711 		}
712 	}
713 
714 	// check for new session ID
715 	var session = obj.Header && obj.Header.context && obj.Header.context.session;
716     ZmCsfeCommand.setSessionId(session);
717 
718 	return params.asyncMode ? result : obj;
719 };
720 
721 /**
722  * @private
723  */
724 ZmCsfeCommand.prototype._handleException =
725 function(ex, params, callback) {
726 	if (!(ex && (ex instanceof ZmCsfeException || ex instanceof AjxSoapException || ex instanceof AjxException))) {
727 		var newEx = new ZmCsfeException();
728 		newEx.method = params.methodNameStr || params.restUri;
729 		newEx.detail = ex ? ex.toString() : "undefined exception";
730 		newEx.code = ZmCsfeException.UNKNOWN_ERROR;
731 		newEx.msg = "Unknown Error";
732 		ex = newEx;
733 	}
734 	if (params.asyncMode) {
735 		callback.run(new ZmCsfeResult(ex, true));
736 	} else {
737 		throw ex;
738 	}
739 };
740