Andrey Lushnikov 2c11b10598
browser(firefox): remove multisession logic (#4039)
This patch:
1. Changes `SimpleChannel` to buffer messages to the namespace that
   hasn't been registered yet. This allows us to create `SimpleChannel`
   per target on the browser side right away.
2. Removes multisession support. Now there's only one `PageAgent` in the
   content process, which talks to a single `PageHandler` on the browser
   side. Both ends can be created as-soon-as-needed; thanks to
   `SimpleChannel` bufferring, no messages will be lost and all messages
   will be delivered in proper order. (This is currently the reason why
   build 1178 flakes on windows).
3. Straightens up the target reporting. Targets are reported as soon
   as they appear on the browser side.

**NOTE:** this doesn't yet remove sessions from protocol.

References #3995
2020-10-02 04:13:42 -07:00

731 lines
26 KiB
JavaScript

/* 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/. */
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
const helper = new Helper();
const IDENTITY_NAME = 'JUGGLER ';
const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100;
const ALL_PERMISSIONS = [
'geo',
'desktop-notification',
];
class DownloadInterceptor {
constructor(registry) {
this._registry = registry
this._handlerToUuid = new Map();
helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request');
}
_onRequest(httpChannel, topic) {
let loadContext = helper.getLoadContext(httpChannel);
if (!loadContext)
return;
if (!loadContext.topFrameElement)
return;
const target = this._registry.targetForBrowser(loadContext.topFrameElement);
if (!target)
return;
target._channelIds.add(httpChannel.channelId);
}
//
// nsIDownloadInterceptor implementation.
//
interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) {
let pageTarget = this._registry._browserBrowsingContextToTarget.get(browsingContext);
// New page downloads won't have browsing contex.
if (!pageTarget)
pageTarget = this._registry._targetForChannel(request);
if (!pageTarget)
return false;
const browserContext = pageTarget.browserContext();
const options = browserContext.downloadOptions;
if (!options)
return false;
const uuid = helper.generateId();
let file = null;
if (options.behavior === 'saveToDisk') {
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(options.downloadsDir);
file.append(uuid);
try {
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
} catch (e) {
dump(`interceptDownloadRequest failed to create file: ${e}\n`);
return false;
}
}
outFile.value = file;
this._handlerToUuid.set(externalAppHandler, uuid);
const downloadInfo = {
uuid,
browserContextId: browserContext.browserContextId,
pageTargetId: pageTarget.id(),
url: request.name,
suggestedFileName: externalAppHandler.suggestedFileName,
};
this._registry.emit(TargetRegistry.Events.DownloadCreated, downloadInfo);
return true;
}
onDownloadComplete(externalAppHandler, canceled, errorName) {
const uuid = this._handlerToUuid.get(externalAppHandler);
if (!uuid)
return;
this._handlerToUuid.delete(externalAppHandler);
const downloadInfo = {
uuid,
};
if (errorName === 'NS_BINDING_ABORTED') {
downloadInfo.canceled = true;
} else {
downloadInfo.error = errorName;
}
this._registry.emit(TargetRegistry.Events.DownloadFinished, downloadInfo);
}
}
class TargetRegistry {
constructor() {
EventEmitter.decorate(this);
this._browserContextIdToBrowserContext = new Map();
this._userContextIdToBrowserContext = new Map();
this._browserToTarget = new Map();
this._browserBrowsingContextToTarget = new Map();
this._browserProxy = null;
// Cleanup containers from previous runs (if any)
for (const identity of ContextualIdentityService.getPublicIdentities()) {
if (identity.name && identity.name.startsWith(IDENTITY_NAME)) {
ContextualIdentityService.remove(identity.userContextId);
ContextualIdentityService.closeContainerTabs(identity.userContextId);
}
}
this._defaultContext = new BrowserContext(this, undefined, undefined);
Services.obs.addObserver({
observe: (subject, topic, data) => {
const browser = subject.ownerElement;
if (!browser)
return;
const target = this._browserToTarget.get(browser);
if (!target)
return;
target.emit('crashed');
target.dispose();
}
}, 'oop-frameloader-crashed');
Services.mm.addMessageListener('juggler:content-ready', {
receiveMessage: message => {
const linkedBrowser = message.target;
const target = this._browserToTarget.get(linkedBrowser);
if (!target)
return;
return {
scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument,
bindings: target.browserContext().bindings,
settings: target.browserContext().settings,
};
},
});
const onTabOpenListener = (appWindow, window, event) => {
const tab = event.target;
const userContextId = tab.userContextId;
const browserContext = this._userContextIdToBrowserContext.get(userContextId);
const hasExplicitSize = appWindow && (appWindow.chromeFlags & Ci.nsIWebBrowserChrome.JUGGLER_WINDOW_EXPLICIT_SIZE) !== 0;
const openerContext = tab.linkedBrowser.browsingContext.opener;
let openerTarget;
if (openerContext) {
// Popups usually have opener context.
openerTarget = this._browserBrowsingContextToTarget.get(openerContext);
} else if (tab.openerTab) {
// Noopener popups from the same window have opener tab instead.
openerTarget = this._browserToTarget.get(tab.openerTab.linkedBrowser);
}
if (!browserContext)
throw new Error(`Internal error: cannot find context for userContextId=${userContextId}`);
const target = new PageTarget(this, window, tab, browserContext, openerTarget);
target.updateUserAgent();
if (!hasExplicitSize)
target.updateViewportSize();
};
const onTabCloseListener = event => {
const tab = event.target;
const linkedBrowser = tab.linkedBrowser;
const target = this._browserToTarget.get(linkedBrowser);
if (target)
target.dispose();
};
const domWindowTabListeners = new Map();
const onOpenWindow = async (appWindow) => {
let domWindow;
if (appWindow instanceof Ci.nsIAppWindow) {
domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
} else {
domWindow = appWindow;
appWindow = null;
}
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
return;
// In persistent mode, window might be opened long ago and might be
// already initialized.
//
// In this case, we want to keep this callback synchronous so that we will call
// `onTabOpenListener` synchronously and before the sync IPc message `juggler:content-ready`.
if (domWindow.document.readyState === 'uninitialized' || domWindow.document.readyState === 'loading') {
// For non-initialized windows, DOMContentLoaded initializes gBrowser
// and starts tab loading (see //browser/base/content/browser.js), so we
// are guaranteed to call `onTabOpenListener` before the sync IPC message
// `juggler:content-ready`.
await helper.awaitEvent(domWindow, 'DOMContentLoaded');
}
if (!domWindow.gBrowser)
return;
const tabContainer = domWindow.gBrowser.tabContainer;
domWindowTabListeners.set(domWindow, [
helper.addEventListener(tabContainer, 'TabOpen', event => onTabOpenListener(appWindow, domWindow, event)),
helper.addEventListener(tabContainer, 'TabClose', onTabCloseListener),
]);
for (const tab of domWindow.gBrowser.tabs)
onTabOpenListener(appWindow, domWindow, { target: tab });
};
const onCloseWindow = window => {
const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
return;
if (!domWindow.gBrowser)
return;
const listeners = domWindowTabListeners.get(domWindow) || [];
domWindowTabListeners.delete(domWindow);
helper.removeListeners(listeners);
for (const tab of domWindow.gBrowser.tabs)
onTabCloseListener({ target: tab });
};
const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService);
extHelperAppSvc.setDownloadInterceptor(new DownloadInterceptor(this));
Services.wm.addListener({ onOpenWindow, onCloseWindow });
for (const win of Services.wm.getEnumerator(null))
onOpenWindow(win);
}
setBrowserProxy(proxy) {
this._browserProxy = proxy;
}
getProxyInfo(channel) {
const originAttributes = channel.loadInfo && channel.loadInfo.originAttributes;
const browserContext = originAttributes ? this.browserContextForUserContextId(originAttributes.userContextId) : null;
// Prefer context proxy and fallback to browser-level proxy.
const proxyInfo = (browserContext && browserContext._proxy) || this._browserProxy;
if (!proxyInfo || proxyInfo.bypass.some(domainSuffix => channel.URI.host.endsWith(domainSuffix)))
return null;
return proxyInfo;
}
defaultContext() {
return this._defaultContext;
}
createBrowserContext(removeOnDetach) {
return new BrowserContext(this, helper.generateId(), removeOnDetach);
}
browserContextForId(browserContextId) {
return this._browserContextIdToBrowserContext.get(browserContextId);
}
browserContextForUserContextId(userContextId) {
return this._userContextIdToBrowserContext.get(userContextId);
}
async newPage({browserContextId}) {
const browserContext = this.browserContextForId(browserContextId);
const features = "chrome,dialog=no,all";
// See _callWithURIToLoad in browser.js for the structure of window.arguments
// window.arguments[1]: unused (bug 871161)
// [2]: referrerInfo (nsIReferrerInfo)
// [3]: postData (nsIInputStream)
// [4]: allowThirdPartyFixup (bool)
// [5]: userContextId (int)
// [6]: originPrincipal (nsIPrincipal)
// [7]: originStoragePrincipal (nsIPrincipal)
// [8]: triggeringPrincipal (nsIPrincipal)
// [9]: allowInheritPrincipal (bool)
// [10]: csp (nsIContentSecurityPolicy)
// [11]: nsOpenWindowInfo
const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlSupports.data = 'about:blank';
args.appendElement(urlSupports); // 0
args.appendElement(undefined); // 1
args.appendElement(undefined); // 2
args.appendElement(undefined); // 3
args.appendElement(undefined); // 4
const userContextIdSupports = Cc[
"@mozilla.org/supports-PRUint32;1"
].createInstance(Ci.nsISupportsPRUint32);
userContextIdSupports.data = browserContext.userContextId;
args.appendElement(userContextIdSupports); // 5
args.appendElement(undefined); // 6
args.appendElement(undefined); // 7
args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8
const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
await waitForWindowReady(window);
if (window.gBrowser.browsers.length !== 1)
throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`);
const browser = window.gBrowser.browsers[0];
const target = this._browserToTarget.get(browser);
browser.focus();
if (browserContext.settings.timezoneId) {
if (await target.hasFailedToOverrideTimezone())
throw new Error('Failed to override timezone');
}
return target.id();
}
targets() {
return Array.from(this._browserToTarget.values());
}
targetForBrowser(browser) {
return this._browserToTarget.get(browser);
}
_targetForChannel(channel) {
const channelId = channel.channelId;
for (const target of this._browserToTarget.values()) {
if (target._channelIds.has(channelId))
return target;
}
return null;
}
}
class PageTarget {
constructor(registry, win, tab, browserContext, opener) {
EventEmitter.decorate(this);
this._targetId = helper.generateId();
this._registry = registry;
this._window = win;
this._gBrowser = win.gBrowser;
this._tab = tab;
this._linkedBrowser = tab.linkedBrowser;
this._browserContext = browserContext;
this._viewportSize = undefined;
this._url = 'about:blank';
this._openerId = opener ? opener.id() : undefined;
this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
this._channelIds = new Set();
const navigationListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
};
this._eventListeners = [
helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
];
this._disposed = false;
browserContext.pages.add(this);
this._registry._browserToTarget.set(this._linkedBrowser, this);
this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this);
this._registry.emit(TargetRegistry.Events.TargetCreated, this);
}
async windowReady() {
await waitForWindowReady(this._window);
}
linkedBrowser() {
return this._linkedBrowser;
}
browserContext() {
return this._browserContext;
}
updateUserAgent() {
this._linkedBrowser.browsingContext.customUserAgent = this._browserContext.defaultUserAgent;
}
async updateViewportSize() {
// Viewport size is defined by three arguments:
// 1. default size. Could be explicit if set as part of `window.open` call, e.g.
// `window.open(url, title, 'width=400,height=400')`
// 2. page viewport size
// 3. browserContext viewport size
//
// The "default size" (1) is only respected when the page is opened.
// Otherwise, explicitly set page viewport prevales over browser context
// default viewport.
const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize;
const actualSize = await setViewportSizeForBrowser(viewportSize, this._linkedBrowser, this._window);
await this._channel.connect('').send('awaitViewportDimensions', {
width: actualSize.width,
height: actualSize.height
});
}
async setViewportSize(viewportSize) {
this._viewportSize = viewportSize;
await this.updateViewportSize();
}
async close(runBeforeUnload = false) {
await this._gBrowser.removeTab(this._tab, {
skipPermitUnload: !runBeforeUnload,
});
}
channel() {
return this._channel;
}
id() {
return this._targetId;
}
info() {
return {
targetId: this.id(),
type: 'page',
browserContextId: this._browserContext.browserContextId,
openerId: this._openerId,
};
}
_onNavigated(aLocation) {
this._url = aLocation.spec;
this._browserContext.grantPermissionsToOrigin(this._url);
}
async ensurePermissions() {
await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
}
async addScriptToEvaluateOnNewDocument(script) {
await this._channel.connect('').send('addScriptToEvaluateOnNewDocument', script).catch(e => void e);
}
async addBinding(name, script) {
await this._channel.connect('').send('addBinding', { name, script }).catch(e => void e);
}
async applyContextSetting(name, value) {
await this._channel.connect('').send('applyContextSetting', { name, value }).catch(e => void e);
}
async hasFailedToOverrideTimezone() {
return await this._channel.connect('').send('hasFailedToOverrideTimezone').catch(e => true);
}
dispose() {
this._disposed = true;
this._browserContext.pages.delete(this);
this._registry._browserToTarget.delete(this._linkedBrowser);
this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext);
helper.removeListeners(this._eventListeners);
this._registry.emit(TargetRegistry.Events.TargetDestroyed, this);
}
}
class BrowserContext {
constructor(registry, browserContextId, removeOnDetach) {
this._registry = registry;
this.browserContextId = browserContextId;
// Default context has userContextId === 0, but we pass undefined to many APIs just in case.
this.userContextId = 0;
if (browserContextId !== undefined) {
const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId);
this.userContextId = identity.userContextId;
}
this._principals = [];
// Maps origins to the permission lists.
this._permissions = new Map();
this._registry._browserContextIdToBrowserContext.set(this.browserContextId, this);
this._registry._userContextIdToBrowserContext.set(this.userContextId, this);
this._proxy = null;
this.removeOnDetach = removeOnDetach;
this.extraHTTPHeaders = undefined;
this.httpCredentials = undefined;
this.requestInterceptionEnabled = undefined;
this.ignoreHTTPSErrors = undefined;
this.downloadOptions = undefined;
this.defaultViewportSize = undefined;
this.defaultUserAgent = null;
this.screencastOptions = undefined;
this.scriptsToEvaluateOnNewDocument = [];
this.bindings = [];
this.settings = {};
this.pages = new Set();
}
async destroy() {
if (this.userContextId !== 0) {
ContextualIdentityService.remove(this.userContextId);
ContextualIdentityService.closeContainerTabs(this.userContextId);
if (this.pages.size) {
await new Promise(f => {
const listener = helper.on(this._registry, TargetRegistry.Events.TargetDestroyed, () => {
if (!this.pages.size) {
helper.removeListeners([listener]);
f();
}
});
});
}
}
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
}
setProxy(proxy) {
this._proxy = proxy;
}
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
if (this.ignoreHTTPSErrors === ignoreHTTPSErrors)
return;
this.ignoreHTTPSErrors = ignoreHTTPSErrors;
const certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
if (ignoreHTTPSErrors) {
Preferences.set("network.stricttransportsecurity.preloadlist", false);
Preferences.set("security.cert_pinning.enforcement_level", 0);
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId);
} else {
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId);
}
}
async setDefaultUserAgent(userAgent) {
this.defaultUserAgent = userAgent;
for (const page of this.pages)
page.updateUserAgent();
}
async setDefaultViewport(viewport) {
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
const promises = Array.from(this.pages).map(page => page.updateViewportSize());
await Promise.all([
this.applySetting('deviceScaleFactor', viewport ? viewport.deviceScaleFactor : undefined),
...promises,
]);
}
async addScriptToEvaluateOnNewDocument(script) {
this.scriptsToEvaluateOnNewDocument.push(script);
await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script)));
}
async addBinding(name, script) {
this.bindings.push({ name, script });
await Promise.all(Array.from(this.pages).map(page => page.addBinding(name, script)));
}
async applySetting(name, value) {
this.settings[name] = value;
await Promise.all(Array.from(this.pages).map(page => page.applyContextSetting(name, value)));
}
async grantPermissions(origin, permissions) {
this._permissions.set(origin, permissions);
const promises = [];
for (const page of this.pages) {
if (origin === '*' || page._url.startsWith(origin)) {
this.grantPermissionsToOrigin(page._url);
promises.push(page.ensurePermissions());
}
}
await Promise.all(promises);
}
resetPermissions() {
for (const principal of this._principals) {
for (const permission of ALL_PERMISSIONS)
Services.perms.removeFromPrincipal(principal, permission);
}
this._principals = [];
this._permissions.clear();
}
grantPermissionsToOrigin(url) {
let origin = Array.from(this._permissions.keys()).find(key => url.startsWith(key));
if (!origin)
origin = '*';
const permissions = this._permissions.get(origin);
if (!permissions)
return;
const attrs = { userContextId: this.userContextId || undefined };
const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(url), attrs);
this._principals.push(principal);
for (const permission of ALL_PERMISSIONS) {
const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION;
Services.perms.addFromPrincipal(principal, permission, action, Ci.nsIPermissionManager.EXPIRE_NEVER, 0 /* expireTime */);
}
}
setCookies(cookies) {
const protocolToSameSite = {
[undefined]: Ci.nsICookie.SAMESITE_NONE,
'Lax': Ci.nsICookie.SAMESITE_LAX,
'Strict': Ci.nsICookie.SAMESITE_STRICT,
};
for (const cookie of cookies) {
const uri = cookie.url ? NetUtil.newURI(cookie.url) : null;
let domain = cookie.domain;
if (!domain) {
if (!uri)
throw new Error('At least one of the url and domain needs to be specified');
domain = uri.host;
}
let path = cookie.path;
if (!path)
path = uri ? dirPath(uri.filePath) : '/';
let secure = false;
if (cookie.secure !== undefined)
secure = cookie.secure;
else if (uri && uri.scheme === 'https')
secure = true;
Services.cookies.add(
domain,
path,
cookie.name,
cookie.value,
secure,
cookie.httpOnly || false,
cookie.expires === undefined || cookie.expires === -1 /* isSession */,
cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires,
{ userContextId: this.userContextId || undefined } /* originAttributes */,
protocolToSameSite[cookie.sameSite],
Ci.nsICookie.SCHEME_UNSET
);
}
}
clearCookies() {
Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId || undefined }));
}
getCookies() {
const result = [];
const sameSiteToProtocol = {
[Ci.nsICookie.SAMESITE_NONE]: 'None',
[Ci.nsICookie.SAMESITE_LAX]: 'Lax',
[Ci.nsICookie.SAMESITE_STRICT]: 'Strict',
};
for (let cookie of Services.cookies.cookies) {
if (cookie.originAttributes.userContextId !== this.userContextId)
continue;
if (cookie.host === 'addons.mozilla.org')
continue;
result.push({
name: cookie.name,
value: cookie.value,
domain: cookie.host,
path: cookie.path,
expires: cookie.isSession ? -1 : cookie.expiry,
size: cookie.name.length + cookie.value.length,
httpOnly: cookie.isHttpOnly,
secure: cookie.isSecure,
session: cookie.isSession,
sameSite: sameSiteToProtocol[cookie.sameSite],
});
}
return result;
}
setScreencastOptions(options) {
this.screencastOptions = options;
}
}
function dirPath(path) {
return path.substring(0, path.lastIndexOf('/') + 1);
}
async function waitForWindowReady(window) {
if (window.delayedStartupPromise) {
await window.delayedStartupPromise;
} else {
await new Promise((resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic) {
if (window == aSubject) {
Services.obs.removeObserver(observer, aTopic);
resolve();
}
}, "browser-delayed-startup-finished");
}));
}
if (window.document.readyState !== 'complete')
await helper.awaitEvent(window, 'load');
}
async function setViewportSizeForBrowser(viewportSize, browser, window) {
await waitForWindowReady(window);
if (viewportSize) {
const {width, height} = viewportSize;
const rect = browser.getBoundingClientRect();
window.resizeBy(width - rect.width, height - rect.height);
browser.style.setProperty('min-width', width + 'px');
browser.style.setProperty('min-height', height + 'px');
browser.style.setProperty('max-width', width + 'px');
browser.style.setProperty('max-height', height + 'px');
} else {
browser.style.removeProperty('min-width');
browser.style.removeProperty('min-height');
browser.style.removeProperty('max-width');
browser.style.removeProperty('max-height');
}
const rect = browser.getBoundingClientRect();
return { width: rect.width, height: rect.height };
}
TargetRegistry.Events = {
TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'),
DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
};
var EXPORTED_SYMBOLS = ['TargetRegistry'];
this.TargetRegistry = TargetRegistry;