1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
|
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Ci } = require("chrome");
const Services = require("Services");
const { Task } = require("devtools/shared/task");
const { BrowserElementWebNavigation } = require("./web-navigation");
const { getStack } = require("devtools/shared/platform/stack");
// A symbol used to hold onto the frame loader from the outer browser while tunneling.
const FRAME_LOADER = Symbol("devtools/responsive/frame-loader");
function debug(msg) {
// console.log(msg);
}
/**
* Properties swapped between browsers by browser.xml's `swapDocShells`. See also the
* list at /devtools/client/responsive.html/docs/browser-swap.md.
*/
const SWAPPED_BROWSER_STATE = [
"_remoteFinder",
"_securityUI",
"_documentURI",
"_documentContentType",
"_contentTitle",
"_characterSet",
"_contentPrincipal",
"_imageDocument",
"_fullZoom",
"_textZoom",
"_isSyntheticDocument",
"_innerWindowID",
"_manifestURI",
];
/**
* This module takes an "outer" <xul:browser> from a browser tab as described by
* Firefox's tabbrowser.xml and wires it up to an "inner" <iframe mozbrowser>
* browser element containing arbitrary page content of interest.
*
* The inner <iframe mozbrowser> element is _just_ the page content. It is not
* enough to to replace <xul:browser> on its own. <xul:browser> comes along
* with lots of associated functionality via XBL bindings defined for such
* elements in browser.xml and remote-browser.xml, and the Firefox UI depends on
* these various things to make the UI function.
*
* By mapping various methods, properties, and messages from the outer browser
* to the inner browser, we can control the content inside the inner browser
* using the standard Firefox UI elements for navigation, reloading, and more.
*
* The approaches used in this module were chosen to avoid needing changes to
* the core browser for this specialized use case. If we start to increase
* usage of <iframe mozbrowser> in the core browser, we should avoid this module
* and instead refactor things to work with mozbrowser directly.
*
* For the moment though, this serves as a sufficient path to connect the
* Firefox UI to a mozbrowser.
*
* @param outer
* A <xul:browser> from a regular browser tab.
* @param inner
* A <iframe mozbrowser> containing page content to be wired up to the
* primary browser UI via the outer browser.
*/
function tunnelToInnerBrowser(outer, inner) {
let browserWindow = outer.ownerDocument.defaultView;
let gBrowser = browserWindow.gBrowser;
let mmTunnel;
return {
start: Task.async(function* () {
if (outer.isRemoteBrowser) {
throw new Error("The outer browser must be non-remote.");
}
if (!inner.isRemoteBrowser) {
throw new Error("The inner browser must be remote.");
}
// Various browser methods access the `frameLoader` property, including:
// * `saveBrowser` from contentAreaUtils.js
// * `docShellIsActive` from remote-browser.xml
// * `hasContentOpener` from remote-browser.xml
// * `preserveLayers` from remote-browser.xml
// * `receiveMessage` from SessionStore.jsm
// In general, these methods are interested in the `frameLoader` for the content,
// so we redirect them to the inner browser's `frameLoader`.
outer[FRAME_LOADER] = outer.frameLoader;
Object.defineProperty(outer, "frameLoader", {
get() {
let stack = getStack();
// One exception is `receiveMessage` from SessionStore.jsm. SessionStore
// expects data updates to come in as messages targeted to a <xul:browser>.
// In addition, it verifies[1] correctness by checking that the received
// message's `targetFrameLoader` property matches the `frameLoader` of the
// <xul:browser>. To keep SessionStore functioning as expected, we give it the
// outer `frameLoader` as if nothing has changed.
// [1]: https://dxr.mozilla.org/mozilla-central/rev/b1b18f25c0ea69d9ee57c4198d577dfcd0129ce1/browser/components/sessionstore/SessionStore.jsm#716
if (stack.caller.filename.endsWith("SessionStore.jsm")) {
return outer[FRAME_LOADER];
}
return inner.frameLoader;
},
configurable: true,
enumerable: true,
});
// The `outerWindowID` of the content is used by browser actions like view source
// and print. They send the ID down to the client to find the right content frame
// to act on.
Object.defineProperty(outer, "outerWindowID", {
get() {
return inner.outerWindowID;
},
configurable: true,
enumerable: true,
});
// The `permanentKey` property on a <xul:browser> is used to index into various maps
// held by the session store. When you swap content around with
// `_swapBrowserDocShells`, these keys are also swapped so they follow the content.
// This means the key that matches the content is on the inner browser. Since we
// want the browser UI to believe the page content is part of the outer browser, we
// copy the content's `permanentKey` up to the outer browser.
debug("Copy inner permanentKey to outer browser");
outer.permanentKey = inner.permanentKey;
// Replace the outer browser's native messageManager with a message manager tunnel
// which we can use to route messages of interest to the inner browser instead.
// Note: The _actual_ messageManager accessible from
// `browser.frameLoader.messageManager` is not overridable and is left unchanged.
// Only the XBL getter `browser.messageManager` is overridden. Browser UI code
// always uses this getter instead of `browser.frameLoader.messageManager` directly,
// so this has the effect of overriding the message manager for browser UI code.
mmTunnel = new MessageManagerTunnel(outer, inner);
// We are tunneling to an inner browser with a specific remoteness, so it is simpler
// for the logic of the browser UI to assume this tab has taken on that remoteness,
// even though it's not true. Since the actions the browser UI performs are sent
// down to the inner browser by this tunnel, the tab's remoteness effectively is the
// remoteness of the inner browser.
outer.setAttribute("remote", "true");
// Clear out any cached state that references the current non-remote XBL binding,
// such as form fill controllers. Otherwise they will remain in place and leak the
// outer docshell.
outer.destroy();
// The XBL binding for remote browsers uses the message manager for many actions in
// the UI and that works well here, since it gives us one main thing we need to
// route to the inner browser (the messages), instead of having to tweak many
// different browser properties. It is safe to alter a XBL binding dynamically.
// The content within is not reloaded.
outer.style.MozBinding = "url(chrome://browser/content/tabbrowser.xml" +
"#tabbrowser-remote-browser)";
// The constructor of the new XBL binding is run asynchronously and there is no
// event to signal its completion. Spin an event loop to watch for properties that
// are set by the contructor.
while (!outer._remoteWebNavigation) {
Services.tm.currentThread.processNextEvent(true);
}
// Replace the `webNavigation` object with our own version which tries to use
// mozbrowser APIs where possible. This replaces the webNavigation object that the
// remote-browser.xml binding creates. We do not care about it's original value
// because stop() will remove the remote-browser.xml binding and these will no
// longer be used.
let webNavigation = new BrowserElementWebNavigation(inner);
webNavigation.copyStateFrom(inner._remoteWebNavigationImpl);
outer._remoteWebNavigation = webNavigation;
outer._remoteWebNavigationImpl = webNavigation;
// Now that we've flipped to the remote browser XBL binding, add `progressListener`
// onto the remote version of `webProgress`. Normally tabbrowser.xml does this step
// when it creates a new browser, etc. Since we manually changed the XBL binding
// above, it caused a fresh webProgress object to be created which does not have any
// listeners added. So, we get the listener that gBrowser is using for the tab and
// reattach it here.
let tab = gBrowser.getTabForBrowser(outer);
let filteredProgressListener = gBrowser._tabFilters.get(tab);
outer.webProgress.addProgressListener(filteredProgressListener);
// Add the inner browser to tabbrowser's WeakMap from browser to tab. This assists
// with tabbrowser's processing of some events such as MozLayerTreeReady which
// bubble up from the remote content frame and trigger tabbrowser to lookup the tab
// associated with the browser that triggered the event.
gBrowser._tabForBrowser.set(inner, tab);
// All of the browser state from content was swapped onto the inner browser. Pull
// this state up to the outer browser.
for (let property of SWAPPED_BROWSER_STATE) {
outer[property] = inner[property];
}
// Expose `PopupNotifications` on the content's owner global.
// This is used by PermissionUI.jsm for permission doorhangers.
// Note: This pollutes the responsive.html tool UI's global.
Object.defineProperty(inner.ownerGlobal, "PopupNotifications", {
get() {
return outer.ownerGlobal.PopupNotifications;
},
configurable: true,
enumerable: true,
});
// Expose `whereToOpenLink` on the content's owner global.
// This is used by ContentClick.jsm when opening links in ways other than just
// navigating the viewport.
// Note: This pollutes the responsive.html tool UI's global.
Object.defineProperty(inner.ownerGlobal, "whereToOpenLink", {
get() {
return outer.ownerGlobal.whereToOpenLink;
},
configurable: true,
enumerable: true,
});
// Add mozbrowser event handlers
inner.addEventListener("mozbrowseropenwindow", this);
}),
handleEvent(event) {
if (event.type != "mozbrowseropenwindow") {
return;
}
// Minimal support for <a target/> and window.open() which just ensures we at
// least open them somewhere (in a new tab). The following things are ignored:
// * Specific target names (everything treated as _blank)
// * Window features
// * window.opener
// These things are deferred for now, since content which does depend on them seems
// outside the main focus of RDM.
let { detail } = event;
event.preventDefault();
let uri = Services.io.newURI(detail.url, null, null);
// This API is used mainly because it's near the path used for <a target/> with
// regular browser tabs (which calls `openURIInFrame`). The more elaborate APIs
// that support openers, window features, etc. didn't seem callable from JS and / or
// this event doesn't give enough info to use them.
browserWindow.browserDOMWindow
.openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
Ci.nsIBrowserDOMWindow.OPEN_NEW);
},
stop() {
let tab = gBrowser.getTabForBrowser(outer);
let filteredProgressListener = gBrowser._tabFilters.get(tab);
// The browser's state has changed over time while the tunnel was active. Push the
// the current state down to the inner browser, so that it follows the content in
// case that browser will be swapped elsewhere.
for (let property of SWAPPED_BROWSER_STATE) {
inner[property] = outer[property];
}
// Remove the inner browser from the WeakMap from browser to tab.
gBrowser._tabForBrowser.delete(inner);
// Remove the progress listener we added manually.
outer.webProgress.removeProgressListener(filteredProgressListener);
// Reset the XBL binding back to the default.
outer.destroy();
outer.style.MozBinding = "";
// Reset @remote since this is now back to a regular, non-remote browser
outer.setAttribute("remote", "false");
// Delete browser window properties exposed on content's owner global
delete inner.ownerGlobal.PopupNotifications;
delete inner.ownerGlobal.whereToOpenLink;
// Remove mozbrowser event handlers
inner.removeEventListener("mozbrowseropenwindow", this);
mmTunnel.destroy();
mmTunnel = null;
// Reset overridden XBL properties and methods. Deleting the override
// means it will fallback to the original XBL binding definitions which
// are on the prototype.
delete outer.frameLoader;
delete outer[FRAME_LOADER];
delete outer.outerWindowID;
// Invalidate outer's permanentKey so that SessionStore stops associating
// things that happen to the outer browser with the content inside in the
// inner browser.
outer.permanentKey = { id: "zombie" };
browserWindow = null;
gBrowser = null;
},
};
}
exports.tunnelToInnerBrowser = tunnelToInnerBrowser;
/**
* This module allows specific messages of interest to be directed from the
* outer browser to the inner browser (and vice versa) in a targetted fashion
* without having to touch the original code paths that use them.
*/
function MessageManagerTunnel(outer, inner) {
if (outer.isRemoteBrowser) {
throw new Error("The outer browser must be non-remote.");
}
this.outer = outer;
this.inner = inner;
this.tunneledMessageNames = new Set();
this.init();
}
MessageManagerTunnel.prototype = {
/**
* Most message manager methods are left alone and are just passed along to
* the outer browser's real message manager.
*/
PASS_THROUGH_METHODS: [
"killChild",
"assertPermission",
"assertContainApp",
"assertAppHasPermission",
"assertAppHasStatus",
"removeDelayedFrameScript",
"getDelayedFrameScripts",
"loadProcessScript",
"removeDelayedProcessScript",
"getDelayedProcessScripts",
"addWeakMessageListener",
"removeWeakMessageListener",
],
/**
* The following methods are overridden with special behavior while tunneling.
*/
OVERRIDDEN_METHODS: [
"loadFrameScript",
"addMessageListener",
"removeMessageListener",
"sendAsyncMessage",
],
OUTER_TO_INNER_MESSAGES: [
// Messages sent from remote-browser.xml
"Browser:PurgeSessionHistory",
"InPermitUnload",
"PermitUnload",
// Messages sent from browser.js
"Browser:Reload",
// Messages sent from SelectParentHelper.jsm
"Forms:DismissedDropDown",
"Forms:MouseOut",
"Forms:MouseOver",
"Forms:SelectDropDownItem",
// Messages sent from SessionStore.jsm
"SessionStore:flush",
],
INNER_TO_OUTER_MESSAGES: [
// Messages sent to RemoteWebProgress.jsm
"Content:LoadURIResult",
"Content:LocationChange",
"Content:ProgressChange",
"Content:SecurityChange",
"Content:StateChange",
"Content:StatusChange",
// Messages sent to remote-browser.xml
"DOMTitleChanged",
"ImageDocumentLoaded",
"Forms:ShowDropDown",
"Forms:HideDropDown",
"InPermitUnload",
"PermitUnload",
// Messages sent to tabbrowser.xml
"contextmenu",
// Messages sent to SelectParentHelper.jsm
"Forms:UpdateDropDown",
// Messages sent to browser.js
"PageVisibility:Hide",
"PageVisibility:Show",
// Messages sent to SessionStore.jsm
"SessionStore:update",
// Messages sent to BrowserTestUtils.jsm
"browser-test-utils:loadEvent",
],
OUTER_TO_INNER_MESSAGE_PREFIXES: [
// Messages sent from nsContextMenu.js
"ContextMenu:",
// Messages sent from DevTools
"debug:",
// Messages sent from findbar.xml
"Findbar:",
// Messages sent from RemoteFinder.jsm
"Finder:",
// Messages sent from InlineSpellChecker.jsm
"InlineSpellChecker:",
// Messages sent from pageinfo.js
"PageInfo:",
// Messages sent from printUtils.js
"Printing:",
// Messages sent from browser-social.js
"Social:",
"PageMetadata:",
// Messages sent from viewSourceUtils.js
"ViewSource:",
],
INNER_TO_OUTER_MESSAGE_PREFIXES: [
// Messages sent to nsContextMenu.js
"ContextMenu:",
// Messages sent to DevTools
"debug:",
// Messages sent to findbar.xml
"Findbar:",
// Messages sent to RemoteFinder.jsm
"Finder:",
// Messages sent to pageinfo.js
"PageInfo:",
// Messages sent to printUtils.js
"Printing:",
// Messages sent to browser-social.js
"Social:",
"PageMetadata:",
// Messages sent to viewSourceUtils.js
"ViewSource:",
],
OUTER_TO_INNER_FRAME_SCRIPTS: [
// DevTools server for OOP frames
"resource://devtools/server/child.js"
],
get outerParentMM() {
if (!this.outer[FRAME_LOADER]) {
return null;
}
return this.outer[FRAME_LOADER].messageManager;
},
get outerChildMM() {
// This is only possible because we require the outer browser to be
// non-remote, so we're able to reach into its window and use the child
// side message manager there.
let docShell = this.outer[FRAME_LOADER].docShell;
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
},
get innerParentMM() {
if (!this.inner.frameLoader) {
return null;
}
return this.inner.frameLoader.messageManager;
},
init() {
for (let method of this.PASS_THROUGH_METHODS) {
// Workaround bug 449811 to ensure a fresh binding each time through the loop
let _method = method;
this[_method] = (...args) => {
if (!this.outerParentMM) {
return null;
}
return this.outerParentMM[_method](...args);
};
}
for (let name of this.INNER_TO_OUTER_MESSAGES) {
this.innerParentMM.addMessageListener(name, this);
this.tunneledMessageNames.add(name);
}
Services.obs.addObserver(this, "message-manager-close", false);
// Replace the outer browser's messageManager with this tunnel
Object.defineProperty(this.outer, "messageManager", {
value: this,
writable: false,
configurable: true,
enumerable: true,
});
},
destroy() {
if (this.destroyed) {
return;
}
this.destroyed = true;
debug("Destroy tunnel");
// Watch for the messageManager to close. In most cases, the caller will stop the
// tunnel gracefully before this, but when the browser window closes or application
// exits, we may not see the high-level close events.
Services.obs.removeObserver(this, "message-manager-close");
// Reset the messageManager. Deleting the override means it will fallback to the
// original XBL binding definitions which are on the prototype.
delete this.outer.messageManager;
for (let name of this.tunneledMessageNames) {
this.innerParentMM.removeMessageListener(name, this);
}
// Some objects may have cached this tunnel as the messageManager for a frame. To
// ensure it keeps working after tunnel close, rewrite the overidden methods as pass
// through methods.
for (let method of this.OVERRIDDEN_METHODS) {
// Workaround bug 449811 to ensure a fresh binding each time through the loop
let _method = method;
this[_method] = (...args) => {
if (!this.outerParentMM) {
return null;
}
return this.outerParentMM[_method](...args);
};
}
},
observe(subject, topic, data) {
if (topic != "message-manager-close") {
return;
}
if (subject == this.innerParentMM) {
debug("Inner messageManager has closed");
this.destroy();
}
if (subject == this.outerParentMM) {
debug("Outer messageManager has closed");
this.destroy();
}
},
loadFrameScript(url, ...args) {
debug(`Calling loadFrameScript for ${url}`);
if (!this.OUTER_TO_INNER_FRAME_SCRIPTS.includes(url)) {
debug(`Should load ${url} into inner?`);
this.outerParentMM.loadFrameScript(url, ...args);
return;
}
debug(`Load ${url} into inner`);
this.innerParentMM.loadFrameScript(url, ...args);
},
addMessageListener(name, ...args) {
debug(`Calling addMessageListener for ${name}`);
debug(`Add outer listener for ${name}`);
// Add an outer listener, just like a simple pass through
this.outerParentMM.addMessageListener(name, ...args);
// If the message name is part of a prefix we're tunneling, we also need to add the
// tunnel as an inner listener.
if (this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix))) {
debug(`Add inner listener for ${name}`);
this.innerParentMM.addMessageListener(name, this);
this.tunneledMessageNames.add(name);
}
},
removeMessageListener(name, ...args) {
debug(`Calling removeMessageListener for ${name}`);
debug(`Remove outer listener for ${name}`);
// Remove an outer listener, just like a simple pass through
this.outerParentMM.removeMessageListener(name, ...args);
// Leave the tunnel as an inner listener for the case of prefix messages to avoid
// tracking counts of add calls. The inner listener will get removed on destroy.
},
sendAsyncMessage(name, ...args) {
debug(`Calling sendAsyncMessage for ${name}`);
if (!this._shouldTunnelOuterToInner(name)) {
debug(`Should ${name} go to inner?`);
this.outerParentMM.sendAsyncMessage(name, ...args);
return;
}
debug(`${name} outer -> inner`);
this.innerParentMM.sendAsyncMessage(name, ...args);
},
receiveMessage({ name, data, objects, principal, sync }) {
if (!this._shouldTunnelInnerToOuter(name)) {
debug(`Received unexpected message ${name}`);
return undefined;
}
debug(`${name} inner -> outer, sync: ${sync}`);
if (sync) {
return this.outerChildMM.sendSyncMessage(name, data, objects, principal);
}
this.outerChildMM.sendAsyncMessage(name, data, objects, principal);
return undefined;
},
_shouldTunnelOuterToInner(name) {
return this.OUTER_TO_INNER_MESSAGES.includes(name) ||
this.OUTER_TO_INNER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
},
_shouldTunnelInnerToOuter(name) {
return this.INNER_TO_OUTER_MESSAGES.includes(name) ||
this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
},
};
|