michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "stable" michael@0: }; michael@0: michael@0: const { ns } = require("./core/namespace"); michael@0: const { emit } = require("./event/core"); michael@0: const { merge } = require("./util/object"); michael@0: const { stringify } = require("./querystring"); michael@0: const { EventTarget } = require("./event/target"); michael@0: const { Class } = require("./core/heritage"); michael@0: const { XMLHttpRequest, forceAllowThirdPartyCookie } = require("./net/xhr"); michael@0: const apiUtils = require("./deprecated/api-utils"); michael@0: const { isValidURI } = require("./url.js"); michael@0: michael@0: const response = ns(); michael@0: const request = ns(); michael@0: michael@0: // Instead of creating a new validator for each request, just make one and michael@0: // reuse it. michael@0: const { validateOptions, validateSingleOption } = new OptionsValidator({ michael@0: url: { michael@0: // Also converts a URL instance to string, bug 857902 michael@0: map: function (url) url.toString(), michael@0: ok: isValidURI michael@0: }, michael@0: headers: { michael@0: map: function (v) v || {}, michael@0: is: ["object"], michael@0: }, michael@0: content: { michael@0: map: function (v) v || null, michael@0: is: ["string", "object", "null"], michael@0: }, michael@0: contentType: { michael@0: map: function (v) v || "application/x-www-form-urlencoded", michael@0: is: ["string"], michael@0: }, michael@0: overrideMimeType: { michael@0: map: function(v) v || null, michael@0: is: ["string", "null"], michael@0: } michael@0: }); michael@0: michael@0: const REUSE_ERROR = "This request object has been used already. You must " + michael@0: "create a new one to make a new request." michael@0: michael@0: // Utility function to prep the request since it's the same between michael@0: // request types michael@0: function runRequest(mode, target) { michael@0: let source = request(target) michael@0: let { xhr, url, content, contentType, headers, overrideMimeType } = source; michael@0: michael@0: let isGetOrHead = (mode == "GET" || mode == "HEAD"); michael@0: michael@0: // If this request has already been used, then we can't reuse it. michael@0: // Throw an error. michael@0: if (xhr) michael@0: throw new Error(REUSE_ERROR); michael@0: michael@0: xhr = source.xhr = new XMLHttpRequest(); michael@0: michael@0: // Build the data to be set. For GET or HEAD requests, we want to append that michael@0: // to the URL before opening the request. michael@0: let data = stringify(content); michael@0: // If the URL already has ? in it, then we want to just use & michael@0: if (isGetOrHead && data) michael@0: url = url + (/\?/.test(url) ? "&" : "?") + data; michael@0: michael@0: // open the request michael@0: xhr.open(mode, url); michael@0: michael@0: michael@0: forceAllowThirdPartyCookie(xhr); michael@0: michael@0: // request header must be set after open, but before send michael@0: xhr.setRequestHeader("Content-Type", contentType); michael@0: michael@0: // set other headers michael@0: Object.keys(headers).forEach(function(name) { michael@0: xhr.setRequestHeader(name, headers[name]); michael@0: }); michael@0: michael@0: // set overrideMimeType michael@0: if (overrideMimeType) michael@0: xhr.overrideMimeType(overrideMimeType); michael@0: michael@0: // handle the readystate, create the response, and call the callback michael@0: xhr.onreadystatechange = function onreadystatechange() { michael@0: if (xhr.readyState === 4) { michael@0: let response = Response(xhr); michael@0: source.response = response; michael@0: emit(target, 'complete', response); michael@0: } michael@0: }; michael@0: michael@0: // actually send the request. michael@0: // We don't want to send data on GET or HEAD requests. michael@0: xhr.send(!isGetOrHead ? data : null); michael@0: } michael@0: michael@0: const Request = Class({ michael@0: extends: EventTarget, michael@0: initialize: function initialize(options) { michael@0: // `EventTarget.initialize` will set event listeners that are named michael@0: // like `onEvent` in this case `onComplete` listener will be set to michael@0: // `complete` event. michael@0: EventTarget.prototype.initialize.call(this, options); michael@0: michael@0: // Copy normalized options. michael@0: merge(request(this), validateOptions(options)); michael@0: }, michael@0: get url() { return request(this).url; }, michael@0: set url(value) { request(this).url = validateSingleOption('url', value); }, michael@0: get headers() { return request(this).headers; }, michael@0: set headers(value) { michael@0: return request(this).headers = validateSingleOption('headers', value); michael@0: }, michael@0: get content() { return request(this).content; }, michael@0: set content(value) { michael@0: request(this).content = validateSingleOption('content', value); michael@0: }, michael@0: get contentType() { return request(this).contentType; }, michael@0: set contentType(value) { michael@0: request(this).contentType = validateSingleOption('contentType', value); michael@0: }, michael@0: get response() { return request(this).response; }, michael@0: delete: function() { michael@0: runRequest('DELETE', this); michael@0: return this; michael@0: }, michael@0: get: function() { michael@0: runRequest('GET', this); michael@0: return this; michael@0: }, michael@0: post: function() { michael@0: runRequest('POST', this); michael@0: return this; michael@0: }, michael@0: put: function() { michael@0: runRequest('PUT', this); michael@0: return this; michael@0: }, michael@0: head: function() { michael@0: runRequest('HEAD', this); michael@0: return this; michael@0: } michael@0: }); michael@0: exports.Request = Request; michael@0: michael@0: const Response = Class({ michael@0: initialize: function initialize(request) { michael@0: response(this).request = request; michael@0: }, michael@0: get text() response(this).request.responseText, michael@0: get xml() { michael@0: throw new Error("Sorry, the 'xml' property is no longer available. " + michael@0: "see bug 611042 for more information."); michael@0: }, michael@0: get status() response(this).request.status, michael@0: get statusText() response(this).request.statusText, michael@0: get json() { michael@0: try { michael@0: return JSON.parse(this.text); michael@0: } catch(error) { michael@0: return null; michael@0: } michael@0: }, michael@0: get headers() { michael@0: let headers = {}, lastKey; michael@0: // Since getAllResponseHeaders() will return null if there are no headers, michael@0: // defend against it by defaulting to "" michael@0: let rawHeaders = response(this).request.getAllResponseHeaders() || ""; michael@0: rawHeaders.split("\n").forEach(function (h) { michael@0: // According to the HTTP spec, the header string is terminated by an empty michael@0: // line, so we can just skip it. michael@0: if (!h.length) { michael@0: return; michael@0: } michael@0: michael@0: let index = h.indexOf(":"); michael@0: // The spec allows for leading spaces, so instead of assuming a single michael@0: // leading space, just trim the values. michael@0: let key = h.substring(0, index).trim(), michael@0: val = h.substring(index + 1).trim(); michael@0: michael@0: // For empty keys, that means that the header value spanned multiple lines. michael@0: // In that case we should append the value to the value of lastKey with a michael@0: // new line. We'll assume lastKey will be set because there should never michael@0: // be an empty key on the first pass. michael@0: if (key) { michael@0: headers[key] = val; michael@0: lastKey = key; michael@0: } michael@0: else { michael@0: headers[lastKey] += "\n" + val; michael@0: } michael@0: }); michael@0: return headers; michael@0: } michael@0: }); michael@0: michael@0: // apiUtils.validateOptions doesn't give the ability to easily validate single michael@0: // options, so this is a wrapper that provides that ability. michael@0: function OptionsValidator(rules) { michael@0: return { michael@0: validateOptions: function (options) { michael@0: return apiUtils.validateOptions(options, rules); michael@0: }, michael@0: validateSingleOption: function (field, value) { michael@0: // We need to create a single rule object from our listed rules. To avoid michael@0: // JavaScript String warnings, check for the field & default to an empty object. michael@0: let singleRule = {}; michael@0: if (field in rules) { michael@0: singleRule[field] = rules[field]; michael@0: } michael@0: let singleOption = {}; michael@0: singleOption[field] = value; michael@0: // This should throw if it's invalid, which will bubble up & out. michael@0: return apiUtils.validateOptions(singleOption, singleRule)[field]; michael@0: } michael@0: }; michael@0: }