diff -r 000000000000 -r 6474c204b198 addon-sdk/source/lib/sdk/deprecated/traits-worker.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,656 @@ +/* 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/. */ + +/** + * + * `deprecated/traits-worker` was previously `content/worker` and kept + * only due to `deprecated/symbiont` using it, which is necessary for + * `widget`, until that reaches deprecation EOL. + * + */ + +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const { Trait } = require('./traits'); +const { EventEmitter, EventEmitterTrait } = require('./events'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timers'); +const { URL } = require('../url'); +const unload = require('../system/unload'); +const observers = require('../system/events'); +const { Cortex } = require('./cortex'); +const { sandbox, evaluate, load } = require("../loader/sandbox"); +const { merge } = require('../util/object'); +const { getInnerId } = require("../window/utils"); +const { getTabForWindow } = require('../tabs/helpers'); +const { getTabForContentWindow } = require('../tabs/utils'); + +/* Trick the linker in order to ensure shipping these files in the XPI. + require('../content/content-worker.js'); + Then, retrieve URL of these files in the XPI: +*/ +let prefix = module.uri.split('deprecated/traits-worker.js')[0]; +const CONTENT_WORKER_URL = prefix + 'content/content-worker.js'; + +// Fetch additional list of domains to authorize access to for each content +// script. It is stored in manifest `metadata` field which contains +// package.json data. This list is originaly defined by authors in +// `permissions` attribute of their package.json addon file. +const permissions = require('@loader/options').metadata['permissions'] || {}; +const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; + +const JS_VERSION = '1.8'; + +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."; + + +const WorkerSandbox = EventEmitter.compose({ + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit() { + // First ensure having a regular array + // (otherwise, `arguments` would be mapped to an object by `stringify`) + let array = Array.slice(arguments); + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + // Ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + self._emitToContent(JSON.stringify(array, replacer)); + }, 0); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync() { + let args = Array.slice(arguments); + return this._emitToContent(args); + }, + + /** + * Tells if content script has at least one listener registered for one event, + * through `self.on('xxx', ...)`. + * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. + */ + hasListenerFor: function hasListenerFor(name) { + return this._hasListenerFor(name); + }, + + /** + * Method called by the worker sandbox when it needs to send a message + */ + _onContentEvent: function onContentEvent(args) { + // As `emit`, we ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + // We emit event to chrome/addon listeners + self._emit.apply(self, JSON.parse(args)); + }, 0); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + constructor: function WorkerSandbox(worker) { + this._addonWorker = worker; + + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // We receive a wrapped window, that may be an xraywrapper if it's content + let window = worker._window; + let proto = window; + + // Eventually use expanded principal sandbox feature, if some are given. + // + // But prevent it when the Worker isn't used for a content script but for + // injecting `addon` object into a Panel, Widget, ... scope. + // That's because: + // 1/ It is useless to use multiple domains as the worker is only used + // to communicate with the addon, + // 2/ By using it it would prevent the document to have access to any JS + // value of the worker. As JS values coming from multiple domain principals + // can't be accessed by "mono-principals" (principal with only one domain). + // Even if this principal is for a domain that is specified in the multiple + // domain principal. + let principals = window; + let wantGlobalProperties = [] + if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) { + principals = EXPANDED_PRINCIPALS.concat(window); + // We have to replace XHR constructor of the content document + // with a custom cross origin one, automagically added by platform code: + delete proto.XMLHttpRequest; + wantGlobalProperties.push("XMLHttpRequest"); + } + + // Instantiate trusted code in another Sandbox in order to prevent content + // script from messing with standard classes used by proxy and API code. + let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); + apiSandbox.console = console; + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = this._sandbox = sandbox(principals, { + sandboxPrototype: proto, + wantXrays: true, + wantGlobalProperties: wantGlobalProperties, + sameZoneAs: window, + metadata: { + SDKContentScript: true, + 'inner-window-id': getInnerId(window) + } + }); + // We have to ensure that window.top and window.parent are the exact same + // object than window object, i.e. the sandbox global object. But not + // always, in case of iframes, top and parent are another window object. + let top = window.top === window ? content : content.top; + let parent = window.parent === window ? content : content.parent; + merge(content, { + // We need "this === window === top" to be true in toplevel scope: + get window() content, + get top() top, + get parent() parent, + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + get unsafeWindow() window.wrappedJSObject + }); + + // Load trusted code that will inject content script API. + // We need to expose JS objects defined in same principal in order to + // avoid having any kind of wrapper. + load(apiSandbox, CONTENT_WORKER_URL); + + // prepare a clean `self.options` + let options = 'contentScriptOptions' in worker ? + JSON.stringify( worker.contentScriptOptions ) : + undefined; + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + // Bug 758203: We have to explicitely define `__exposedProps__` in order + // to allow access to these chrome object attributes from this sandbox with + // content priviledges + // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers + let chromeAPI = { + timers: { + setTimeout: timer.setTimeout, + setInterval: timer.setInterval, + clearTimeout: timer.clearTimeout, + clearInterval: timer.clearInterval, + __exposedProps__: { + setTimeout: 'r', + setInterval: 'r', + clearTimeout: 'r', + clearInterval: 'r' + } + }, + sandbox: { + evaluate: evaluate, + __exposedProps__: { + evaluate: 'r', + } + }, + __exposedProps__: { + timers: 'r', + sandbox: 'r', + } + }; + let onEvent = this._onContentEvent.bind(this); + // `ContentWorker` is defined in CONTENT_WORKER_URL file + let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); + this._emitToContent = result.emitToContent; + this._hasListenerFor = result.hasListenerFor; + + // Handle messages send by this script: + let self = this; + // console.xxx calls + this.on("console", function consoleListener(kind) { + console[kind].apply(console, Array.slice(arguments, 1)); + }); + + // self.postMessage calls + this.on("message", function postMessage(data) { + // destroyed? + if (self._addonWorker) + self._addonWorker._emit('message', data); + }); + + // self.port.emit calls + this.on("event", function portEmit(name, args) { + // destroyed? + if (self._addonWorker) + self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments); + }); + + // unwrap, recreate and propagate async Errors thrown from content-script + this.on("error", function onError({instanceOfError, value}) { + if (self._addonWorker) { + let error = value; + if (instanceOfError) { + error = new Error(value.message, value.fileName, value.lineNumber); + error.stack = value.stack; + error.name = value.name; + } + self._addonWorker._emit('error', error); + } + }); + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (worker._injectInDocument) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + Object.defineProperty(win, "addon", { + value: content.self + } + ); + } + + // Inject our `console` into target document if worker doesn't have a tab + // (e.g Panel, PageWorker, Widget). + // `worker.tab` can't be used because bug 804935. + if (!getTabForContentWindow(window)) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + + // export our chrome console to content window as described here: + // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn + let con = Cu.createObjectIn(win); + + let genPropDesc = function genPropDesc(fun) { + return { enumerable: true, configurable: true, writable: true, + value: console[fun] }; + } + + const properties = { + log: genPropDesc('log'), + info: genPropDesc('info'), + warn: genPropDesc('warn'), + error: genPropDesc('error'), + debug: genPropDesc('debug'), + trace: genPropDesc('trace'), + dir: genPropDesc('dir'), + group: genPropDesc('group'), + groupCollapsed: genPropDesc('groupCollapsed'), + groupEnd: genPropDesc('groupEnd'), + time: genPropDesc('time'), + timeEnd: genPropDesc('timeEnd'), + profile: genPropDesc('profile'), + profileEnd: genPropDesc('profileEnd'), + __noSuchMethod__: { enumerable: true, configurable: true, writable: true, + value: function() {} } + }; + + Object.defineProperties(con, properties); + Cu.makeObjectPropsNormal(con); + + win.console = con; + }; + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) ? worker.contentScript : null; + + if (contentScriptFile) { + if (Array.isArray(contentScriptFile)) + this._importScripts.apply(this, contentScriptFile); + else + this._importScripts(contentScriptFile); + } + if (contentScript) { + this._evaluate( + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy() { + this.emitSync("detach"); + this._sandbox = null; + this._addonWorker = null; + }, + + /** + * JavaScript sandbox where all the content scripts are evaluated. + * {Sandbox} + */ + _sandbox: null, + + /** + * Reference to the addon side of the worker. + * @type {Worker} + */ + _addonWorker: null, + + /** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ + _evaluate: function(code, filename) { + try { + evaluate(this._sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + this._addonWorker._emit('error', e); + } + }, + /** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `"error"` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ + _importScripts: function _importScripts(url) { + let urls = Array.slice(arguments, 0); + for each (let contentScriptFile in urls) { + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(this._sandbox, String(uri)); + else + throw Error("Unsupported `contentScriptFile` url: " + String(uri)); + } + catch(e) { + this._addonWorker._emit('error', e); + } + } + } +}); + +/** + * 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 = EventEmitter.compose({ + on: Trait.required, + _removeAllListeners: Trait.required, + + // List of messages fired before worker is initialized + get _earlyEvents() { + delete this._earlyEvents; + this._earlyEvents = []; + return this._earlyEvents; + }, + + /** + * 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 args = ['message'].concat(Array.slice(arguments)); + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + processMessage.apply(this, args); + }, + + /** + * EventEmitter, that behaves (calls listeners) asynchronously. + * A way to send customized messages to / from the worker. + * Events from in the worker can be observed / emitted via + * worker.on / worker.emit. + */ + get port() { + // We generate dynamically this attribute as it needs to be accessible + // before Worker.constructor gets called. (For ex: Panel) + + // create an event emitter that receive and send events from/to the worker + this._port = EventEmitterTrait.create({ + emit: this._emitEventToContent.bind(this) + }); + + // expose wrapped port, that exposes only public properties: + // We need to destroy this getter in order to be able to set the + // final value. We need to update only public port attribute as we never + // try to access port attribute from private API. + delete this._public.port; + this._public.port = Cortex(this._port); + // Replicate public port to the private object + delete this.port; + this.port = this._public.port; + + return this._port; + }, + + /** + * Same object than this.port but private API. + * Allow access to _emit, in order to send event to port. + */ + _port: null, + + /** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ + _emitEventToContent: function () { + let args = ['event'].concat(Array.slice(arguments)); + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + processMessage.apply(this, args); + }, + + // 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, + + constructor: function Worker(options) { + 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; + + this._setListeners(options); + + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send events during worker initialization. + this.port; + + this._documentUnload = this._documentUnload.bind(this); + this._pageShow = this._pageShow.bind(this); + this._pageHide = this._pageHide.bind(this); + + if ("window" in options) this._attach(options.window); + }, + + _setListeners: function(options) { + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('onDetach' in options) + this.on('detach', options.onDetach); + }, + + _attach: function(window) { + this._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 + this._windowID = getInnerId(this._window); + observers.on("inner-window-destroyed", this._documentUnload); + + // Listen to pagehide event in order to freeze the content script + // while the document is frozen in bfcache: + this._window.addEventListener("pageshow", this._pageShow, true); + this._window.addEventListener("pagehide", this._pageHide, true); + + // will set this._contentWorker pointing to the private API: + this._contentWorker = WorkerSandbox(this); + + // Mainly enable worker.port.emit to send event to the content worker + this._inited = true; + this._frozen = false; + + // Process all events and messages that were fired before the + // worker was initialized. + this._earlyEvents.forEach((function (args) { + processMessage.apply(this, args); + }).bind(this)); + }, + + _documentUnload: function _documentUnload({ subject, data }) { + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != this._windowID) return false; + this._workerCleanup(); + return true; + }, + + _pageShow: function _pageShow() { + this._contentWorker.emitSync("pageshow"); + this._emit("pageshow"); + this._frozen = false; + }, + + _pageHide: function _pageHide() { + this._contentWorker.emitSync("pagehide"); + this._emit("pagehide"); + this._frozen = true; + }, + + get url() { + // this._window will be null after detach + return this._window ? this._window.document.location.href : null; + }, + + get tab() { + // this._window will be null after detach + if (this._window) + return getTabForWindow(this._window); + return null; + }, + + /** + * Tells content worker to unload itself and + * removes all the references from itself. + */ + destroy: function destroy() { + this._workerCleanup(); + this._inited = true; + this._removeAllListeners(); + }, + + /** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ + _workerCleanup: function _workerCleanup() { + // maybe unloaded before content side is created + // As Symbiont call worker.constructor on document load + if (this._contentWorker) + this._contentWorker.destroy(); + this._contentWorker = null; + if (this._window) { + this._window.removeEventListener("pageshow", this._pageShow, true); + this._window.removeEventListener("pagehide", this._pageHide, true); + } + this._window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (this._windowID) { + this._windowID = null; + observers.off("inner-window-destroyed", this._documentUnload); + this._earlyEvents.length = 0; + this._emit("detach"); + } + this._inited = false; + }, + + /** + * Receive an event from the content script that need to be sent to + * worker.port. Provide a way for composed object to catch all events. + */ + _onContentScriptEvent: function _onContentScriptEvent() { + this._port._emit.apply(this._port, arguments); + }, + + /** + * 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, + + /** + * Flag to enable `addon` object injection in document. (bug 612726) + * @type {Boolean} + */ + _injectInDocument: false +}); + +/** + * 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 () { + if (!this._contentWorker) + throw new Error(ERR_DESTROYED); + if (this._frozen) + throw new Error(ERR_FROZEN); + + this._contentWorker.emit.apply(null, Array.slice(arguments)); +} + +exports.Worker = Worker;