Skip to content

Commit b1a5a02

Browse files
authored
feat(rpc): client-side parameters validation (#3069)
1 parent e56e148 commit b1a5a02

16 files changed

+1685
-60
lines changed

src/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@ export class Page extends EventEmitter {
387387

388388
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) {
389389
if (options.media !== undefined)
390-
assert(options.media === null || types.mediaTypes.has(options.media), 'media: expected one of (screen|print)');
390+
assert(options.media === null || types.mediaTypes.has(options.media), 'media: expected one of (screen|print|null)');
391391
if (options.colorScheme !== undefined)
392-
assert(options.colorScheme === null || types.colorSchemes.has(options.colorScheme), 'colorScheme: expected one of (dark|light|no-preference)');
392+
assert(options.colorScheme === null || types.colorSchemes.has(options.colorScheme), 'colorScheme: expected one of (dark|light|no-preference|null)');
393393
if (options.media !== undefined)
394394
this._state.mediaType = options.media;
395395
if (options.colorScheme !== undefined)

src/rpc/channels.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,8 +620,8 @@ export type PageCloseParams = {
620620
};
621621
export type PageCloseResult = void;
622622
export type PageEmulateMediaParams = {
623-
media?: 'screen' | 'print' | 'reset',
624-
colorScheme?: 'dark' | 'light' | 'no-preference' | 'reset',
623+
media?: 'screen' | 'print' | 'null',
624+
colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
625625
};
626626
export type PageEmulateMediaResult = void;
627627
export type PageExposeBindingParams = {

src/rpc/client/browserType.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { BrowserServer } from './browserServer';
2323
import { LoggerSink } from '../../loggerSink';
2424
import { headersObjectToArray, envObjectToArray } from '../serializers';
2525
import { serializeArgument } from './jsHandle';
26+
import { assert } from '../../helper';
2627

2728
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
2829

@@ -48,6 +49,8 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
4849
const logger = options.logger;
4950
options = { ...options, logger: undefined };
5051
return this._wrapApiCall('browserType.launch', async () => {
52+
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
53+
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
5154
const launchOptions: BrowserTypeLaunchParams = {
5255
...options,
5356
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,

src/rpc/client/channelOwner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
6666
return obj.addListener;
6767
if (prop === 'removeEventListener')
6868
return obj.removeListener;
69-
return (params: any) => this._connection.sendMessageToServer({ guid, method: String(prop), params });
69+
return (params: any) => this._connection.sendMessageToServer(this._type, guid, String(prop), params);
7070
},
7171
});
7272
(this._channel as any)._object = this;

src/rpc/client/connection.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ChromiumBrowser } from './chromiumBrowser';
3838
import { ChromiumBrowserContext } from './chromiumBrowserContext';
3939
import { Selectors } from './selectors';
4040
import { Stream } from './stream';
41+
import { validateParams } from './validator';
4142

4243
class Root extends ChannelOwner<Channel, {}> {
4344
constructor(connection: Connection) {
@@ -63,9 +64,10 @@ export class Connection {
6364
return new Promise(f => this._waitingForObject.set(guid, f));
6465
}
6566

66-
async sendMessageToServer(message: { guid: string, method: string, params: any }): Promise<any> {
67+
async sendMessageToServer(type: string, guid: string, method: string, params: any): Promise<any> {
6768
const id = ++this._lastId;
68-
const converted = { id, ...message, params: this._replaceChannelsWithGuids(message.params) };
69+
const validated = method === 'debugScopeState' ? params : validateParams(type, method, params);
70+
const converted = { id, guid, method, params: validated };
6971
debug('pw:channel:command')(converted);
7072
this.onmessage(converted);
7173
return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject }));
@@ -97,22 +99,6 @@ export class Connection {
9799
object._channel.emit(method, this._replaceGuidsWithChannels(params));
98100
}
99101

100-
private _replaceChannelsWithGuids(payload: any): any {
101-
if (!payload)
102-
return payload;
103-
if (Array.isArray(payload))
104-
return payload.map(p => this._replaceChannelsWithGuids(p));
105-
if (payload._object instanceof ChannelOwner)
106-
return { guid: payload._object._guid };
107-
if (typeof payload === 'object') {
108-
const result: any = {};
109-
for (const key of Object.keys(payload))
110-
result[key] = this._replaceChannelsWithGuids(payload[key]);
111-
return result;
112-
}
113-
return payload;
114-
}
115-
116102
private _replaceGuidsWithChannels(payload: any): any {
117103
if (!payload)
118104
return payload;

src/rpc/client/elementHandle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
123123
});
124124
}
125125

126-
async selectText(options: types.TimeoutOptions): Promise<void> {
126+
async selectText(options: types.TimeoutOptions = {}): Promise<void> {
127127
return this._wrapApiCall('elementHandle.selectText', async () => {
128128
await this._elementChannel.selectText(options);
129129
});

src/rpc/client/frame.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { assertMaxArguments, helper } from '../../helper';
18+
import { assertMaxArguments, helper, assert } from '../../helper';
1919
import * as types from '../../types';
2020
import { FrameChannel, FrameInitializer, FrameNavigatedEvent } from '../channels';
2121
import { BrowserContext } from './browserContext';
@@ -89,7 +89,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
8989

9090
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
9191
return this._wrapApiCall(this._apiName('goto'), async () => {
92-
return network.Response.fromNullable((await this._channel.goto({ url, ...options })).response);
92+
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
93+
return network.Response.fromNullable((await this._channel.goto({ url, ...options, waitUntil })).response);
9394
});
9495
}
9596

@@ -188,6 +189,10 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
188189

189190
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
190191
return this._wrapApiCall(this._apiName('waitForSelector'), async () => {
192+
if ((options as any).visibility)
193+
throw new Error('options.visibility is not supported, did you mean options.state?');
194+
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
195+
throw new Error('options.waitFor is not supported, did you mean options.state?');
191196
const result = await this._channel.waitForSelector({ selector, ...options });
192197
return ElementHandle.fromNullable(result.element) as ElementHandle<Element> | null;
193198
});
@@ -234,7 +239,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
234239

235240
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
236241
return this._wrapApiCall(this._apiName('setContent'), async () => {
237-
await this._channel.setContent({ html, ...options });
242+
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
243+
await this._channel.setContent({ html, ...options, waitUntil });
238244
});
239245
}
240246

@@ -272,9 +278,11 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
272278
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
273279
return this._wrapApiCall(this._apiName('addStyleTag'), async () => {
274280
const copy = { ...options };
275-
if (copy.path)
281+
if (copy.path) {
276282
copy.content = (await fsReadFileAsync(copy.path)).toString();
277-
return ElementHandle.from((await this._channel.addStyleTag({ ...options })).element);
283+
copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/';
284+
}
285+
return ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
278286
});
279287
}
280288

