summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/content/page-mod.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/content/page-mod.js')
-rw-r--r--toolkit/jetpack/sdk/content/page-mod.js236
1 files changed, 236 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/content/page-mod.js b/toolkit/jetpack/sdk/content/page-mod.js
new file mode 100644
index 000000000..8ff9b1e7b
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/page-mod.js
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "stable"
+};
+
+const { getAttachEventType } = require('../content/utils');
+const { Class } = require('../core/heritage');
+const { Disposable } = require('../core/disposable');
+const { WeakReference } = require('../core/reference');
+const { WorkerChild } = require('./worker-child');
+const { EventTarget } = require('../event/target');
+const { on, emit, once, setListeners } = require('../event/core');
+const { on: domOn, removeListener: domOff } = require('../dom/events');
+const { isRegExp, isUndefined } = require('../lang/type');
+const { merge } = require('../util/object');
+const { isBrowser, getFrames } = require('../window/utils');
+const { getTabs, getURI: getTabURI } = require('../tabs/utils');
+const { ignoreWindow } = require('../private-browsing/utils');
+const { Style } = require("../stylesheet/style");
+const { attach, detach } = require("../content/mod");
+const { has, hasAny } = require("../util/array");
+const { Rules } = require("../util/rules");
+const { List, addListItem, removeListItem } = require('../util/list');
+const { when } = require("../system/unload");
+const { uuid } = require('../util/uuid');
+const { frames, process } = require('../remote/child');
+
+const pagemods = new Map();
+const styles = new WeakMap();
+var styleFor = (mod) => styles.get(mod);
+
+// Helper functions
+var modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
+
+/**
+ * PageMod constructor (exported below).
+ * @constructor
+ */
+const ChildPageMod = Class({
+ implements: [
+ EventTarget,
+ Disposable,
+ ],
+ setup: function PageMod(model) {
+ merge(this, model);
+
+ // Set listeners on {PageMod} itself, not the underlying worker,
+ // like `onMessage`, as it'll get piped.
+ setListeners(this, model);
+
+ function deserializeRules(rules) {
+ for (let rule of rules) {
+ yield rule.type == "string" ? rule.value
+ : new RegExp(rule.pattern, rule.flags);
+ }
+ }
+
+ let include = [...deserializeRules(this.include)];
+ this.include = Rules();
+ this.include.add.apply(this.include, include);
+
+ let exclude = [...deserializeRules(this.exclude)];
+ this.exclude = Rules();
+ this.exclude.add.apply(this.exclude, exclude);
+
+ if (this.contentStyle || this.contentStyleFile) {
+ styles.set(this, Style({
+ uri: this.contentStyleFile,
+ source: this.contentStyle
+ }));
+ }
+
+ pagemods.set(this.id, this);
+ this.seenDocuments = new WeakMap();
+
+ // `applyOnExistingDocuments` has to be called after `pagemods.add()`
+ // otherwise its calls to `onContent` method won't do anything.
+ if (has(this.attachTo, 'existing'))
+ applyOnExistingDocuments(this);
+ },
+
+ dispose: function() {
+ let style = styleFor(this);
+ if (style)
+ detach(style);
+
+ for (let i in this.include)
+ this.include.remove(this.include[i]);
+
+ pagemods.delete(this.id);
+ }
+});
+
+function onContentWindow({ target: document }) {
+ // Return if we have no pagemods
+ if (pagemods.size === 0)
+ return;
+
+ let window = document.defaultView;
+ // XML documents don't have windows, and we don't yet support them.
+ if (!window)
+ return;
+
+ // Frame event listeners are bound to the frame the event came from by default
+ let frame = this;
+ // We apply only on documents in tabs of Firefox
+ if (!frame.isTab)
+ return;
+
+ // When the tab is private, only addons with 'private-browsing' flag in
+ // their package.json can apply content script to private documents
+ if (ignoreWindow(window))
+ return;
+
+ for (let pagemod of pagemods.values()) {
+ if (modMatchesURI(pagemod, window.location.href))
+ onContent(pagemod, window);
+ }
+}
+frames.addEventListener("DOMDocElementInserted", onContentWindow, true);
+
+function applyOnExistingDocuments (mod) {
+ for (let frame of frames) {
+ // Fake a newly created document
+ let window = frame.content;
+ // on startup with e10s, contentWindow might not exist yet,
+ // in which case we will get notified by "document-element-inserted".
+ if (!window || !window.frames)
+ return;
+ let uri = window.location.href;
+ if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
+ onContent(mod, window);
+ if (has(mod.attachTo, "frame"))
+ getFrames(window).
+ filter(iframe => modMatchesURI(mod, iframe.location.href)).
+ forEach(frame => onContent(mod, frame));
+ }
+}
+
+function createWorker(mod, window) {
+ let workerId = String(uuid());
+
+ // Instruct the parent to connect to this worker. Do this first so the parent
+ // side is connected before the worker attempts to send any messages there
+ let frame = frames.getFrameForWindow(window.top);
+ frame.port.emit('sdk/page-mod/worker-create', mod.id, {
+ id: workerId,
+ url: window.location.href
+ });
+
+ // Create a child worker and notify the parent
+ let worker = WorkerChild({
+ id: workerId,
+ window: window,
+ contentScript: mod.contentScript,
+ contentScriptFile: mod.contentScriptFile,
+ contentScriptOptions: mod.contentScriptOptions
+ });
+
+ once(worker, 'detach', () => worker.destroy());
+}
+
+function onContent (mod, window) {
+ let isTopDocument = window.top === window;
+ // Is a top level document and `top` is not set, ignore
+ if (isTopDocument && !has(mod.attachTo, "top"))
+ return;
+ // Is a frame document and `frame` is not set, ignore
+ if (!isTopDocument && !has(mod.attachTo, "frame"))
+ return;
+
+ // ensure we attach only once per document
+ let seen = mod.seenDocuments;
+ if (seen.has(window.document))
+ return;
+ seen.set(window.document, true);
+
+ let style = styleFor(mod);
+ if (style)
+ attach(style, window);
+
+ // Immediately evaluate content script if the document state is already
+ // matching contentScriptWhen expectations
+ if (isMatchingAttachState(mod, window)) {
+ createWorker(mod, window);
+ return;
+ }
+
+ let eventName = getAttachEventType(mod) || 'load';
+ domOn(window, eventName, function onReady (e) {
+ if (e.target.defaultView !== window)
+ return;
+ domOff(window, eventName, onReady, true);
+ createWorker(mod, window);
+
+ // Attaching is asynchronous so if the document is already loaded we will
+ // miss the pageshow event so send a synthetic one.
+ if (window.document.readyState == "complete") {
+ mod.on('attach', worker => {
+ try {
+ worker.send('pageshow');
+ emit(worker, 'pageshow');
+ }
+ catch (e) {
+ // This can fail if an earlier attach listener destroyed the worker
+ }
+ });
+ }
+ }, true);
+}
+
+function isMatchingAttachState (mod, window) {
+ let state = window.document.readyState;
+ return 'start' === mod.contentScriptWhen ||
+ // Is `load` event already dispatched?
+ 'complete' === state ||
+ // Is DOMContentLoaded already dispatched and waiting for it?
+ ('ready' === mod.contentScriptWhen && state === 'interactive')
+}
+
+process.port.on('sdk/page-mod/create', (process, model) => {
+ if (pagemods.has(model.id))
+ return;
+
+ new ChildPageMod(model);
+});
+
+process.port.on('sdk/page-mod/destroy', (process, id) => {
+ let mod = pagemods.get(id);
+ if (mod)
+ mod.destroy();
+});