toolkit/components/social/FrameWorkerContent.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 * You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 6 */
michael@0 7
michael@0 8 "use strict";
michael@0 9
michael@0 10 // the singleton frameworker, available for (ab)use by tests.
michael@0 11 let frameworker;
michael@0 12
michael@0 13 (function () { // bug 673569 workaround :(
michael@0 14
michael@0 15 /*
michael@0 16 * This is an implementation of a "Shared Worker" using a remote <browser>
michael@0 17 * element hosted in the hidden DOM window. This is the "content script"
michael@0 18 * implementation - it runs in the child process but has chrome permissions.
michael@0 19 *
michael@0 20 * A set of new APIs that simulate a shared worker are introduced to a sandbox
michael@0 21 * by cloning methods from the worker's JS origin.
michael@0 22 */
michael@0 23
michael@0 24 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
michael@0 25
michael@0 26 Cu.import("resource://gre/modules/Services.jsm");
michael@0 27 Cu.import("resource://gre/modules/MessagePortBase.jsm");
michael@0 28
michael@0 29 function navigate(url) {
michael@0 30 let webnav = docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 31 webnav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
michael@0 32 }
michael@0 33
michael@0 34 /**
michael@0 35 * FrameWorker
michael@0 36 *
michael@0 37 * A FrameWorker is a <browser> element hosted by the hiddenWindow.
michael@0 38 * It is constructed with the URL of some JavaScript that will be run in
michael@0 39 * the context of the browser; the script does not have a full DOM but is
michael@0 40 * instead run in a sandbox that has a select set of methods cloned from the
michael@0 41 * URL's domain.
michael@0 42 */
michael@0 43 function FrameWorker(url, name, origin, exposeLocalStorage) {
michael@0 44 this.url = url;
michael@0 45 this.name = name || url;
michael@0 46 this.ports = new Map(); // all unclosed ports, including ones yet to be entangled
michael@0 47 this.loaded = false;
michael@0 48 this.origin = origin;
michael@0 49 this._injectController = null;
michael@0 50 this.exposeLocalStorage = exposeLocalStorage;
michael@0 51
michael@0 52 this.load();
michael@0 53 }
michael@0 54
michael@0 55 FrameWorker.prototype = {
michael@0 56 load: function FrameWorker_loadWorker() {
michael@0 57 this._injectController = function(doc, topic, data) {
michael@0 58 if (!doc.defaultView || doc.defaultView != content) {
michael@0 59 return;
michael@0 60 }
michael@0 61 this._maybeRemoveInjectController();
michael@0 62 try {
michael@0 63 this.createSandbox();
michael@0 64 } catch (e) {
michael@0 65 Cu.reportError("FrameWorker: failed to create sandbox for " + this.url + ". " + e);
michael@0 66 }
michael@0 67 }.bind(this);
michael@0 68
michael@0 69 Services.obs.addObserver(this._injectController, "document-element-inserted", false);
michael@0 70 navigate(this.url);
michael@0 71 },
michael@0 72
michael@0 73 _maybeRemoveInjectController: function() {
michael@0 74 if (this._injectController) {
michael@0 75 Services.obs.removeObserver(this._injectController, "document-element-inserted");
michael@0 76 this._injectController = null;
michael@0 77 }
michael@0 78 },
michael@0 79
michael@0 80 createSandbox: function createSandbox() {
michael@0 81 let workerWindow = content;
michael@0 82 let sandbox = new Cu.Sandbox(workerWindow);
michael@0 83
michael@0 84 // copy the window apis onto the sandbox namespace only functions or
michael@0 85 // objects that are naturally a part of an iframe, I'm assuming they are
michael@0 86 // safe to import this way
michael@0 87 let workerAPI = ['WebSocket', 'atob', 'btoa',
michael@0 88 'clearInterval', 'clearTimeout', 'dump',
michael@0 89 'setInterval', 'setTimeout', 'XMLHttpRequest',
michael@0 90 'FileReader', 'Blob', 'EventSource', 'indexedDB',
michael@0 91 'location', 'Worker'];
michael@0 92
michael@0 93 // Only expose localStorage if the caller opted-in
michael@0 94 if (this.exposeLocalStorage) {
michael@0 95 workerAPI.push('localStorage');
michael@0 96 }
michael@0 97
michael@0 98 // Bug 798660 - XHR, WebSocket and Worker have issues in a sandbox and need
michael@0 99 // to be unwrapped to work
michael@0 100 let needsWaive = ['XMLHttpRequest', 'WebSocket', 'Worker'];
michael@0 101 // Methods need to be bound with the proper |this|.
michael@0 102 let needsBind = ['atob', 'btoa', 'dump', 'setInterval', 'clearInterval',
michael@0 103 'setTimeout', 'clearTimeout'];
michael@0 104 workerAPI.forEach(function(fn) {
michael@0 105 try {
michael@0 106 if (needsWaive.indexOf(fn) != -1)
michael@0 107 sandbox[fn] = XPCNativeWrapper.unwrap(workerWindow)[fn];
michael@0 108 else if (needsBind.indexOf(fn) != -1)
michael@0 109 sandbox[fn] = workerWindow[fn].bind(workerWindow);
michael@0 110 else
michael@0 111 sandbox[fn] = workerWindow[fn];
michael@0 112 }
michael@0 113 catch(e) {
michael@0 114 Cu.reportError("FrameWorker: failed to import API "+fn+"\n"+e+"\n");
michael@0 115 }
michael@0 116 });
michael@0 117 // the "navigator" object in a worker is a subset of the full navigator;
michael@0 118 // specifically, just the interfaces 'NavigatorID' and 'NavigatorOnLine'
michael@0 119 let navigator = {
michael@0 120 __exposedProps__: {
michael@0 121 "appName": "r",
michael@0 122 "appVersion": "r",
michael@0 123 "platform": "r",
michael@0 124 "userAgent": "r",
michael@0 125 "onLine": "r"
michael@0 126 },
michael@0 127 // interface NavigatorID
michael@0 128 appName: workerWindow.navigator.appName,
michael@0 129 appVersion: workerWindow.navigator.appVersion,
michael@0 130 platform: workerWindow.navigator.platform,
michael@0 131 userAgent: workerWindow.navigator.userAgent,
michael@0 132 // interface NavigatorOnLine
michael@0 133 get onLine() workerWindow.navigator.onLine
michael@0 134 };
michael@0 135 sandbox.navigator = navigator;
michael@0 136
michael@0 137 // Our importScripts function needs to 'eval' the script code from inside
michael@0 138 // a function, but using eval() directly means functions in the script
michael@0 139 // don't end up in the global scope.
michael@0 140 sandbox._evalInSandbox = function(s, url) {
michael@0 141 let baseURI = Services.io.newURI(workerWindow.location.href, null, null);
michael@0 142 Cu.evalInSandbox(s, sandbox, "1.8",
michael@0 143 Services.io.newURI(url, null, baseURI).spec, 1);
michael@0 144 };
michael@0 145
michael@0 146 // and we delegate ononline and onoffline events to the worker.
michael@0 147 // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#workerglobalscope
michael@0 148 workerWindow.addEventListener('offline', function fw_onoffline(event) {
michael@0 149 Cu.evalInSandbox("onoffline();", sandbox);
michael@0 150 }, false);
michael@0 151 workerWindow.addEventListener('online', function fw_ononline(event) {
michael@0 152 Cu.evalInSandbox("ononline();", sandbox);
michael@0 153 }, false);
michael@0 154
michael@0 155 sandbox._postMessage = function fw_postMessage(d, o) {
michael@0 156 workerWindow.postMessage(d, o)
michael@0 157 };
michael@0 158 sandbox._addEventListener = function fw_addEventListener(t, l, c) {
michael@0 159 workerWindow.addEventListener(t, l, c)
michael@0 160 };
michael@0 161
michael@0 162 // Note we don't need to stash |sandbox| in |this| as the unload handler
michael@0 163 // has a reference in its closure, so it can't die until that handler is
michael@0 164 // removed - at which time we've explicitly killed it anyway.
michael@0 165 let worker = this;
michael@0 166
michael@0 167 workerWindow.addEventListener("DOMContentLoaded", function loadListener() {
michael@0 168 workerWindow.removeEventListener("DOMContentLoaded", loadListener);
michael@0 169
michael@0 170 // no script, error out now rather than creating ports, etc
michael@0 171 let scriptText = workerWindow.document.body.textContent.trim();
michael@0 172 if (!scriptText) {
michael@0 173 Cu.reportError("FrameWorker: Empty worker script received");
michael@0 174 notifyWorkerError();
michael@0 175 return;
michael@0 176 }
michael@0 177
michael@0 178 // now that we've got the script text, remove it from the DOM;
michael@0 179 // no need for it to keep occupying memory there
michael@0 180 workerWindow.document.body.textContent = "";
michael@0 181
michael@0 182 // the content has loaded the js file as text - first inject the magic
michael@0 183 // port-handling code into the sandbox.
michael@0 184 try {
michael@0 185 Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortBase.jsm", sandbox);
michael@0 186 Services.scriptloader.loadSubScript("resource://gre/modules/MessagePortWorker.js", sandbox);
michael@0 187 }
michael@0 188 catch (e) {
michael@0 189 Cu.reportError("FrameWorker: Error injecting port code into content side of the worker: " + e + "\n" + e.stack);
michael@0 190 notifyWorkerError();
michael@0 191 return;
michael@0 192 }
michael@0 193
michael@0 194 // and wire up the client message handling.
michael@0 195 try {
michael@0 196 initClientMessageHandler();
michael@0 197 }
michael@0 198 catch (e) {
michael@0 199 Cu.reportError("FrameWorker: Error setting up event listener for chrome side of the worker: " + e + "\n" + e.stack);
michael@0 200 notifyWorkerError();
michael@0 201 return;
michael@0 202 }
michael@0 203
michael@0 204 // Now get the worker js code and eval it into the sandbox
michael@0 205 try {
michael@0 206 Cu.evalInSandbox(scriptText, sandbox, "1.8", workerWindow.location.href, 1);
michael@0 207 } catch (e) {
michael@0 208 Cu.reportError("FrameWorker: Error evaluating worker script for " + worker.name + ": " + e + "; " +
michael@0 209 (e.lineNumber ? ("Line #" + e.lineNumber) : "") +
michael@0 210 (e.stack ? ("\n" + e.stack) : ""));
michael@0 211 notifyWorkerError();
michael@0 212 return;
michael@0 213 }
michael@0 214
michael@0 215 // so finally we are ready to roll - dequeue all the pending connects
michael@0 216 worker.loaded = true;
michael@0 217 for (let [,port] of worker.ports) { // enumeration is in insertion order
michael@0 218 if (!port._entangled) {
michael@0 219 try {
michael@0 220 port._createWorkerAndEntangle(worker);
michael@0 221 }
michael@0 222 catch(e) {
michael@0 223 Cu.reportError("FrameWorker: Failed to entangle worker port: " + e + "\n" + e.stack);
michael@0 224 }
michael@0 225 }
michael@0 226 }
michael@0 227 });
michael@0 228
michael@0 229 // the 'unload' listener cleans up the worker and the sandbox. This
michael@0 230 // will be triggered by the window unloading as part of shutdown or reload.
michael@0 231 workerWindow.addEventListener("unload", function unloadListener() {
michael@0 232 workerWindow.removeEventListener("unload", unloadListener);
michael@0 233 worker.loaded = false;
michael@0 234 // No need to close ports - the worker probably wont see a
michael@0 235 // social.port-closing message and certainly isn't going to have time to
michael@0 236 // do anything if it did see it.
michael@0 237 worker.ports.clear();
michael@0 238 if (sandbox) {
michael@0 239 Cu.nukeSandbox(sandbox);
michael@0 240 sandbox = null;
michael@0 241 }
michael@0 242 });
michael@0 243 },
michael@0 244 };
michael@0 245
michael@0 246 const FrameWorkerManager = {
michael@0 247 init: function() {
michael@0 248 // first, setup the docShell to disable some types of content
michael@0 249 docShell.allowAuth = false;
michael@0 250 docShell.allowPlugins = false;
michael@0 251 docShell.allowImages = false;
michael@0 252 docShell.allowMedia = false;
michael@0 253 docShell.allowWindowControl = false;
michael@0 254
michael@0 255 addMessageListener("frameworker:init", this._onInit);
michael@0 256 addMessageListener("frameworker:connect", this._onConnect);
michael@0 257 addMessageListener("frameworker:port-message", this._onPortMessage);
michael@0 258 addMessageListener("frameworker:cookie-get", this._onCookieGet);
michael@0 259 },
michael@0 260
michael@0 261 // This new frameworker is being created. This should only be called once.
michael@0 262 _onInit: function(msg) {
michael@0 263 let {url, name, origin, exposeLocalStorage} = msg.data;
michael@0 264 frameworker = new FrameWorker(url, name, origin, exposeLocalStorage);
michael@0 265 },
michael@0 266
michael@0 267 // A new port is being established for this frameworker.
michael@0 268 _onConnect: function(msg) {
michael@0 269 let port = new ClientPort(msg.data.portId);
michael@0 270 frameworker.ports.set(msg.data.portId, port);
michael@0 271 if (frameworker.loaded && !frameworker.reloading)
michael@0 272 port._createWorkerAndEntangle(frameworker);
michael@0 273 },
michael@0 274
michael@0 275 // A message related to a port.
michael@0 276 _onPortMessage: function(msg) {
michael@0 277 // find the "client" port for this message and have it post it into
michael@0 278 // the worker.
michael@0 279 let port = frameworker.ports.get(msg.data.portId);
michael@0 280 port._dopost(msg.data);
michael@0 281 },
michael@0 282
michael@0 283 _onCookieGet: function(msg) {
michael@0 284 sendAsyncMessage("frameworker:cookie-get-response", content.document.cookie);
michael@0 285 },
michael@0 286
michael@0 287 };
michael@0 288
michael@0 289 FrameWorkerManager.init();
michael@0 290
michael@0 291 // This is the message listener for the chrome side of the world - ie, the
michael@0 292 // port that exists with chrome permissions inside the <browser/> (ie, in the
michael@0 293 // content process if a remote browser is used).
michael@0 294 function initClientMessageHandler() {
michael@0 295 function _messageHandler(event) {
michael@0 296 // We will ignore all messages destined for otherType.
michael@0 297 let data = event.data;
michael@0 298 let portid = data.portId;
michael@0 299 let port;
michael@0 300 if (!data.portFromType || data.portFromType !== "worker") {
michael@0 301 // this is a message posted by ourself so ignore it.
michael@0 302 return;
michael@0 303 }
michael@0 304 switch (data.portTopic) {
michael@0 305 // No "port-create" here - client ports are created explicitly.
michael@0 306 case "port-connection-error":
michael@0 307 // onconnect failed, we cannot connect the port, the worker has
michael@0 308 // become invalid
michael@0 309 notifyWorkerError();
michael@0 310 break;
michael@0 311 case "port-close":
michael@0 312 // the worker side of the port was closed, so close this side too.
michael@0 313 port = frameworker.ports.get(portid);
michael@0 314 if (!port) {
michael@0 315 // port already closed (which will happen when we call port.close()
michael@0 316 // below - the worker side will send us this message but we've
michael@0 317 // already closed it.)
michael@0 318 return;
michael@0 319 }
michael@0 320 frameworker.ports.delete(portid);
michael@0 321 port.close();
michael@0 322 break;
michael@0 323
michael@0 324 case "port-message":
michael@0 325 // the client posted a message to this worker port.
michael@0 326 port = frameworker.ports.get(portid);
michael@0 327 if (!port) {
michael@0 328 return;
michael@0 329 }
michael@0 330 port._onmessage(data.data);
michael@0 331 break;
michael@0 332
michael@0 333 default:
michael@0 334 break;
michael@0 335 }
michael@0 336 }
michael@0 337 // this can probably go once debugged and working correctly!
michael@0 338 function messageHandler(event) {
michael@0 339 try {
michael@0 340 _messageHandler(event);
michael@0 341 } catch (ex) {
michael@0 342 Cu.reportError("FrameWorker: Error handling client port control message: " + ex + "\n" + ex.stack);
michael@0 343 }
michael@0 344 }
michael@0 345 content.addEventListener('message', messageHandler);
michael@0 346 }
michael@0 347
michael@0 348 /**
michael@0 349 * ClientPort
michael@0 350 *
michael@0 351 * Client side of the entangled ports. This is just a shim that sends messages
michael@0 352 * back to the "parent" port living in the chrome process.
michael@0 353 *
michael@0 354 * constructor:
michael@0 355 * @param {integer} portid
michael@0 356 */
michael@0 357 function ClientPort(portid) {
michael@0 358 // messages posted to the worker before the worker has loaded.
michael@0 359 this._pendingMessagesOutgoing = [];
michael@0 360 AbstractPort.call(this, portid);
michael@0 361 }
michael@0 362
michael@0 363 ClientPort.prototype = {
michael@0 364 __proto__: AbstractPort.prototype,
michael@0 365 _portType: "client",
michael@0 366 // _entangled records if the port has ever been entangled (although may be
michael@0 367 // reset during a reload).
michael@0 368 _entangled: false,
michael@0 369
michael@0 370 _createWorkerAndEntangle: function fw_ClientPort_createWorkerAndEntangle(worker) {
michael@0 371 this._entangled = true;
michael@0 372 this._postControlMessage("port-create");
michael@0 373 for (let message of this._pendingMessagesOutgoing) {
michael@0 374 this._dopost(message);
michael@0 375 }
michael@0 376 this._pendingMessagesOutgoing = [];
michael@0 377 // The client side of the port might have been closed before it was
michael@0 378 // "entangled" with the worker, in which case we need to disentangle it
michael@0 379 if (this._closed) {
michael@0 380 worker.ports.delete(this._portid);
michael@0 381 }
michael@0 382 },
michael@0 383
michael@0 384 _dopost: function fw_ClientPort_dopost(data) {
michael@0 385 if (!this._entangled) {
michael@0 386 this._pendingMessagesOutgoing.push(data);
michael@0 387 } else {
michael@0 388 content.postMessage(data, "*");
michael@0 389 }
michael@0 390 },
michael@0 391
michael@0 392 // we are just a "shim" - any messages we get are just forwarded back to
michael@0 393 // the chrome parent process.
michael@0 394 _onmessage: function(data) {
michael@0 395 sendAsyncMessage("frameworker:port-message", {portId: this._portid, data: data});
michael@0 396 },
michael@0 397
michael@0 398 _onerror: function fw_ClientPort_onerror(err) {
michael@0 399 Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack);
michael@0 400 },
michael@0 401
michael@0 402 close: function fw_ClientPort_close() {
michael@0 403 if (this._closed) {
michael@0 404 return; // already closed.
michael@0 405 }
michael@0 406 // a leaky abstraction due to the worker spec not specifying how the
michael@0 407 // other end of a port knows it is closing.
michael@0 408 this.postMessage({topic: "social.port-closing"});
michael@0 409 AbstractPort.prototype.close.call(this);
michael@0 410 // this._pendingMessagesOutgoing should still be drained, as a closed
michael@0 411 // port will still get "entangled" quickly enough to deliver the messages.
michael@0 412 }
michael@0 413 }
michael@0 414
michael@0 415 function notifyWorkerError() {
michael@0 416 sendAsyncMessage("frameworker:notify-worker-error", {origin: frameworker.origin});
michael@0 417 }
michael@0 418
michael@0 419 }());

mercurial