From 53bcccd9e780c749e984658ba8cab18dc0b4a617 Mon Sep 17 00:00:00 2001 From: Simon Kornblith Date: Tue, 10 Jul 2012 02:35:00 -0400 Subject: [PATCH] Add Zotero.HTTP.promise(), a replacement for Zotero.HTTP.doGet/doPost/doHead/doOptions that returns a promise. Zotero.HTTP.promise() can potentially reject its promise with two new exceptions. Zotero.HTTP.UnexpectedStatusException is thrown if the status code is not 2XX, and Zotero.HTTP.BrowserOfflineException is thrown if the browser is offline. --- chrome/content/zotero/xpcom/http.js | 191 ++++++++++++++++++++++++---- 1 file changed, 163 insertions(+), 28 deletions(-) diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index f7c5c1b6c4..82e96a1c80 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -3,18 +3,146 @@ * @namespace */ Zotero.HTTP = new function() { - this.WebDAV = {}; - + Components.utils.import("resource://zotero/q.jsm"); /** - * Send an HTTP GET request via XMLHTTPRequest - * - * @param {nsIURI|String} url URL to request - * @param {Function} onDone Callback to be executed upon request completion - * @param {String} responseCharset Character set to force on the response - * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object - * @return {Boolean} True if the request was sent, or false if the browser is offline - */ + * Exception returned for unexpected status when promise* is used + * @constructor + */ + this.UnexpectedStatusException = function(xmlhttp) { + this.xmlhttp = xmlhttp; + this.status = xmlhttp.status; + this.message = "XMLHttpRequest received unexpected status "+this.status; + }; + + this.UnexpectedStatusException.prototype.toString = function() { + return this.message; + }; + + /** + * Exception returned if the browser is offline when promise* is used + * @constructor + */ + this.BrowserOfflineException = function() { + this.message = "XMLHttpRequest could not complete because the browser is offline"; + }; + this.BrowserOfflineException.prototype.toString = function() { + return this.message; + }; + + /** + * Get a promise for a HTTP request + * + * @param {String} method The method of the request ("GET", "POST", "HEAD", or "OPTIONS") + * @param {nsIURI|String} url URL to request + * @param {Object} [options] Options for HTTP request: + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object + * @return {Promise} A promise resolved with the XMLHttpRequest object if the request + * succeeds, or rejected if the browser is offline or a non-2XX status response + * code is received. + */ + this.promise = function promise(method, url, options) { + if (url instanceof Components.interfaces.nsIURI) { + // Don't display password in console + var disp = url.clone(); + if (disp.password) { + disp.password = "********"; + } + url = url.spec; + } + + if(options && options.body) { + var bodyStart = options.body.substr(0, 1024); + // Don't display sync password or session id in console + bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********'); + bodyStart = bodyStart.replace(/sessionid=[^&]+/, 'sessionid=********'); + + Zotero.debug("HTTP "+method+" " + + (options.body.length > 1024 ? + bodyStart + '... (' + options.body.length + ' chars)' : bodyStart) + + " to " + (disp ? disp.spec : url)); + } else { + Zotero.debug("HTTP GET " + url); + } + + if (this.browserIsOffline()) { + return Q.fcall(function() { + Zotero.debug("HTTP GET " + url + " failed: Browser is offline"); + throw new this.BrowserOfflineException(); + }); + } + + var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(); + // Prevent certificate/authentication dialogs from popping up + xmlhttp.mozBackgroundRequest = true; + xmlhttp.open(method, url, true); + + // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) + var channel = xmlhttp.channel; + channel.QueryInterface(Components.interfaces.nsIHttpChannelInternal); + channel.forceAllowThirdPartyCookie = true; + + // Set charset + if (options && options.responseCharset) { + channel.contentCharset = responseCharset; + } + + // Disable caching if requested + if(options && options.dontCache) { + channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; + } + + // Send headers + var headers = {}; + if (options && options.body && !headers["Content-Type"]) { + headers["Content-Type"] = "application/x-www-form-urlencoded"; + } + for (var header in headers) { + xmlhttp.setRequestHeader(header, headers[header]); + } + + var deferred = Q.defer(); + + xmlhttp.onloadend = function() { + var status = xmlhttp.status; + if(status >= 200 && status < 300) { + Zotero.debug("HTTP GET " + url + " succeeded"); + deferred.resolve(xmlhttp); + } else { + Zotero.debug("HTTP GET " + url + " failed: Unexpected status code "+xmlhttp.status); + deferred.reject(new Zotero.HTTP.UnexpectedStatusException(xmlhttp)); + } + }; + + if(options && options.cookieSandbox) { + options.cookieSandbox.attachToInterfaceRequestor(xmlhttp); + } + + xmlhttp.send((options && options.body) || null); + + return deferred.promise; + }; + + /** + * Send an HTTP GET request via XMLHTTPRequest + * + * @param {nsIURI|String} url URL to request + * @param {Function} onDone Callback to be executed upon request completion + * @param {String} responseCharset Character set to force on the response + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object + * @return {XMLHttpRequest} The XMLHttpRequest object if the request was sent, or + * false if the browser is offline + * @deprecated Use {@link Zotero.HTTP.promise} + */ this.doGet = function(url, onDone, responseCharset, cookieSandbox) { if (url instanceof Components.interfaces.nsIURI) { // Don't display password in console @@ -64,16 +192,18 @@ Zotero.HTTP = new function() { } /** - * Send an HTTP POST request via XMLHTTPRequest - * - * @param {String} url URL to request - * @param {String} body Request body - * @param {Function} onDone Callback to be executed upon request completion - * @param {String} headers Request HTTP headers - * @param {String} responseCharset Character set to force on the response - * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object - * @return {Boolean} True if the request was sent, or false if the browser is offline - */ + * Send an HTTP POST request via XMLHTTPRequest + * + * @param {String} url URL to request + * @param {String} body Request body + * @param {Function} onDone Callback to be executed upon request completion + * @param {String} headers Request HTTP headers + * @param {String} responseCharset Character set to force on the response + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object + * @return {XMLHttpRequest} The XMLHttpRequest object if the request was sent, or + * false if the browser is offline + * @deprecated Use {@link Zotero.HTTP.promise} + */ this.doPost = function(url, body, onDone, headers, responseCharset, cookieSandbox) { if (url instanceof Components.interfaces.nsIURI) { // Don't display password in console @@ -148,14 +278,16 @@ Zotero.HTTP = new function() { } /** - * Send an HTTP HEAD request via XMLHTTPRequest - * - * @param {String} url URL to request - * @param {Function} onDone Callback to be executed upon request completion - * @param {Object} requestHeaders HTTP headers to include with request - * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object - * @return {Boolean} True if the request was sent, or false if the browser is offline - */ + * Send an HTTP HEAD request via XMLHTTPRequest + * + * @param {String} url URL to request + * @param {Function} onDone Callback to be executed upon request completion + * @param {Object} requestHeaders HTTP headers to include with request + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object + * @return {XMLHttpRequest} The XMLHttpRequest object if the request was sent, or + * false if the browser is offline + * @deprecated Use {@link Zotero.HTTP.promise} + */ this.doHead = function(url, onDone, requestHeaders, cookieSandbox) { if (url instanceof Components.interfaces.nsIURI) { // Don't display password in console @@ -213,6 +345,7 @@ Zotero.HTTP = new function() { * @param {nsIURI} url * @param {Function} onDone * @return {XMLHTTPRequest} + * @deprecated Use {@link Zotero.HTTP.promise} */ this.doOptions = function (uri, callback) { // Don't display password in console @@ -243,6 +376,8 @@ Zotero.HTTP = new function() { // WebDAV methods // + this.WebDAV = {}; + /** * Send a WebDAV PROP* request via XMLHTTPRequest *