Wed, 31 Dec 2014 06:09:35 +0100
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;