| Index: remoting/webapp/crd/js/xhr.js
|
| diff --git a/remoting/webapp/crd/js/xhr.js b/remoting/webapp/crd/js/xhr.js
|
| index 8bf20878cdbe2535300b02035f604c49529af26b..561631fba9bf58e4818fc117ed92e9037b58c8be 100644
|
| --- a/remoting/webapp/crd/js/xhr.js
|
| +++ b/remoting/webapp/crd/js/xhr.js
|
| @@ -12,26 +12,83 @@
|
| /** @suppress {duplicate} */
|
| var remoting = remoting || {};
|
|
|
| -/** Namespace for XHR functions */
|
| -/** @type {Object} */
|
| -remoting.xhr = remoting.xhr || {};
|
| -
|
| /**
|
| - * Takes an associative array of parameters and urlencodes it.
|
| + * Executes an arbitrary HTTP method asynchronously.
|
| *
|
| - * @param {Object<string,string>} paramHash The parameter key/value pairs.
|
| - * @return {string} URLEncoded version of paramHash.
|
| + * @constructor
|
| + * @param {remoting.XhrParams} params
|
| */
|
| -remoting.xhr.urlencodeParamHash = function(paramHash) {
|
| - var paramArray = [];
|
| - for (var key in paramHash) {
|
| - paramArray.push(encodeURIComponent(key) +
|
| - '=' + encodeURIComponent(paramHash[key]));
|
| +remoting.Xhr = function(params) {
|
| + // Extract fields that can be used more or less as-is.
|
| + var method = params.method;
|
| + var url = params.url;
|
| + var headers = remoting.Xhr.removeNullFields_(params.headers);
|
| + var withCredentials = params.withCredentials || false;
|
| + var onDone = params.onDone;
|
| +
|
| + // Validate the response type and save it for later.
|
| + base.debug.assert(
|
| + params.responseType === undefined ||
|
| + params.responseType == 'text' ||
|
| + params.responseType == 'json' ||
|
| + params.responseType == 'none');
|
| + /** @const {string} */
|
| + this.responseType_ = params.responseType || 'text';
|
| +
|
| + // Apply URL parameters.
|
| + var parameterString = '';
|
| + if (typeof(params.urlParams) === 'string') {
|
| + parameterString = params.urlParams;
|
| + } else if (typeof(params.urlParams) === 'object') {
|
| + parameterString = remoting.Xhr.urlencodeParamHash(
|
| + remoting.Xhr.removeNullFields_(params.urlParams));
|
| }
|
| - if (paramArray.length > 0) {
|
| - return paramArray.join('&');
|
| + if (parameterString) {
|
| + base.debug.assert(url.indexOf('?') == -1);
|
| + url += '?' + parameterString;
|
| }
|
| - return '';
|
| +
|
| + // 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');
|
| + }
|
| +
|
| + // Convert the content fields to a single text content variable.
|
| + /** @type {?string} */
|
| + var content = null;
|
| + if (params.textContent !== undefined) {
|
| + content = params.textContent;
|
| + } else if (params.formContent !== undefined) {
|
| + if (!('Content-type' in headers)) {
|
| + headers['Content-type'] = 'application/x-www-form-urlencoded';
|
| + }
|
| + content = remoting.Xhr.urlencodeParamHash(params.formContent);
|
| + } else if (params.jsonContent !== undefined) {
|
| + if (!('Content-type' in headers)) {
|
| + headers['Content-type'] = 'application/json; charset=UTF-8';
|
| + }
|
| + content = JSON.stringify(params.jsonContent);
|
| + }
|
| +
|
| + // Apply the oauthToken field.
|
| + if (params.oauthToken !== undefined) {
|
| + base.debug.assert(!('Authorization' in headers));
|
| + headers['Authorization'] = 'Bearer ' + params.oauthToken;
|
| + }
|
| +
|
| + /** @const {!base.Deferred<remoting.Xhr.Response>} */
|
| + this.deferred_ = new base.Deferred();
|
| + if (onDone) {
|
| + this.then(onDone);
|
| + }
|
| +
|
| + /** @const {!XMLHttpRequest} */
|
| + this.nativeXhr_ = new XMLHttpRequest();
|
| +
|
| + this.startInternal_(method, url, content, headers, withCredentials);
|
| };
|
|
|
| /**
|
| @@ -41,8 +98,6 @@ remoting.xhr.urlencodeParamHash = function(paramHash) {
|
| *
|
| * url: The URL to request.
|
| *
|
| - * onDone: Function to call when the XHR finishes.
|
| -
|
| * urlParams: (optional) Parameters to be appended to the URL.
|
| * Null-valued parameters are omitted.
|
| *
|
| @@ -61,106 +116,90 @@ remoting.xhr.urlencodeParamHash = function(paramHash) {
|
| *
|
| * withCredentials: (optional) Value of the XHR's withCredentials field.
|
| *
|
| + * useOauth: (optional) If true, use OAuth2 to authenticate the request.
|
| + * Not compatible with the oauthToken field.
|
| + *
|
| * oauthToken: (optional) An OAuth2 token used to construct an
|
| - * Authentication header.
|
| + * Authentication header. Not compatible with the useOauth field.
|
| + *
|
| + * responseType: (optional) Request a response of a specific type,
|
| + * either 'text', 'json', or 'none' to ignore the response body
|
| + * entirely. Default: 'text'.
|
| *
|
| * @typedef {{
|
| * method: string,
|
| * url:string,
|
| - * onDone:(function(XMLHttpRequest):void),
|
| * urlParams:(string|Object<string,?string>|undefined),
|
| * textContent:(string|undefined),
|
| * formContent:(Object|undefined),
|
| * jsonContent:(*|undefined),
|
| * headers:(Object<string,?string>|undefined),
|
| * withCredentials:(boolean|undefined),
|
| - * oauthToken:(string|undefined)
|
| + * useOauth:(boolean|undefined),
|
| + * oauthToken:(string|undefined),
|
| + * responseType:(string|undefined),
|
| + * onDone:((function(remoting.Xhr.Response):void)|undefined)
|
| * }}
|
| */
|
| remoting.XhrParams;
|
|
|
| /**
|
| - * Returns a copy of the input object with all null or undefined
|
| - * fields removed.
|
| + * Values produced by a request.
|
| *
|
| - * @param {Object<string,?string>|undefined} input
|
| - * @return {!Object<string,string>}
|
| - * @private
|
| + * status: The HTTP status code.
|
| + *
|
| + * statusText: The HTTP status description.
|
| + *
|
| + * responseUrl: The response URL, if any.
|
| + *
|
| + * responseText: The content of the response as a string. Valid only
|
| + * if responseType is set to 'text' or the request failed.
|
| + *
|
| + * responseJson: The content of the response as a parsed JSON value.
|
| + * Valid only if the request succeeded and the responseType is set
|
| + * to 'json'.
|
| + *
|
| + * @typedef {{
|
| + * status:number,
|
| + * statusText:string,
|
| + * responseUrl:string,
|
| + * responseText:string,
|
| + * responseJson:*
|
| + * }}
|
| */
|
| -remoting.xhr.removeNullFields_ = function(input) {
|
| - /** @type {!Object<string,string>} */
|
| - var result = {};
|
| - if (input) {
|
| - for (var field in input) {
|
| - var value = input[field];
|
| - if (value != null) {
|
| - result[field] = value;
|
| - }
|
| - }
|
| - }
|
| - return result;
|
| +remoting.Xhr.Response;
|
| +
|
| +/**
|
| + * @param {function(remoting.Xhr.Response):void} onDone
|
| + * @return {void}
|
| + */
|
| +remoting.Xhr.prototype.then = function(onDone) {
|
| + this.promise().then(onDone);
|
| };
|
|
|
| /**
|
| - * Executes an arbitrary HTTP method asynchronously.
|
| + * Gets a promise that is resolved when the request completes.
|
| *
|
| - * @param {remoting.XhrParams} params
|
| - * @return {XMLHttpRequest} The XMLHttpRequest object.
|
| + * If an HTTP status code outside the 200..299 range is returned, the
|
| + * promise is rejected with a remoting.Error by default. Setting
|
| + * resolveOnErrorStatus to true in the constructor parameters allows
|
| + * the promise to resolve for any HTTP status code.
|
| + *
|
| + * Any error that prevents the HTTP request from succeeding causes
|
| + * this promise to be rejected.
|
| + *
|
| + * @return {!Promise<!remoting.Xhr.Response>}
|
| */
|
| -remoting.xhr.start = function(params) {
|
| - // Extract fields that can be used more or less as-is.
|
| - var method = params.method;
|
| - var url = params.url;
|
| - var onDone = params.onDone;
|
| - var headers = remoting.xhr.removeNullFields_(params.headers);
|
| - var withCredentials = params.withCredentials || false;
|
| -
|
| - // Apply URL parameters.
|
| - var parameterString = '';
|
| - if (typeof(params.urlParams) === 'string') {
|
| - parameterString = params.urlParams;
|
| - } else if (typeof(params.urlParams) === 'object') {
|
| - parameterString = remoting.xhr.urlencodeParamHash(
|
| - 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');
|
| - }
|
| -
|
| - // Convert the content fields to a single text content variable.
|
| - /** @type {?string} */
|
| - var content = null;
|
| - if (params.textContent !== undefined) {
|
| - content = params.textContent;
|
| - } else if (params.formContent !== undefined) {
|
| - if (!('Content-type' in headers)) {
|
| - headers['Content-type'] = 'application/x-www-form-urlencoded';
|
| - }
|
| - content = remoting.xhr.urlencodeParamHash(params.formContent);
|
| - } else if (params.jsonContent !== undefined) {
|
| - if (!('Content-type' in headers)) {
|
| - headers['Content-type'] = 'application/json; charset=UTF-8';
|
| - }
|
| - content = JSON.stringify(params.jsonContent);
|
| - }
|
| -
|
| - // Apply the oauthToken field.
|
| - if (params.oauthToken !== undefined) {
|
| - base.debug.assert(!('Authorization' in headers));
|
| - headers['Authorization'] = 'Bearer ' + params.oauthToken;
|
| - }
|
| +remoting.Xhr.prototype.promise = function() {
|
| + return this.deferred_.promise();
|
| +};
|
|
|
| - return remoting.xhr.startInternal_(
|
| - method, url, onDone, content, headers, withCredentials);
|
| +/**
|
| + * Aborts the HTTP request. Does nothing is the request has finished
|
| + * already.
|
| + */
|
| +remoting.Xhr.prototype.abort = function() {
|
| + return this.nativeXhr_.abort();
|
| };
|
|
|
| /**
|
| @@ -168,23 +207,16 @@ remoting.xhr.start = function(params) {
|
| *
|
| * @param {string} method
|
| * @param {string} url
|
| - * @param {function(XMLHttpRequest):void} onDone
|
| * @param {?string} content
|
| * @param {!Object<string,string>} headers
|
| * @param {boolean} withCredentials
|
| - * @return {XMLHttpRequest} The XMLHttpRequest object.
|
| * @private
|
| */
|
| -remoting.xhr.startInternal_ = function(
|
| - method, url, onDone, content, headers, withCredentials) {
|
| - /** @type {XMLHttpRequest} */
|
| - var xhr = new XMLHttpRequest();
|
| - xhr.onreadystatechange = function() {
|
| - if (xhr.readyState != 4) {
|
| - return;
|
| - }
|
| - onDone(xhr);
|
| - };
|
| +remoting.Xhr.prototype.startInternal_ = function(
|
| + method, url, content, headers, withCredentials) {
|
| + var xhr = this.nativeXhr_;
|
| +
|
| + xhr.onreadystatechange = this.onReadyStateChange_.bind(this);
|
|
|
| xhr.open(method, url, true);
|
| for (var key in headers) {
|
| @@ -192,21 +224,80 @@ remoting.xhr.startInternal_ = function(
|
| }
|
| xhr.withCredentials = withCredentials;
|
| xhr.send(content);
|
| - return xhr;
|
| +};
|
| +
|
| +remoting.Xhr.prototype.onReadyStateChange_ = function(onDone) {
|
| + var xhr = this.nativeXhr_;
|
| +
|
| + if (xhr.readyState != 4) {
|
| + return;
|
| + }
|
| +
|
| + var succeeded = 200 <= xhr.status && xhr.status < 300;
|
| + var response = {
|
| + status: xhr.status,
|
| + statusText: xhr.statusText,
|
| + responseUrl: xhr.responseURL,
|
| + responseText: !succeeded || this.responseType_ == 'text' ?
|
| + xhr.responseText : '<invalid responseText>',
|
| + responseJson: succeeded && this.responseType_ == 'json' ?
|
| + xhr.response : ['<invalid responseJson>']
|
| + };
|
| + this.deferred_.resolve(response);
|
| +};
|
| +
|
| +/**
|
| + * Returns a copy of the input object with all null or undefined
|
| + * fields removed.
|
| + *
|
| + * @param {Object<string,?string>|undefined} input
|
| + * @return {!Object<string,string>}
|
| + * @private
|
| + */
|
| +remoting.Xhr.removeNullFields_ = function(input) {
|
| + /** @type {!Object<string,string>} */
|
| + var result = {};
|
| + if (input) {
|
| + for (var field in input) {
|
| + var value = input[field];
|
| + if (value != null) {
|
| + result[field] = value;
|
| + }
|
| + }
|
| + }
|
| + return result;
|
| +};
|
| +
|
| +/**
|
| + * Takes an associative array of parameters and urlencodes it.
|
| + *
|
| + * @param {Object<string,string>} paramHash The parameter key/value pairs.
|
| + * @return {string} URLEncoded version of paramHash.
|
| + */
|
| +remoting.Xhr.urlencodeParamHash = function(paramHash) {
|
| + var paramArray = [];
|
| + for (var key in paramHash) {
|
| + paramArray.push(encodeURIComponent(key) +
|
| + '=' + encodeURIComponent(paramHash[key]));
|
| + }
|
| + if (paramArray.length > 0) {
|
| + return paramArray.join('&');
|
| + }
|
| + return '';
|
| };
|
|
|
| /**
|
| * Generic success/failure response proxy.
|
| + * TODO(jrw): Stop using this.
|
| *
|
| * @param {function():void} onDone
|
| * @param {function(!remoting.Error):void} onError
|
| - * @return {function(XMLHttpRequest):void}
|
| + * @return {function(remoting.Xhr.Response):void}
|
| */
|
| -remoting.xhr.defaultResponse = function(onDone, onError) {
|
| - /** @param {XMLHttpRequest} xhr */
|
| - var result = function(xhr) {
|
| - var error =
|
| - remoting.Error.fromHttpStatus(/** @type {number} */ (xhr.status));
|
| +remoting.Xhr.defaultResponse = function(onDone, onError) {
|
| + /** @param {remoting.Xhr.Response} xhrr */
|
| + var result = function(xhrr) {
|
| + var error = remoting.Error.fromHttpStatus(xhrr.status);
|
| if (!error.isError()) {
|
| onDone();
|
| } else {
|
|
|