123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- /* 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 { Class } = require('./core/heritage');
- const { on, emit, off, setListeners } = require('./event/core');
- const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils');
- const { detach, attach, destroy, WorkerHost } = require('./content/utils');
- const { Worker } = require('./content/worker');
- const { Disposable } = require('./core/disposable');
- const { EventTarget } = require('./event/target');
- const { unload } = require('./system/unload');
- const { events, streamEventsFrom } = require('./content/events');
- const { getAttachEventType } = require('./content/utils');
- const { window } = require('./addon/window');
- const { getParentWindow } = require('./window/utils');
- const { create: makeFrame, getDocShell } = require('./frame/utils');
- const { contract } = require('./util/contract');
- const { contract: loaderContract } = require('./content/loader');
- const { has } = require('./util/array');
- const { Rules } = require('./util/rules');
- const { merge } = require('./util/object');
- const views = WeakMap();
- const workers = WeakMap();
- const pages = WeakMap();
- const readyEventNames = [
- 'DOMContentLoaded',
- 'document-element-inserted',
- 'load'
- ];
- function workerFor(page) workers.get(page)
- function pageFor(view) pages.get(view)
- function viewFor(page) views.get(page)
- function isDisposed (page) !views.get(page, false)
- let pageContract = contract(merge({
- allow: {
- is: ['object', 'undefined', 'null'],
- map: function (allow) { return { script: !allow || allow.script !== false }}
- },
- onMessage: {
- is: ['function', 'undefined']
- },
- include: {
- is: ['string', 'array', 'undefined']
- },
- contentScriptWhen: {
- is: ['string', 'undefined']
- }
- }, loaderContract.rules));
- function enableScript (page) {
- getDocShell(viewFor(page)).allowJavascript = true;
- }
- function disableScript (page) {
- getDocShell(viewFor(page)).allowJavascript = false;
- }
- function Allow (page) {
- return {
- get script() { return getDocShell(viewFor(page)).allowJavascript; },
- set script(value) { return value ? enableScript(page) : disableScript(page); }
- };
- }
- function injectWorker ({page}) {
- let worker = workerFor(page);
- let view = viewFor(page);
- if (isValidURL(page, view.contentDocument.URL))
- attach(worker, view.contentWindow);
- }
- function isValidURL(page, url) !page.rules || page.rules.matchesAny(url)
- const Page = Class({
- implements: [
- EventTarget,
- Disposable
- ],
- extends: WorkerHost(workerFor),
- setup: function Page(options) {
- let page = this;
- options = pageContract(options);
- let view = makeFrame(window.document, {
- nodeName: 'iframe',
- type: 'content',
- uri: options.contentURL,
- allowJavascript: options.allow.script,
- allowPlugins: true,
- allowAuth: true
- });
- ['contentScriptFile', 'contentScript', 'contentScriptWhen']
- .forEach(prop => page[prop] = options[prop]);
- views.set(this, view);
- pages.set(view, this);
- // Set listeners on the {Page} object itself, not the underlying worker,
- // like `onMessage`, as it gets piped
- setListeners(this, options);
- let worker = new Worker(stripListeners(options));
- workers.set(this, worker);
- pipe(worker, this);
- if (this.include || options.include) {
- this.rules = Rules();
- this.rules.add.apply(this.rules, [].concat(this.include || options.include));
- }
- },
- get allow() { return Allow(this); },
- set allow(value) {
- let allowJavascript = pageContract({ allow: value }).allow.script;
- return allowJavascript ? enableScript(this) : disableScript(this);
- },
- get contentURL() { return viewFor(this).getAttribute('src'); },
- set contentURL(value) {
- if (!isValidURL(this, value)) return;
- let view = viewFor(this);
- let contentURL = pageContract({ contentURL: value }).contentURL;
- view.setAttribute('src', contentURL);
- },
- dispose: function () {
- if (isDisposed(this)) return;
- let view = viewFor(this);
- if (view.parentNode) view.parentNode.removeChild(view);
- views.delete(this);
- destroy(workers.get(this));
- },
- toString: function () { return '[object Page]' }
- });
- exports.Page = Page;
- let pageEvents = streamMerge([events, streamEventsFrom(window)]);
- let readyEvents = filter(pageEvents, isReadyEvent);
- let formattedEvents = map(readyEvents, function({target, type}) {
- return { type: type, page: pageFromDoc(target) };
- });
- let pageReadyEvents = filter(formattedEvents, function({page, type}) {
- return getAttachEventType(page) === type});
- on(pageReadyEvents, 'data', injectWorker);
- function isReadyEvent ({type}) {
- return has(readyEventNames, type);
- }
- /*
- * Takes a document, finds its doc shell tree root and returns the
- * matching Page instance if found
- */
- function pageFromDoc(doc) {
- let parentWindow = getParentWindow(doc.defaultView), page;
- if (!parentWindow) return;
- let frames = parentWindow.document.getElementsByTagName('iframe');
- for (let i = frames.length; i--;)
- if (frames[i].contentDocument === doc && (page = pageFor(frames[i])))
- return page;
- return null;
- }
|