Skip to content

Commit dd850ad

Browse files
authored
api(eval): allow non-toplevel handles as eval arguments (#1404)
1 parent 045277d commit dd850ad

10 files changed

+274
-174
lines changed

package.json

-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
"progress": "^2.0.3",
5151
"proxy-from-env": "^1.1.0",
5252
"rimraf": "^3.0.2",
53-
"uuid": "^3.4.0",
5453
"ws": "^6.1.0"
5554
},
5655
"devDependencies": {
@@ -60,7 +59,6 @@
6059
"@types/pngjs": "^3.4.0",
6160
"@types/proxy-from-env": "^1.0.0",
6261
"@types/rimraf": "^2.0.2",
63-
"@types/uuid": "^3.4.6",
6462
"@types/ws": "^6.0.1",
6563
"@typescript-eslint/eslint-plugin": "^2.6.1",
6664
"@typescript-eslint/parser": "^2.6.1",

src/chromium/crExecutionContext.ts

+24-44
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,35 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
5555
if (typeof pageFunction !== 'function')
5656
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
5757

58-
let functionText = pageFunction.toString();
59-
try {
60-
new Function('(' + functionText + ')');
61-
} catch (e1) {
62-
// This means we might have a function shorthand. Try another
63-
// time prefixing 'function '.
64-
if (functionText.startsWith('async '))
65-
functionText = 'async function ' + functionText.substring('async '.length);
66-
else
67-
functionText = 'function ' + functionText;
68-
try {
69-
new Function('(' + functionText + ')');
70-
} catch (e2) {
71-
// We tried hard to serialize, but there's a weird beast here.
72-
throw new Error('Passed function is not well-serializable!');
58+
const { functionText, values, handles } = js.prepareFunctionCall<Protocol.Runtime.CallArgument>(pageFunction, context, args, (value: any) => {
59+
if (typeof value === 'bigint') // eslint-disable-line valid-typeof
60+
return { handle: { unserializableValue: `${value.toString()}n` } };
61+
if (Object.is(value, -0))
62+
return { handle: { unserializableValue: '-0' } };
63+
if (Object.is(value, Infinity))
64+
return { handle: { unserializableValue: 'Infinity' } };
65+
if (Object.is(value, -Infinity))
66+
return { handle: { unserializableValue: '-Infinity' } };
67+
if (Object.is(value, NaN))
68+
return { handle: { unserializableValue: 'NaN' } };
69+
if (value && (value instanceof js.JSHandle)) {
70+
const remoteObject = toRemoteObject(value);
71+
if (remoteObject.unserializableValue)
72+
return { handle: { unserializableValue: remoteObject.unserializableValue } };
73+
if (!remoteObject.objectId)
74+
return { value: remoteObject.value };
75+
return { handle: { objectId: remoteObject.objectId } };
7376
}
74-
}
77+
return { value };
78+
});
7579

7680
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
7781
functionDeclaration: functionText + '\n' + suffix + '\n',
7882
executionContextId: this._contextId,
79-
arguments: args.map(convertArgument.bind(this)),
83+
arguments: [
84+
...values.map(value => ({ value })),
85+
...handles,
86+
],
8087
returnByValue,
8188
awaitPromise: true,
8289
userGesture: true
@@ -85,33 +92,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
8592
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
8693
return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject);
8794

88-
function convertArgument(arg: any): any {
89-
if (typeof arg === 'bigint') // eslint-disable-line valid-typeof
90-
return { unserializableValue: `${arg.toString()}n` };
91-
if (Object.is(arg, -0))
92-
return { unserializableValue: '-0' };
93-
if (Object.is(arg, Infinity))
94-
return { unserializableValue: 'Infinity' };
95-
if (Object.is(arg, -Infinity))
96-
return { unserializableValue: '-Infinity' };
97-
if (Object.is(arg, NaN))
98-
return { unserializableValue: 'NaN' };
99-
const objectHandle = arg && (arg instanceof js.JSHandle) ? arg : null;
100-
if (objectHandle) {
101-
if (objectHandle._context !== context)
102-
throw new Error('JSHandles can be evaluated only in the context they were created!');
103-
if (objectHandle._disposed)
104-
throw new Error('JSHandle is disposed!');
105-
const remoteObject = toRemoteObject(objectHandle);
106-
if (remoteObject.unserializableValue)
107-
return { unserializableValue: remoteObject.unserializableValue };
108-
if (!remoteObject.objectId)
109-
return { value: remoteObject.value };
110-
return { objectId: remoteObject.objectId };
111-
}
112-
return { value: arg };
113-
}
114-
11595
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
11696
if (error.message.includes('Object reference chain is too long'))
11797
return {result: {type: 'undefined'}};

src/firefox/ffExecutionContext.ts

+16-35
Original file line numberDiff line numberDiff line change
@@ -44,45 +44,26 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
4444
if (typeof pageFunction !== 'function')
4545
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
4646

47-
let functionText = pageFunction.toString();
48-
try {
49-
new Function('(' + functionText + ')');
50-
} catch (e1) {
51-
// This means we might have a function shorthand. Try another
52-
// time prefixing 'function '.
53-
if (functionText.startsWith('async '))
54-
functionText = 'async function ' + functionText.substring('async '.length);
55-
else
56-
functionText = 'function ' + functionText;
57-
try {
58-
new Function('(' + functionText + ')');
59-
} catch (e2) {
60-
// We tried hard to serialize, but there's a weird beast here.
61-
throw new Error('Passed function is not well-serializable!');
62-
}
63-
}
64-
const protocolArgs = args.map(arg => {
65-
if (arg instanceof js.JSHandle) {
66-
if (arg._context !== context)
67-
throw new Error('JSHandles can be evaluated only in the context they were created!');
68-
if (arg._disposed)
69-
throw new Error('JSHandle is disposed!');
70-
return this._toCallArgument(arg._remoteObject);
71-
}
72-
if (Object.is(arg, Infinity))
73-
return {unserializableValue: 'Infinity'};
74-
if (Object.is(arg, -Infinity))
75-
return {unserializableValue: '-Infinity'};
76-
if (Object.is(arg, -0))
77-
return {unserializableValue: '-0'};
78-
if (Object.is(arg, NaN))
79-
return {unserializableValue: 'NaN'};
80-
return {value: arg};
47+
const { functionText, values, handles } = js.prepareFunctionCall<Protocol.Runtime.CallFunctionArgument>(pageFunction, context, args, (value: any) => {
48+
if (Object.is(value, -0))
49+
return { handle: { unserializableValue: '-0' } };
50+
if (Object.is(value, Infinity))
51+
return { handle: { unserializableValue: 'Infinity' } };
52+
if (Object.is(value, -Infinity))
53+
return { handle: { unserializableValue: '-Infinity' } };
54+
if (Object.is(value, NaN))
55+
return { handle: { unserializableValue: 'NaN' } };
56+
if (value && (value instanceof js.JSHandle))
57+
return { handle: this._toCallArgument(value._remoteObject) };
58+
return { value };
8159
});
8260

8361
const payload = await this._session.send('Runtime.callFunction', {
8462
functionDeclaration: functionText,
85-
args: protocolArgs,
63+
args: [
64+
...values.map(value => ({ value })),
65+
...handles,
66+
],
8667
returnByValue,
8768
executionContextId: this._executionContextId
8869
}).catch(rewriteError);

src/javascript.ts

+108
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import * as types from './types';
1818
import * as dom from './dom';
19+
import * as platform from './platform';
1920

2021
export interface ExecutionContextDelegate {
2122
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
@@ -102,3 +103,110 @@ export class JSHandle<T = any> {
102103
return this._context._delegate.handleToString(this, true /* includeType */);
103104
}
104105
}
106+
107+
export function prepareFunctionCall<T>(
108+
pageFunction: Function,
109+
context: ExecutionContext,
110+
args: any[],
111+
toCallArgumentIfNeeded: (value: any) => { handle?: T, value?: any }): { functionText: string, values: any[], handles: T[] } {
112+
113+
let functionText = pageFunction.toString();
114+
try {
115+
new Function('(' + functionText + ')');
116+
} catch (e1) {
117+
// This means we might have a function shorthand. Try another
118+
// time prefixing 'function '.
119+
if (functionText.startsWith('async '))
120+
functionText = 'async function ' + functionText.substring('async '.length);
121+
else
122+
functionText = 'function ' + functionText;
123+
try {
124+
new Function('(' + functionText + ')');
125+
} catch (e2) {
126+
// We tried hard to serialize, but there's a weird beast here.
127+
throw new Error('Passed function is not well-serializable!');
128+
}
129+
}
130+
131+
const guids: string[] = [];
132+
const handles: T[] = [];
133+
const pushHandle = (handle: T): string => {
134+
const guid = platform.guid();
135+
guids.push(guid);
136+
handles.push(handle);
137+
return guid;
138+
};
139+
140+
const visited = new Set<any>();
141+
let error: string | undefined;
142+
const visit = (arg: any, depth: number): any => {
143+
if (!depth) {
144+
error = 'Argument nesting is too deep';
145+
return;
146+
}
147+
if (visited.has(arg)) {
148+
error = 'Argument is a circular structure';
149+
return;
150+
}
151+
if (Array.isArray(arg)) {
152+
visited.add(arg);
153+
const result = [];
154+
for (let i = 0; i < arg.length; ++i)
155+
result.push(visit(arg[i], depth - 1));
156+
visited.delete(arg);
157+
return result;
158+
}
159+
if (arg && (typeof arg === 'object') && !(arg instanceof JSHandle)) {
160+
visited.add(arg);
161+
const result: any = {};
162+
for (const name of Object.keys(arg))
163+
result[name] = visit(arg[name], depth - 1);
164+
visited.delete(arg);
165+
return result;
166+
}
167+
if (arg && (arg instanceof JSHandle)) {
168+
if (arg._context !== context)
169+
throw new Error('JSHandles can be evaluated only in the context they were created!');
170+
if (arg._disposed)
171+
throw new Error('JSHandle is disposed!');
172+
}
173+
const { handle, value } = toCallArgumentIfNeeded(arg);
174+
if (handle)
175+
return pushHandle(handle);
176+
return value;
177+
};
178+
179+
args = args.map(arg => visit(arg, 100));
180+
if (error)
181+
throw new Error(error);
182+
183+
if (!guids.length)
184+
return { functionText, values: args, handles: [] };
185+
186+
functionText = `(...__playwright__args__) => {
187+
return (${functionText})(...(() => {
188+
const args = __playwright__args__;
189+
__playwright__args__ = undefined;
190+
const argCount = args[0];
191+
const handleCount = args[argCount + 1];
192+
const handles = { __proto__: null };
193+
for (let i = 0; i < handleCount; i++)
194+
handles[args[argCount + 2 + i]] = args[argCount + 2 + handleCount + i];
195+
const visit = (arg) => {
196+
if ((typeof arg === 'string') && (arg in handles))
197+
return handles[arg];
198+
if (arg && (typeof arg === 'object')) {
199+
for (const name of Object.keys(arg))
200+
arg[name] = visit(arg[name]);
201+
}
202+
return arg;
203+
};
204+
const result = [];
205+
for (let i = 0; i < argCount; i++)
206+
result[i] = visit(args[i + 1]);
207+
return result;
208+
})());
209+
}`;
210+
211+
return { functionText, values: [ args.length, ...args, guids.length, ...guids ], handles };
212+
}

src/platform.ts

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import * as png from 'pngjs';
2626
import * as http from 'http';
2727
import * as https from 'https';
2828
import * as NodeWebSocket from 'ws';
29+
import * as crypto from 'crypto';
2930

3031
import { assert, helper } from './helper';
3132
import { ConnectionTransport } from './transport';
@@ -300,6 +301,14 @@ export function makeWaitForNextTask() {
300301
};
301302
}
302303

304+
export function guid(): string {
305+
if (isNode)
306+
return crypto.randomBytes(16).toString('hex');
307+
const a = new Uint8Array(16);
308+
window.crypto.getRandomValues(a);
309+
return Array.from(a).map(b => b.toString(16).padStart(2, '0')).join('');
310+
}
311+
303312
// 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to
304313
// avoid missing incoming messages.
305314
export async function connectToWebsocket<T>(url: string, onopen: (transport: ConnectionTransport) => Promise<T>): Promise<T> {

src/server/webkit.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import { kBrowserCloseMessageId } from '../webkit/wkConnection';
3030
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
3131
import { ConnectionTransport } from '../transport';
3232
import * as ws from 'ws';
33-
import * as uuidv4 from 'uuid/v4';
3433
import { ConnectOptions, LaunchType } from '../browser';
3534
import { BrowserServer } from './browserServer';
3635
import { Events } from '../events';
@@ -263,7 +262,7 @@ class SequenceNumberMixer<V> {
263262

264263
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) {
265264
const server = new ws.Server({ port });
266-
const guid = uuidv4();
265+
const guid = platform.guid();
267266
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
268267
const pendingBrowserContextCreations = new Set<number>();
269268
const pendingBrowserContextDeletions = new Map<number, string>();

src/web.webpack.config.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ module.exports = {
2727
options: {
2828
transpileOnly: true
2929
},
30-
exclude: /node_modules/
30+
exclude: [
31+
/node_modules/,
32+
/crypto/,
33+
]
3134
}
3235
]
3336
},
@@ -41,6 +44,7 @@ module.exports = {
4144
path: path.resolve(__dirname, '../')
4245
},
4346
externals: {
47+
'crypto': 'dummy',
4448
'events': 'dummy',
4549
'fs': 'dummy',
4650
'path': 'dummy',

0 commit comments

Comments
 (0)