toolkit/components/social/FrameWorkerContent.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:703b01365043
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 */
7
8 "use strict";
9
10 // the singleton frameworker, available for (ab)use by tests.
11 let frameworker;
12
13 (function () { // bug 673569 workaround :(
14
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 */
23
24 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
25
26 Cu.import("resource://gre/modules/Services.jsm");
27 Cu.import("resource://gre/modules/MessagePortBase.jsm");
28
29 function navigate(url) {
30 let webnav = docShell.QueryInterface(Ci.nsIWebNavigation);
31 webnav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
32 }
33
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;
51
52 this.load();
53 }
54
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);
68
69 Services.obs.addObserver(this._injectController, "document-element-inserted", false);
70 navigate(this.url);
71 },
72
73 _maybeRemoveInjectController: function() {
74 if (this._injectController) {
75 Services.obs.removeObserver(this._injectController, "document-element-inserted");
76 this._injectController = null;
77 }
78 },
79
80 createSandbox: function createSandbox() {
81 let workerWindow = content;
82 let sandbox = new Cu.Sandbox(workerWindow);
83
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'];
92
93 // Only expose localStorage if the caller opted-in
94 if (this.exposeLocalStorage) {
95 workerAPI.push('localStorage');
96 }
97
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;
136
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 };
145
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);
154
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 };
161
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;
166
167 workerWindow.addEventListener("DOMContentLoaded", function loadListener() {
168 workerWindow.removeEventListener("DOMContentLoaded", loadListener);
169
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 }
177
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 = "";
181
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 }
193
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 }
203
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 }
214
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 });
228
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 };
245
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;
254
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 },
260
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 },
266
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 },
274
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 },
282
283 _onCookieGet: function(msg) {
284 sendAsyncMessage("frameworker:cookie-get-response", content.document.cookie);
285 },
286
287 };
288
289 FrameWorkerManager.init();
290
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;
323
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;
332
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 }
347
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 }
362
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,
369
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 },
383
384 _dopost: function fw_ClientPort_dopost(data) {
385 if (!this._entangled) {
386 this._pendingMessagesOutgoing.push(data);
387 } else {
388 content.postMessage(data, "*");
389 }
390 },
391
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 },
397
398 _onerror: function fw_ClientPort_onerror(err) {
399 Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack);
400 },
401
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 }
414
415 function notifyWorkerError() {
416 sendAsyncMessage("frameworker:notify-worker-error", {origin: frameworker.origin});
417 }
418
419 }());

mercurial