Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
|
Jamie
2015/03/16 18:06:34
This file is missing from the latest patch-set.
John Williams
2015/03/16 23:15:00
No idea what happened there. Saved by the reflog.
| |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * Simple utilities for making XHRs more pleasant. | 7 * Simple utilities for making XHRs more pleasant. |
| 8 */ | 8 */ |
| 9 | 9 |
| 10 'use strict'; | 10 'use strict'; |
| 11 | 11 |
| 12 /** @suppress {duplicate} */ | 12 /** @suppress {duplicate} */ |
| 13 var remoting = remoting || {}; | 13 var remoting = remoting || {}; |
| 14 | 14 |
| 15 /** Namespace for XHR functions */ | 15 /** |
| 16 /** @type {Object} */ | 16 * Executes an arbitrary HTTP method asynchronously. |
| 17 remoting.xhr = remoting.xhr || {}; | 17 * |
| 18 * @constructor | |
| 19 * @param {remoting.XhrParams} params | |
| 20 */ | |
| 21 remoting.Xhr = function(params) { | |
| 22 // Extract fields that can be used more or less as-is. | |
| 23 var method = params.method; | |
| 24 var url = params.url; | |
| 25 var headers = remoting.Xhr.removeNullFields_(params.headers); | |
| 26 var withCredentials = params.withCredentials || false; | |
| 27 var onDone = params.onDone; | |
| 18 | 28 |
| 19 /** | 29 // Validate the response type and save it for later. |
| 20 * Takes an associative array of parameters and urlencodes it. | 30 base.debug.assert( |
| 21 * | 31 params.responseType === undefined || |
| 22 * @param {Object<string,string>} paramHash The parameter key/value pairs. | 32 params.responseType == 'text' || |
| 23 * @return {string} URLEncoded version of paramHash. | 33 params.responseType == 'json' || |
| 24 */ | 34 params.responseType == 'none'); |
|
Jamie
2015/03/12 20:15:43
Nit: Indentation.
John Williams
2015/03/14 03:35:33
Done.
| |
| 25 remoting.xhr.urlencodeParamHash = function(paramHash) { | 35 /** @const {string} */ |
| 26 var paramArray = []; | 36 this.responseType_ = params.responseType || 'text'; |
| 27 for (var key in paramHash) { | 37 |
| 28 paramArray.push(encodeURIComponent(key) + | 38 // Apply URL parameters. |
| 29 '=' + encodeURIComponent(paramHash[key])); | 39 var parameterString = ''; |
| 40 if (typeof(params.urlParams) === 'string') { | |
| 41 parameterString = params.urlParams; | |
| 42 } else if (typeof(params.urlParams) === 'object') { | |
| 43 parameterString = remoting.Xhr.urlencodeParamHash( | |
| 44 remoting.Xhr.removeNullFields_(params.urlParams)); | |
| 30 } | 45 } |
| 31 if (paramArray.length > 0) { | 46 if (parameterString) { |
| 32 return paramArray.join('&'); | 47 base.debug.assert(url.indexOf('?') == -1); |
| 48 url += '?' + parameterString; | |
| 33 } | 49 } |
| 34 return ''; | 50 |
| 51 // Check that the content spec is consistent. | |
| 52 if ((Number(params.textContent !== undefined) + | |
| 53 Number(params.formContent !== undefined) + | |
| 54 Number(params.jsonContent !== undefined)) > 1) { | |
| 55 throw new Error( | |
| 56 'may only specify one of textContent, formContent, and jsonContent'); | |
| 57 } | |
| 58 | |
| 59 // Convert the content fields to a single text content variable. | |
| 60 /** @type {?string} */ | |
| 61 var content = null; | |
| 62 if (params.textContent !== undefined) { | |
| 63 content = params.textContent; | |
| 64 } else if (params.formContent !== undefined) { | |
| 65 if (!('Content-type' in headers)) { | |
| 66 headers['Content-type'] = 'application/x-www-form-urlencoded'; | |
| 67 } | |
| 68 content = remoting.Xhr.urlencodeParamHash(params.formContent); | |
| 69 } else if (params.jsonContent !== undefined) { | |
| 70 if (!('Content-type' in headers)) { | |
| 71 headers['Content-type'] = 'application/json; charset=UTF-8'; | |
| 72 } | |
| 73 content = JSON.stringify(params.jsonContent); | |
| 74 } | |
| 75 | |
| 76 // Apply the oauthToken field. | |
| 77 if (params.oauthToken !== undefined) { | |
| 78 base.debug.assert(!('Authorization' in headers)); | |
| 79 headers['Authorization'] = 'Bearer ' + params.oauthToken; | |
| 80 } | |
| 81 | |
| 82 /** @const {!base.Deferred<remoting.Xhr.Response>} */ | |
| 83 this.deferred_ = new base.Deferred(); | |
| 84 if (onDone) { | |
| 85 this.then(onDone); | |
| 86 } | |
| 87 | |
| 88 /** @const {!XMLHttpRequest} */ | |
| 89 this.nativeXhr_ = new XMLHttpRequest(); | |
| 90 | |
| 91 this.startInternal_(method, url, content, headers, withCredentials); | |
|
Jamie
2015/03/12 20:15:43
I would prefer that the ctor doesn't implicitly se
John Williams
2015/03/14 03:35:33
Done.
| |
| 35 }; | 92 }; |
| 36 | 93 |
| 37 /** | 94 /** |
| 38 * Parameters for the 'start' function. | 95 * Parameters for the 'start' function. |
| 39 * | 96 * |
| 40 * method: The HTTP method to use. | 97 * method: The HTTP method to use. |
| 41 * | 98 * |
| 42 * url: The URL to request. | 99 * url: The URL to request. |
| 43 * | 100 * |
| 44 * onDone: Function to call when the XHR finishes. | |
| 45 | |
| 46 * urlParams: (optional) Parameters to be appended to the URL. | 101 * urlParams: (optional) Parameters to be appended to the URL. |
| 47 * Null-valued parameters are omitted. | 102 * Null-valued parameters are omitted. |
| 48 * | 103 * |
| 49 * textContent: (optional) Text to be sent as the request body. | 104 * textContent: (optional) Text to be sent as the request body. |
| 50 * | 105 * |
| 51 * formContent: (optional) Data to be URL-encoded and sent as the | 106 * formContent: (optional) Data to be URL-encoded and sent as the |
| 52 * request body. Causes Content-type header to be set | 107 * request body. Causes Content-type header to be set |
| 53 * appropriately. | 108 * appropriately. |
| 54 * | 109 * |
| 55 * jsonContent: (optional) Data to be JSON-encoded and sent as the | 110 * jsonContent: (optional) Data to be JSON-encoded and sent as the |
| 56 * request body. Causes Content-type header to be set | 111 * request body. Causes Content-type header to be set |
| 57 * appropriately. | 112 * appropriately. |
| 58 * | 113 * |
| 59 * headers: (optional) Additional request headers to be sent. | 114 * headers: (optional) Additional request headers to be sent. |
| 60 * Null-valued headers are omitted. | 115 * Null-valued headers are omitted. |
| 61 * | 116 * |
| 62 * withCredentials: (optional) Value of the XHR's withCredentials field. | 117 * withCredentials: (optional) Value of the XHR's withCredentials field. |
| 63 * | 118 * |
| 119 * useOauth: (optional) If true, use OAuth2 to authenticate the request. | |
| 120 * Not compatible with the oauthToken field. | |
| 121 * | |
| 64 * oauthToken: (optional) An OAuth2 token used to construct an | 122 * oauthToken: (optional) An OAuth2 token used to construct an |
| 65 * Authentication header. | 123 * Authentication header. Not compatible with the useOauth field. |
| 124 * | |
| 125 * responseType: (optional) Request a response of a specific type, | |
| 126 * either 'text', 'json', or 'none' to ignore the response body | |
| 127 * entirely. Default: 'text'. | |
|
Jamie
2015/03/12 20:15:43
Maybe make this an enumerated type so that the tes
John Williams
2015/03/14 03:35:33
Done, but I'm not sure I like the result. Enumera
| |
| 66 * | 128 * |
| 67 * @typedef {{ | 129 * @typedef {{ |
| 68 * method: string, | 130 * method: string, |
| 69 * url:string, | 131 * url:string, |
| 70 * onDone:(function(XMLHttpRequest):void), | |
| 71 * urlParams:(string|Object<string,?string>|undefined), | 132 * urlParams:(string|Object<string,?string>|undefined), |
| 72 * textContent:(string|undefined), | 133 * textContent:(string|undefined), |
| 73 * formContent:(Object|undefined), | 134 * formContent:(Object|undefined), |
| 74 * jsonContent:(*|undefined), | 135 * jsonContent:(*|undefined), |
| 75 * headers:(Object<string,?string>|undefined), | 136 * headers:(Object<string,?string>|undefined), |
| 76 * withCredentials:(boolean|undefined), | 137 * withCredentials:(boolean|undefined), |
| 77 * oauthToken:(string|undefined) | 138 * useOauth:(boolean|undefined), |
| 139 * oauthToken:(string|undefined), | |
| 140 * responseType:(string|undefined), | |
| 141 * onDone:((function(remoting.Xhr.Response):void)|undefined) | |
| 78 * }} | 142 * }} |
| 79 */ | 143 */ |
| 80 remoting.XhrParams; | 144 remoting.XhrParams; |
| 81 | 145 |
| 82 /** | 146 /** |
| 83 * Returns a copy of the input object with all null or undefined | 147 * Values produced by a request. |
| 84 * fields removed. | |
| 85 * | 148 * |
| 86 * @param {Object<string,?string>|undefined} input | 149 * status: The HTTP status code. |
| 87 * @return {!Object<string,string>} | 150 * |
| 88 * @private | 151 * statusText: The HTTP status description. |
| 152 * | |
| 153 * responseUrl: The response URL, if any. | |
| 154 * | |
| 155 * responseText: The content of the response as a string. Valid only | |
| 156 * if responseType is set to 'text' or the request failed. | |
| 157 * | |
| 158 * responseJson: The content of the response as a parsed JSON value. | |
| 159 * Valid only if the request succeeded and the responseType is set | |
| 160 * to 'json'. | |
| 161 * | |
| 162 * @typedef {{ | |
| 163 * status:number, | |
| 164 * statusText:string, | |
| 165 * responseUrl:string, | |
| 166 * responseText:string, | |
| 167 * responseJson:* | |
| 168 * }} | |
| 89 */ | 169 */ |
| 90 remoting.xhr.removeNullFields_ = function(input) { | 170 remoting.Xhr.Response; |
| 91 /** @type {!Object<string,string>} */ | 171 |
| 92 var result = {}; | 172 /** |
| 93 if (input) { | 173 * @param {function(remoting.Xhr.Response):void} onDone |
| 94 for (var field in input) { | 174 * @return {void} |
| 95 var value = input[field]; | 175 */ |
| 96 if (value != null) { | 176 remoting.Xhr.prototype.then = function(onDone) { |
| 97 result[field] = value; | 177 this.promise().then(onDone); |
| 98 } | |
| 99 } | |
| 100 } | |
| 101 return result; | |
| 102 }; | 178 }; |
| 103 | 179 |
| 104 /** | 180 /** |
| 105 * Executes an arbitrary HTTP method asynchronously. | 181 * Gets a promise that is resolved when the request completes. |
| 106 * | 182 * |
| 107 * @param {remoting.XhrParams} params | 183 * If an HTTP status code outside the 200..299 range is returned, the |
| 108 * @return {XMLHttpRequest} The XMLHttpRequest object. | 184 * promise is rejected with a remoting.Error by default. Setting |
| 185 * resolveOnErrorStatus to true in the constructor parameters allows | |
| 186 * the promise to resolve for any HTTP status code. | |
|
Jamie
2015/03/12 20:15:43
There's no other mention of resolveOnErrorStatus.
John Williams
2015/03/14 03:35:33
Fixed.
| |
| 187 * | |
| 188 * Any error that prevents the HTTP request from succeeding causes | |
| 189 * this promise to be rejected. | |
| 190 * | |
| 191 * @return {!Promise<!remoting.Xhr.Response>} | |
| 109 */ | 192 */ |
| 110 remoting.xhr.start = function(params) { | 193 remoting.Xhr.prototype.promise = function() { |
| 111 // Extract fields that can be used more or less as-is. | 194 return this.deferred_.promise(); |
| 112 var method = params.method; | |
| 113 var url = params.url; | |
| 114 var onDone = params.onDone; | |
| 115 var headers = remoting.xhr.removeNullFields_(params.headers); | |
| 116 var withCredentials = params.withCredentials || false; | |
| 117 | |
| 118 // Apply URL parameters. | |
| 119 var parameterString = ''; | |
| 120 if (typeof(params.urlParams) === 'string') { | |
| 121 parameterString = params.urlParams; | |
| 122 } else if (typeof(params.urlParams) === 'object') { | |
| 123 parameterString = remoting.xhr.urlencodeParamHash( | |
| 124 remoting.xhr.removeNullFields_(params.urlParams)); | |
| 125 } | |
| 126 if (parameterString) { | |
| 127 base.debug.assert(url.indexOf('?') == -1); | |
| 128 url += '?' + parameterString; | |
| 129 } | |
| 130 | |
| 131 // Check that the content spec is consistent. | |
| 132 if ((Number(params.textContent !== undefined) + | |
| 133 Number(params.formContent !== undefined) + | |
| 134 Number(params.jsonContent !== undefined)) > 1) { | |
| 135 throw new Error( | |
| 136 'may only specify one of textContent, formContent, and jsonContent'); | |
| 137 } | |
| 138 | |
| 139 // Convert the content fields to a single text content variable. | |
| 140 /** @type {?string} */ | |
| 141 var content = null; | |
| 142 if (params.textContent !== undefined) { | |
| 143 content = params.textContent; | |
| 144 } else if (params.formContent !== undefined) { | |
| 145 if (!('Content-type' in headers)) { | |
| 146 headers['Content-type'] = 'application/x-www-form-urlencoded'; | |
| 147 } | |
| 148 content = remoting.xhr.urlencodeParamHash(params.formContent); | |
| 149 } else if (params.jsonContent !== undefined) { | |
| 150 if (!('Content-type' in headers)) { | |
| 151 headers['Content-type'] = 'application/json; charset=UTF-8'; | |
| 152 } | |
| 153 content = JSON.stringify(params.jsonContent); | |
| 154 } | |
| 155 | |
| 156 // Apply the oauthToken field. | |
| 157 if (params.oauthToken !== undefined) { | |
| 158 base.debug.assert(!('Authorization' in headers)); | |
| 159 headers['Authorization'] = 'Bearer ' + params.oauthToken; | |
| 160 } | |
| 161 | |
| 162 return remoting.xhr.startInternal_( | |
| 163 method, url, onDone, content, headers, withCredentials); | |
| 164 }; | 195 }; |
| 165 | 196 |
| 166 /** | 197 /** |
| 198 * Aborts the HTTP request. Does nothing is the request has finished | |
| 199 * already. | |
| 200 */ | |
| 201 remoting.Xhr.prototype.abort = function() { | |
| 202 return this.nativeXhr_.abort(); | |
| 203 }; | |
| 204 | |
| 205 /** | |
| 167 * Executes an arbitrary HTTP method asynchronously. | 206 * Executes an arbitrary HTTP method asynchronously. |
| 168 * | 207 * |
| 169 * @param {string} method | 208 * @param {string} method |
| 170 * @param {string} url | 209 * @param {string} url |
| 171 * @param {function(XMLHttpRequest):void} onDone | |
| 172 * @param {?string} content | 210 * @param {?string} content |
| 173 * @param {!Object<string,string>} headers | 211 * @param {!Object<string,string>} headers |
| 174 * @param {boolean} withCredentials | 212 * @param {boolean} withCredentials |
| 175 * @return {XMLHttpRequest} The XMLHttpRequest object. | |
| 176 * @private | 213 * @private |
| 177 */ | 214 */ |
| 178 remoting.xhr.startInternal_ = function( | 215 remoting.Xhr.prototype.startInternal_ = function( |
| 179 method, url, onDone, content, headers, withCredentials) { | 216 method, url, content, headers, withCredentials) { |
| 180 /** @type {XMLHttpRequest} */ | 217 var xhr = this.nativeXhr_; |
| 181 var xhr = new XMLHttpRequest(); | 218 |
| 182 xhr.onreadystatechange = function() { | 219 xhr.onreadystatechange = this.onReadyStateChange_.bind(this); |
| 183 if (xhr.readyState != 4) { | |
| 184 return; | |
| 185 } | |
| 186 onDone(xhr); | |
| 187 }; | |
| 188 | 220 |
| 189 xhr.open(method, url, true); | 221 xhr.open(method, url, true); |
| 190 for (var key in headers) { | 222 for (var key in headers) { |
| 191 xhr.setRequestHeader(key, headers[key]); | 223 xhr.setRequestHeader(key, headers[key]); |
| 192 } | 224 } |
| 193 xhr.withCredentials = withCredentials; | 225 xhr.withCredentials = withCredentials; |
| 194 xhr.send(content); | 226 xhr.send(content); |
| 195 return xhr; | 227 }; |
| 228 | |
| 229 remoting.Xhr.prototype.onReadyStateChange_ = function(onDone) { | |
| 230 var xhr = this.nativeXhr_; | |
| 231 | |
| 232 if (xhr.readyState != 4) { | |
| 233 return; | |
| 234 } | |
| 235 | |
| 236 var succeeded = 200 <= xhr.status && xhr.status < 300; | |
| 237 var response = { | |
| 238 status: xhr.status, | |
| 239 statusText: xhr.statusText, | |
| 240 responseUrl: xhr.responseURL, | |
| 241 responseText: !succeeded || this.responseType_ == 'text' ? | |
| 242 xhr.responseText : '<invalid responseText>', | |
| 243 responseJson: succeeded && this.responseType_ == 'json' ? | |
| 244 xhr.response : ['<invalid responseJson>'] | |
| 245 }; | |
| 246 this.deferred_.resolve(response); | |
| 247 }; | |
| 248 | |
| 249 /** | |
| 250 * Returns a copy of the input object with all null or undefined | |
| 251 * fields removed. | |
| 252 * | |
| 253 * @param {Object<string,?string>|undefined} input | |
| 254 * @return {!Object<string,string>} | |
| 255 * @private | |
| 256 */ | |
| 257 remoting.Xhr.removeNullFields_ = function(input) { | |
| 258 /** @type {!Object<string,string>} */ | |
| 259 var result = {}; | |
| 260 if (input) { | |
| 261 for (var field in input) { | |
| 262 var value = input[field]; | |
| 263 if (value != null) { | |
| 264 result[field] = value; | |
| 265 } | |
| 266 } | |
| 267 } | |
| 268 return result; | |
| 269 }; | |
| 270 | |
| 271 /** | |
| 272 * Takes an associative array of parameters and urlencodes it. | |
| 273 * | |
| 274 * @param {Object<string,string>} paramHash The parameter key/value pairs. | |
| 275 * @return {string} URLEncoded version of paramHash. | |
| 276 */ | |
| 277 remoting.Xhr.urlencodeParamHash = function(paramHash) { | |
| 278 var paramArray = []; | |
| 279 for (var key in paramHash) { | |
| 280 paramArray.push(encodeURIComponent(key) + | |
| 281 '=' + encodeURIComponent(paramHash[key])); | |
| 282 } | |
| 283 if (paramArray.length > 0) { | |
| 284 return paramArray.join('&'); | |
| 285 } | |
| 286 return ''; | |
| 196 }; | 287 }; |
| 197 | 288 |
| 198 /** | 289 /** |
| 199 * Generic success/failure response proxy. | 290 * Generic success/failure response proxy. |
| 291 * TODO(jrw): Stop using this. | |
| 200 * | 292 * |
| 201 * @param {function():void} onDone | 293 * @param {function():void} onDone |
| 202 * @param {function(!remoting.Error):void} onError | 294 * @param {function(!remoting.Error):void} onError |
| 203 * @return {function(XMLHttpRequest):void} | 295 * @return {function(remoting.Xhr.Response):void} |
| 204 */ | 296 */ |
| 205 remoting.xhr.defaultResponse = function(onDone, onError) { | 297 remoting.Xhr.defaultResponse = function(onDone, onError) { |
| 206 /** @param {XMLHttpRequest} xhr */ | 298 /** @param {remoting.Xhr.Response} xhrr */ |
| 207 var result = function(xhr) { | 299 var result = function(xhrr) { |
| 208 var error = | 300 var error = remoting.Error.fromHttpStatus(xhrr.status); |
| 209 remoting.Error.fromHttpStatus(/** @type {number} */ (xhr.status)); | |
| 210 if (!error.isError()) { | 301 if (!error.isError()) { |
| 211 onDone(); | 302 onDone(); |
| 212 } else { | 303 } else { |
| 213 onError(error); | 304 onError(error); |
| 214 } | 305 } |
| 215 }; | 306 }; |
| 216 return result; | 307 return result; |
| 217 }; | 308 }; |
| OLD | NEW |