123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- /* 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": "unstable"
- };
- const { Class } = require('../core/heritage');
- const { EventTarget } = require('../event/target');
- const { on, off, emit, setListeners } = require('../event/core');
- const {
- attach, detach, destroy
- } = require('./utils');
- const { method } = require('../lang/functional');
- const { Ci, Cu, Cc } = require('chrome');
- const unload = require('../system/unload');
- const events = require('../system/events');
- const { getInnerId } = require("../window/utils");
- const { WorkerSandbox } = require('./sandbox');
- const { getTabForWindow } = require('../tabs/helpers');
- // A weak map of workers to hold private attributes that
- // should not be exposed
- const workers = new WeakMap();
- let modelFor = (worker) => workers.get(worker);
- const ERR_DESTROYED =
- "Couldn't find the worker to receive this message. " +
- "The script may not be initialized yet, or may already have been unloaded.";
- const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
- "until it is visible again.";
- /**
- * Message-passing facility for communication between code running
- * in the content and add-on process.
- * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
- */
- const Worker = Class({
- implements: [EventTarget],
- initialize: function WorkerConstructor (options) {
- // Save model in weak map to not expose properties
- let model = createModel();
- workers.set(this, model);
- options = options || {};
- if ('contentScriptFile' in options)
- this.contentScriptFile = options.contentScriptFile;
- if ('contentScriptOptions' in options)
- this.contentScriptOptions = options.contentScriptOptions;
- if ('contentScript' in options)
- this.contentScript = options.contentScript;
- if ('injectInDocument' in options)
- this.injectInDocument = !!options.injectInDocument;
- setListeners(this, options);
- unload.ensure(this, "destroy");
- // Ensure that worker.port is initialized for contentWorker to be able
- // to send events during worker initialization.
- this.port = createPort(this);
- model.documentUnload = documentUnload.bind(this);
- model.pageShow = pageShow.bind(this);
- model.pageHide = pageHide.bind(this);
- if ('window' in options)
- attach(this, options.window);
- },
- /**
- * Sends a message to the worker's global scope. Method takes single
- * argument, which represents data to be sent to the worker. The data may
- * be any primitive type value or `JSON`. Call of this method asynchronously
- * emits `message` event with data value in the global scope of this
- * symbiont.
- *
- * `message` event listeners can be set either by calling
- * `self.on` with a first argument string `"message"` or by
- * implementing `onMessage` function in the global scope of this worker.
- * @param {Number|String|JSON} data
- */
- postMessage: function (...data) {
- let model = modelFor(this);
- let args = ['message'].concat(data);
- if (!model.inited) {
- model.earlyEvents.push(args);
- return;
- }
- processMessage.apply(null, [this].concat(args));
- },
- get url () {
- let model = modelFor(this);
- // model.window will be null after detach
- return model.window ? model.window.document.location.href : null;
- },
- get contentURL () {
- let model = modelFor(this);
- return model.window ? model.window.document.URL : null;
- },
- get tab () {
- let model = modelFor(this);
- // model.window will be null after detach
- if (model.window)
- return getTabForWindow(model.window);
- return null;
- },
- // Implemented to provide some of the previous features of exposing sandbox
- // so that Worker can be extended
- getSandbox: function () {
- return modelFor(this).contentWorker;
- },
- toString: function () { return '[object Worker]'; },
- attach: method(attach),
- detach: method(detach),
- destroy: method(destroy)
- });
- exports.Worker = Worker;
- attach.define(Worker, function (worker, window) {
- let model = modelFor(worker);
- model.window = window;
- // Track document unload to destroy this worker.
- // We can't watch for unload event on page's window object as it
- // prevents bfcache from working:
- // https://developer.mozilla.org/En/Working_with_BFCache
- model.windowID = getInnerId(model.window);
- events.on("inner-window-destroyed", model.documentUnload);
- // Listen to pagehide event in order to freeze the content script
- // while the document is frozen in bfcache:
- model.window.addEventListener("pageshow", model.pageShow, true);
- model.window.addEventListener("pagehide", model.pageHide, true);
- // will set model.contentWorker pointing to the private API:
- model.contentWorker = WorkerSandbox(worker, model.window);
- // Mainly enable worker.port.emit to send event to the content worker
- model.inited = true;
- model.frozen = false;
- // Fire off `attach` event
- emit(worker, 'attach', window);
- // Process all events and messages that were fired before the
- // worker was initialized.
- model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
- });
- /**
- * Remove all internal references to the attached document
- * Tells _port to unload itself and removes all the references from itself.
- */
- detach.define(Worker, function (worker) {
- let model = modelFor(worker);
- // maybe unloaded before content side is created
- if (model.contentWorker)
- model.contentWorker.destroy();
- model.contentWorker = null;
- if (model.window) {
- model.window.removeEventListener("pageshow", model.pageShow, true);
- model.window.removeEventListener("pagehide", model.pageHide, true);
- }
- model.window = null;
- // This method may be called multiple times,
- // avoid dispatching `detach` event more than once
- if (model.windowID) {
- model.windowID = null;
- events.off("inner-window-destroyed", model.documentUnload);
- model.earlyEvents.length = 0;
- emit(worker, 'detach');
- }
- model.inited = false;
- });
- /**
- * Tells content worker to unload itself and
- * removes all the references from itself.
- */
- destroy.define(Worker, function (worker) {
- detach(worker);
- modelFor(worker).inited = true;
- // Specifying no type or listener removes all listeners
- // from target
- off(worker);
- });
- /**
- * Events fired by workers
- */
- function documentUnload ({ subject, data }) {
- let model = modelFor(this);
- let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
- if (innerWinID != model.windowID) return false;
- detach(this);
- return true;
- }
- function pageShow () {
- let model = modelFor(this);
- model.contentWorker.emitSync('pageshow');
- emit(this, 'pageshow');
- model.frozen = false;
- }
- function pageHide () {
- let model = modelFor(this);
- model.contentWorker.emitSync('pagehide');
- emit(this, 'pagehide');
- model.frozen = true;
- }
- /**
- * Fired from postMessage and emitEventToContent, or from the earlyMessage
- * queue when fired before the content is loaded. Sends arguments to
- * contentWorker if able
- */
- function processMessage (worker, ...args) {
- let model = modelFor(worker) || {};
- if (!model.contentWorker)
- throw new Error(ERR_DESTROYED);
- if (model.frozen)
- throw new Error(ERR_FROZEN);
- model.contentWorker.emit.apply(null, args);
- }
- function createModel () {
- return {
- // List of messages fired before worker is initialized
- earlyEvents: [],
- // Is worker connected to the content worker sandbox ?
- inited: false,
- // Is worker being frozen? i.e related document is frozen in bfcache.
- // Content script should not be reachable if frozen.
- frozen: true,
- /**
- * Reference to the content side of the worker.
- * @type {WorkerGlobalScope}
- */
- contentWorker: null,
- /**
- * Reference to the window that is accessible from
- * the content scripts.
- * @type {Object}
- */
- window: null
- };
- }
- function createPort (worker) {
- let port = EventTarget();
- port.emit = emitEventToContent.bind(null, worker);
- return port;
- }
- /**
- * Emit a custom event to the content script,
- * i.e. emit this event on `self.port`
- */
- function emitEventToContent (worker, ...eventArgs) {
- let model = modelFor(worker);
- let args = ['event'].concat(eventArgs);
- if (!model.inited) {
- model.earlyEvents.push(args);
- return;
- }
- processMessage.apply(null, [worker].concat(args));
- }
|