fix(utility): create utility world when web security is disabled (#31458)

Reverts previous attempt at #31096

Fixes: https://github.com/microsoft/playwright/issues/31431
Fixes: https://github.com/microsoft/playwright/issues/31442
This commit is contained in:
Pavel Feldman 2024-06-27 09:29:20 -07:00 committed by GitHub
parent 87785d6092
commit c9e673c6dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 138 additions and 50 deletions

View File

@ -24,6 +24,7 @@ import type { Download } from './download';
import type * as frames from './frames';
import { helper } from './helper';
import * as network from './network';
import { InitScript } from './page';
import type { PageDelegate } from './page';
import { Page, PageBinding } from './page';
import type { Progress, ProgressController } from './progress';
@ -84,7 +85,7 @@ export abstract class BrowserContext extends SdkObject {
private _customCloseHandler?: () => Promise<any>;
readonly _tempDirs: string[] = [];
private _settingStorageState = false;
readonly initScripts: string[] = [];
readonly initScripts: InitScript[] = [];
private _routesInFlight = new Set<network.Route>();
private _debugger!: Debugger;
_closeReason: string | undefined;
@ -266,7 +267,7 @@ export abstract class BrowserContext extends SdkObject {
protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise<void>;
protected abstract doClearPermissions(): Promise<void>;
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
protected abstract doAddInitScript(expression: string): Promise<void>;
protected abstract doAddInitScript(initScript: InitScript): Promise<void>;
protected abstract doRemoveInitScripts(): Promise<void>;
protected abstract doExposeBinding(binding: PageBinding): Promise<void>;
protected abstract doRemoveExposedBindings(): Promise<void>;
@ -403,9 +404,10 @@ export abstract class BrowserContext extends SdkObject {
this._options.httpCredentials = { username, password: password || '' };
}
async addInitScript(script: string) {
this.initScripts.push(script);
await this.doAddInitScript(script);
async addInitScript(source: string) {
const initScript = new InitScript(source);
this.initScripts.push(initScript);
await this.doAddInitScript(initScript);
}
async _removeInitScripts(): Promise<void> {

View File

@ -21,7 +21,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert, createGuid } from '../../utils';
import * as network from '../network';
import type { PageBinding, PageDelegate, Worker } from '../page';
import type { InitScript, PageBinding, PageDelegate, Worker } from '../page';
import { Page } from '../page';
import { Frame } from '../frames';
import type { Dialog } from '../dialog';
@ -486,9 +486,9 @@ export class CRBrowserContext extends BrowserContext {
await (sw as CRServiceWorker).updateHttpCredentials();
}
async doAddInitScript(source: string) {
async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
await (page._delegate as CRPage).addInitScript(source);
await (page._delegate as CRPage).addInitScript(initScript);
}
async doRemoveInitScripts() {

View File

@ -26,7 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames';
import { helper } from '../helper';
import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page';
import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page, Worker } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
@ -256,8 +256,8 @@ export class CRPage implements PageDelegate {
return this._go(+1);
}
async addInitScript(source: string, world: types.World = 'main'): Promise<void> {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world));
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
}
async removeInitScripts() {
@ -511,6 +511,20 @@ class FrameSession {
this._addRendererListeners();
}
const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!];
for (const frame of localFrames) {
// Note: frames might be removed before we send these.
this._client._sendMayFail('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME,
});
for (const binding of this._crPage._browserContext._pageBindings.values())
frame.evaluateExpression(binding.source).catch(e => {});
for (const initScript of this._crPage._browserContext.initScripts)
frame.evaluateExpression(initScript.source).catch(e => {});
}
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) {
// Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page
@ -520,20 +534,6 @@ class FrameSession {
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
});
} else {
const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!];
for (const frame of localFrames) {
// Note: frames might be removed before we send these.
this._client._sendMayFail('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME,
});
for (const binding of this._crPage._browserContext._pageBindings.values())
frame.evaluateExpression(binding.source).catch(e => {});
for (const source of this._crPage._browserContext.initScripts)
frame.evaluateExpression(source).catch(e => {});
}
this._firstNonInitialNavigationCommittedFulfill();
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
}
@ -575,10 +575,10 @@ class FrameSession {
promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings())
promises.push(this._initBinding(binding));
for (const source of this._crPage._browserContext.initScripts)
promises.push(this._evaluateOnNewDocument(source, 'main'));
for (const source of this._crPage._page.initScripts)
promises.push(this._evaluateOnNewDocument(source, 'main'));
for (const initScript of this._crPage._browserContext.initScripts)
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
for (const initScript of this._crPage._page.initScripts)
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
if (screencastOptions)
promises.push(this._startVideoRecording(screencastOptions));
}
@ -1099,9 +1099,9 @@ class FrameSession {
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
}
async _evaluateOnNewDocument(source: string, world: types.World): Promise<void> {
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source, worldName });
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
this._evaluateOnNewDocumentIdentifiers.push(identifier);
}

View File

