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 +}