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/. */
4 'use strict';
6 module.metadata = {
7 'stability': 'unstable'
8 };
10 const { Class } = require('../core/heritage');
11 const { EventTarget } = require('../event/target');
12 const { on, off, emit } = require('../event/core');
13 const { requiresAddonGlobal } = require('./utils');
14 const { delay: async } = require('../lang/functional');
15 const { Ci, Cu, Cc } = require('chrome');
16 const timer = require('../timers');
17 const { URL } = require('../url');
18 const { sandbox, evaluate, load } = require('../loader/sandbox');
19 const { merge } = require('../util/object');
20 const { getTabForContentWindow } = require('../tabs/utils');
21 const { getInnerId } = require('../window/utils');
22 const { PlainTextConsole } = require('../console/plain-text');
24 // WeakMap of sandboxes so we can access private values
25 const sandboxes = new WeakMap();
27 /* Trick the linker in order to ensure shipping these files in the XPI.
28 require('./content-worker.js');
29 Then, retrieve URL of these files in the XPI:
30 */
31 let prefix = module.uri.split('sandbox.js')[0];
32 const CONTENT_WORKER_URL = prefix + 'content-worker.js';
33 const metadata = require('@loader/options').metadata;
35 // Fetch additional list of domains to authorize access to for each content
36 // script. It is stored in manifest `metadata` field which contains
37 // package.json data. This list is originaly defined by authors in
38 // `permissions` attribute of their package.json addon file.
39 const permissions = (metadata && metadata['permissions']) || {};
40 const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
42 const waiveSecurityMembrane = !!permissions['unsafe-content-script'];
44 const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager;
45 const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].
46 getService(Ci.nsIScriptSecurityManager);
48 const JS_VERSION = '1.8';
50 const WorkerSandbox = Class({
51 implements: [ EventTarget ],
53 /**
54 * Emit a message to the worker content sandbox
55 */
56 emit: function emit(type, ...args) {
57 // JSON.stringify is buggy with cross-sandbox values,
58 // it may return "{}" on functions. Use a replacer to match them correctly.
59 let replacer = (k, v) =>
60 typeof(v) === "function"
61 ? (type === "console" ? Function.toString.call(v) : void(0))
62 : v;
64 // Ensure having an asynchronous behavior
65 async(() =>
66 emitToContent(this, JSON.stringify([type, ...args], replacer))
67 );
68 },
70 /**
71 * Synchronous version of `emit`.
72 * /!\ Should only be used when it is strictly mandatory /!\
73 * Doesn't ensure passing only JSON values.
74 * Mainly used by context-menu in order to avoid breaking it.
75 */
76 emitSync: function emitSync(...args) {
77 return emitToContent(this, args);
78 },
80 /**
81 * Tells if content script has at least one listener registered for one event,
82 * through `self.on('xxx', ...)`.
83 * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
84 */
85 hasListenerFor: function hasListenerFor(name) {
86 return modelFor(this).hasListenerFor(name);
87 },
89 /**
90 * Configures sandbox and loads content scripts into it.
91 * @param {Worker} worker
92 * content worker
93 */
94 initialize: function WorkerSandbox(worker, window) {
95 let model = {};
96 sandboxes.set(this, model);
97 model.worker = worker;
98 // We receive a wrapped window, that may be an xraywrapper if it's content
99 let proto = window;
101 // TODO necessary?
102 // Ensure that `emit` has always the right `this`
103 this.emit = this.emit.bind(this);
104 this.emitSync = this.emitSync.bind(this);
106 // Use expanded principal for content-script if the content is a
107 // regular web content for better isolation.
108 // (This behavior can be turned off for now with the unsafe-content-script
109 // flag to give addon developers time for making the necessary changes)
110 // But prevent it when the Worker isn't used for a content script but for
111 // injecting `addon` object into a Panel, Widget, ... scope.
112 // That's because:
113 // 1/ It is useless to use multiple domains as the worker is only used
114 // to communicate with the addon,
115 // 2/ By using it it would prevent the document to have access to any JS
116 // value of the worker. As JS values coming from multiple domain principals
117 // can't be accessed by 'mono-principals' (principal with only one domain).
118 // Even if this principal is for a domain that is specified in the multiple
119 // domain principal.
120 let principals = window;
121 let wantGlobalProperties = [];
122 let isSystemPrincipal = secMan.isSystemPrincipal(
123 window.document.nodePrincipal);
124 if (!isSystemPrincipal && !requiresAddonGlobal(worker)) {
125 if (EXPANDED_PRINCIPALS.length > 0) {
126 // We have to replace XHR constructor of the content document
127 // with a custom cross origin one, automagically added by platform code:
128 delete proto.XMLHttpRequest;
129 wantGlobalProperties.push('XMLHttpRequest');
130 }
131 if (!waiveSecurityMembrane)
132 principals = EXPANDED_PRINCIPALS.concat(window);
133 }
135 // Instantiate trusted code in another Sandbox in order to prevent content
136 // script from messing with standard classes used by proxy and API code.
137 let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
138 apiSandbox.console = console;
140 // Create the sandbox and bind it to window in order for content scripts to
141 // have access to all standard globals (window, document, ...)
142 let content = sandbox(principals, {
143 sandboxPrototype: proto,
144 wantXrays: true,
145 wantGlobalProperties: wantGlobalProperties,
146 wantExportHelpers: true,
147 sameZoneAs: window,
148 metadata: {
149 SDKContentScript: true,
150 'inner-window-id': getInnerId(window)
151 }
152 });
153 model.sandbox = content;
155 // We have to ensure that window.top and window.parent are the exact same
156 // object than window object, i.e. the sandbox global object. But not
157 // always, in case of iframes, top and parent are another window object.
158 let top = window.top === window ? content : content.top;
159 let parent = window.parent === window ? content : content.parent;
160 merge(content, {
161 // We need 'this === window === top' to be true in toplevel scope:
162 get window() content,
163 get top() top,
164 get parent() parent,
165 // Use the Greasemonkey naming convention to provide access to the
166 // unwrapped window object so the content script can access document
167 // JavaScript values.
168 // NOTE: this functionality is experimental and may change or go away
169 // at any time!
170 get unsafeWindow() window.wrappedJSObject
171 });
173 // Load trusted code that will inject content script API.
174 // We need to expose JS objects defined in same principal in order to
175 // avoid having any kind of wrapper.
176 load(apiSandbox, CONTENT_WORKER_URL);
178 // prepare a clean `self.options`
179 let options = 'contentScriptOptions' in worker ?
180 JSON.stringify(worker.contentScriptOptions) :
181 undefined;
183 // Then call `inject` method and communicate with this script
184 // by trading two methods that allow to send events to the other side:
185 // - `onEvent` called by content script
186 // - `result.emitToContent` called by addon script
187 // Bug 758203: We have to explicitely define `__exposedProps__` in order
188 // to allow access to these chrome object attributes from this sandbox with
189 // content priviledges
190 // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
191 let onEvent = onContentEvent.bind(null, this);
192 // `ContentWorker` is defined in CONTENT_WORKER_URL file
193 let chromeAPI = createChromeAPI();
194 let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
196 // Merge `emitToContent` and `hasListenerFor` into our private
197 // model of the WorkerSandbox so we can communicate with content
198 // script
199 merge(model, result);
201 let console = new PlainTextConsole(null, getInnerId(window));
203 // Handle messages send by this script:
204 setListeners(this, console);
206 // Inject `addon` global into target document if document is trusted,
207 // `addon` in document is equivalent to `self` in content script.
208 if (requiresAddonGlobal(worker)) {
209 Object.defineProperty(getUnsafeWindow(window), 'addon', {
210 value: content.self
211 }
212 );
213 }
215 // Inject our `console` into target document if worker doesn't have a tab
216 // (e.g Panel, PageWorker, Widget).
217 // `worker.tab` can't be used because bug 804935.
218 if (!getTabForContentWindow(window)) {
219 let win = getUnsafeWindow(window);
221 // export our chrome console to content window, as described here:
222 // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
223 let con = Cu.createObjectIn(win);
225 let genPropDesc = function genPropDesc(fun) {
226 return { enumerable: true, configurable: true, writable: true,
227 value: console[fun] };
228 }
230 const properties = {
231 log: genPropDesc('log'),
232 info: genPropDesc('info'),
233 warn: genPropDesc('warn'),
234 error: genPropDesc('error'),
235 debug: genPropDesc('debug'),
236 trace: genPropDesc('trace'),
237 dir: genPropDesc('dir'),
238 group: genPropDesc('group'),
239 groupCollapsed: genPropDesc('groupCollapsed'),
240 groupEnd: genPropDesc('groupEnd'),
241 time: genPropDesc('time'),
242 timeEnd: genPropDesc('timeEnd'),
243 profile: genPropDesc('profile'),
244 profileEnd: genPropDesc('profileEnd'),
245 __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
246 value: function() {} }
247 };
249 Object.defineProperties(con, properties);
250 Cu.makeObjectPropsNormal(con);
252 win.console = con;
253 };
255 // The order of `contentScriptFile` and `contentScript` evaluation is
256 // intentional, so programs can load libraries like jQuery from script URLs
257 // and use them in scripts.
258 let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
259 : null,
260 contentScript = ('contentScript' in worker) ? worker.contentScript : null;
262 if (contentScriptFile)
263 importScripts.apply(null, [this].concat(contentScriptFile));
264 if (contentScript) {
265 evaluateIn(
266 this,
267 Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
268 );
269 }
270 },
271 destroy: function destroy(reason) {
272 if (typeof reason != 'string')
273 reason = '';
274 this.emitSync('event', 'detach', reason);
275 let model = modelFor(this);
276 model.sandbox = null
277 model.worker = null;
278 },
280 });
282 exports.WorkerSandbox = WorkerSandbox;
284 /**
285 * Imports scripts to the sandbox by reading files under urls and
286 * evaluating its source. If exception occurs during evaluation
287 * `'error'` event is emitted on the worker.
288 * This is actually an analog to the `importScript` method in web
289 * workers but in our case it's not exposed even though content
290 * scripts may be able to do it synchronously since IO operation
291 * takes place in the UI process.
292 */
293 function importScripts (workerSandbox, ...urls) {
294 let { worker, sandbox } = modelFor(workerSandbox);
295 for (let i in urls) {
296 let contentScriptFile = urls[i];
297 try {
298 let uri = URL(contentScriptFile);
299 if (uri.scheme === 'resource')
300 load(sandbox, String(uri));
301 else
302 throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
303 }
304 catch(e) {
305 emit(worker, 'error', e);
306 }
307 }
308 }
310 function setListeners (workerSandbox, console) {
311 let { worker } = modelFor(workerSandbox);
312 // console.xxx calls
313 workerSandbox.on('console', function consoleListener (kind, ...args) {
314 console[kind].apply(console, args);
315 });
317 // self.postMessage calls
318 workerSandbox.on('message', function postMessage(data) {
319 // destroyed?
320 if (worker)
321 emit(worker, 'message', data);
322 });
324 // self.port.emit calls
325 workerSandbox.on('event', function portEmit (...eventArgs) {
326 // If not destroyed, emit event information to worker
327 // `eventArgs` has the event name as first element,
328 // and remaining elements are additional arguments to pass
329 if (worker)
330 emit.apply(null, [worker.port].concat(eventArgs));
331 });
333 // unwrap, recreate and propagate async Errors thrown from content-script
334 workerSandbox.on('error', function onError({instanceOfError, value}) {
335 if (worker) {
336 let error = value;
337 if (instanceOfError) {
338 error = new Error(value.message, value.fileName, value.lineNumber);
339 error.stack = value.stack;
340 error.name = value.name;
341 }
342 emit(worker, 'error', error);
343 }
344 });
345 }
347 /**
348 * Evaluates code in the sandbox.
349 * @param {String} code
350 * JavaScript source to evaluate.
351 * @param {String} [filename='javascript:' + code]
352 * Name of the file
353 */
354 function evaluateIn (workerSandbox, code, filename) {
355 let { worker, sandbox } = modelFor(workerSandbox);
356 try {
357 evaluate(sandbox, code, filename || 'javascript:' + code);
358 }
359 catch(e) {
360 emit(worker, 'error', e);
361 }
362 }
364 /**
365 * Method called by the worker sandbox when it needs to send a message
366 */
367 function onContentEvent (workerSandbox, args) {
368 // As `emit`, we ensure having an asynchronous behavior
369 async(function () {
370 // We emit event to chrome/addon listeners
371 emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
372 });
373 }
376 function modelFor (workerSandbox) {
377 return sandboxes.get(workerSandbox);
378 }
380 function getUnsafeWindow (win) {
381 return win.wrappedJSObject || win;
382 }
384 function emitToContent (workerSandbox, args) {
385 return modelFor(workerSandbox).emitToContent(args);
386 }
388 function createChromeAPI () {
389 return {
390 timers: {
391 setTimeout: timer.setTimeout,
392 setInterval: timer.setInterval,
393 clearTimeout: timer.clearTimeout,
394 clearInterval: timer.clearInterval,
395 __exposedProps__: {
396 setTimeout: 'r',
397 setInterval: 'r',
398 clearTimeout: 'r',
399 clearInterval: 'r'
400 },
401 },
402 sandbox: {
403 evaluate: evaluate,
404 __exposedProps__: {
405 evaluate: 'r'
406 }
407 },
408 __exposedProps__: {
409 timers: 'r',
410 sandbox: 'r'
411 }
412 };
413 }