summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/accountcreation/FetchHTTP.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/accountcreation/FetchHTTP.jsm')
-rw-r--r--comm/mail/components/accountcreation/FetchHTTP.jsm401
1 files changed, 401 insertions, 0 deletions
diff --git a/comm/mail/components/accountcreation/FetchHTTP.jsm b/comm/mail/components/accountcreation/FetchHTTP.jsm
new file mode 100644
index 0000000000..54b3629906
--- /dev/null
+++ b/comm/mail/components/accountcreation/FetchHTTP.jsm
@@ -0,0 +1,401 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This is a small wrapper around XMLHttpRequest, which solves various
+ * inadequacies of the API, e.g. error handling. It is entirely generic and
+ * can be used for purposes outside of even mail.
+ *
+ * It does not provide download progress, but assumes that the
+ * fetched resource is so small (<1 10 KB) that the roundtrip and
+ * response generation is far more significant than the
+ * download time of the response. In other words, it's fine for RPC,
+ * but not for bigger file downloads.
+ */
+
+const EXPORTED_SYMBOLS = ["FetchHTTP"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm");
+
+const {
+ Abortable,
+ alertPrompt,
+ assert,
+ ddump,
+ Exception,
+ gAccountSetupLogger,
+ getStringBundle,
+ UserCancelledException,
+} = AccountCreationUtils;
+
+/**
+ * Set up a fetch.
+ *
+ * @param {string} url - URL of the server function.
+ * ATTENTION: The caller needs to make sure that the URL is secure to call.
+ * @param {object} args - Additional parameters as properties, see below
+ *
+ * @param {Function({string} result)} successCallback
+ * Called when the server call worked (no errors).
+ * |result| will contain the body of the HTTP response, as string.
+ * @param {Function(ex)} errorCallback
+ * Called in case of error. ex contains the error
+ * with a user-displayable but not localized |.message| and maybe a
+ * |.code|, which can be either
+ * - an nsresult error code,
+ * - an HTTP result error code (0...1000) or
+ * - negative: 0...-100 :
+ * -2 = can't resolve server in DNS etc.
+ * -4 = response body (e.g. XML) malformed
+ *
+ * The following optional parameters are supported as properties of the |args| object:
+ *
+ * @param {Object, associative array} urlArgs - Parameters to add
+ * to the end of the URL as query string. E.g.
+ * { foo: "bla", bar: "blub blub" } will add "?foo=bla&bar=blub%20blub"
+ * to the URL
+ * (unless the URL already has a "?", then it adds "&foo...").
+ * The values will be urlComponentEncoded, so pass them unencoded.
+ * @param {Object, associative array} headers - HTTP headers to be added
+ * to the HTTP request.
+ * { foo: "blub blub" } will add HTTP header "Foo: Blub blub".
+ * The values will be passed verbatim.
+ * @param {boolean} post - HTTP GET or POST
+ * Only influences the HTTP request method,
+ * i.e. first line of the HTTP request, not the body or parameters.
+ * Use POST when you modify server state,
+ * GET when you only request information.
+ * Default is GET.
+ * @param {Object, associative array} bodyFormArgs - Like urlArgs,
+ * just that the params will be sent x-url-encoded in the body,
+ * like a HTML form post.
+ * The values will be urlComponentEncoded, so pass them unencoded.
+ * This cannot be used together with |uploadBody|.
+ * @param {object} uploadBody - Arbitrary object, which to use as
+ * body of the HTTP request. Will also set the mimetype accordingly.
+ * Only supported object types, currently only JXON is supported
+ * (sending XML).
+ * Usually, you have nothing to upload, so just pass |null|.
+ * Only supported object types, currently supported:
+ * JXON -> sending XML
+ * JS object -> sending JSON
+ * string -> sending text/plain
+ * If you want to override the body mimetype, set header Content-Type below.
+ * Usually, you have nothing to upload, so just leave it at |null|.
+ * Default |null|.
+ * @param {boolean} allowCache (default true)
+ * @param {string} username (default null = no authentication)
+ * @param {string} password (default null = no authentication)
+ * @param {boolean} allowAuthPrompt (default true)
+ * @param {boolean} requireSecureAuth (default false)
+ * Ignore the username and password unless we are using https:
+ * This also applies to both https: to http: and http: to https: redirects.
+ */
+function FetchHTTP(url, args, successCallback, errorCallback) {
+ assert(typeof successCallback == "function", "BUG: successCallback");
+ assert(typeof errorCallback == "function", "BUG: errorCallback");
+ this._url = lazy.Sanitizer.string(url);
+ if (!args) {
+ args = {};
+ }
+ if (!args.urlArgs) {
+ args.urlArgs = {};
+ }
+ if (!args.headers) {
+ args.headers = {};
+ }
+
+ this._args = args;
+ this._args.post = lazy.Sanitizer.boolean(args.post || false); // default false
+ this._args.allowCache =
+ "allowCache" in args ? lazy.Sanitizer.boolean(args.allowCache) : true; // default true
+ this._args.allowAuthPrompt = lazy.Sanitizer.boolean(
+ args.allowAuthPrompt || false
+ ); // default false
+ this._args.requireSecureAuth = lazy.Sanitizer.boolean(
+ args.requireSecureAuth || false
+ ); // default false
+ this._args.timeout = lazy.Sanitizer.integer(args.timeout || 5000); // default 5 seconds
+ this._successCallback = successCallback;
+ this._errorCallback = errorCallback;
+ this._logger = gAccountSetupLogger;
+ this._logger.info("Requesting <" + url + ">");
+}
+FetchHTTP.prototype = {
+ __proto__: Abortable.prototype,
+ _url: null, // URL as passed to ctor, without arguments
+ _args: null,
+ _successCallback: null,
+ _errorCallback: null,
+ _request: null, // the XMLHttpRequest object
+ result: null,
+
+ start() {
+ let url = this._url;
+ for (let name in this._args.urlArgs) {
+ url +=
+ (!url.includes("?") ? "?" : "&") +
+ name +
+ "=" +
+ encodeURIComponent(this._args.urlArgs[name]);
+ }
+ this._request = new XMLHttpRequest();
+ let request = this._request;
+ request.mozBackgroundRequest = !this._args.allowAuthPrompt;
+ let username = null,
+ password = null;
+ if (url.startsWith("https:") || !this._args.requireSecureAuth) {
+ username = this._args.username;
+ password = this._args.password;
+ }
+ request.open(
+ this._args.post ? "POST" : "GET",
+ url,
+ true,
+ username,
+ password
+ );
+ request.channel.loadGroup = null;
+ request.timeout = this._args.timeout;
+ // needs bug 407190 patch v4 (or higher) - uncomment if that lands.
+ // try {
+ // var channel = request.channel.QueryInterface(Ci.nsIHttpChannel2);
+ // channel.connectTimeout = 5;
+ // channel.requestTimeout = 5;
+ // } catch (e) { dump(e + "\n"); }
+
+ if (!this._args.allowCache) {
+ // Disable Mozilla HTTP cache
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ }
+
+ // body
+ let mimetype = null;
+ let body = this._args.uploadBody;
+ if (typeof body == "object" && "nodeType" in body) {
+ // XML
+ mimetype = "text/xml; charset=UTF-8";
+ body = new XMLSerializer().serializeToString(body);
+ } else if (typeof body == "object") {
+ // JSON
+ mimetype = "text/json; charset=UTF-8";
+ body = JSON.stringify(body);
+ } else if (typeof body == "string") {
+ // Plaintext
+ // You can override the mimetype with { headers: {"Content-Type" : "text/foo" } }
+ mimetype = "text/plain; charset=UTF-8";
+ // body already set above
+ } else if (this._args.bodyFormArgs) {
+ mimetype = "application/x-www-form-urlencoded; charset=UTF-8";
+ body = "";
+ for (let name in this._args.bodyFormArgs) {
+ body +=
+ (body ? "&" : "") +
+ name +
+ "=" +
+ encodeURIComponent(this._args.bodyFormArgs[name]);
+ }
+ }
+
+ // Headers
+ if (mimetype && !("Content-Type" in this._args.headers)) {
+ request.setRequestHeader("Content-Type", mimetype);
+ }
+ if (username && password) {
+ // workaround, because open(..., username, password) does not work.
+ request.setRequestHeader(
+ "Authorization",
+ "Basic " +
+ btoa(
+ // btoa() takes a BinaryString.
+ String.fromCharCode(
+ ...new TextEncoder().encode(username + ":" + password)
+ )
+ )
+ );
+ }
+ for (let name in this._args.headers) {
+ request.setRequestHeader(name, this._args.headers[name]);
+ if (name == "Cookie") {
+ // Websites are not allowed to set this, but chrome is.
+ // Nevertheless, the cookie lib later overwrites our header.
+ // request.channel.setCookie(this._args.headers[name]); -- crashes
+ // So, deactivate that Firefox cookie lib.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
+ }
+ }
+
+ var me = this;
+ request.onload = function () {
+ me._response(true);
+ };
+ request.onerror = function () {
+ me._response(false);
+ };
+ request.ontimeout = function () {
+ me._response(false);
+ };
+ request.send(body);
+ // Store the original stack so we can use it if there is an exception
+ this._callStack = Error().stack;
+ },
+ _response(success, exStored) {
+ try {
+ var errorCode = null;
+ var errorStr = null;
+
+ if (
+ success &&
+ this._request.status >= 200 &&
+ this._request.status < 300
+ ) {
+ // HTTP level success
+ try {
+ // response
+ var mimetype = this._request.getResponseHeader("Content-Type");
+ if (!mimetype) {
+ mimetype = "";
+ }
+ mimetype = mimetype.split(";")[0];
+ if (
+ mimetype == "text/xml" ||
+ mimetype == "application/xml" ||
+ mimetype == "text/rdf"
+ ) {
+ // XML
+ this.result = JXON.build(this._request.responseXML);
+ } else if (
+ mimetype == "text/json" ||
+ mimetype == "application/json"
+ ) {
+ // JSON
+ this.result = JSON.parse(this._request.responseText);
+ } else {
+ // Plaintext (fallback)
+ // ddump("mimetype: " + mimetype + " only supported as text");
+ this.result = this._request.responseText;
+ }
+ } catch (e) {
+ success = false;
+ errorStr = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties"
+ ).GetStringFromName("bad_response_content.error");
+ errorCode = -4;
+ }
+ } else if (
+ this._args.username &&
+ this._request.responseURL.replace(/\/\/.*@/, "//") != this._url &&
+ this._request.responseURL.startsWith(
+ this._args.requireSecureAuth ? "https" : "http"
+ ) &&
+ !this._isRetry
+ ) {
+ // Redirects lack auth, see <https://stackoverflow.com/a/28411170>
+ this._logger.info(
+ "Call to <" +
+ this._url +
+ "> was redirected to <" +
+ this._request.responseURL +
+ ">, and failed. Re-trying the new URL with authentication again."
+ );
+ this._url = this._request.responseURL;
+ this._isRetry = true;
+ this.start();
+ return;
+ } else {
+ success = false;
+ try {
+ errorCode = this._request.status;
+ errorStr = this._request.statusText;
+ } catch (e) {
+ // In case .statusText throws (it's marked as [Throws] in the webidl),
+ // continue with empty errorStr.
+ }
+ if (!errorStr) {
+ // If we can't resolve the hostname in DNS etc., .statusText is empty.
+ errorCode = -2;
+ errorStr = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties"
+ ).GetStringFromName("cannot_contact_server.error");
+ ddump(errorStr + " on <" + this._url + ">");
+ }
+ }
+
+ // Callbacks
+ if (success) {
+ try {
+ this._successCallback(this.result);
+ } catch (e) {
+ e.stack = this._callStack;
+ this._error(e);
+ }
+ } else if (exStored) {
+ this._error(exStored);
+ } else {
+ // Put the caller's stack into the exception
+ let e = new ServerException(errorStr, errorCode, this._url);
+ e.stack = this._callStack;
+ this._error(e);
+ }
+
+ if (this._finishedCallback) {
+ try {
+ this._finishedCallback(this);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ } catch (e) {
+ // error in our fetchhttp._response() code
+ this._error(e);
+ }
+ },
+ _error(e) {
+ try {
+ this._errorCallback(e);
+ } catch (e) {
+ // error in errorCallback, too!
+ console.error(e);
+ alertPrompt("Error in errorCallback for fetchhttp", e);
+ }
+ },
+ /**
+ * Call this between start() and finishedCallback fired.
+ */
+ cancel(ex) {
+ assert(!this.result, "Call already returned");
+
+ this._request.abort();
+
+ // Need to manually call error handler
+ // <https://bugzilla.mozilla.org/show_bug.cgi?id=218236#c11>
+ this._response(false, ex ? ex : new UserCancelledException());
+ },
+ /**
+ * Allows caller or lib to be notified when the call is done.
+ * This is useful to enable and disable a Cancel button in the UI,
+ * which allows to cancel the network request.
+ */
+ setFinishedCallback(finishedCallback) {
+ this._finishedCallback = finishedCallback;
+ },
+};
+
+function ServerException(msg, code, uri) {
+ Exception.call(this, msg);
+ this.code = code;
+ this.uri = uri;
+}
+ServerException.prototype = Object.create(Exception.prototype);
+ServerException.prototype.constructor = ServerException;