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": "deprecated" michael@0: }; michael@0: michael@0: const { Worker } = require('./traits-worker'); michael@0: const { Loader } = require('../content/loader'); michael@0: const hiddenFrames = require('../frame/hidden-frame'); michael@0: const { on, off } = require('../system/events'); michael@0: const unload = require('../system/unload'); michael@0: const { getDocShell } = require("../frame/utils"); michael@0: const { ignoreWindow } = require('../private-browsing/utils'); michael@0: michael@0: // Everything coming from add-on's xpi considered an asset. michael@0: const assetsURI = require('../self').data.url().replace(/data\/$/, ""); michael@0: michael@0: /** michael@0: * This trait is layered on top of `Worker` and in contrast to symbiont michael@0: * Worker constructor requires `content` option that represents content michael@0: * that will be loaded in the provided frame, if frame is not provided michael@0: * Worker will create hidden one. michael@0: */ michael@0: const Symbiont = Worker.resolve({ michael@0: constructor: '_initWorker', michael@0: destroy: '_workerDestroy' michael@0: }).compose(Loader, { michael@0: michael@0: /** michael@0: * The constructor requires all the options that are required by michael@0: * `require('content').Worker` with the difference that the `frame` option michael@0: * is optional. If `frame` is not provided, `contentURL` is expected. michael@0: * @param {Object} options michael@0: * @param {String} options.contentURL michael@0: * URL of a content to load into `this._frame` and create worker for. michael@0: * @param {Element} [options.frame] michael@0: * iframe element that is used to load `options.contentURL` into. michael@0: * if frame is not provided hidden iframe will be created. michael@0: */ michael@0: constructor: function Symbiont(options) { michael@0: options = options || {}; michael@0: michael@0: if ('contentURL' in options) michael@0: this.contentURL = options.contentURL; michael@0: if ('contentScriptWhen' in options) michael@0: this.contentScriptWhen = options.contentScriptWhen; michael@0: if ('contentScriptOptions' in options) michael@0: this.contentScriptOptions = options.contentScriptOptions; michael@0: if ('contentScriptFile' in options) michael@0: this.contentScriptFile = options.contentScriptFile; michael@0: if ('contentScript' in options) michael@0: this.contentScript = options.contentScript; michael@0: if ('allow' in options) michael@0: this.allow = options.allow; michael@0: if ('onError' in options) michael@0: this.on('error', options.onError); michael@0: if ('onMessage' in options) michael@0: this.on('message', options.onMessage); michael@0: if ('frame' in options) { michael@0: this._initFrame(options.frame); michael@0: } michael@0: else { michael@0: let self = this; michael@0: this._hiddenFrame = hiddenFrames.HiddenFrame({ michael@0: onReady: function onFrame() { michael@0: self._initFrame(this.element); michael@0: }, michael@0: onUnload: function onUnload() { michael@0: // Bug 751211: Remove reference to _frame when hidden frame is michael@0: // automatically removed on unload, otherwise we are going to face michael@0: // "dead object" exception michael@0: self.destroy(); michael@0: } michael@0: }); michael@0: hiddenFrames.add(this._hiddenFrame); michael@0: } michael@0: michael@0: unload.ensure(this._public, "destroy"); michael@0: }, michael@0: michael@0: destroy: function destroy() { michael@0: this._workerDestroy(); michael@0: this._unregisterListener(); michael@0: this._frame = null; michael@0: if (this._hiddenFrame) { michael@0: hiddenFrames.remove(this._hiddenFrame); michael@0: this._hiddenFrame = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * XUL iframe or browser elements with attribute `type` being `content`. michael@0: * Used to create `ContentSymbiont` from. michael@0: * @type {nsIFrame|nsIBrowser} michael@0: */ michael@0: _frame: null, michael@0: michael@0: /** michael@0: * Listener to the `'frameReady"` event (emitted when `iframe` is ready). michael@0: * Removes listener, sets right permissions to the frame and loads content. michael@0: */ michael@0: _initFrame: function _initFrame(frame) { michael@0: if (this._loadListener) michael@0: this._unregisterListener(); michael@0: michael@0: this._frame = frame; michael@0: michael@0: if (getDocShell(frame)) { michael@0: this._reallyInitFrame(frame); michael@0: } michael@0: else { michael@0: if (this._waitForFrame) { michael@0: off('content-document-global-created', this._waitForFrame); michael@0: } michael@0: this._waitForFrame = this.__waitForFrame.bind(this, frame); michael@0: on('content-document-global-created', this._waitForFrame); michael@0: } michael@0: }, michael@0: michael@0: __waitForFrame: function _waitForFrame(frame, { subject: win }) { michael@0: if (frame.contentWindow == win) { michael@0: off('content-document-global-created', this._waitForFrame); michael@0: delete this._waitForFrame; michael@0: this._reallyInitFrame(frame); michael@0: } michael@0: }, michael@0: michael@0: _reallyInitFrame: function _reallyInitFrame(frame) { michael@0: getDocShell(frame).allowJavascript = this.allow.script; michael@0: frame.setAttribute("src", this._contentURL); michael@0: michael@0: // Inject `addon` object in document if we load a document from michael@0: // one of our addon folder and if no content script are defined. bug 612726 michael@0: let isDataResource = michael@0: typeof this._contentURL == "string" && michael@0: this._contentURL.indexOf(assetsURI) == 0; michael@0: let hasContentScript = michael@0: (Array.isArray(this.contentScript) ? this.contentScript.length > 0 michael@0: : !!this.contentScript) || michael@0: (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0 michael@0: : !!this.contentScriptFile); michael@0: // If we have to inject `addon` we have to do it before document michael@0: // script execution, so during `start`: michael@0: this._injectInDocument = isDataResource && !hasContentScript; michael@0: if (this._injectInDocument) michael@0: this.contentScriptWhen = "start"; michael@0: michael@0: if ((frame.contentDocument.readyState == "complete" || michael@0: (frame.contentDocument.readyState == "interactive" && michael@0: this.contentScriptWhen != 'end' )) && michael@0: frame.contentDocument.location == this._contentURL) { michael@0: // In some cases src doesn't change and document is already ready michael@0: // (for ex: when the user moves a widget while customizing toolbars.) michael@0: this._onInit(); michael@0: return; michael@0: } michael@0: michael@0: let self = this; michael@0: michael@0: if ('start' == this.contentScriptWhen) { michael@0: this._loadEvent = 'start'; michael@0: on('document-element-inserted', michael@0: this._loadListener = function onStart({ subject: doc }) { michael@0: let window = doc.defaultView; michael@0: michael@0: if (ignoreWindow(window)) { michael@0: return; michael@0: } michael@0: michael@0: if (window && window == frame.contentWindow) { michael@0: self._unregisterListener(); michael@0: self._onInit(); michael@0: } michael@0: michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; michael@0: let self = this; michael@0: this._loadEvent = eventName; michael@0: frame.addEventListener(eventName, michael@0: this._loadListener = function _onReady(event) { michael@0: michael@0: if (event.target != frame.contentDocument) michael@0: return; michael@0: self._unregisterListener(); michael@0: michael@0: self._onInit(); michael@0: michael@0: }, true); michael@0: michael@0: }, michael@0: michael@0: /** michael@0: * Unregister listener that watchs for document being ready to be injected. michael@0: * This listener is registered in `Symbiont._initFrame`. michael@0: */ michael@0: _unregisterListener: function _unregisterListener() { michael@0: if (this._waitForFrame) { michael@0: off('content-document-global-created', this._waitForFrame); michael@0: delete this._waitForFrame; michael@0: } michael@0: michael@0: if (!this._loadListener) michael@0: return; michael@0: if (this._loadEvent == "start") { michael@0: off('document-element-inserted', this._loadListener); michael@0: } michael@0: else { michael@0: this._frame.removeEventListener(this._loadEvent, this._loadListener, michael@0: true); michael@0: } michael@0: this._loadListener = null; michael@0: }, michael@0: michael@0: /** michael@0: * Called by Symbiont itself when the frame is ready to load michael@0: * content scripts according to contentScriptWhen. Overloaded by Panel. michael@0: */ michael@0: _onInit: function () { michael@0: this._initWorker({ window: this._frame.contentWindow }); michael@0: } michael@0: michael@0: }); michael@0: exports.Symbiont = Symbiont;