1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/page-mod.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,257 @@ 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 +"use strict"; 1.8 + 1.9 +module.metadata = { 1.10 + "stability": "stable" 1.11 +}; 1.12 + 1.13 +const observers = require('./system/events'); 1.14 +const { contract: loaderContract } = require('./content/loader'); 1.15 +const { contract } = require('./util/contract'); 1.16 +const { getAttachEventType, WorkerHost } = require('./content/utils'); 1.17 +const { Class } = require('./core/heritage'); 1.18 +const { Disposable } = require('./core/disposable'); 1.19 +const { WeakReference } = require('./core/reference'); 1.20 +const { Worker } = require('./content/worker'); 1.21 +const { EventTarget } = require('./event/target'); 1.22 +const { on, emit, once, setListeners } = require('./event/core'); 1.23 +const { on: domOn, removeListener: domOff } = require('./dom/events'); 1.24 +const { pipe } = require('./event/utils'); 1.25 +const { isRegExp } = require('./lang/type'); 1.26 +const { merge } = require('./util/object'); 1.27 +const { windowIterator } = require('./deprecated/window-utils'); 1.28 +const { isBrowser, getFrames } = require('./window/utils'); 1.29 +const { getTabs, getTabContentWindow, getTabForContentWindow, 1.30 + getURI: getTabURI } = require('./tabs/utils'); 1.31 +const { ignoreWindow } = require('sdk/private-browsing/utils'); 1.32 +const { Style } = require("./stylesheet/style"); 1.33 +const { attach, detach } = require("./content/mod"); 1.34 +const { has, hasAny } = require("./util/array"); 1.35 +const { Rules } = require("./util/rules"); 1.36 +const { List, addListItem, removeListItem } = require('./util/list'); 1.37 +const { when: unload } = require("./system/unload"); 1.38 + 1.39 +// Valid values for `attachTo` option 1.40 +const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame']; 1.41 + 1.42 +const pagemods = new Set(); 1.43 +const workers = new WeakMap(); 1.44 +const styles = new WeakMap(); 1.45 +const models = new WeakMap(); 1.46 +let modelFor = (mod) => models.get(mod); 1.47 +let workerFor = (mod) => workers.get(mod); 1.48 +let styleFor = (mod) => styles.get(mod); 1.49 + 1.50 +// Bind observer 1.51 +observers.on('document-element-inserted', onContentWindow); 1.52 +unload(() => observers.off('document-element-inserted', onContentWindow)); 1.53 + 1.54 +let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string'; 1.55 + 1.56 +// Validation Contracts 1.57 +const modOptions = { 1.58 + // contentStyle* / contentScript* are sharing the same validation constraints, 1.59 + // so they can be mostly reused, except for the messages. 1.60 + contentStyle: merge(Object.create(loaderContract.rules.contentScript), { 1.61 + msg: 'The `contentStyle` option must be a string or an array of strings.' 1.62 + }), 1.63 + contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { 1.64 + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' 1.65 + }), 1.66 + include: { 1.67 + is: ['string', 'array', 'regexp'], 1.68 + ok: (rule) => { 1.69 + if (isRegExpOrString(rule)) 1.70 + return true; 1.71 + if (Array.isArray(rule) && rule.length > 0) 1.72 + return rule.every(isRegExpOrString); 1.73 + return false; 1.74 + }, 1.75 + msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.' 1.76 + }, 1.77 + attachTo: { 1.78 + is: ['string', 'array', 'undefined'], 1.79 + map: function (attachTo) { 1.80 + if (!attachTo) return ['top', 'frame']; 1.81 + if (typeof attachTo === 'string') return [attachTo]; 1.82 + return attachTo; 1.83 + }, 1.84 + ok: function (attachTo) { 1.85 + return hasAny(attachTo, ['top', 'frame']) && 1.86 + attachTo.every(has.bind(null, ['top', 'frame', 'existing'])); 1.87 + }, 1.88 + msg: 'The `attachTo` option must be a string or an array of strings. ' + 1.89 + 'The only valid options are "existing", "top" and "frame", and must ' + 1.90 + 'contain at least "top" or "frame" values.' 1.91 + }, 1.92 +}; 1.93 + 1.94 +const modContract = contract(merge({}, loaderContract.rules, modOptions)); 1.95 + 1.96 +/** 1.97 + * PageMod constructor (exported below). 1.98 + * @constructor 1.99 + */ 1.100 +const PageMod = Class({ 1.101 + implements: [ 1.102 + modContract.properties(modelFor), 1.103 + EventTarget, 1.104 + Disposable, 1.105 + WeakReference 1.106 + ], 1.107 + extends: WorkerHost(workerFor), 1.108 + setup: function PageMod(options) { 1.109 + let mod = this; 1.110 + let model = modContract(options); 1.111 + models.set(this, model); 1.112 + 1.113 + // Set listeners on {PageMod} itself, not the underlying worker, 1.114 + // like `onMessage`, as it'll get piped. 1.115 + setListeners(this, options); 1.116 + 1.117 + let include = model.include; 1.118 + model.include = Rules(); 1.119 + model.include.add.apply(model.include, [].concat(include)); 1.120 + 1.121 + if (model.contentStyle || model.contentStyleFile) { 1.122 + styles.set(mod, Style({ 1.123 + uri: model.contentStyleFile, 1.124 + source: model.contentStyle 1.125 + })); 1.126 + } 1.127 + 1.128 + pagemods.add(this); 1.129 + 1.130 + // `applyOnExistingDocuments` has to be called after `pagemods.add()` 1.131 + // otherwise its calls to `onContent` method won't do anything. 1.132 + if (has(model.attachTo, 'existing')) 1.133 + applyOnExistingDocuments(mod); 1.134 + }, 1.135 + 1.136 + dispose: function() { 1.137 + let style = styleFor(this); 1.138 + if (style) 1.139 + detach(style); 1.140 + 1.141 + for (let i in this.include) 1.142 + this.include.remove(this.include[i]); 1.143 + 1.144 + pagemods.delete(this); 1.145 + } 1.146 +}); 1.147 +exports.PageMod = PageMod; 1.148 + 1.149 +function onContentWindow({ subject: document }) { 1.150 + // Return if we have no pagemods 1.151 + if (pagemods.size === 0) 1.152 + return; 1.153 + 1.154 + let window = document.defaultView; 1.155 + // XML documents don't have windows, and we don't yet support them. 1.156 + if (!window) 1.157 + return; 1.158 + // We apply only on documents in tabs of Firefox 1.159 + if (!getTabForContentWindow(window)) 1.160 + return; 1.161 + 1.162 + // When the tab is private, only addons with 'private-browsing' flag in 1.163 + // their package.json can apply content script to private documents 1.164 + if (ignoreWindow(window)) 1.165 + return; 1.166 + 1.167 + for (let pagemod of pagemods) { 1.168 + if (pagemod.include.matchesAny(document.URL)) 1.169 + onContent(pagemod, window); 1.170 + } 1.171 +} 1.172 + 1.173 +// Returns all tabs on all currently opened windows 1.174 +function getAllTabs() { 1.175 + let tabs = []; 1.176 + // Iterate over all chrome windows 1.177 + for (let window in windowIterator()) { 1.178 + if (!isBrowser(window)) 1.179 + continue; 1.180 + tabs = tabs.concat(getTabs(window)); 1.181 + } 1.182 + return tabs; 1.183 +} 1.184 + 1.185 +function applyOnExistingDocuments (mod) { 1.186 + let tabs = getAllTabs(); 1.187 + 1.188 + tabs.forEach(function (tab) { 1.189 + // Fake a newly created document 1.190 + let window = getTabContentWindow(tab); 1.191 + if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab))) 1.192 + onContent(mod, window); 1.193 + if (has(mod.attachTo, "frame")) { 1.194 + getFrames(window). 1.195 + filter((iframe) => mod.include.matchesAny(iframe.location.href)). 1.196 + forEach((frame) => onContent(mod, frame)); 1.197 + } 1.198 + }); 1.199 +} 1.200 + 1.201 +function createWorker (mod, window) { 1.202 + let worker = Worker({ 1.203 + window: window, 1.204 + contentScript: mod.contentScript, 1.205 + contentScriptFile: mod.contentScriptFile, 1.206 + contentScriptOptions: mod.contentScriptOptions, 1.207 + // Bug 980468: Syntax errors from scripts can happen before the worker 1.208 + // can set up an error handler. They are per-mod rather than per-worker 1.209 + // so are best handled at the mod level. 1.210 + onError: (e) => emit(mod, 'error', e) 1.211 + }); 1.212 + workers.set(mod, worker); 1.213 + pipe(worker, mod); 1.214 + emit(mod, 'attach', worker); 1.215 + once(worker, 'detach', function detach() { 1.216 + worker.destroy(); 1.217 + }); 1.218 +} 1.219 + 1.220 +function onContent (mod, window) { 1.221 + // not registered yet 1.222 + if (!pagemods.has(mod)) 1.223 + return; 1.224 + 1.225 + let isTopDocument = window.top === window; 1.226 + // Is a top level document and `top` is not set, ignore 1.227 + if (isTopDocument && !has(mod.attachTo, "top")) 1.228 + return; 1.229 + // Is a frame document and `frame` is not set, ignore 1.230 + if (!isTopDocument && !has(mod.attachTo, "frame")) 1.231 + return; 1.232 + 1.233 + let style = styleFor(mod); 1.234 + if (style) 1.235 + attach(style, window); 1.236 + 1.237 + // Immediatly evaluate content script if the document state is already 1.238 + // matching contentScriptWhen expectations 1.239 + if (isMatchingAttachState(mod, window)) { 1.240 + createWorker(mod, window); 1.241 + return; 1.242 + } 1.243 + 1.244 + let eventName = getAttachEventType(mod) || 'load'; 1.245 + domOn(window, eventName, function onReady (e) { 1.246 + if (e.target.defaultView !== window) 1.247 + return; 1.248 + domOff(window, eventName, onReady, true); 1.249 + createWorker(mod, window); 1.250 + }, true); 1.251 +} 1.252 + 1.253 +function isMatchingAttachState (mod, window) { 1.254 + let state = window.document.readyState; 1.255 + return 'start' === mod.contentScriptWhen || 1.256 + // Is `load` event already dispatched? 1.257 + 'complete' === state || 1.258 + // Is DOMContentLoaded already dispatched and waiting for it? 1.259 + ('ready' === mod.contentScriptWhen && state === 'interactive') 1.260 +}