addon-sdk/source/lib/sdk/request.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/request.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,228 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +module.metadata = {
    1.11 +  "stability": "stable"
    1.12 +};
    1.13 +
    1.14 +const { ns } = require("./core/namespace");
    1.15 +const { emit } = require("./event/core");
    1.16 +const { merge } = require("./util/object");
    1.17 +const { stringify } = require("./querystring");
    1.18 +const { EventTarget } = require("./event/target");
    1.19 +const { Class } = require("./core/heritage");
    1.20 +const { XMLHttpRequest, forceAllowThirdPartyCookie } = require("./net/xhr");
    1.21 +const apiUtils = require("./deprecated/api-utils");
    1.22 +const { isValidURI } = require("./url.js");
    1.23 +
    1.24 +const response = ns();
    1.25 +const request = ns();
    1.26 +
    1.27 +// Instead of creating a new validator for each request, just make one and
    1.28 +// reuse it.
    1.29 +const { validateOptions, validateSingleOption } = new OptionsValidator({
    1.30 +  url: {
    1.31 +    // Also converts a URL instance to string, bug 857902
    1.32 +    map: function (url) url.toString(),
    1.33 +    ok: isValidURI
    1.34 +  },
    1.35 +  headers: {
    1.36 +    map: function (v) v || {},
    1.37 +    is:  ["object"],
    1.38 +  },
    1.39 +  content: {
    1.40 +    map: function (v) v || null,
    1.41 +    is:  ["string", "object", "null"],
    1.42 +  },
    1.43 +  contentType: {
    1.44 +    map: function (v) v || "application/x-www-form-urlencoded",
    1.45 +    is:  ["string"],
    1.46 +  },
    1.47 +  overrideMimeType: {
    1.48 +    map: function(v) v || null,
    1.49 +    is: ["string", "null"],
    1.50 +  }
    1.51 +});
    1.52 +
    1.53 +const REUSE_ERROR = "This request object has been used already. You must " +
    1.54 +                    "create a new one to make a new request."
    1.55 +
    1.56 +// Utility function to prep the request since it's the same between
    1.57 +// request types
    1.58 +function runRequest(mode, target) {
    1.59 +  let source = request(target)
    1.60 +  let { xhr, url, content, contentType, headers, overrideMimeType } = source;
    1.61 +
    1.62 +  let isGetOrHead = (mode == "GET" || mode == "HEAD");
    1.63 +
    1.64 +  // If this request has already been used, then we can't reuse it.
    1.65 +  // Throw an error.
    1.66 +  if (xhr)
    1.67 +    throw new Error(REUSE_ERROR);
    1.68 +
    1.69 +  xhr = source.xhr = new XMLHttpRequest();
    1.70 +
    1.71 +  // Build the data to be set. For GET or HEAD requests, we want to append that
    1.72 +  // to the URL before opening the request.
    1.73 +  let data = stringify(content);
    1.74 +  // If the URL already has ? in it, then we want to just use &
    1.75 +  if (isGetOrHead && data)
    1.76 +    url = url + (/\?/.test(url) ? "&" : "?") + data;
    1.77 +
    1.78 +  // open the request
    1.79 +  xhr.open(mode, url);
    1.80 +
    1.81 +
    1.82 +  forceAllowThirdPartyCookie(xhr);
    1.83 +
    1.84 +  // request header must be set after open, but before send
    1.85 +  xhr.setRequestHeader("Content-Type", contentType);
    1.86 +
    1.87 +  // set other headers
    1.88 +  Object.keys(headers).forEach(function(name) {
    1.89 +    xhr.setRequestHeader(name, headers[name]);
    1.90 +  });
    1.91 +
    1.92 +  // set overrideMimeType
    1.93 +  if (overrideMimeType)
    1.94 +    xhr.overrideMimeType(overrideMimeType);
    1.95 +
    1.96 +  // handle the readystate, create the response, and call the callback
    1.97 +  xhr.onreadystatechange = function onreadystatechange() {
    1.98 +    if (xhr.readyState === 4) {
    1.99 +      let response = Response(xhr);
   1.100 +      source.response = response;
   1.101 +      emit(target, 'complete', response);
   1.102 +    }
   1.103 +  };
   1.104 +
   1.105 +  // actually send the request.
   1.106 +  // We don't want to send data on GET or HEAD requests.
   1.107 +  xhr.send(!isGetOrHead ? data : null);
   1.108 +}
   1.109 +
   1.110 +const Request = Class({
   1.111 +  extends: EventTarget,
   1.112 +  initialize: function initialize(options) {
   1.113 +    // `EventTarget.initialize` will set event listeners that are named
   1.114 +    // like `onEvent` in this case `onComplete` listener will be set to
   1.115 +    // `complete` event.
   1.116 +    EventTarget.prototype.initialize.call(this, options);
   1.117 +
   1.118 +    // Copy normalized options.
   1.119 +    merge(request(this), validateOptions(options));
   1.120 +  },
   1.121 +  get url() { return request(this).url; },
   1.122 +  set url(value) { request(this).url = validateSingleOption('url', value); },
   1.123 +  get headers() { return request(this).headers; },
   1.124 +  set headers(value) {
   1.125 +    return request(this).headers = validateSingleOption('headers', value);
   1.126 +  },
   1.127 +  get content() { return request(this).content; },
   1.128 +  set content(value) {
   1.129 +    request(this).content = validateSingleOption('content', value);
   1.130 +  },
   1.131 +  get contentType() { return request(this).contentType; },
   1.132 +  set contentType(value) {
   1.133 +    request(this).contentType = validateSingleOption('contentType', value);
   1.134 +  },
   1.135 +  get response() { return request(this).response; },
   1.136 +  delete: function() {
   1.137 +    runRequest('DELETE', this);
   1.138 +    return this;
   1.139 +  },
   1.140 +  get: function() {
   1.141 +    runRequest('GET', this);
   1.142 +    return this;
   1.143 +  },
   1.144 +  post: function() {
   1.145 +    runRequest('POST', this);
   1.146 +    return this;
   1.147 +  },
   1.148 +  put: function() {
   1.149 +    runRequest('PUT', this);
   1.150 +    return this;
   1.151 +  },
   1.152 +  head: function() {
   1.153 +    runRequest('HEAD', this);
   1.154 +    return this;
   1.155 +  }
   1.156 +});
   1.157 +exports.Request = Request;
   1.158 +
   1.159 +const Response = Class({
   1.160 +  initialize: function initialize(request) {
   1.161 +    response(this).request = request;
   1.162 +  },
   1.163 +  get text() response(this).request.responseText,
   1.164 +  get xml() {
   1.165 +    throw new Error("Sorry, the 'xml' property is no longer available. " +
   1.166 +                    "see bug 611042 for more information.");
   1.167 +  },
   1.168 +  get status() response(this).request.status,
   1.169 +  get statusText() response(this).request.statusText,
   1.170 +  get json() {
   1.171 +    try {
   1.172 +      return JSON.parse(this.text);
   1.173 +    } catch(error) {
   1.174 +      return null;
   1.175 +    }
   1.176 +  },
   1.177 +  get headers() {
   1.178 +    let headers = {}, lastKey;
   1.179 +    // Since getAllResponseHeaders() will return null if there are no headers,
   1.180 +    // defend against it by defaulting to ""
   1.181 +    let rawHeaders = response(this).request.getAllResponseHeaders() || "";
   1.182 +    rawHeaders.split("\n").forEach(function (h) {
   1.183 +      // According to the HTTP spec, the header string is terminated by an empty
   1.184 +      // line, so we can just skip it.
   1.185 +      if (!h.length) {
   1.186 +        return;
   1.187 +      }
   1.188 +
   1.189 +      let index = h.indexOf(":");
   1.190 +      // The spec allows for leading spaces, so instead of assuming a single
   1.191 +      // leading space, just trim the values.
   1.192 +      let key = h.substring(0, index).trim(),
   1.193 +          val = h.substring(index + 1).trim();
   1.194 +
   1.195 +      // For empty keys, that means that the header value spanned multiple lines.
   1.196 +      // In that case we should append the value to the value of lastKey with a
   1.197 +      // new line. We'll assume lastKey will be set because there should never
   1.198 +      // be an empty key on the first pass.
   1.199 +      if (key) {
   1.200 +        headers[key] = val;
   1.201 +        lastKey = key;
   1.202 +      }
   1.203 +      else {
   1.204 +        headers[lastKey] += "\n" + val;
   1.205 +      }
   1.206 +    });
   1.207 +    return headers;
   1.208 +  }
   1.209 +});
   1.210 +
   1.211 +// apiUtils.validateOptions doesn't give the ability to easily validate single
   1.212 +// options, so this is a wrapper that provides that ability.
   1.213 +function OptionsValidator(rules) {
   1.214 +  return {
   1.215 +    validateOptions: function (options) {
   1.216 +      return apiUtils.validateOptions(options, rules);
   1.217 +    },
   1.218 +    validateSingleOption: function (field, value) {
   1.219 +      // We need to create a single rule object from our listed rules. To avoid
   1.220 +      // JavaScript String warnings, check for the field & default to an empty object.
   1.221 +      let singleRule = {};
   1.222 +      if (field in rules) {
   1.223 +        singleRule[field] = rules[field];
   1.224 +      }
   1.225 +      let singleOption = {};
   1.226 +      singleOption[field] = value;
   1.227 +      // This should throw if it's invalid, which will bubble up & out.
   1.228 +      return apiUtils.validateOptions(singleOption, singleRule)[field];
   1.229 +    }
   1.230 +  };
   1.231 +}

mercurial