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 | "use strict"; |
michael@0 | 5 | |
michael@0 | 6 | module.metadata = { |
michael@0 | 7 | "stability": "stable" |
michael@0 | 8 | }; |
michael@0 | 9 | |
michael@0 | 10 | const observers = require('./system/events'); |
michael@0 | 11 | const { contract: loaderContract } = require('./content/loader'); |
michael@0 | 12 | const { contract } = require('./util/contract'); |
michael@0 | 13 | const { getAttachEventType, WorkerHost } = require('./content/utils'); |
michael@0 | 14 | const { Class } = require('./core/heritage'); |
michael@0 | 15 | const { Disposable } = require('./core/disposable'); |
michael@0 | 16 | const { WeakReference } = require('./core/reference'); |
michael@0 | 17 | const { Worker } = require('./content/worker'); |
michael@0 | 18 | const { EventTarget } = require('./event/target'); |
michael@0 | 19 | const { on, emit, once, setListeners } = require('./event/core'); |
michael@0 | 20 | const { on: domOn, removeListener: domOff } = require('./dom/events'); |
michael@0 | 21 | const { pipe } = require('./event/utils'); |
michael@0 | 22 | const { isRegExp } = require('./lang/type'); |
michael@0 | 23 | const { merge } = require('./util/object'); |
michael@0 | 24 | const { windowIterator } = require('./deprecated/window-utils'); |
michael@0 | 25 | const { isBrowser, getFrames } = require('./window/utils'); |
michael@0 | 26 | const { getTabs, getTabContentWindow, getTabForContentWindow, |
michael@0 | 27 | getURI: getTabURI } = require('./tabs/utils'); |
michael@0 | 28 | const { ignoreWindow } = require('sdk/private-browsing/utils'); |
michael@0 | 29 | const { Style } = require("./stylesheet/style"); |
michael@0 | 30 | const { attach, detach } = require("./content/mod"); |
michael@0 | 31 | const { has, hasAny } = require("./util/array"); |
michael@0 | 32 | const { Rules } = require("./util/rules"); |
michael@0 | 33 | const { List, addListItem, removeListItem } = require('./util/list'); |
michael@0 | 34 | const { when: unload } = require("./system/unload"); |
michael@0 | 35 | |
michael@0 | 36 | // Valid values for `attachTo` option |
michael@0 | 37 | const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame']; |
michael@0 | 38 | |
michael@0 | 39 | const pagemods = new Set(); |
michael@0 | 40 | const workers = new WeakMap(); |
michael@0 | 41 | const styles = new WeakMap(); |
michael@0 | 42 | const models = new WeakMap(); |
michael@0 | 43 | let modelFor = (mod) => models.get(mod); |
michael@0 | 44 | let workerFor = (mod) => workers.get(mod); |
michael@0 | 45 | let styleFor = (mod) => styles.get(mod); |
michael@0 | 46 | |
michael@0 | 47 | // Bind observer |
michael@0 | 48 | observers.on('document-element-inserted', onContentWindow); |
michael@0 | 49 | unload(() => observers.off('document-element-inserted', onContentWindow)); |
michael@0 | 50 | |
michael@0 | 51 | let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string'; |
michael@0 | 52 | |
michael@0 | 53 | // Validation Contracts |
michael@0 | 54 | const modOptions = { |
michael@0 | 55 | // contentStyle* / contentScript* are sharing the same validation constraints, |
michael@0 | 56 | // so they can be mostly reused, except for the messages. |
michael@0 | 57 | contentStyle: merge(Object.create(loaderContract.rules.contentScript), { |
michael@0 | 58 | msg: 'The `contentStyle` option must be a string or an array of strings.' |
michael@0 | 59 | }), |
michael@0 | 60 | contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { |
michael@0 | 61 | msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' |
michael@0 | 62 | }), |
michael@0 | 63 | include: { |
michael@0 | 64 | is: ['string', 'array', 'regexp'], |
michael@0 | 65 | ok: (rule) => { |
michael@0 | 66 | if (isRegExpOrString(rule)) |
michael@0 | 67 | return true; |
michael@0 | 68 | if (Array.isArray(rule) && rule.length > 0) |
michael@0 | 69 | return rule.every(isRegExpOrString); |
michael@0 | 70 | return false; |
michael@0 | 71 | }, |
michael@0 | 72 | msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.' |
michael@0 | 73 | }, |
michael@0 | 74 | attachTo: { |
michael@0 | 75 | is: ['string', 'array', 'undefined'], |
michael@0 | 76 | map: function (attachTo) { |
michael@0 | 77 | if (!attachTo) return ['top', 'frame']; |
michael@0 | 78 | if (typeof attachTo === 'string') return [attachTo]; |
michael@0 | 79 | return attachTo; |
michael@0 | 80 | }, |
michael@0 | 81 | ok: function (attachTo) { |
michael@0 | 82 | return hasAny(attachTo, ['top', 'frame']) && |
michael@0 | 83 | attachTo.every(has.bind(null, ['top', 'frame', 'existing'])); |
michael@0 | 84 | }, |
michael@0 | 85 | msg: 'The `attachTo` option must be a string or an array of strings. ' + |
michael@0 | 86 | 'The only valid options are "existing", "top" and "frame", and must ' + |
michael@0 | 87 | 'contain at least "top" or "frame" values.' |
michael@0 | 88 | }, |
michael@0 | 89 | }; |
michael@0 | 90 | |
michael@0 | 91 | const modContract = contract(merge({}, loaderContract.rules, modOptions)); |
michael@0 | 92 | |
michael@0 | 93 | /** |
michael@0 | 94 | * PageMod constructor (exported below). |
michael@0 | 95 | * @constructor |
michael@0 | 96 | */ |
michael@0 | 97 | const PageMod = Class({ |
michael@0 | 98 | implements: [ |
michael@0 | 99 | modContract.properties(modelFor), |
michael@0 | 100 | EventTarget, |
michael@0 | 101 | Disposable, |
michael@0 | 102 | WeakReference |
michael@0 | 103 | ], |
michael@0 | 104 | extends: WorkerHost(workerFor), |
michael@0 | 105 | setup: function PageMod(options) { |
michael@0 | 106 | let mod = this; |
michael@0 | 107 | let model = modContract(options); |
michael@0 | 108 | models.set(this, model); |
michael@0 | 109 | |
michael@0 | 110 | // Set listeners on {PageMod} itself, not the underlying worker, |
michael@0 | 111 | // like `onMessage`, as it'll get piped. |
michael@0 | 112 | setListeners(this, options); |
michael@0 | 113 | |
michael@0 | 114 | let include = model.include; |
michael@0 | 115 | model.include = Rules(); |
michael@0 | 116 | model.include.add.apply(model.include, [].concat(include)); |
michael@0 | 117 | |
michael@0 | 118 | if (model.contentStyle || model.contentStyleFile) { |
michael@0 | 119 | styles.set(mod, Style({ |
michael@0 | 120 | uri: model.contentStyleFile, |
michael@0 | 121 | source: model.contentStyle |
michael@0 | 122 | })); |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | pagemods.add(this); |
michael@0 | 126 | |
michael@0 | 127 | // `applyOnExistingDocuments` has to be called after `pagemods.add()` |
michael@0 | 128 | // otherwise its calls to `onContent` method won't do anything. |
michael@0 | 129 | if (has(model.attachTo, 'existing')) |
michael@0 | 130 | applyOnExistingDocuments(mod); |
michael@0 | 131 | }, |
michael@0 | 132 | |
michael@0 | 133 | dispose: function() { |
michael@0 | 134 | let style = styleFor(this); |
michael@0 | 135 | if (style) |
michael@0 | 136 | detach(style); |
michael@0 | 137 | |
michael@0 | 138 | for (let i in this.include) |
michael@0 | 139 | this.include.remove(this.include[i]); |
michael@0 | 140 | |
michael@0 | 141 | pagemods.delete(this); |
michael@0 | 142 | } |
michael@0 | 143 | }); |
michael@0 | 144 | exports.PageMod = PageMod; |
michael@0 | 145 | |
michael@0 | 146 | function onContentWindow({ subject: document }) { |
michael@0 | 147 | // Return if we have no pagemods |
michael@0 | 148 | if (pagemods.size === 0) |
michael@0 | 149 | return; |
michael@0 | 150 | |
michael@0 | 151 | let window = document.defaultView; |
michael@0 | 152 | // XML documents don't have windows, and we don't yet support them. |
michael@0 | 153 | if (!window) |
michael@0 | 154 | return; |
michael@0 | 155 | // We apply only on documents in tabs of Firefox |
michael@0 | 156 | if (!getTabForContentWindow(window)) |
michael@0 | 157 | return; |
michael@0 | 158 | |
michael@0 | 159 | // When the tab is private, only addons with 'private-browsing' flag in |
michael@0 | 160 | // their package.json can apply content script to private documents |
michael@0 | 161 | if (ignoreWindow(window)) |
michael@0 | 162 | return; |
michael@0 | 163 | |
michael@0 | 164 | for (let pagemod of pagemods) { |
michael@0 | 165 | if (pagemod.include.matchesAny(document.URL)) |
michael@0 | 166 | onContent(pagemod, window); |
michael@0 | 167 | } |
michael@0 | 168 | } |
michael@0 | 169 | |
michael@0 | 170 | // Returns all tabs on all currently opened windows |
michael@0 | 171 | function getAllTabs() { |
michael@0 | 172 | let tabs = []; |
michael@0 | 173 | // Iterate over all chrome windows |
michael@0 | 174 | for (let window in windowIterator()) { |
michael@0 | 175 | if (!isBrowser(window)) |
michael@0 | 176 | continue; |
michael@0 | 177 | tabs = tabs.concat(getTabs(window)); |
michael@0 | 178 | } |
michael@0 | 179 | return tabs; |
michael@0 | 180 | } |
michael@0 | 181 | |
michael@0 | 182 | function applyOnExistingDocuments (mod) { |
michael@0 | 183 | let tabs = getAllTabs(); |
michael@0 | 184 | |
michael@0 | 185 | tabs.forEach(function (tab) { |
michael@0 | 186 | // Fake a newly created document |
michael@0 | 187 | let window = getTabContentWindow(tab); |
michael@0 | 188 | if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab))) |
michael@0 | 189 | onContent(mod, window); |
michael@0 | 190 | if (has(mod.attachTo, "frame")) { |
michael@0 | 191 | getFrames(window). |
michael@0 | 192 | filter((iframe) => mod.include.matchesAny(iframe.location.href)). |
michael@0 | 193 | forEach((frame) => onContent(mod, frame)); |
michael@0 | 194 | } |
michael@0 | 195 | }); |
michael@0 | 196 | } |
michael@0 | 197 | |
michael@0 | 198 | function createWorker (mod, window) { |
michael@0 | 199 | let worker = Worker({ |
michael@0 | 200 | window: window, |
michael@0 | 201 | contentScript: mod.contentScript, |
michael@0 | 202 | contentScriptFile: mod.contentScriptFile, |
michael@0 | 203 | contentScriptOptions: mod.contentScriptOptions, |
michael@0 | 204 | // Bug 980468: Syntax errors from scripts can happen before the worker |
michael@0 | 205 | // can set up an error handler. They are per-mod rather than per-worker |
michael@0 | 206 | // so are best handled at the mod level. |
michael@0 | 207 | onError: (e) => emit(mod, 'error', e) |
michael@0 | 208 | }); |
michael@0 | 209 | workers.set(mod, worker); |
michael@0 | 210 | pipe(worker, mod); |
michael@0 | 211 | emit(mod, 'attach', worker); |
michael@0 | 212 | once(worker, 'detach', function detach() { |
michael@0 | 213 | worker.destroy(); |
michael@0 | 214 | }); |
michael@0 | 215 | } |
michael@0 | 216 | |
michael@0 | 217 | function onContent (mod, window) { |
michael@0 | 218 | // not registered yet |
michael@0 | 219 | if (!pagemods.has(mod)) |
michael@0 | 220 | return; |
michael@0 | 221 | |
michael@0 | 222 | let isTopDocument = window.top === window; |
michael@0 | 223 | // Is a top level document and `top` is not set, ignore |
michael@0 | 224 | if (isTopDocument && !has(mod.attachTo, "top")) |
michael@0 | 225 | return; |
michael@0 | 226 | // Is a frame document and `frame` is not set, ignore |
michael@0 | 227 | if (!isTopDocument && !has(mod.attachTo, "frame")) |
michael@0 | 228 | return; |
michael@0 | 229 | |
michael@0 | 230 | let style = styleFor(mod); |
michael@0 | 231 | if (style) |
michael@0 | 232 | attach(style, window); |
michael@0 | 233 | |
michael@0 | 234 | // Immediatly evaluate content script if the document state is already |
michael@0 | 235 | // matching contentScriptWhen expectations |
michael@0 | 236 | if (isMatchingAttachState(mod, window)) { |
michael@0 | 237 | createWorker(mod, window); |
michael@0 | 238 | return; |
michael@0 | 239 | } |
michael@0 | 240 | |
michael@0 | 241 | let eventName = getAttachEventType(mod) || 'load'; |
michael@0 | 242 | domOn(window, eventName, function onReady (e) { |
michael@0 | 243 | if (e.target.defaultView !== window) |
michael@0 | 244 | return; |
michael@0 | 245 | domOff(window, eventName, onReady, true); |
michael@0 | 246 | createWorker(mod, window); |
michael@0 | 247 | }, true); |
michael@0 | 248 | } |
michael@0 | 249 | |
michael@0 | 250 | function isMatchingAttachState (mod, window) { |
michael@0 | 251 | let state = window.document.readyState; |
michael@0 | 252 | return 'start' === mod.contentScriptWhen || |
michael@0 | 253 | // Is `load` event already dispatched? |
michael@0 | 254 | 'complete' === state || |
michael@0 | 255 | // Is DOMContentLoaded already dispatched and waiting for it? |
michael@0 | 256 | ('ready' === mod.contentScriptWhen && state === 'interactive') |
michael@0 | 257 | } |