mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 22:22:53 +03:00
b74a6b78ef
We currently might double-attach to the target in `BrowserHandler` since we iterate over all targets, and then subscribe to the additional event when target is getting initialized. This patch fixes this race condition and should unblock the roll to r1177. References #3995
764 lines
27 KiB
JavaScript
764 lines
27 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 {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
|
|
const {NetworkHandler} = ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js");
|
|
const {RuntimeHandler} = ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js");
|
|
const {AccessibilityHandler} = ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js");
|
|
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;
|
|
|
|
const sessions = [];
|
|
const readyData = { sessions, target };
|
|
target.markAsReported();
|
|
this.emit(TargetRegistry.Events.TargetCreated, readyData);
|
|
return {
|
|
scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument,
|
|
bindings: target.browserContext().bindings,
|
|
settings: target.browserContext().settings,
|
|
sessionIds: sessions.map(session => session.sessionId()),
|
|
};
|
|
},
|
|
});
|
|
|
|
const onTabOpenListener = (appWindow, window, event) => {
|
|
const tab = event.target;
|
|
const userContextId = tab.userContextId;
|
|
const browserContext = this._userContextIdToBrowserContext.get(userContextId);
|
|
const hasExplicitSize = (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) => {
|
|
if (!(appWindow instanceof Ci.nsIAppWindow))
|
|
return;
|
|
const domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
|
|
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');
|
|
}
|
|
await target.reportedPromise();
|
|
return target.id();
|
|
}
|
|
|
|
reportedTargets() {
|
|
return Array.from(this._browserToTarget.values()).filter(pageTarget => pageTarget._isReported);
|
|
}
|
|
|
|
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._isReported = false;
|
|
this._reportedPromise = new Promise(resolve => {
|
|
this._reportedCallback = resolve;
|
|
});
|
|
|
|
this._disposed = false;
|
|
browserContext.pages.add(this);
|
|
this._registry._browserToTarget.set(this._linkedBrowser, this);
|
|
this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this);
|
|
}
|
|
|
|
reportedPromise() {
|
|
return this._reportedPromise;
|
|
}
|
|
|
|
markAsReported() {
|
|
this._isReported = true;
|
|
this._reportedCallback();
|
|
}
|
|
|
|
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 = 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();
|
|
}
|
|
|
|
connectSession(session) {
|
|
this._channel.connect('').send('attach', { sessionId: session.sessionId() });
|
|
}
|
|
|
|
disconnectSession(session) {
|
|
if (!this._disposed)
|
|
this._channel.connect('').emit('detach', { sessionId: session.sessionId() });
|
|
}
|
|
|
|
async close(runBeforeUnload = false) {
|
|
await this._gBrowser.removeTab(this._tab, {
|
|
skipPermitUnload: !runBeforeUnload,
|
|
});
|
|
}
|
|
|
|
initSession(session) {
|
|
const pageHandler = new PageHandler(this, session, this._channel);
|
|
const networkHandler = new NetworkHandler(this, session, this._channel);
|
|
session.registerHandler('Page', pageHandler);
|
|
session.registerHandler('Network', networkHandler);
|
|
session.registerHandler('Runtime', new RuntimeHandler(session, this._channel));
|
|
session.registerHandler('Accessibility', new AccessibilityHandler(session, this._channel));
|
|
pageHandler.enable();
|
|
networkHandler.enable();
|
|
}
|
|
|
|
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);
|
|
if (this._isReported)
|
|
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');
|
|
}
|
|
|
|
function setViewportSizeForBrowser(viewportSize, browser, 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;
|