addon-sdk/source/lib/sdk/deprecated/traits-worker.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /**
     6  *
     7  * `deprecated/traits-worker` was previously `content/worker` and kept
     8  * only due to `deprecated/symbiont` using it, which is necessary for
     9  * `widget`, until that reaches deprecation EOL.
    10  *
    11  */
    13 "use strict";
    15 module.metadata = {
    16   "stability": "deprecated"
    17 };
    19 const { Trait } = require('./traits');
    20 const { EventEmitter, EventEmitterTrait } = require('./events');
    21 const { Ci, Cu, Cc } = require('chrome');
    22 const timer = require('../timers');
    23 const { URL } = require('../url');
    24 const unload = require('../system/unload');
    25 const observers = require('../system/events');
    26 const { Cortex } = require('./cortex');
    27 const { sandbox, evaluate, load } = require("../loader/sandbox");
    28 const { merge } = require('../util/object');
    29 const { getInnerId } = require("../window/utils");
    30 const { getTabForWindow } = require('../tabs/helpers');
    31 const { getTabForContentWindow } = require('../tabs/utils');
    33 /* Trick the linker in order to ensure shipping these files in the XPI.
    34   require('../content/content-worker.js');
    35   Then, retrieve URL of these files in the XPI:
    36 */
    37 let prefix = module.uri.split('deprecated/traits-worker.js')[0];
    38 const CONTENT_WORKER_URL = prefix + 'content/content-worker.js';
    40 // Fetch additional list of domains to authorize access to for each content
    41 // script. It is stored in manifest `metadata` field which contains
    42 // package.json data. This list is originaly defined by authors in
    43 // `permissions` attribute of their package.json addon file.
    44 const permissions = require('@loader/options').metadata['permissions'] || {};
    45 const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
    47 const JS_VERSION = '1.8';
    49 const ERR_DESTROYED =
    50   "Couldn't find the worker to receive this message. " +
    51   "The script may not be initialized yet, or may already have been unloaded.";
    53 const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
    54                    "until it is visible again.";
    57 const WorkerSandbox = EventEmitter.compose({
    59   /**
    60    * Emit a message to the worker content sandbox
    61    */
    62   emit: function emit() {
    63     // First ensure having a regular array
    64     // (otherwise, `arguments` would be mapped to an object by `stringify`)
    65     let array = Array.slice(arguments);
    66     // JSON.stringify is buggy with cross-sandbox values,
    67     // it may return "{}" on functions. Use a replacer to match them correctly.
    68     function replacer(k, v) {
    69       return typeof v === "function" ? undefined : v;
    70     }
    71     // Ensure having an asynchronous behavior
    72     let self = this;
    73     timer.setTimeout(function () {
    74       self._emitToContent(JSON.stringify(array, replacer));
    75     }, 0);
    76   },
    78   /**
    79    * Synchronous version of `emit`.
    80    * /!\ Should only be used when it is strictly mandatory /!\
    81    *     Doesn't ensure passing only JSON values.
    82    *     Mainly used by context-menu in order to avoid breaking it.
    83    */
    84   emitSync: function emitSync() {
    85     let args = Array.slice(arguments);
    86     return this._emitToContent(args);
    87   },
    89   /**
    90    * Tells if content script has at least one listener registered for one event,
    91    * through `self.on('xxx', ...)`.
    92    * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
    93    */
    94   hasListenerFor: function hasListenerFor(name) {
    95     return this._hasListenerFor(name);
    96   },
    98   /**
    99    * Method called by the worker sandbox when it needs to send a message
   100    */
   101   _onContentEvent: function onContentEvent(args) {
   102     // As `emit`, we ensure having an asynchronous behavior
   103     let self = this;
   104     timer.setTimeout(function () {
   105       // We emit event to chrome/addon listeners
   106       self._emit.apply(self, JSON.parse(args));
   107     }, 0);
   108   },
   110   /**
   111    * Configures sandbox and loads content scripts into it.
   112    * @param {Worker} worker
   113    *    content worker
   114    */
   115   constructor: function WorkerSandbox(worker) {
   116     this._addonWorker = worker;
   118     // Ensure that `emit` has always the right `this`
   119     this.emit = this.emit.bind(this);
   120     this.emitSync = this.emitSync.bind(this);
   122     // We receive a wrapped window, that may be an xraywrapper if it's content
   123     let window = worker._window;
   124     let proto = window;
   126     // Eventually use expanded principal sandbox feature, if some are given.
   127     //
   128     // But prevent it when the Worker isn't used for a content script but for
   129     // injecting `addon` object into a Panel, Widget, ... scope.
   130     // That's because:
   131     // 1/ It is useless to use multiple domains as the worker is only used
   132     // to communicate with the addon,
   133     // 2/ By using it it would prevent the document to have access to any JS
   134     // value of the worker. As JS values coming from multiple domain principals
   135     // can't be accessed by "mono-principals" (principal with only one domain).
   136     // Even if this principal is for a domain that is specified in the multiple
   137     // domain principal.
   138     let principals  = window;
   139     let wantGlobalProperties = []
   140     if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
   141       principals = EXPANDED_PRINCIPALS.concat(window);
   142       // We have to replace XHR constructor of the content document
   143       // with a custom cross origin one, automagically added by platform code:
   144       delete proto.XMLHttpRequest;
   145       wantGlobalProperties.push("XMLHttpRequest");
   146     }
   148     // Instantiate trusted code in another Sandbox in order to prevent content
   149     // script from messing with standard classes used by proxy and API code.
   150     let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
   151     apiSandbox.console = console;
   153     // Create the sandbox and bind it to window in order for content scripts to
   154     // have access to all standard globals (window, document, ...)
   155     let content = this._sandbox = sandbox(principals, {
   156       sandboxPrototype: proto,
   157       wantXrays: true,
   158       wantGlobalProperties: wantGlobalProperties,
   159       sameZoneAs: window,
   160       metadata: {
   161         SDKContentScript: true,
   162         'inner-window-id': getInnerId(window)
   163       }
   164     });
   165     // We have to ensure that window.top and window.parent are the exact same
   166     // object than window object, i.e. the sandbox global object. But not
   167     // always, in case of iframes, top and parent are another window object.
   168     let top = window.top === window ? content : content.top;
   169     let parent = window.parent === window ? content : content.parent;
   170     merge(content, {
   171       // We need "this === window === top" to be true in toplevel scope:
   172       get window() content,
   173       get top() top,
   174       get parent() parent,
   175       // Use the Greasemonkey naming convention to provide access to the
   176       // unwrapped window object so the content script can access document
   177       // JavaScript values.
   178       // NOTE: this functionality is experimental and may change or go away
   179       // at any time!
   180       get unsafeWindow() window.wrappedJSObject
   181     });
   183     // Load trusted code that will inject content script API.
   184     // We need to expose JS objects defined in same principal in order to
   185     // avoid having any kind of wrapper.
   186     load(apiSandbox, CONTENT_WORKER_URL);
   188     // prepare a clean `self.options`
   189     let options = 'contentScriptOptions' in worker ?
   190       JSON.stringify( worker.contentScriptOptions ) :
   191       undefined;
   193     // Then call `inject` method and communicate with this script
   194     // by trading two methods that allow to send events to the other side:
   195     //   - `onEvent` called by content script
   196     //   - `result.emitToContent` called by addon script
   197     // Bug 758203: We have to explicitely define `__exposedProps__` in order
   198     // to allow access to these chrome object attributes from this sandbox with
   199     // content priviledges
   200     // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
   201     let chromeAPI = {
   202       timers: {
   203         setTimeout: timer.setTimeout,
   204         setInterval: timer.setInterval,
   205         clearTimeout: timer.clearTimeout,
   206         clearInterval: timer.clearInterval,
   207         __exposedProps__: {
   208           setTimeout: 'r',
   209           setInterval: 'r',
   210           clearTimeout: 'r',
   211           clearInterval: 'r'
   212         }
   213       },
   214       sandbox: {
   215         evaluate: evaluate,
   216         __exposedProps__: {
   217           evaluate: 'r',
   218         }
   219       },
   220       __exposedProps__: {
   221         timers: 'r',
   222         sandbox: 'r',
   223       }
   224     };
   225     let onEvent = this._onContentEvent.bind(this);
   226     // `ContentWorker` is defined in CONTENT_WORKER_URL file
   227     let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
   228     this._emitToContent = result.emitToContent;
   229     this._hasListenerFor = result.hasListenerFor;
   231     // Handle messages send by this script:
   232     let self = this;
   233     // console.xxx calls
   234     this.on("console", function consoleListener(kind) {
   235       console[kind].apply(console, Array.slice(arguments, 1));
   236     });
   238     // self.postMessage calls
   239     this.on("message", function postMessage(data) {
   240       // destroyed?
   241       if (self._addonWorker)
   242         self._addonWorker._emit('message', data);
   243     });
   245     // self.port.emit calls
   246     this.on("event", function portEmit(name, args) {
   247       // destroyed?
   248       if (self._addonWorker)
   249         self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
   250     });
   252     // unwrap, recreate and propagate async Errors thrown from content-script
   253     this.on("error", function onError({instanceOfError, value}) {
   254       if (self._addonWorker) {
   255         let error = value;
   256         if (instanceOfError) {
   257           error = new Error(value.message, value.fileName, value.lineNumber);
   258           error.stack = value.stack;
   259           error.name = value.name;
   260         }
   261         self._addonWorker._emit('error', error);
   262       }
   263     });
   265     // Inject `addon` global into target document if document is trusted,
   266     // `addon` in document is equivalent to `self` in content script.
   267     if (worker._injectInDocument) {
   268       let win = window.wrappedJSObject ? window.wrappedJSObject : window;
   269       Object.defineProperty(win, "addon", {
   270           value: content.self
   271         }
   272       );
   273     }
   275     // Inject our `console` into target document if worker doesn't have a tab
   276     // (e.g Panel, PageWorker, Widget).
   277     // `worker.tab` can't be used because bug 804935.
   278     if (!getTabForContentWindow(window)) {
   279       let win = window.wrappedJSObject ? window.wrappedJSObject : window;
   281       // export our chrome console to content window as described here:
   282       // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
   283       let con = Cu.createObjectIn(win);
   285       let genPropDesc = function genPropDesc(fun) {
   286         return { enumerable: true, configurable: true, writable: true,
   287           value: console[fun] };
   288       }
   290       const properties = {
   291         log: genPropDesc('log'),
   292         info: genPropDesc('info'),
   293         warn: genPropDesc('warn'),
   294         error: genPropDesc('error'),
   295         debug: genPropDesc('debug'),
   296         trace: genPropDesc('trace'),
   297         dir: genPropDesc('dir'),
   298         group: genPropDesc('group'),
   299         groupCollapsed: genPropDesc('groupCollapsed'),
   300         groupEnd: genPropDesc('groupEnd'),
   301         time: genPropDesc('time'),
   302         timeEnd: genPropDesc('timeEnd'),
   303         profile: genPropDesc('profile'),
   304         profileEnd: genPropDesc('profileEnd'),
   305        __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
   306                             value: function() {} }
   307       };
   309       Object.defineProperties(con, properties);
   310       Cu.makeObjectPropsNormal(con);
   312       win.console = con;
   313     };
   315     // The order of `contentScriptFile` and `contentScript` evaluation is
   316     // intentional, so programs can load libraries like jQuery from script URLs
   317     // and use them in scripts.
   318     let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
   319           : null,
   320         contentScript = ('contentScript' in worker) ? worker.contentScript : null;
   322     if (contentScriptFile) {
   323       if (Array.isArray(contentScriptFile))
   324         this._importScripts.apply(this, contentScriptFile);
   325       else
   326         this._importScripts(contentScriptFile);
   327     }
   328     if (contentScript) {
   329       this._evaluate(
   330         Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
   331       );
   332     }
   333   },
   334   destroy: function destroy() {
   335     this.emitSync("detach");
   336     this._sandbox = null;
   337     this._addonWorker = null;
   338   },
   340   /**
   341    * JavaScript sandbox where all the content scripts are evaluated.
   342    * {Sandbox}
   343    */
   344   _sandbox: null,
   346   /**
   347    * Reference to the addon side of the worker.
   348    * @type {Worker}
   349    */
   350   _addonWorker: null,
   352   /**
   353    * Evaluates code in the sandbox.
   354    * @param {String} code
   355    *    JavaScript source to evaluate.
   356    * @param {String} [filename='javascript:' + code]
   357    *    Name of the file
   358    */
   359   _evaluate: function(code, filename) {
   360     try {
   361       evaluate(this._sandbox, code, filename || 'javascript:' + code);
   362     }
   363     catch(e) {
   364       this._addonWorker._emit('error', e);
   365     }
   366   },
   367   /**
   368    * Imports scripts to the sandbox by reading files under urls and
   369    * evaluating its source. If exception occurs during evaluation
   370    * `"error"` event is emitted on the worker.
   371    * This is actually an analog to the `importScript` method in web
   372    * workers but in our case it's not exposed even though content
   373    * scripts may be able to do it synchronously since IO operation
   374    * takes place in the UI process.
   375    */
   376   _importScripts: function _importScripts(url) {
   377     let urls = Array.slice(arguments, 0);
   378     for each (let contentScriptFile in urls) {
   379       try {
   380         let uri = URL(contentScriptFile);
   381         if (uri.scheme === 'resource')
   382           load(this._sandbox, String(uri));
   383         else
   384           throw Error("Unsupported `contentScriptFile` url: " + String(uri));
   385       }
   386       catch(e) {
   387         this._addonWorker._emit('error', e);
   388       }
   389     }
   390   }
   391 });
   393 /**
   394  * Message-passing facility for communication between code running
   395  * in the content and add-on process.
   396  * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
   397  */
   398 const Worker = EventEmitter.compose({
   399   on: Trait.required,
   400   _removeAllListeners: Trait.required,
   402   // List of messages fired before worker is initialized
   403   get _earlyEvents() {
   404     delete this._earlyEvents;
   405     this._earlyEvents = [];
   406     return this._earlyEvents;
   407   },
   409   /**
   410    * Sends a message to the worker's global scope. Method takes single
   411    * argument, which represents data to be sent to the worker. The data may
   412    * be any primitive type value or `JSON`. Call of this method asynchronously
   413    * emits `message` event with data value in the global scope of this
   414    * symbiont.
   415    *
   416    * `message` event listeners can be set either by calling
   417    * `self.on` with a first argument string `"message"` or by
   418    * implementing `onMessage` function in the global scope of this worker.
   419    * @param {Number|String|JSON} data
   420    */
   421   postMessage: function (data) {
   422     let args = ['message'].concat(Array.slice(arguments));
   423     if (!this._inited) {
   424       this._earlyEvents.push(args);
   425       return;
   426     }
   427     processMessage.apply(this, args);
   428   },
   430   /**
   431    * EventEmitter, that behaves (calls listeners) asynchronously.
   432    * A way to send customized messages to / from the worker.
   433    * Events from in the worker can be observed / emitted via
   434    * worker.on / worker.emit.
   435    */
   436   get port() {
   437     // We generate dynamically this attribute as it needs to be accessible
   438     // before Worker.constructor gets called. (For ex: Panel)
   440     // create an event emitter that receive and send events from/to the worker
   441     this._port = EventEmitterTrait.create({
   442       emit: this._emitEventToContent.bind(this)
   443     });
   445     // expose wrapped port, that exposes only public properties:
   446     // We need to destroy this getter in order to be able to set the
   447     // final value. We need to update only public port attribute as we never
   448     // try to access port attribute from private API.
   449     delete this._public.port;
   450     this._public.port = Cortex(this._port);
   451     // Replicate public port to the private object
   452     delete this.port;
   453     this.port = this._public.port;
   455     return this._port;
   456   },
   458   /**
   459    * Same object than this.port but private API.
   460    * Allow access to _emit, in order to send event to port.
   461    */
   462   _port: null,
   464   /**
   465    * Emit a custom event to the content script,
   466    * i.e. emit this event on `self.port`
   467    */
   468   _emitEventToContent: function () {
   469     let args = ['event'].concat(Array.slice(arguments));
   470     if (!this._inited) {
   471       this._earlyEvents.push(args);
   472       return;
   473     }
   474     processMessage.apply(this, args);
   475   },
   477   // Is worker connected to the content worker sandbox ?
   478   _inited: false,
   480   // Is worker being frozen? i.e related document is frozen in bfcache.
   481   // Content script should not be reachable if frozen.
   482   _frozen: true,
   484   constructor: function Worker(options) {
   485     options = options || {};
   487     if ('contentScriptFile' in options)
   488       this.contentScriptFile = options.contentScriptFile;
   489     if ('contentScriptOptions' in options)
   490       this.contentScriptOptions = options.contentScriptOptions;
   491     if ('contentScript' in options)
   492       this.contentScript = options.contentScript;
   494     this._setListeners(options);
   496     unload.ensure(this._public, "destroy");
   498     // Ensure that worker._port is initialized for contentWorker to be able
   499     // to send events during worker initialization.
   500     this.port;
   502     this._documentUnload = this._documentUnload.bind(this);
   503     this._pageShow = this._pageShow.bind(this);
   504     this._pageHide = this._pageHide.bind(this);
   506     if ("window" in options) this._attach(options.window);
   507   },
   509   _setListeners: function(options) {
   510     if ('onError' in options)
   511       this.on('error', options.onError);
   512     if ('onMessage' in options)
   513       this.on('message', options.onMessage);
   514     if ('onDetach' in options)
   515       this.on('detach', options.onDetach);
   516   },
   518   _attach: function(window) {
   519     this._window = window;
   520     // Track document unload to destroy this worker.
   521     // We can't watch for unload event on page's window object as it
   522     // prevents bfcache from working:
   523     // https://developer.mozilla.org/En/Working_with_BFCache
   524     this._windowID = getInnerId(this._window);
   525     observers.on("inner-window-destroyed", this._documentUnload);
   527     // Listen to pagehide event in order to freeze the content script
   528     // while the document is frozen in bfcache:
   529     this._window.addEventListener("pageshow", this._pageShow, true);
   530     this._window.addEventListener("pagehide", this._pageHide, true);
   532     // will set this._contentWorker pointing to the private API:
   533     this._contentWorker = WorkerSandbox(this);
   535     // Mainly enable worker.port.emit to send event to the content worker
   536     this._inited = true;
   537     this._frozen = false;
   539     // Process all events and messages that were fired before the
   540     // worker was initialized.
   541     this._earlyEvents.forEach((function (args) {
   542       processMessage.apply(this, args);
   543     }).bind(this));
   544   },
   546   _documentUnload: function _documentUnload({ subject, data }) {
   547     let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
   548     if (innerWinID != this._windowID) return false;
   549     this._workerCleanup();
   550     return true;
   551   },
   553   _pageShow: function _pageShow() {
   554     this._contentWorker.emitSync("pageshow");
   555     this._emit("pageshow");
   556     this._frozen = false;
   557   },
   559   _pageHide: function _pageHide() {
   560     this._contentWorker.emitSync("pagehide");
   561     this._emit("pagehide");
   562     this._frozen = true;
   563   },
   565   get url() {
   566     // this._window will be null after detach
   567     return this._window ? this._window.document.location.href : null;
   568   },
   570   get tab() {
   571     // this._window will be null after detach
   572     if (this._window)
   573       return getTabForWindow(this._window);
   574     return null;
   575   },
   577   /**
   578    * Tells content worker to unload itself and
   579    * removes all the references from itself.
   580    */
   581   destroy: function destroy() {
   582     this._workerCleanup();
   583     this._inited = true;
   584     this._removeAllListeners();
   585   },
   587   /**
   588    * Remove all internal references to the attached document
   589    * Tells _port to unload itself and removes all the references from itself.
   590    */
   591   _workerCleanup: function _workerCleanup() {
   592     // maybe unloaded before content side is created
   593     // As Symbiont call worker.constructor on document load
   594     if (this._contentWorker)
   595       this._contentWorker.destroy();
   596     this._contentWorker = null;
   597     if (this._window) {
   598       this._window.removeEventListener("pageshow", this._pageShow, true);
   599       this._window.removeEventListener("pagehide", this._pageHide, true);
   600     }
   601     this._window = null;
   602     // This method may be called multiple times,
   603     // avoid dispatching `detach` event more than once
   604     if (this._windowID) {
   605       this._windowID = null;
   606       observers.off("inner-window-destroyed", this._documentUnload);
   607       this._earlyEvents.length = 0;
   608       this._emit("detach");
   609     }
   610     this._inited = false;
   611   },
   613   /**
   614    * Receive an event from the content script that need to be sent to
   615    * worker.port. Provide a way for composed object to catch all events.
   616    */
   617   _onContentScriptEvent: function _onContentScriptEvent() {
   618     this._port._emit.apply(this._port, arguments);
   619   },
   621   /**
   622    * Reference to the content side of the worker.
   623    * @type {WorkerGlobalScope}
   624    */
   625   _contentWorker: null,
   627   /**
   628    * Reference to the window that is accessible from
   629    * the content scripts.
   630    * @type {Object}
   631    */
   632   _window: null,
   634   /**
   635    * Flag to enable `addon` object injection in document. (bug 612726)
   636    * @type {Boolean}
   637    */
   638   _injectInDocument: false
   639 });
   641 /**
   642  * Fired from postMessage and _emitEventToContent, or from the _earlyMessage
   643  * queue when fired before the content is loaded. Sends arguments to
   644  * contentWorker if able
   645  */
   647 function processMessage () {
   648   if (!this._contentWorker)
   649     throw new Error(ERR_DESTROYED);
   650   if (this._frozen)
   651     throw new Error(ERR_FROZEN);
   653   this._contentWorker.emit.apply(null, Array.slice(arguments));
   654 }
   656 exports.Worker = Worker;

mercurial