Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>;
Expand Down Expand Up @@ -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> {
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/server/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand Down
46 changes: 23 additions & 23 deletions packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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)));
}
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/server/firefox/ffBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand Down
11 changes: 6 additions & 5 deletions packages/playwright-core/src/server/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
27 changes: 22 additions & 5 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)}];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}];
const hasInitScript = globalThis.__pwInitScripts['${guid}'];

Here and below?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like JSON.stringify for consistency and safety when concating scripts.

if (hasInitScript)
return;
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
${source}
})();`;
}
}

class FrameThrottler {
private _acks: (() => void)[] = [];
private _defaultInterval: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/webkit/wkBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
Expand Down
8 changes: 4 additions & 4 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}

Expand All @@ -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');
}

Expand Down
68 changes: 68 additions & 0 deletions tests/library/chromium/disable-web-security.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});