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