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