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.

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

mercurial