@@ -378,6 +386,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
378386
async waitForFunction<R>(pageFunction: Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>;
379387
async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<SmartHandle<R>> {
380388
return this._wrapApiCall(this._apiName('waitForFunction'), async () => {
389+
if (typeof options.polling === 'string')
390+
assert(options.polling === 'raf', 'Unknown polling option: ' + options.polling);
381391
const result = await this._channel.waitForFunction({
382392
...options,
383393
pollingInterval: options.polling === 'raf' ? undefined : options.polling,
@@ -396,7 +406,7 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
396406
}
397407
}
398408

399-
function verifyLoadState(name: string, waitUntil: types.LifecycleEvent): types.LifecycleEvent {
409+
export function verifyLoadState(name: string, waitUntil: types.LifecycleEvent): types.LifecycleEvent {
400410
if (waitUntil as unknown === 'networkidle0')
401411
waitUntil = 'networkidle';
402412
if (!types.kLifecycleEvents.has(waitUntil))

src/rpc/client/page.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { Dialog } from './dialog';
2929
import { Download } from './download';
3030
import { ElementHandle } from './elementHandle';
3131
import { Worker } from './worker';
32-
import { Frame, FunctionWithSource, GotoOptions } from './frame';
32+
import { Frame, FunctionWithSource, GotoOptions, verifyLoadState } from './frame';
3333
import { Keyboard, Mouse } from './input';
3434
import { Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
3535
import { Request, Response, Route, RouteHandler } from './network';
@@ -300,7 +300,8 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
300300

301301
async reload(options: types.NavigateOptions = {}): Promise<Response | null> {
302302
return this._wrapApiCall('page.reload', async () => {
303-
return Response.fromNullable((await this._channel.reload(options)).response);
303+
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
304+
return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response);
304305
});
305306
}
306307

@@ -346,21 +347,23 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
346347

347348
async goBack(options: types.NavigateOptions = {}): Promise<Response | null> {
348349
return this._wrapApiCall('page.goBack', async () => {
349-
return Response.fromNullable((await this._channel.goBack(options)).response);
350+
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
351+
return Response.fromNullable((await this._channel.goBack({ ...options, waitUntil })).response);
350352
});
351353
}
352354

353355
async goForward(options: types.NavigateOptions = {}): Promise<Response | null> {
354356
return this._wrapApiCall('page.goForward', async () => {
355-
return Response.fromNullable((await this._channel.goForward(options)).response);
357+
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
358+
return Response.fromNullable((await this._channel.goForward({ ...options, waitUntil })).response);
356359
});
357360
}
358361

359362
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) {
360363
return this._wrapApiCall('page.emulateMedia', async () => {
361364
await this._channel.emulateMedia({
362-
media: options.media === null ? 'reset' : options.media,
363-
colorScheme: options.colorScheme === null ? 'reset' : options.colorScheme,
365+
media: options.media === null ? 'null' : options.media,
366+
colorScheme: options.colorScheme === null ? 'null' : options.colorScheme,
364367
});
365368
});
366369
}

0 commit comments

Comments
 (0)