addon-sdk/source/lib/sdk/page-mod.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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 }

mercurial