Index: remoting/webapp/crd/js/xhr.js |
diff --git a/remoting/webapp/crd/js/xhr.js b/remoting/webapp/crd/js/xhr.js |
index cdefb0aa4d81d45d2dee8763a2d4f19c3404c651..78d441eadd27d3435faad33f9e71e50230465b55 100644 |
--- a/remoting/webapp/crd/js/xhr.js |
+++ b/remoting/webapp/crd/js/xhr.js |
@@ -17,13 +17,10 @@ var remoting = remoting || {}; |
* @param {remoting.Xhr.Params} params |
*/ |
remoting.Xhr = function(params) { |
- /** @private @const {!XMLHttpRequest} */ |
- this.nativeXhr_ = new XMLHttpRequest(); |
- this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this); |
- this.nativeXhr_.withCredentials = params.withCredentials || false; |
+ remoting.Xhr.checkParams_(params); |
/** @private @const */ |
- this.responseType_ = params.responseType || remoting.Xhr.ResponseType.TEXT; |
+ this.ignoreErrors_ = params.ignoreErrors || null; |
// Apply URL parameters. |
var url = params.url; |
@@ -35,92 +32,87 @@ remoting.Xhr = function(params) { |
remoting.Xhr.removeNullFields_(params.urlParams)); |
} |
if (parameterString) { |
- base.debug.assert(url.indexOf('?') == -1); |
url += '?' + parameterString; |
} |
- // Check that the content spec is consistent. |
- if ((Number(params.textContent !== undefined) + |
- Number(params.formContent !== undefined) + |
- Number(params.jsonContent !== undefined)) > 1) { |
- throw new Error( |
- 'may only specify one of textContent, formContent, and jsonContent'); |
- } |
- |
// Prepare the build modified headers. |
- var headers = remoting.Xhr.removeNullFields_(params.headers); |
+ /** @const */ |
+ this.headers_ = remoting.Xhr.removeNullFields_(params.headers); |
// Convert the content fields to a single text content variable. |
/** @private {?string} */ |
this.content_ = null; |
if (params.textContent !== undefined) { |
+ this.maybeSetContentType_('text/plain'); |
this.content_ = params.textContent; |
} else if (params.formContent !== undefined) { |
- if (!('Content-type' in headers)) { |
- headers['Content-type'] = 'application/x-www-form-urlencoded'; |
- } |
+ this.maybeSetContentType_('application/x-www-form-urlencoded'); |
this.content_ = remoting.Xhr.urlencodeParamHash(params.formContent); |
} else if (params.jsonContent !== undefined) { |
- if (!('Content-type' in headers)) { |
- headers['Content-type'] = 'application/json; charset=UTF-8'; |
- } |
+ this.maybeSetContentType_('application/json'); |
this.content_ = JSON.stringify(params.jsonContent); |
} |
// Apply the oauthToken field. |
if (params.oauthToken !== undefined) { |
- base.debug.assert(!('Authorization' in headers)); |
- headers['Authorization'] = 'Bearer ' + params.oauthToken; |
+ this.setAuthToken_(params.oauthToken); |
} |
- this.nativeXhr_.open(params.method, url, true); |
- for (var key in headers) { |
- this.nativeXhr_.setRequestHeader(key, headers[key]); |
+ /** @private @const {boolean} */ |
+ this.acceptJson_ = params.acceptJson || false; |
+ if (this.acceptJson_) { |
+ this.maybeSetHeader_('Accept', 'application/json'); |
} |
+ // Apply useIdentity field. |
+ /** @const {boolean} */ |
+ this.useIdentity_ = params.useIdentity || false; |
+ |
+ /** @private @const {!XMLHttpRequest} */ |
+ this.nativeXhr_ = new XMLHttpRequest(); |
+ this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this); |
+ this.nativeXhr_.withCredentials = params.withCredentials || false; |
+ this.nativeXhr_.open(params.method, url, true); |
+ |
/** @private {base.Deferred<!remoting.Xhr.Response>} */ |
this.deferred_ = null; |
}; |
/** |
- * @enum {string} |
- */ |
-remoting.Xhr.ResponseType = { |
- TEXT: 'TEXT', // Request a plain text response (default). |
- JSON: 'JSON', // Request a JSON response. |
- NONE: 'NONE' // Don't request any response. |
-}; |
- |
-/** |
- * Parameters for the 'start' function. |
+ * Parameters for the 'start' function. Unless otherwise noted, all |
+ * parameters are optional. |
* |
- * method: The HTTP method to use. |
+ * method: (required) The HTTP method to use. |
* |
- * url: The URL to request. |
+ * url: (required) The URL to request. |
* |
- * urlParams: (optional) Parameters to be appended to the URL. |
- * Null-valued parameters are omitted. |
+ * urlParams: Parameters to be appended to the URL. Null-valued |
+ * parameters are omitted. |
* |
- * textContent: (optional) Text to be sent as the request body. |
+ * textContent: Text to be sent as the request body. |
* |
- * formContent: (optional) Data to be URL-encoded and sent as the |
- * request body. Causes Content-type header to be set |
- * appropriately. |
+ * formContent: Data to be URL-encoded and sent as the request body. |
+ * Causes Content-type header to be set appropriately. |
* |
- * jsonContent: (optional) Data to be JSON-encoded and sent as the |
- * request body. Causes Content-type header to be set |
- * appropriately. |
+ * jsonContent: Data to be JSON-encoded and sent as the request body. |
+ * Causes Content-type header to be set appropriately. |
* |
- * headers: (optional) Additional request headers to be sent. |
- * Null-valued headers are omitted. |
+ * headers: Additional request headers to be sent. Null-valued |
+ * headers are omitted. |
* |
- * withCredentials: (optional) Value of the XHR's withCredentials field. |
+ * withCredentials: Value of the XHR's withCredentials field. |
* |
- * oauthToken: (optional) An OAuth2 token used to construct an |
- * Authentication header. |
+ * oauthToken: An OAuth2 token used to construct an Authentication |
+ * header. |
* |
- * responseType: (optional) Request a response of a specific |
- * type. Default: TEXT. |
+ * useIdentity: Use identity API to get an OAuth2 token. |
+ * |
+ * ignoreErrors: List of error types (arising from HTTP result codes) |
+ * to ignore. If null (the default) no HTTP status code is |
+ * treated as an error. |
+ * |
+ * acceptJson: If true, send an Accept header indicating that a JSON |
+ * response is expected. |
* |
* @typedef {{ |
* method: string, |
@@ -132,7 +124,9 @@ remoting.Xhr.ResponseType = { |
* headers:(Object<string,?string>|undefined), |
* withCredentials:(boolean|undefined), |
* oauthToken:(string|undefined), |
- * responseType:(remoting.Xhr.ResponseType|undefined) |
+ * useIdentity:(boolean|undefined), |
+ * ignoreErrors:(Array<remoting.Error.Tag>|undefined), |
+ * acceptJson:(boolean|undefined) |
* }} |
*/ |
remoting.Xhr.Params; |
@@ -160,22 +154,121 @@ remoting.Xhr.prototype.abort = function() { |
*/ |
remoting.Xhr.prototype.start = function() { |
if (this.deferred_ == null) { |
- var xhr = this.nativeXhr_; |
- xhr.send(this.content_); |
- this.content_ = null; // for gc |
this.deferred_ = new base.Deferred(); |
+ |
+ // Send the XHR, possibly after getting an OAuth token. |
+ var self = this; |
+ if (this.useIdentity_) { |
+ remoting.identity.getToken().then(function(token) { |
+ self.setAuthToken_(token); |
+ self.sendXhr_(); |
+ }, this.deferred_.reject.bind(this.deferred_)); |
+ } else { |
+ this.sendXhr_(); |
+ } |
} |
return this.deferred_.promise(); |
}; |
/** |
+ * @param {remoting.Xhr.Params} params |
+ * @throws {Error} if params are invalid |
+ */ |
+remoting.Xhr.checkParams_ = function(params) { |
+ if (params.urlParams) { |
+ if (params.url.indexOf('?') != -1) { |
+ throw new Error('URL may not contain "?" when urlParams is set'); |
+ } |
+ if (params.url.indexOf('#') != -1) { |
+ throw new Error('URL may not contain "#" when urlParams is set'); |
+ } |
+ } |
+ |
+ if ((Number(params.textContent !== undefined) + |
+ Number(params.formContent !== undefined) + |
+ Number(params.jsonContent !== undefined)) > 1) { |
+ throw new Error( |
+ 'may only specify one of textContent, formContent, and jsonContent'); |
+ } |
+ |
+ if (params.useIdentity && params.oauthToken !== undefined) { |
+ throw new Error('may not specify both useIdentity and oauthToken'); |
+ } |
+ |
+ if ((params.useIdentity || params.oauthToken !== undefined) && |
+ params.headers && |
+ params.headers['Authorization'] != null) { |
+ throw new Error( |
+ 'may not specify useIdentity or oauthToken ' + |
+ 'with an Authorization header'); |
+ } |
+}; |
+ |
+/** |
+ * @param {string} token |
+ * @private |
+ */ |
+remoting.Xhr.prototype.setAuthToken_ = function(token) { |
+ this.setHeader_('Authorization', 'Bearer ' + token); |
+}; |
+ |
+/** |
+ * @param {string} type |
+ * @private |
+ */ |
+remoting.Xhr.prototype.maybeSetContentType_ = function(type) { |
+ this.maybeSetHeader_('Content-type', type + '; charset=UTF-8'); |
+}; |
+ |
+/** |
+ * @param {string} key |
+ * @param {string} value |
+ * @private |
+ */ |
+remoting.Xhr.prototype.setHeader_ = function(key, value) { |
+ var wasSet = this.maybeSetHeader_(key, value); |
+ base.debug.assert(wasSet); |
+}; |
+ |
+/** |
+ * @param {string} key |
+ * @param {string} value |
+ * @return {boolean} |
+ * @private |
+ */ |
+remoting.Xhr.prototype.maybeSetHeader_ = function(key, value) { |
+ if (!(key in this.headers_)) { |
+ this.headers_[key] = value; |
+ return true; |
+ } |
+ return false; |
+}; |
+ |
+/** @private */ |
+remoting.Xhr.prototype.sendXhr_ = function() { |
+ for (var key in this.headers_) { |
+ this.nativeXhr_.setRequestHeader(key, this.headers_[key]); |
+ } |
+ this.nativeXhr_.send(this.content_); |
+ this.content_ = null; // for gc |
+}; |
+ |
+/** |
* @private |
*/ |
remoting.Xhr.prototype.onReadyStateChange_ = function() { |
var xhr = this.nativeXhr_; |
if (xhr.readyState == 4) { |
- // See comments at remoting.Xhr.Response. |
- this.deferred_.resolve(new remoting.Xhr.Response(xhr, this.responseType_)); |
+ var error = remoting.Error.fromHttpStatus(xhr.status); |
+ if (error.isNone() || |
+ this.ignoreErrors_ == null || |
+ error.hasTag.apply(error, this.ignoreErrors_)) { |
+ // See comments at remoting.Xhr.Response. |
+ this.deferred_.resolve(new remoting.Xhr.Response( |
+ xhr, this.acceptJson_)); |
+ } else { |
+ this.deferred_.reject(error); |
+ } |
} |
}; |
@@ -189,11 +282,11 @@ remoting.Xhr.prototype.onReadyStateChange_ = function() { |
* |
* @constructor |
* @param {!XMLHttpRequest} xhr |
- * @param {remoting.Xhr.ResponseType} type |
+ * @param {boolean} allowJson |
*/ |
-remoting.Xhr.Response = function(xhr, type) { |
+remoting.Xhr.Response = function(xhr, allowJson) { |
/** @private @const */ |
- this.type_ = type; |
+ this.allowJson_ = allowJson; |
/** |
* The HTTP status code. |
@@ -225,10 +318,12 @@ remoting.Xhr.Response.prototype.getText = function() { |
}; |
/** |
+ * Get the JSON content of the response. Requires acceptJson to have |
+ * been true in the request. |
* @return {*} The parsed JSON content of the response. |
*/ |
remoting.Xhr.Response.prototype.getJson = function() { |
- base.debug.assert(this.type_ == remoting.Xhr.ResponseType.JSON); |
+ base.debug.assert(this.allowJson_); |
return JSON.parse(this.text_); |
}; |
@@ -271,33 +366,3 @@ remoting.Xhr.urlencodeParamHash = function(paramHash) { |
} |
return ''; |
}; |
- |
-/** |
- * Generic success/failure response proxy. |
- * |
- * TODO(jrw): Stop using this and move default error handling directly |
- * into Xhr class. |
- * |
- * @param {function():void} onDone |
- * @param {function(!remoting.Error):void} onError |
- * @param {Array<remoting.Error.Tag>=} opt_ignoreErrors |
- * @return {function(!remoting.Xhr.Response):void} |
- */ |
-remoting.Xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) { |
- /** @param {!remoting.Xhr.Response} response */ |
- var result = function(response) { |
- var error = remoting.Error.fromHttpStatus(response.status); |
- if (error.isNone()) { |
- onDone(); |
- return; |
- } |
- |
- if (opt_ignoreErrors && error.hasTag.apply(error, opt_ignoreErrors)) { |
- onDone(); |
- return; |
- } |
- |
- onError(error); |
- }; |
- return result; |
-}; |