michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "stable" michael@0: }; michael@0: michael@0: const observers = require('./system/events'); michael@0: const { contract: loaderContract } = require('./content/loader'); michael@0: const { contract } = require('./util/contract'); michael@0: const { getAttachEventType, WorkerHost } = require('./content/utils'); michael@0: const { Class } = require('./core/heritage'); michael@0: const { Disposable } = require('./core/disposable'); michael@0: const { WeakReference } = require('./core/reference'); michael@0: const { Worker } = require('./content/worker'); michael@0: const { EventTarget } = require('./event/target'); michael@0: const { on, emit, once, setListeners } = require('./event/core'); michael@0: const { on: domOn, removeListener: domOff } = require('./dom/events'); michael@0: const { pipe } = require('./event/utils'); michael@0: const { isRegExp } = require('./lang/type'); michael@0: const { merge } = require('./util/object'); michael@0: const { windowIterator } = require('./deprecated/window-utils'); michael@0: const { isBrowser, getFrames } = require('./window/utils'); michael@0: const { getTabs, getTabContentWindow, getTabForContentWindow, michael@0: getURI: getTabURI } = require('./tabs/utils'); michael@0: const { ignoreWindow } = require('sdk/private-browsing/utils'); michael@0: const { Style } = require("./stylesheet/style"); michael@0: const { attach, detach } = require("./content/mod"); michael@0: const { has, hasAny } = require("./util/array"); michael@0: const { Rules } = require("./util/rules"); michael@0: const { List, addListItem, removeListItem } = require('./util/list'); michael@0: const { when: unload } = require("./system/unload"); michael@0: michael@0: // Valid values for `attachTo` option michael@0: const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame']; michael@0: michael@0: const pagemods = new Set(); michael@0: const workers = new WeakMap(); michael@0: const styles = new WeakMap(); michael@0: const models = new WeakMap(); michael@0: let modelFor = (mod) => models.get(mod); michael@0: let workerFor = (mod) => workers.get(mod); michael@0: let styleFor = (mod) => styles.get(mod); michael@0: michael@0: // Bind observer michael@0: observers.on('document-element-inserted', onContentWindow); michael@0: unload(() => observers.off('document-element-inserted', onContentWindow)); michael@0: michael@0: let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string'; michael@0: michael@0: // Validation Contracts michael@0: const modOptions = { michael@0: // contentStyle* / contentScript* are sharing the same validation constraints, michael@0: // so they can be mostly reused, except for the messages. michael@0: contentStyle: merge(Object.create(loaderContract.rules.contentScript), { michael@0: msg: 'The `contentStyle` option must be a string or an array of strings.' michael@0: }), michael@0: contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { michael@0: msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' michael@0: }), michael@0: include: { michael@0: is: ['string', 'array', 'regexp'], michael@0: ok: (rule) => { michael@0: if (isRegExpOrString(rule)) michael@0: return true; michael@0: if (Array.isArray(rule) && rule.length > 0) michael@0: return rule.every(isRegExpOrString); michael@0: return false; michael@0: }, michael@0: 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: }, michael@0: attachTo: { michael@0: is: ['string', 'array', 'undefined'], michael@0: map: function (attachTo) { michael@0: if (!attachTo) return ['top', 'frame']; michael@0: if (typeof attachTo === 'string') return [attachTo]; michael@0: return attachTo; michael@0: }, michael@0: ok: function (attachTo) { michael@0: return hasAny(attachTo, ['top', 'frame']) && michael@0: attachTo.every(has.bind(null, ['top', 'frame', 'existing'])); michael@0: }, michael@0: msg: 'The `attachTo` option must be a string or an array of strings. ' + michael@0: 'The only valid options are "existing", "top" and "frame", and must ' + michael@0: 'contain at least "top" or "frame" values.' michael@0: }, michael@0: }; michael@0: michael@0: const modContract = contract(merge({}, loaderContract.rules, modOptions)); michael@0: michael@0: /** michael@0: * PageMod constructor (exported below). michael@0: * @constructor michael@0: */ michael@0: const PageMod = Class({ michael@0: implements: [ michael@0: modContract.properties(modelFor), michael@0: EventTarget, michael@0: Disposable, michael@0: WeakReference michael@0: ], michael@0: extends: WorkerHost(workerFor), michael@0: setup: function PageMod(options) { michael@0: let mod = this; michael@0: let model = modContract(options); michael@0: models.set(this, model); michael@0: michael@0: // Set listeners on {PageMod} itself, not the underlying worker, michael@0: // like `onMessage`, as it'll get piped. michael@0: setListeners(this, options); michael@0: michael@0: let include = model.include; michael@0: model.include = Rules(); michael@0: model.include.add.apply(model.include, [].concat(include)); michael@0: michael@0: if (model.contentStyle || model.contentStyleFile) { michael@0: styles.set(mod, Style({ michael@0: uri: model.contentStyleFile, michael@0: source: model.contentStyle michael@0: })); michael@0: } michael@0: michael@0: pagemods.add(this); michael@0: michael@0: // `applyOnExistingDocuments` has to be called after `pagemods.add()` michael@0: // otherwise its calls to `onContent` method won't do anything. michael@0: if (has(model.attachTo, 'existing')) michael@0: applyOnExistingDocuments(mod); michael@0: }, michael@0: michael@0: dispose: function() { michael@0: let style = styleFor(this); michael@0: if (style) michael@0: detach(style); michael@0: michael@0: for (let i in this.include) michael@0: this.include.remove(this.include[i]); michael@0: michael@0: pagemods.delete(this); michael@0: } michael@0: }); michael@0: exports.PageMod = PageMod; michael@0: michael@0: function onContentWindow({ subject: document }) { michael@0: // Return if we have no pagemods michael@0: if (pagemods.size === 0) michael@0: return; michael@0: michael@0: let window = document.defaultView; michael@0: // XML documents don't have windows, and we don't yet support them. michael@0: if (!window) michael@0: return; michael@0: // We apply only on documents in tabs of Firefox michael@0: if (!getTabForContentWindow(window)) michael@0: return; michael@0: michael@0: // When the tab is private, only addons with 'private-browsing' flag in michael@0: // their package.json can apply content script to private documents michael@0: if (ignoreWindow(window)) michael@0: return; michael@0: michael@0: for (let pagemod of pagemods) { michael@0: if (pagemod.include.matchesAny(document.URL)) michael@0: onContent(pagemod, window); michael@0: } michael@0: } michael@0: michael@0: // Returns all tabs on all currently opened windows michael@0: function getAllTabs() { michael@0: let tabs = []; michael@0: // Iterate over all chrome windows michael@0: for (let window in windowIterator()) { michael@0: if (!isBrowser(window)) michael@0: continue; michael@0: tabs = tabs.concat(getTabs(window)); michael@0: } michael@0: return tabs; michael@0: } michael@0: michael@0: function applyOnExistingDocuments (mod) { michael@0: let tabs = getAllTabs(); michael@0: michael@0: tabs.forEach(function (tab) { michael@0: // Fake a newly created document michael@0: let window = getTabContentWindow(tab); michael@0: if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab))) michael@0: onContent(mod, window); michael@0: if (has(mod.attachTo, "frame")) { michael@0: getFrames(window). michael@0: filter((iframe) => mod.include.matchesAny(iframe.location.href)). michael@0: forEach((frame) => onContent(mod, frame)); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: function createWorker (mod, window) { michael@0: let worker = Worker({ michael@0: window: window, michael@0: contentScript: mod.contentScript, michael@0: contentScriptFile: mod.contentScriptFile, michael@0: contentScriptOptions: mod.contentScriptOptions, michael@0: // Bug 980468: Syntax errors from scripts can happen before the worker michael@0: // can set up an error handler. They are per-mod rather than per-worker michael@0: // so are best handled at the mod level. michael@0: onError: (e) => emit(mod, 'error', e) michael@0: }); michael@0: workers.set(mod, worker); michael@0: pipe(worker, mod); michael@0: emit(mod, 'attach', worker); michael@0: once(worker, 'detach', function detach() { michael@0: worker.destroy(); michael@0: }); michael@0: } michael@0: michael@0: function onContent (mod, window) { michael@0: // not registered yet michael@0: if (!pagemods.has(mod)) michael@0: return; michael@0: michael@0: let isTopDocument = window.top === window; michael@0: // Is a top level document and `top` is not set, ignore michael@0: if (isTopDocument && !has(mod.attachTo, "top")) michael@0: return; michael@0: // Is a frame document and `frame` is not set, ignore michael@0: if (!isTopDocument && !has(mod.attachTo, "frame")) michael@0: return; michael@0: michael@0: let style = styleFor(mod); michael@0: if (style) michael@0: attach(style, window); michael@0: michael@0: // Immediatly evaluate content script if the document state is already michael@0: // matching contentScriptWhen expectations michael@0: if (isMatchingAttachState(mod, window)) { michael@0: createWorker(mod, window); michael@0: return; michael@0: } michael@0: michael@0: let eventName = getAttachEventType(mod) || 'load'; michael@0: domOn(window, eventName, function onReady (e) { michael@0: if (e.target.defaultView !== window) michael@0: return; michael@0: domOff(window, eventName, onReady, true); michael@0: createWorker(mod, window); michael@0: }, true); michael@0: } michael@0: michael@0: function isMatchingAttachState (mod, window) { michael@0: let state = window.document.readyState; michael@0: return 'start' === mod.contentScriptWhen || michael@0: // Is `load` event already dispatched? michael@0: 'complete' === state || michael@0: // Is DOMContentLoaded already dispatched and waiting for it? michael@0: ('ready' === mod.contentScriptWhen && state === 'interactive') michael@0: }