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

mercurial