Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | module.metadata = { |
michael@0 | 8 | "stability": "stable" |
michael@0 | 9 | }; |
michael@0 | 10 | |
michael@0 | 11 | const { ns } = require("./core/namespace"); |
michael@0 | 12 | const { emit } = require("./event/core"); |
michael@0 | 13 | const { merge } = require("./util/object"); |
michael@0 | 14 | const { stringify } = require("./querystring"); |
michael@0 | 15 | const { EventTarget } = require("./event/target"); |
michael@0 | 16 | const { Class } = require("./core/heritage"); |
michael@0 | 17 | const { XMLHttpRequest, forceAllowThirdPartyCookie } = require("./net/xhr"); |
michael@0 | 18 | const apiUtils = require("./deprecated/api-utils"); |
michael@0 | 19 | const { isValidURI } = require("./url.js"); |
michael@0 | 20 | |
michael@0 | 21 | const response = ns(); |
michael@0 | 22 | const request = ns(); |
michael@0 | 23 | |
michael@0 | 24 | // Instead of creating a new validator for each request, just make one and |
michael@0 | 25 | // reuse it. |
michael@0 | 26 | const { validateOptions, validateSingleOption } = new OptionsValidator({ |
michael@0 | 27 | url: { |
michael@0 | 28 | // Also converts a URL instance to string, bug 857902 |
michael@0 | 29 | map: function (url) url.toString(), |
michael@0 | 30 | ok: isValidURI |
michael@0 | 31 | }, |
michael@0 | 32 | headers: { |
michael@0 | 33 | map: function (v) v || {}, |
michael@0 | 34 | is: ["object"], |
michael@0 | 35 | }, |
michael@0 | 36 | content: { |
michael@0 | 37 | map: function (v) v || null, |
michael@0 | 38 | is: ["string", "object", "null"], |
michael@0 | 39 | }, |
michael@0 | 40 | contentType: { |
michael@0 | 41 | map: function (v) v || "application/x-www-form-urlencoded", |
michael@0 | 42 | is: ["string"], |
michael@0 | 43 | }, |
michael@0 | 44 | overrideMimeType: { |
michael@0 | 45 | map: function(v) v || null, |
michael@0 | 46 | is: ["string", "null"], |
michael@0 | 47 | } |
michael@0 | 48 | }); |
michael@0 | 49 | |
michael@0 | 50 | const REUSE_ERROR = "This request object has been used already. You must " + |
michael@0 | 51 | "create a new one to make a new request." |
michael@0 | 52 | |
michael@0 | 53 | // Utility function to prep the request since it's the same between |
michael@0 | 54 | // request types |
michael@0 | 55 | function runRequest(mode, target) { |
michael@0 | 56 | let source = request(target) |
michael@0 | 57 | let { xhr, url, content, contentType, headers, overrideMimeType } = source; |
michael@0 | 58 | |
michael@0 | 59 | let isGetOrHead = (mode == "GET" || mode == "HEAD"); |
michael@0 | 60 | |
michael@0 | 61 | // If this request has already been used, then we can't reuse it. |
michael@0 | 62 | // Throw an error. |
michael@0 | 63 | if (xhr) |
michael@0 | 64 | throw new Error(REUSE_ERROR); |
michael@0 | 65 | |
michael@0 | 66 | xhr = source.xhr = new XMLHttpRequest(); |
michael@0 | 67 | |
michael@0 | 68 | // Build the data to be set. For GET or HEAD requests, we want to append that |
michael@0 | 69 | // to the URL before opening the request. |
michael@0 | 70 | let data = stringify(content); |
michael@0 | 71 | // If the URL already has ? in it, then we want to just use & |
michael@0 | 72 | if (isGetOrHead && data) |
michael@0 | 73 | url = url + (/\?/.test(url) ? "&" : "?") + data; |
michael@0 | 74 | |
michael@0 | 75 | // open the request |
michael@0 | 76 | xhr.open(mode, url); |
michael@0 | 77 | |
michael@0 | 78 | |
michael@0 | 79 | forceAllowThirdPartyCookie(xhr); |
michael@0 | 80 | |
michael@0 | 81 | // request header must be set after open, but before send |
michael@0 | 82 | xhr.setRequestHeader("Content-Type", contentType); |
michael@0 | 83 | |
michael@0 | 84 | // set other headers |
michael@0 | 85 | Object.keys(headers).forEach(function(name) { |
michael@0 | 86 | xhr.setRequestHeader(name, headers[name]); |
michael@0 | 87 | }); |
michael@0 | 88 | |
michael@0 | 89 | // set overrideMimeType |
michael@0 | 90 | if (overrideMimeType) |
michael@0 | 91 | xhr.overrideMimeType(overrideMimeType); |
michael@0 | 92 | |
michael@0 | 93 | // handle the readystate, create the response, and call the callback |
michael@0 | 94 | xhr.onreadystatechange = function onreadystatechange() { |
michael@0 | 95 | if (xhr.readyState === 4) { |
michael@0 | 96 | let response = Response(xhr); |
michael@0 | 97 | source.response = response; |
michael@0 | 98 | emit(target, 'complete', response); |
michael@0 | 99 | } |
michael@0 | 100 | }; |
michael@0 | 101 | |
michael@0 | 102 | // actually send the request. |
michael@0 | 103 | // We don't want to send data on GET or HEAD requests. |
michael@0 | 104 | xhr.send(!isGetOrHead ? data : null); |
michael@0 | 105 | } |
michael@0 | 106 | |
michael@0 | 107 | const Request = Class({ |
michael@0 | 108 | extends: EventTarget, |
michael@0 | 109 | initialize: function initialize(options) { |
michael@0 | 110 | // `EventTarget.initialize` will set event listeners that are named |
michael@0 | 111 | // like `onEvent` in this case `onComplete` listener will be set to |
michael@0 | 112 | // `complete` event. |
michael@0 | 113 | EventTarget.prototype.initialize.call(this, options); |
michael@0 | 114 | |
michael@0 | 115 | // Copy normalized options. |
michael@0 | 116 | merge(request(this), validateOptions(options)); |
michael@0 | 117 | }, |
michael@0 | 118 | get url() { return request(this).url; }, |
michael@0 | 119 | set url(value) { request(this).url = validateSingleOption('url', value); }, |
michael@0 | 120 | get headers() { return request(this).headers; }, |
michael@0 | 121 | set headers(value) { |
michael@0 | 122 | return request(this).headers = validateSingleOption('headers', value); |
michael@0 | 123 | }, |
michael@0 | 124 | get content() { return request(this).content; }, |
michael@0 | 125 | set content(value) { |
michael@0 | 126 | request(this).content = validateSingleOption('content', value); |
michael@0 | 127 | }, |
michael@0 | 128 | get contentType() { return request(this).contentType; }, |
michael@0 | 129 | set contentType(value) { |
michael@0 | 130 | request(this).contentType = validateSingleOption('contentType', value); |
michael@0 | 131 | }, |
michael@0 | 132 | get response() { return request(this).response; }, |
michael@0 | 133 | delete: function() { |
michael@0 | 134 | runRequest('DELETE', this); |
michael@0 | 135 | return this; |
michael@0 | 136 | }, |
michael@0 | 137 | get: function() { |
michael@0 | 138 | runRequest('GET', this); |
michael@0 | 139 | return this; |
michael@0 | 140 | }, |
michael@0 | 141 | post: function() { |
michael@0 | 142 | runRequest('POST', this); |
michael@0 | 143 | return this; |
michael@0 | 144 | }, |
michael@0 | 145 | put: function() { |
michael@0 | 146 | runRequest('PUT', this); |
michael@0 | 147 | return this; |
michael@0 | 148 | }, |
michael@0 | 149 | head: function() { |
michael@0 | 150 | runRequest('HEAD', this); |
michael@0 | 151 | return this; |
michael@0 | 152 | } |
michael@0 | 153 | }); |
michael@0 | 154 | exports.Request = Request; |
michael@0 | 155 | |
michael@0 | 156 | const Response = Class({ |
michael@0 | 157 | initialize: function initialize(request) { |
michael@0 | 158 | response(this).request = request; |
michael@0 | 159 | }, |
michael@0 | 160 | get text() response(this).request.responseText, |
michael@0 | 161 | get xml() { |
michael@0 | 162 | throw new Error("Sorry, the 'xml' property is no longer available. " + |
michael@0 | 163 | "see bug 611042 for more information."); |
michael@0 | 164 | }, |
michael@0 | 165 | get status() response(this).request.status, |
michael@0 | 166 | get statusText() response(this).request.statusText, |
michael@0 | 167 | get json() { |
michael@0 | 168 | try { |
michael@0 | 169 | return JSON.parse(this.text); |
michael@0 | 170 | } catch(error) { |
michael@0 | 171 | return null; |
michael@0 | 172 | } |
michael@0 | 173 | }, |
michael@0 | 174 | get headers() { |
michael@0 | 175 | let headers = {}, lastKey; |
michael@0 | 176 | // Since getAllResponseHeaders() will return null if there are no headers, |
michael@0 | 177 | // defend against it by defaulting to "" |
michael@0 | 178 | let rawHeaders = response(this).request.getAllResponseHeaders() || ""; |
michael@0 | 179 | rawHeaders.split("\n").forEach(function (h) { |
michael@0 | 180 | // According to the HTTP spec, the header string is terminated by an empty |
michael@0 | 181 | // line, so we can just skip it. |
michael@0 | 182 | if (!h.length) { |
michael@0 | 183 | return; |
michael@0 | 184 | } |
michael@0 | 185 | |
michael@0 | 186 | let index = h.indexOf(":"); |
michael@0 | 187 | // The spec allows for leading spaces, so instead of assuming a single |
michael@0 | 188 | // leading space, just trim the values. |
michael@0 | 189 | let key = h.substring(0, index).trim(), |
michael@0 | 190 | val = h.substring(index + 1).trim(); |
michael@0 | 191 | |
michael@0 | 192 | // For empty keys, that means that the header value spanned multiple lines. |
michael@0 | 193 | // In that case we should append the value to the value of lastKey with a |
michael@0 | 194 | // new line. We'll assume lastKey will be set because there should never |
michael@0 | 195 | // be an empty key on the first pass. |
michael@0 | 196 | if (key) { |
michael@0 | 197 | headers[key] = val; |
michael@0 | 198 | lastKey = key; |
michael@0 | 199 | } |
michael@0 | 200 | else { |
michael@0 | 201 | headers[lastKey] += "\n" + val; |
michael@0 | 202 | } |
michael@0 | 203 | }); |
michael@0 | 204 | return headers; |
michael@0 | 205 | } |
michael@0 | 206 | }); |
michael@0 | 207 | |
michael@0 | 208 | // apiUtils.validateOptions doesn't give the ability to easily validate single |
michael@0 | 209 | // options, so this is a wrapper that provides that ability. |
michael@0 | 210 | function OptionsValidator(rules) { |
michael@0 | 211 | return { |
michael@0 | 212 | validateOptions: function (options) { |
michael@0 | 213 | return apiUtils.validateOptions(options, rules); |
michael@0 | 214 | }, |
michael@0 | 215 | validateSingleOption: function (field, value) { |
michael@0 | 216 | // We need to create a single rule object from our listed rules. To avoid |
michael@0 | 217 | // JavaScript String warnings, check for the field & default to an empty object. |
michael@0 | 218 | let singleRule = {}; |
michael@0 | 219 | if (field in rules) { |
michael@0 | 220 | singleRule[field] = rules[field]; |
michael@0 | 221 | } |
michael@0 | 222 | let singleOption = {}; |
michael@0 | 223 | singleOption[field] = value; |
michael@0 | 224 | // This should throw if it's invalid, which will bubble up & out. |
michael@0 | 225 | return apiUtils.validateOptions(singleOption, singleRule)[field]; |
michael@0 | 226 | } |
michael@0 | 227 | }; |
michael@0 | 228 | } |