Files
tubestation/addon-sdk/source/lib/sdk/content/page-mod.js
Kris Maglione 803885db85 Bug 1314861: Lazily load most SDK module imports. r=rpl
MozReview-Commit-ID: 3mneEkzljiU
2017-04-07 18:11:32 -07:00

231 lines
6.9 KiB
JavaScript

/* 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"
};
lazyRequire(this, '../content/utils', 'getAttachEventType');
const { Class } = require('../core/heritage');
const { Disposable } = require('../core/disposable');
lazyRequire(this, './worker-child', 'WorkerChild');
const { EventTarget } = require('../event/target');
const { on, emit, once, setListeners } = require('../event/core');
lazyRequire(this, '../dom/events',{'on': 'domOn', 'removeListener': 'domOff'});
lazyRequire(this, '../util/object', "merge");
lazyRequire(this, '../window/utils', "getFrames");
lazyRequire(this, '../private-browsing/utils', "ignoreWindow");
lazyRequire(this, '../stylesheet/style', 'Style');
lazyRequire(this, '../content/mod', 'attach', 'detach');
lazyRequire(this, '../util/rules', 'Rules');
lazyRequire(this, '../util/uuid', '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 (this.attachTo.includes('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 (mod.attachTo.includes("top") && modMatchesURI(mod, uri))
onContent(mod, window);
if (mod.attachTo.includes("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 && !mod.attachTo.includes("top"))
return;
// Is a frame document and `frame` is not set, ignore
if (!isTopDocument && !mod.attachTo.includes("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();
});