diff options
Diffstat (limited to 'tools/addon-sdk-1.5/packages/addon-kit/lib/request.js')
-rw-r--r-- | tools/addon-sdk-1.5/packages/addon-kit/lib/request.js | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.5/packages/addon-kit/lib/request.js b/tools/addon-sdk-1.5/packages/addon-kit/lib/request.js new file mode 100644 index 0000000..3704add --- /dev/null +++ b/tools/addon-sdk-1.5/packages/addon-kit/lib/request.js @@ -0,0 +1,276 @@ +/* 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/. */ + +"use strict"; +const xpcom = require("api-utils/xpcom"); +const xhr = require("api-utils/xhr"); +const errors = require("api-utils/errors"); +const apiUtils = require("api-utils/api-utils"); + +// Ugly but will fix with: https://bugzilla.mozilla.org/show_bug.cgi?id=596248 +const EventEmitter = require('api-utils/events').EventEmitter.compose({ + constructor: function EventEmitter() this +}); + +// Instead of creating a new validator for each request, just make one and reuse it. +const validator = new OptionsValidator({ + url: { + //XXXzpao should probably verify that url is a valid url as well + is: ["string"] + }, + headers: { + map: function (v) v || {}, + is: ["object"], + }, + content: { + map: function (v) v || null, + is: ["string", "object", "null"], + }, + contentType: { + map: function (v) v || "application/x-www-form-urlencoded", + is: ["string"], + }, + overrideMimeType: { + map: function(v) v || null, + is: ["string", "null"], + } +}); + +const REUSE_ERROR = "This request object has been used already. You must " + + "create a new one to make a new request." + +function Request(options) { + const self = EventEmitter(), + _public = self._public; + // request will hold the actual XHR object + let request; + let response; + + if ('onComplete' in options) + self.on('complete', options.onComplete) + options = validator.validateOptions(options); + + // function to prep the request since it's the same between GET and POST + function makeRequest(mode) { + // If this request has already been used, then we can't reuse it. Throw an error. + if (request) { + throw new Error(REUSE_ERROR); + } + + request = new xhr.XMLHttpRequest(); + + let url = options.url; + // Build the data to be set. For GET requests, we want to append that to + // the URL before opening the request. + let data = makeQueryString(options.content); + if (mode == "GET" && data) { + // If the URL already has ? in it, then we want to just use & + url = url + (/\?/.test(url) ? "&" : "?") + data; + } + + // open the request + request.open(mode, url); + + // request header must be set after open, but before send + request.setRequestHeader("Content-Type", options.contentType); + + // set other headers + for (let k in options.headers) { + request.setRequestHeader(k, options.headers[k]); + } + + // set overrideMimeType + if (options.overrideMimeType) { + request.overrideMimeType(options.overrideMimeType); + } + + // handle the readystate, create the response, and call the callback + request.onreadystatechange = function () { + if (request.readyState == 4) { + response = new Response(request); + errors.catchAndLog(function () { + self._emit('complete', response); + })(); + } + } + + // actually send the request. we only want to send data on POST requests + request.send(mode == "POST" ? data : null); + } + + // Map these setters/getters to the options + ["url", "headers", "content", "contentType"].forEach(function (k) { + _public.__defineGetter__(k, function () options[k]); + _public.__defineSetter__(k, function (v) { + // This will automatically rethrow errors from apiUtils.validateOptions. + return options[k] = validator.validateSingleOption(k, v); + }); + }); + + // response should be available as a getter + _public.__defineGetter__("response", function () response); + + _public.get = function () { + makeRequest("GET"); + return this; + }; + + _public.post = function () { + makeRequest("POST"); + return this; + }; + + return _public; +} +exports.Request = Request; + +// Converts an object of unordered key-vals to a string that can be passed +// as part of a request +function makeQueryString(content) { + // Explicitly return null if we have null, and empty string, or empty object. + if (!content) { + return null; + } + + // If content is already a string, just return it as is. + if (typeof(content) == "string") { + return content; + } + + // At this point we have a k:v object. Iterate over it and encode each value. + // Arrays and nested objects will get encoded as needed. For example... + // + // { foo: [1, 2, { omg: "bbq", "all your base!": "are belong to us" }], bar: "baz" } + // + // will be encoded as + // + // foo[0]=1&foo[1]=2&foo[2][omg]=bbq&foo[2][all+your+base!]=are+belong+to+us&bar=baz + // + // Keys (including "[" and "]") and values will be encoded with + // fixedEncodeURIComponent before returning. + // + // Execution was inspired by jQuery, but some details have changed and numeric + // array keys are included (whereas they are not in jQuery). + + let encodedContent = []; + function add(key, val) { + encodedContent.push(fixedEncodeURIComponent(key) + "=" + + fixedEncodeURIComponent(val)); + } + + function make(key, val) { + if (typeof(val) === "object" && val !== null) { + for ([k, v] in Iterator(val)) { + make(key + "[" + k + "]", v); + } + } + else { + add(key, val) + } + } + for ([k, v] in Iterator(content)) { + make(k, v); + } + return encodedContent.join("&"); + + //XXXzpao In theory, we can just use a FormData object on 1.9.3, but I had + // trouble getting that working. It would also be nice to stay + // backwards-compat as long as possible. Keeping this in for now... + // let formData = Cc["@mozilla.org/files/formdata;1"]. + // createInstance(Ci.nsIDOMFormData); + // for ([k, v] in Iterator(content)) { + // formData.append(k, v); + // } + // return formData; +} + + +// encodes a string safely for application/x-www-form-urlencoded +// adheres to RFC 3986 +// see https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Functions/encodeURIComponent +function fixedEncodeURIComponent (str) { + return encodeURIComponent(str).replace(/%20/g, "+").replace(/!/g, "%21"). + replace(/'/g, "%27").replace(/\(/g, "%28"). + replace(/\)/g, "%29").replace(/\*/g, "%2A"); +} + +function Response(request) { + // Define the straight mappings of our value to original request value + xpcom.utils.defineLazyGetter(this, "text", function () request.responseText); + xpcom.utils.defineLazyGetter(this, "xml", function () { + throw new Error("Sorry, the 'xml' property is no longer available. " + + "see bug 611042 for more information."); + }); + xpcom.utils.defineLazyGetter(this, "status", function () request.status); + xpcom.utils.defineLazyGetter(this, "statusText", function () request.statusText); + + // this.json should be the JS object, so we need to attempt to parse it. + xpcom.utils.defineLazyGetter(this, "json", function () { + let _json = null; + try { + _json = JSON.parse(this.text); + } + catch (e) {} + return _json; + }); + + // this.headers also should be a JS object, so we need to split up the raw + // headers string provided by the request. + xpcom.utils.defineLazyGetter(this, "headers", function () { + let _headers = {}; + let lastKey; + // Since getAllResponseHeaders() will return null if there are no headers, + // defend against it by defaulting to "" + let rawHeaders = request.getAllResponseHeaders() || ""; + rawHeaders.split("\n").forEach(function (h) { + // According to the HTTP spec, the header string is terminated by an empty + // line, so we can just skip it. + if (!h.length) { + return; + } + + let index = h.indexOf(":"); + // The spec allows for leading spaces, so instead of assuming a single + // leading space, just trim the values. + let key = h.substring(0, index).trim(), + val = h.substring(index + 1).trim(); + + // For empty keys, that means that the header value spanned multiple lines. + // In that case we should append the value to the value of lastKey with a + // new line. We'll assume lastKey will be set because there should never + // be an empty key on the first pass. + if (key) { + _headers[key] = val; + lastKey = key; + } + else { + _headers[lastKey] += "\n" + val; + } + }); + return _headers; + }) +} + +// apiUtils.validateOptions doesn't give the ability to easily validate single +// options, so this is a wrapper that provides that ability. +function OptionsValidator(rules) { + this.rules = rules; + + this.validateOptions = function (options) { + return apiUtils.validateOptions(options, this.rules); + } + + this.validateSingleOption = function (field, value) { + // We need to create a single rule object from our listed rules. To avoid + // JavaScript String warnings, check for the field & default to an empty object. + let singleRule = {}; + if (field in this.rules) { + singleRule[field] = this.rules[field]; + } + let singleOption = {}; + singleOption[field] = value; + // This should throw if it's invalid, which will bubble up & out. + return apiUtils.validateOptions(singleOption, singleRule)[field]; + } +} |