mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-26 21:33:38 +03:00
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:
parent
87785d6092
commit
c9e673c6dc
@ -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> {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
68
tests/library/chromium/disable-web-security.spec.ts
Normal file
68
tests/library/chromium/disable-web-security.spec.ts
Normal 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);
|
||||
});
|
Loading…
Reference in New Issue
Block a user