@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser';
import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network';
import type { Page, PageBinding, PageDelegate } from '../page';
import type { InitScript, Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@ -352,8 +352,8 @@ export class FFBrowserContext extends BrowserContext {
await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials });
}
async doAddInitScript(source: string) {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) });
async doAddInitScript(initScript: InitScript) {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script: script.source })) });
}
async doRemoveInitScripts() {

View File

@ -21,6 +21,7 @@ import type * as frames from '../frames';
import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import type { PageBinding, PageDelegate } from '../page';
import { InitScript } from '../page';
import { Page, Worker } from '../page';
import type * as types from '../types';
import { getAccessibilityTree } from './ffAccessibility';
@ -56,7 +57,7 @@ export class FFPage implements PageDelegate {
private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>();
private _screencastId: string | undefined;
private _initScripts: { script: string, worldName?: string }[] = [];
private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session;
@ -113,7 +114,7 @@ export class FFPage implements PageDelegate {
});
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
this.addInitScript('', UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
}
potentiallyUninitializedPage(): Page {
@ -406,9 +407,9 @@ export class FFPage implements PageDelegate {
return success;
}
async addInitScript(script: string, worldName?: string): Promise<void> {
this._initScripts.push({ script, worldName });
await this._session.send('Page.setInitScripts', { scripts: this._initScripts });
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
this._initScripts.push({ initScript, worldName });
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
}
async removeInitScripts() {

View File

@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser';
import type { Progress } from './progress';
import { ProgressController } from './progress';
import { LongStandingScope, assert, isError } from '../utils';
import { LongStandingScope, assert, createGuid, isError } from '../utils';
import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../utils/debugLogger';
import type { ImageComparatorOptions } from '../utils/comparators';
@ -56,7 +56,7 @@ export interface PageDelegate {
goForward(): Promise<boolean>;
exposeBinding(binding: PageBinding): Promise<void>;
removeExposedBindings(): Promise<void>;
addInitScript(source: string): Promise<void>;
addInitScript(initScript: InitScript): Promise<void>;
removeInitScripts(): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
potentiallyUninitializedPage(): Page;
@ -154,7 +154,7 @@ export class Page extends SdkObject {
private _emulatedMedia: Partial<EmulatedMedia> = {};
private _interceptFileChooser = false;
private readonly _pageBindings = new Map<string, PageBinding>();
readonly initScripts: string[] = [];
readonly initScripts: InitScript[] = [];
readonly _screenshotter: Screenshotter;
readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility;
@ -527,8 +527,9 @@ export class Page extends SdkObject {
}
async addInitScript(source: string) {
this.initScripts.push(source);
await this._delegate.addInitScript(source);
const initScript = new InitScript(source);
this.initScripts.push(initScript);
await this._delegate.addInitScript(initScript);
}
async _removeInitScripts() {
@ -905,6 +906,22 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript
(globalThis as any)[bindingName].__installed = true;
}
export class InitScript {
readonly source: string;
constructor(source: string) {
const guid = createGuid();
this.source = `(() => {
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}];
if (hasInitScript)
return;
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
${source}
})();`;
}
}
class FrameThrottler {
private _acks: (() => void)[] = [];
private _defaultInterval: number;

View File

@ -22,7 +22,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { assert } from '../../utils';
import { eventsHelper } from '../../utils/eventsHelper';
import * as network from '../network';
import type { Page, PageBinding, PageDelegate } from '../page';
import type { InitScript, Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@ -315,7 +315,7 @@ export class WKBrowserContext extends BrowserContext {
await (page._delegate as WKPage).updateHttpCredentials();
}
async doAddInitScript(source: string) {
async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript();
}

View File

@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
import { helper } from '../helper';
import type { JSHandle } from '../javascript';
import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page';
import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
@ -777,7 +777,7 @@ export class WKPage implements PageDelegate {
await this._updateBootstrapScript();
}
async addInitScript(script: string): Promise<void> {
async addInitScript(initScript: InitScript): Promise<void> {
await this._updateBootstrapScript();
}
@ -797,8 +797,8 @@ export class WKPage implements PageDelegate {
for (const binding of this._page.allBindings())
scripts.push(binding.source);
scripts.push(...this._browserContext.initScripts);
scripts.push(...this._page.initScripts);
scripts.push(...this._browserContext.initScripts.map(s => s.source));
scripts.push(...this._page.initScripts.map(s => s.source));
return scripts.join(';\n');
}

View File

@ -0,0 +1,68 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { contextTest as it, expect } from '../../config/browserTest';
it.use({
launchOptions: async ({ launchOptions }, use) => {
await use({ ...launchOptions, args: ['--disable-web-security'] });
}
});
it('test utility world in popup w/ --disable-web-security', async ({ page, server }) => {
server.setRoute('/main.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<a href="${server.PREFIX}/target.html" target="_blank">Click me</a>`);
});
server.setRoute('/target.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<html></html>`);
});
await page.goto(server.PREFIX + '/main.html');
const page1Promise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Click me' }).click();
const page1 = await page1Promise;
await expect(page1).toHaveURL(/target/);
});
it('test init script w/ --disable-web-security', async ({ page, server }) => {
server.setRoute('/main.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<a href="${server.PREFIX}/target.html" target="_blank">Click me</a>`);
});
server.setRoute('/target.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<html></html>`);
});
await page.context().addInitScript('window.injected = 123');
await page.goto(server.PREFIX + '/main.html');
const page1Promise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Click me' }).click();
const page1 = await page1Promise;
const value = await page1.evaluate('window.injected');
expect(value).toBe(123);
});