single-file-cli 2.0.76 → 2.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.sh +1 -1
- package/deno.json +1 -1
- package/lib/browser.js +0 -3
- package/lib/cdp-client-util.js +95 -0
- package/lib/cdp-client.js +593 -506
- package/lib/deno-polyfill.js +3 -1
- package/lib/single-file-bundle.js +1 -1
- package/lib/single-file-script.js +103 -28
- package/lib/version.js +1 -1
- package/options.js +156 -107
- package/package.json +2 -2
- package/single-file-cli-api.js +1 -1
package/lib/cdp-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright 2010-
|
|
2
|
+
* Copyright 2010-2025 Gildas Lormeau
|
|
3
3
|
* contact : gildas.lormeau <at> gmail.com
|
|
4
4
|
*
|
|
5
5
|
* This file is part of SingleFile.
|
|
@@ -21,436 +21,379 @@
|
|
|
21
21
|
* Source.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
/* global setTimeout, clearTimeout, URL,
|
|
24
|
+
/* global setTimeout, clearTimeout, URL, AbortController */
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
import {
|
|
27
|
+
launchBrowser,
|
|
28
|
+
closeBrowser
|
|
29
|
+
} from "./browser.js";
|
|
30
|
+
import {
|
|
31
|
+
CDP,
|
|
32
|
+
options
|
|
33
|
+
} from "simple-cdp";
|
|
34
|
+
import {
|
|
35
|
+
FETCH_FUNCTION_NAME,
|
|
36
|
+
RESOLVE_FETCH_FUNCTION_NAME,
|
|
37
|
+
REJECT_FETCH_FUNCTION_NAME,
|
|
38
|
+
getScriptSource,
|
|
39
|
+
getHookScriptSource,
|
|
40
|
+
getPageDataScriptSource
|
|
41
|
+
} from "./single-file-script.js";
|
|
42
|
+
import {
|
|
43
|
+
fetch,
|
|
44
|
+
waitForTimeout,
|
|
45
|
+
arrayBufferToBase64,
|
|
46
|
+
getAlternativeUrl
|
|
47
|
+
} from "./cdp-client-util.js";
|
|
29
48
|
|
|
30
49
|
const LOAD_TIMEOUT_ERROR = "ERR_LOAD_TIMEOUT";
|
|
31
50
|
const CAPTURE_TIMEOUT_ERROR = "ERR_CAPTURE_TIMEOUT";
|
|
32
51
|
const NETWORK_STATES = ["InteractiveTime", "networkIdle", "networkAlmostIdle", "load", "DOMContentLoaded"];
|
|
33
52
|
const MINIMIZED_WINDOW_STATE = "minimized";
|
|
34
53
|
const SINGLE_FILE_WORLD_NAME = "singlefile";
|
|
35
|
-
const EMPTY_PAGE_URL = "about:blank";
|
|
36
54
|
const CAPTURE_SCREENSHOT_FUNCTION_NAME = "captureScreenshot";
|
|
37
55
|
const PRINT_TO_PDF_FUNCTION_NAME = "printToPDF";
|
|
38
56
|
const SET_SCREENSHOT_FUNCTION_NAME = "setScreenshot";
|
|
39
57
|
const SET_PDF_FUNCTION_NAME = "setPDF";
|
|
40
58
|
const SET_PAGE_DATA_FUNCTION_NAME = "setPageData";
|
|
41
|
-
const
|
|
59
|
+
const BINDING_CALLED_EVENT_TYPE = "bindingCalled";
|
|
42
60
|
|
|
43
|
-
export {
|
|
61
|
+
export {
|
|
62
|
+
initialize,
|
|
63
|
+
getPageData,
|
|
64
|
+
closeBrowser
|
|
65
|
+
};
|
|
44
66
|
|
|
45
67
|
async function initialize(singleFileOptions) {
|
|
46
68
|
if (singleFileOptions.browserServer) {
|
|
47
69
|
options.apiUrl = singleFileOptions.browserServer;
|
|
48
70
|
} else {
|
|
49
|
-
|
|
71
|
+
const LOCALHOST = "http://localhost:";
|
|
72
|
+
const browserOptions = {};
|
|
73
|
+
browserOptions.args = singleFileOptions.browserArgs;
|
|
74
|
+
browserOptions.headless = singleFileOptions.browserHeadless;
|
|
75
|
+
browserOptions.executablePath = singleFileOptions.browserExecutablePath;
|
|
76
|
+
browserOptions.debug = singleFileOptions.browserDebug;
|
|
77
|
+
browserOptions.disableWebSecurity = singleFileOptions.browserDisableWebSecurity;
|
|
78
|
+
browserOptions.width = singleFileOptions.browserWidth;
|
|
79
|
+
browserOptions.height = singleFileOptions.browserHeight;
|
|
80
|
+
browserOptions.userAgent = singleFileOptions.userAgent;
|
|
81
|
+
browserOptions.httpProxyServer = singleFileOptions.httpProxyServer;
|
|
82
|
+
options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
|
|
50
83
|
}
|
|
51
84
|
}
|
|
52
85
|
|
|
53
86
|
async function getPageData(options) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
87
|
+
const EMPTY_PAGE_URL = "about:blank";
|
|
88
|
+
const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {} };
|
|
56
89
|
let targetInfo;
|
|
57
90
|
try {
|
|
58
|
-
|
|
59
|
-
debugMessages.push([Date.now(), ["Loading page", EMPTY_PAGE_URL]]);
|
|
60
|
-
}
|
|
91
|
+
logData(["Loading page", EMPTY_PAGE_URL], pageContext);
|
|
61
92
|
targetInfo = await CDP.createTarget(EMPTY_PAGE_URL);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
await Browser.setWindowBounds({ windowId, bounds: { windowState: MINIMIZED_WINDOW_STATE } });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
|
|
83
|
-
if (options.debugMessagesFile) {
|
|
84
|
-
debugMessages.push([Date.now(), ["Ignoring HTTPS errors"]]);
|
|
85
|
-
}
|
|
86
|
-
await Security.setIgnoreCertificateErrors({ ignore: true });
|
|
87
|
-
}
|
|
88
|
-
if (options.browserByPassCSP === undefined || options.browserByPassCSP) {
|
|
89
|
-
if (options.debugMessagesFile) {
|
|
90
|
-
debugMessages.push([Date.now(), ["Bypassing CSP"]]);
|
|
91
|
-
}
|
|
92
|
-
await Page.setBypassCSP({ enabled: true });
|
|
93
|
-
}
|
|
94
|
-
if (options.browserMobileEmulation || options.browserDeviceWidth || options.browserDeviceHeight || options.browserDeviceScaleFactor || options.platform || options.acceptLanguage) {
|
|
95
|
-
if (options.browserMobileEmulation || options.browserDeviceWidth || options.browserDeviceHeight || options.browserDeviceScaleFactor) {
|
|
96
|
-
let browserDeviceWidth;
|
|
97
|
-
if (!options.browserDeviceWidth) {
|
|
98
|
-
const { result } = await Runtime.evaluate({ expression: "window.innerWidth" });
|
|
99
|
-
browserDeviceWidth = result.value;
|
|
100
|
-
}
|
|
101
|
-
let browserDeviceHeight;
|
|
102
|
-
if (!options.browserDeviceHeight) {
|
|
103
|
-
const { result } = await Runtime.evaluate({ expression: "window.innerHeight" });
|
|
104
|
-
browserDeviceHeight = result.value;
|
|
105
|
-
}
|
|
106
|
-
let browserDeviceScaleFactor;
|
|
107
|
-
if (!options.browserDeviceScaleFactor) {
|
|
108
|
-
const { result } = await Runtime.evaluate({ expression: "window.devicePixelRatio" });
|
|
109
|
-
browserDeviceScaleFactor = result.value;
|
|
110
|
-
}
|
|
111
|
-
const deviceMetricsOptions = {
|
|
112
|
-
mobile: Boolean(options.browserMobileEmulation),
|
|
113
|
-
width: options.browserDeviceWidth || (options.browserMobileEmulation ? 360 : options.width || browserDeviceWidth),
|
|
114
|
-
height: options.browserDeviceHeight || (options.browserMobileEmulation ? 800 : options.height || browserDeviceHeight),
|
|
115
|
-
deviceScaleFactor: options.browserDeviceScaleFactor || (options.browserMobileEmulation ? 2 : browserDeviceScaleFactor)
|
|
116
|
-
};
|
|
117
|
-
if (options.debugMessagesFile) {
|
|
118
|
-
debugMessages.push([Date.now(), ["Emulating device metrics", JSON.stringify(deviceMetricsOptions)]]);
|
|
119
|
-
}
|
|
120
|
-
await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
|
|
121
|
-
}
|
|
122
|
-
if (options.browserMobileEmulation || options.platform || options.acceptLanguage) {
|
|
123
|
-
const { userAgent, product } = await Browser.getVersion();
|
|
124
|
-
const agentOptions = {
|
|
125
|
-
userAgent: options.userAgent || (options.browserMobileEmulation ? "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) " + product + " Mobile Safari/537.36" : userAgent)
|
|
126
|
-
};
|
|
127
|
-
if (options.acceptLanguage) {
|
|
128
|
-
agentOptions.acceptLanguage = options.acceptLanguage;
|
|
129
|
-
}
|
|
130
|
-
if (options.platform || options.browserMobileEmulation) {
|
|
131
|
-
agentOptions.platform = options.platform || "Android";
|
|
132
|
-
}
|
|
133
|
-
if (options.debugMessagesFile) {
|
|
134
|
-
debugMessages.push([Date.now(), ["Emulating user agent", JSON.stringify(agentOptions)]]);
|
|
135
|
-
}
|
|
136
|
-
await Emulation.setUserAgentOverride(agentOptions);
|
|
137
|
-
}
|
|
93
|
+
const cdp = new CDP(targetInfo);
|
|
94
|
+
await setupConsoleLogging(cdp, pageContext);
|
|
95
|
+
await setupBrowserWindow(cdp, targetInfo.id, pageContext);
|
|
96
|
+
await setupSecurity(cdp, pageContext);
|
|
97
|
+
await setupDeviceEmulation(cdp, pageContext);
|
|
98
|
+
await setupNetworkInterception(cdp, pageContext);
|
|
99
|
+
await setupScriptInjection(cdp, pageContext);
|
|
100
|
+
const contextId = await getContextId(cdp, pageContext);
|
|
101
|
+
const pageDataPromise = setupPageDataCapture(cdp, contextId, pageContext);
|
|
102
|
+
await setupBindings(cdp, contextId, pageContext);
|
|
103
|
+
await capturePageData(cdp, contextId, pageContext);
|
|
104
|
+
await disableCdpDomains(cdp, pageContext);
|
|
105
|
+
return await finalizePageData(pageDataPromise, pageContext);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (shouldRetryWithFallback(error)) {
|
|
108
|
+
return await retryWithFallback();
|
|
138
109
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
110
|
+
attachDebugInfo(error, pageContext);
|
|
111
|
+
throw error;
|
|
112
|
+
} finally {
|
|
113
|
+
logData(["Closing page"], pageContext);
|
|
114
|
+
await closeTarget();
|
|
115
|
+
logData(["Finishing"], pageContext);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shouldRetryWithFallback(error) {
|
|
119
|
+
return error.code === LOAD_TIMEOUT_ERROR &&
|
|
120
|
+
options.browserWaitUntilFallback &&
|
|
121
|
+
options.browserWaitUntil &&
|
|
122
|
+
NETWORK_STATES.indexOf(options.browserWaitUntil) < NETWORK_STATES.length - 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function retryWithFallback() {
|
|
126
|
+
const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
|
|
127
|
+
logData(["Retrying with waitUntil", browserWaitUntil], pageContext);
|
|
128
|
+
options.browserWaitUntil = browserWaitUntil;
|
|
129
|
+
await closeTarget();
|
|
130
|
+
return await getPageData(options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function closeTarget() {
|
|
134
|
+
if (targetInfo && !options.browserDebug) {
|
|
135
|
+
await CDP.closeTarget(targetInfo.id);
|
|
136
|
+
targetInfo = null;
|
|
156
137
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function setupConsoleLogging({ Console }, { options, consoleMessages, debugMessages }) {
|
|
142
|
+
const CONSOLE_MESSAGE_ADDED_EVENT_TYPE = "messageAdded";
|
|
143
|
+
if (options.consoleMessagesFile) {
|
|
144
|
+
logData(["Enabling console messages"], { options, debugMessages });
|
|
145
|
+
await Console.enable();
|
|
146
|
+
Console.addEventListener(CONSOLE_MESSAGE_ADDED_EVENT_TYPE, ({ params }) => {
|
|
147
|
+
consoleMessages.push(params.message);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function setupBrowserWindow({ Browser }, targetId, { options, debugMessages }) {
|
|
153
|
+
if (options.browserStartMinimized) {
|
|
154
|
+
const { windowId, bounds } = await Browser.getWindowForTarget({ targetId });
|
|
155
|
+
if (bounds.windowState !== MINIMIZED_WINDOW_STATE) {
|
|
156
|
+
logData(["Minimizing window"], { options, debugMessages });
|
|
157
|
+
await Browser.setWindowBounds({ windowId, bounds: { windowState: MINIMIZED_WINDOW_STATE } });
|
|
161
158
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function setupSecurity({ Security }, { options, debugMessages }) {
|
|
163
|
+
if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
|
|
164
|
+
logData(["Ignoring HTTPS errors"], { options, debugMessages });
|
|
165
|
+
await Security.setIgnoreCertificateErrors({ ignore: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages }) {
|
|
170
|
+
const needsDeviceMetrics = options.browserMobileEmulation || options.browserDeviceWidth ||
|
|
171
|
+
options.browserDeviceHeight || options.browserDeviceScaleFactor;
|
|
172
|
+
const needsUserAgent = options.browserMobileEmulation || options.platform || options.acceptLanguage;
|
|
173
|
+
if (needsDeviceMetrics) {
|
|
174
|
+
await setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages });
|
|
175
|
+
}
|
|
176
|
+
if (needsUserAgent) {
|
|
177
|
+
await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages }) {
|
|
182
|
+
const INNER_WIDTH_PROPERTY = "window.innerWidth";
|
|
183
|
+
const INNER_HEIGHT_PROPERTY = "window.innerHeight";
|
|
184
|
+
const DEVICE_PIXEL_RATIO_PROPERTY = "window.devicePixelRatio";
|
|
185
|
+
const browserDeviceWidth = options.browserDeviceWidth ||
|
|
186
|
+
(await Runtime.evaluate({ expression: INNER_WIDTH_PROPERTY })).result.value;
|
|
187
|
+
const browserDeviceHeight = options.browserDeviceHeight ||
|
|
188
|
+
(await Runtime.evaluate({ expression: INNER_HEIGHT_PROPERTY })).result.value;
|
|
189
|
+
const browserDeviceScaleFactor = options.browserDeviceScaleFactor ||
|
|
190
|
+
(await Runtime.evaluate({ expression: DEVICE_PIXEL_RATIO_PROPERTY })).result.value;
|
|
191
|
+
const deviceMetricsOptions = {
|
|
192
|
+
mobile: Boolean(options.browserMobileEmulation),
|
|
193
|
+
width: options.browserDeviceWidth || (options.browserMobileEmulation ? 360 : options.width || browserDeviceWidth),
|
|
194
|
+
height: options.browserDeviceHeight || (options.browserMobileEmulation ? 800 : options.height || browserDeviceHeight),
|
|
195
|
+
deviceScaleFactor: options.browserDeviceScaleFactor || (options.browserMobileEmulation ? 2 : browserDeviceScaleFactor)
|
|
196
|
+
};
|
|
197
|
+
logData(["Emulating device metrics", JSON.stringify(deviceMetricsOptions)], { options, debugMessages });
|
|
198
|
+
await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }) {
|
|
202
|
+
const ANDROID_PLATFORM = "Android";
|
|
203
|
+
const { userAgent, product } = await Browser.getVersion();
|
|
204
|
+
const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${product} Mobile Safari/537.36`;
|
|
205
|
+
const agentOptions = {
|
|
206
|
+
userAgent: options.userAgent || (options.browserMobileEmulation ? defaultMobileUA : userAgent)
|
|
207
|
+
};
|
|
208
|
+
if (options.acceptLanguage) {
|
|
209
|
+
agentOptions.acceptLanguage = options.acceptLanguage;
|
|
210
|
+
}
|
|
211
|
+
if (options.platform || options.browserMobileEmulation) {
|
|
212
|
+
agentOptions.platform = options.platform || ANDROID_PLATFORM;
|
|
213
|
+
}
|
|
214
|
+
logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
|
|
215
|
+
await Emulation.setUserAgentOverride(agentOptions);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo }) {
|
|
219
|
+
const REQUEST_STAGE = "Request";
|
|
220
|
+
const RESPONSE_STAGE = "Response";
|
|
221
|
+
const DENY_BEHAVIOR = "deny";
|
|
222
|
+
const handleAuthRequests = Boolean(options.httpProxyUsername);
|
|
223
|
+
const patterns = handleAuthRequests ?
|
|
224
|
+
[{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
|
|
225
|
+
[{ requestStage: RESPONSE_STAGE }];
|
|
226
|
+
await Fetch.enable({ handleAuthRequests, patterns });
|
|
227
|
+
if (handleAuthRequests) {
|
|
228
|
+
setupProxyAuth({ Fetch }, { options, debugMessages });
|
|
229
|
+
}
|
|
230
|
+
setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo });
|
|
231
|
+
if (options.httpHeaders) {
|
|
232
|
+
await setupHttpHeaders({ Network }, { options, debugMessages });
|
|
233
|
+
}
|
|
234
|
+
if (options.emulateMediaFeatures) {
|
|
235
|
+
await setupMediaFeatures({ Emulation }, { options, debugMessages });
|
|
236
|
+
}
|
|
237
|
+
if (options.browserCookies && options.browserCookies.length) {
|
|
238
|
+
await setupCookies({ Network }, { options, debugMessages });
|
|
239
|
+
}
|
|
240
|
+
await Browser.setDownloadBehavior({ behavior: DENY_BEHAVIOR });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function setupProxyAuth({ Fetch }, { options, debugMessages }) {
|
|
244
|
+
const AUTH_REQUIRED_EVENT_TYPE = "authRequired";
|
|
245
|
+
const PROVIDE_CREDENTIALS_RESPONSE = "ProvideCredentials";
|
|
246
|
+
Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, async ({ params }) => {
|
|
247
|
+
logData(["Authenticating"], { options, debugMessages });
|
|
248
|
+
await Fetch.continueWithAuth({
|
|
249
|
+
requestId: params.requestId,
|
|
250
|
+
authChallengeResponse: {
|
|
251
|
+
response: PROVIDE_CREDENTIALS_RESPONSE,
|
|
252
|
+
username: options.httpProxyUsername,
|
|
253
|
+
password: options.httpProxyPassword
|
|
203
254
|
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo }) {
|
|
260
|
+
const REQUEST_PAUSED_EVENT_TYPE = "requestPaused";
|
|
261
|
+
const ABORTED_ERROR_REASON = "Aborted";
|
|
262
|
+
const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
|
|
263
|
+
Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, async ({ params }) => {
|
|
264
|
+
const { requestId, request } = params;
|
|
265
|
+
captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
|
|
266
|
+
if (shouldBlockRequest(request.url)) {
|
|
204
267
|
try {
|
|
205
|
-
await Fetch.
|
|
268
|
+
await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON });
|
|
269
|
+
return;
|
|
206
270
|
} catch {
|
|
207
271
|
// ignored
|
|
208
272
|
}
|
|
209
|
-
});
|
|
210
|
-
if (options.httpHeaders) {
|
|
211
|
-
if (options.debugMessagesFile) {
|
|
212
|
-
debugMessages.push([Date.now(), ["Setting HTTP headers", JSON.stringify(options.httpHeaders)]]);
|
|
213
|
-
}
|
|
214
|
-
await Network.enable();
|
|
215
|
-
await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
|
|
216
273
|
}
|
|
217
|
-
if (options.emulateMediaFeatures) {
|
|
218
|
-
for (const mediaFeature of options.emulateMediaFeatures) {
|
|
219
|
-
if (options.debugMessagesFile) {
|
|
220
|
-
debugMessages.push([Date.now(), ["Emulating media feature", mediaFeature.name, mediaFeature.value]]);
|
|
221
|
-
}
|
|
222
|
-
await Emulation.setEmulatedMedia({
|
|
223
|
-
media: mediaFeature.name,
|
|
224
|
-
features: mediaFeature.value.split(",").map(feature => feature.trim())
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (options.browserCookies && options.browserCookies.length) {
|
|
229
|
-
if (options.debugMessagesFile) {
|
|
230
|
-
debugMessages.push([Date.now(), ["Setting cookies", JSON.stringify(options.browserCookies)]]);
|
|
231
|
-
}
|
|
232
|
-
await Network.setCookies({ cookies: options.browserCookies });
|
|
233
|
-
}
|
|
234
|
-
await Browser.setDownloadBehavior({ behavior: "deny" });
|
|
235
|
-
await Page.addScriptToEvaluateOnNewDocument({
|
|
236
|
-
source: getHookScriptSource(),
|
|
237
|
-
runImmediately: true
|
|
238
|
-
});
|
|
239
|
-
await Page.addScriptToEvaluateOnNewDocument({
|
|
240
|
-
source: await getScriptSource(options),
|
|
241
|
-
runImmediately: true,
|
|
242
|
-
worldName: SINGLE_FILE_WORLD_NAME
|
|
243
|
-
});
|
|
244
|
-
const [contextId] = await Promise.all([
|
|
245
|
-
loadPage({ Page, Runtime }, options, debugMessages),
|
|
246
|
-
options.browserDebug ? waitForDebuggerReady({ Debugger }) : Promise.resolve()
|
|
247
|
-
]);
|
|
248
|
-
await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
|
|
249
|
-
if (options.embedScreenshot && options.compressContent) {
|
|
250
|
-
await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextId: contextId });
|
|
251
|
-
Runtime.addEventListener("bindingCalled", async ({ params }) => {
|
|
252
|
-
if (params.name === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
|
|
253
|
-
if (options.debugMessagesFile) {
|
|
254
|
-
debugMessages.push([Date.now(), ["Capturing screenshot"]]);
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
let screenshotOptions = { captureBeyondViewport: true };
|
|
258
|
-
if (options.embedScreenshotOptions) {
|
|
259
|
-
try {
|
|
260
|
-
screenshotOptions = JSON.parse(options.embedScreenshotOptions);
|
|
261
|
-
} catch {
|
|
262
|
-
// ignored
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
screenshotOptions.format = "png";
|
|
266
|
-
const { data } = await Page.captureScreenshot(screenshotOptions);
|
|
267
|
-
await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
|
|
268
|
-
} catch {
|
|
269
|
-
await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
if (options.embedPdf) {
|
|
275
|
-
await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextId: contextId });
|
|
276
|
-
Runtime.addEventListener("bindingCalled", async ({ params }) => {
|
|
277
|
-
if (params.name === PRINT_TO_PDF_FUNCTION_NAME) {
|
|
278
|
-
if (options.debugMessagesFile) {
|
|
279
|
-
debugMessages.push([Date.now(), ["Printing to PDF", options.embedPdfOptions || ""]]);
|
|
280
|
-
}
|
|
281
|
-
let pdfOptions = {};
|
|
282
|
-
if (options.embedPdfOptions) {
|
|
283
|
-
try {
|
|
284
|
-
pdfOptions = JSON.parse(options.embedPdfOptions);
|
|
285
|
-
} catch {
|
|
286
|
-
// ignored
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const { data } = await Page.printToPDF(pdfOptions);
|
|
291
|
-
await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
|
|
292
|
-
} catch {
|
|
293
|
-
await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
const pageDataPromise = new Promise(resolve => {
|
|
299
|
-
let pageDataResponse = "";
|
|
300
|
-
Runtime.addEventListener("bindingCalled", ({ params }) => {
|
|
301
|
-
if (params.name === SET_PAGE_DATA_FUNCTION_NAME) {
|
|
302
|
-
const { payload } = params;
|
|
303
|
-
if (payload.length) {
|
|
304
|
-
pageDataResponse += payload;
|
|
305
|
-
} else {
|
|
306
|
-
if (options.debugMessagesFile) {
|
|
307
|
-
debugMessages.push([Date.now(), ["Setting page data"]]);
|
|
308
|
-
}
|
|
309
|
-
const result = JSON.parse(pageDataResponse);
|
|
310
|
-
if (result.content instanceof Array) {
|
|
311
|
-
result.content = new Uint8Array(result.content);
|
|
312
|
-
}
|
|
313
|
-
resolve(result);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
if (options.browserWaitDelay) {
|
|
319
|
-
if (options.debugMessagesFile) {
|
|
320
|
-
debugMessages.push([Date.now(), [`Waiting ${options.browserWaitDelay} ms`]]);
|
|
321
|
-
}
|
|
322
|
-
await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
|
|
323
|
-
}
|
|
324
|
-
const captureTimeoutAbortController = new AbortController();
|
|
325
|
-
const captureTimeoutAbortSignal = captureTimeoutAbortController.signal;
|
|
326
274
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const { result } = await Promise.race([
|
|
331
|
-
Runtime.evaluate({
|
|
332
|
-
expression: `(${getPageDataScriptSource.toString()})(${JSON.stringify(options)},${JSON.stringify([SET_SCREENSHOT_FUNCTION_NAME, SET_PDF_FUNCTION_NAME, SET_PAGE_DATA_FUNCTION_NAME, CAPTURE_SCREENSHOT_FUNCTION_NAME, PRINT_TO_PDF_FUNCTION_NAME])})`,
|
|
333
|
-
awaitPromise: true,
|
|
334
|
-
returnByValue: true,
|
|
335
|
-
contextId
|
|
336
|
-
}),
|
|
337
|
-
waitForTimeout(captureTimeoutAbortSignal, options.browserCaptureMaxTime, "Capture timeout", CAPTURE_TIMEOUT_ERROR)
|
|
338
|
-
]);
|
|
339
|
-
const { subtype, description } = result;
|
|
340
|
-
if (subtype === "error") {
|
|
341
|
-
throw new Error(description);
|
|
342
|
-
}
|
|
343
|
-
} finally {
|
|
344
|
-
if (!captureTimeoutAbortSignal.aborted) {
|
|
345
|
-
captureTimeoutAbortController.abort();
|
|
346
|
-
}
|
|
347
|
-
await Runtime.disable();
|
|
348
|
-
await Page.disable();
|
|
349
|
-
if (options.httpHeaders) {
|
|
350
|
-
await Network.disable();
|
|
351
|
-
}
|
|
275
|
+
await Fetch.continueRequest({ requestId });
|
|
276
|
+
} catch {
|
|
277
|
+
// ignored
|
|
352
278
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
function shouldBlockRequest(requestUrl) {
|
|
282
|
+
if (!options.blockedURLPatterns || !options.blockedURLPatterns.length) {
|
|
283
|
+
return false;
|
|
357
284
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
285
|
+
const blockedURL = options.blockedURLPatterns.find(pattern =>
|
|
286
|
+
new RegExp(pattern).test(requestUrl)
|
|
287
|
+
);
|
|
288
|
+
if (blockedURL) {
|
|
289
|
+
logData(["Blocking request", requestUrl], { options, debugMessages });
|
|
290
|
+
return true;
|
|
361
291
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function captureHttpInfo(params, urlState, { options, debugMessages, httpInfo }) {
|
|
297
|
+
const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
|
|
298
|
+
const DOCUMENT_RESOURCE_TYPE = "Document";
|
|
299
|
+
const LOCATION_HEADER_NAME = "location";
|
|
300
|
+
const { request, resourceType, responseHeaders, responseStatusCode, responseStatusText } = params;
|
|
301
|
+
const shouldCapture = resourceType === DOCUMENT_RESOURCE_TYPE &&
|
|
302
|
+
options.outputJson &&
|
|
303
|
+
!httpInfo.request &&
|
|
304
|
+
responseStatusCode !== undefined &&
|
|
305
|
+
(request.url === urlState.url || request.url === urlState.alternativeUrl);
|
|
306
|
+
if (shouldCapture) {
|
|
307
|
+
if (REDIRECT_STATUS_CODES.includes(responseStatusCode)) {
|
|
308
|
+
const redirect = responseHeaders.find(header => header.name.toLowerCase() === LOCATION_HEADER_NAME)?.value;
|
|
309
|
+
if (redirect) {
|
|
310
|
+
urlState.url = new URL(redirect, urlState.url).href;
|
|
374
311
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
312
|
+
logData(["Redirecting", urlState.url], { options, debugMessages });
|
|
313
|
+
} else {
|
|
314
|
+
Object.assign(httpInfo, {
|
|
315
|
+
request: {
|
|
316
|
+
url: request.url,
|
|
317
|
+
method: request.method,
|
|
318
|
+
headers: request.headers,
|
|
319
|
+
referrerPolicy: request.referrerPolicy
|
|
320
|
+
},
|
|
321
|
+
resourceType,
|
|
322
|
+
response: {
|
|
323
|
+
status: responseStatusCode,
|
|
324
|
+
statusText: responseStatusText,
|
|
325
|
+
headers: responseHeaders
|
|
326
|
+
}
|
|
327
|
+
});
|
|
390
328
|
}
|
|
391
329
|
}
|
|
330
|
+
}
|
|
392
331
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
332
|
+
async function setupHttpHeaders({ Network }, { options, debugMessages }) {
|
|
333
|
+
logData(["Setting HTTP headers", JSON.stringify(options.httpHeaders)], { options, debugMessages });
|
|
334
|
+
await Network.enable();
|
|
335
|
+
await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
|
|
339
|
+
for (const mediaFeature of options.emulateMediaFeatures) {
|
|
340
|
+
logData(["Emulating media feature", mediaFeature.name, mediaFeature.value], { options, debugMessages });
|
|
341
|
+
await Emulation.setEmulatedMedia({
|
|
342
|
+
media: mediaFeature.name,
|
|
343
|
+
features: mediaFeature.value.split(",").map(feature => feature.trim())
|
|
344
|
+
});
|
|
398
345
|
}
|
|
399
346
|
}
|
|
400
347
|
|
|
401
|
-
function
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
348
|
+
async function setupCookies({ Network }, { options, debugMessages }) {
|
|
349
|
+
logData(["Setting cookies", JSON.stringify(options.browserCookies)], { options, debugMessages });
|
|
350
|
+
await Network.setCookies({ cookies: options.browserCookies });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function setupScriptInjection({ Page }, { options }) {
|
|
354
|
+
await Page.addScriptToEvaluateOnNewDocument({
|
|
355
|
+
source: getHookScriptSource(),
|
|
356
|
+
runImmediately: true
|
|
357
|
+
});
|
|
358
|
+
await Page.addScriptToEvaluateOnNewDocument({
|
|
359
|
+
source: await getScriptSource(options),
|
|
360
|
+
runImmediately: true,
|
|
361
|
+
worldName: SINGLE_FILE_WORLD_NAME
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function getContextId({ Debugger, Page, Runtime }, { options, debugMessages }) {
|
|
366
|
+
const [contextId] = await Promise.all([
|
|
367
|
+
loadPage({ Page, Runtime }, { options, debugMessages }),
|
|
368
|
+
options.browserDebug ? waitForDebuggerReady() : Promise.resolve()
|
|
369
|
+
]);
|
|
370
|
+
return contextId;
|
|
371
|
+
|
|
372
|
+
async function waitForDebuggerReady() {
|
|
373
|
+
await Debugger.enable();
|
|
374
|
+
await Debugger.pause();
|
|
375
|
+
await new Promise(resolve => {
|
|
376
|
+
const RESUMED_EVENT = "resumed";
|
|
377
|
+
Debugger.addEventListener(RESUMED_EVENT, onResumed);
|
|
378
|
+
function onResumed() {
|
|
379
|
+
Debugger.removeEventListener(RESUMED_EVENT, onResumed);
|
|
380
|
+
resolve();
|
|
424
381
|
}
|
|
425
|
-
};
|
|
382
|
+
});
|
|
426
383
|
}
|
|
427
|
-
const MAX_CONTENT_SIZE = 32 * 1024 * 1024;
|
|
428
|
-
return singlefile.getPageData(options).then(data => {
|
|
429
|
-
if (data.content instanceof Uint8Array) {
|
|
430
|
-
data.content = Array.from(data.content);
|
|
431
|
-
}
|
|
432
|
-
data = JSON.stringify(data);
|
|
433
|
-
let indexData = 0;
|
|
434
|
-
do {
|
|
435
|
-
globalThis[SET_PAGE_DATA_FUNCTION_NAME](data.slice(indexData, indexData + MAX_CONTENT_SIZE));
|
|
436
|
-
indexData += MAX_CONTENT_SIZE;
|
|
437
|
-
} while (indexData < data.length);
|
|
438
|
-
globalThis[SET_PAGE_DATA_FUNCTION_NAME]("");
|
|
439
|
-
});
|
|
440
384
|
}
|
|
441
385
|
|
|
442
|
-
async function loadPage({ Page, Runtime }, options, debugMessages) {
|
|
386
|
+
async function loadPage({ Page, Runtime }, { options, debugMessages }) {
|
|
387
|
+
const LOAD_TIMEOUT_ERROR_MESSAGE = "Load timeout";
|
|
443
388
|
await Runtime.enable();
|
|
444
389
|
await Page.enable();
|
|
445
390
|
const loadTimeoutAbortController = new AbortController();
|
|
446
391
|
const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
|
|
447
392
|
try {
|
|
448
|
-
|
|
449
|
-
debugMessages.push([Date.now(), ["Loading page", options.url]]);
|
|
450
|
-
}
|
|
393
|
+
logData(["Loading page", options.url], { options, debugMessages });
|
|
451
394
|
const [contextId] = await Promise.race([
|
|
452
|
-
Promise.all([getTopFrameContextId({ Page, Runtime }, options, debugMessages), Page.navigate({ url: options.url })]),
|
|
453
|
-
waitForTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime,
|
|
395
|
+
Promise.all([getTopFrameContextId({ Page, Runtime }, { options, debugMessages }), Page.navigate({ url: options.url })]),
|
|
396
|
+
waitForTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime, LOAD_TIMEOUT_ERROR_MESSAGE, LOAD_TIMEOUT_ERROR)
|
|
454
397
|
]);
|
|
455
398
|
return contextId;
|
|
456
399
|
} finally {
|
|
@@ -462,167 +405,311 @@ async function loadPage({ Page, Runtime }, options, debugMessages) {
|
|
|
462
405
|
}
|
|
463
406
|
}
|
|
464
407
|
|
|
465
|
-
async function getTopFrameContextId({ Page, Runtime }, options, debugMessages) {
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
408
|
+
async function getTopFrameContextId({ Page, Runtime }, { options, debugMessages }) {
|
|
409
|
+
await Page.setLifecycleEventsEnabled({ enabled: true });
|
|
410
|
+
const state = { topFrameId: undefined, contextIds: [] };
|
|
411
|
+
const removeContextListener = setupContextCreatedListener({ Runtime }, state);
|
|
469
412
|
try {
|
|
470
|
-
|
|
471
|
-
await
|
|
472
|
-
|
|
473
|
-
debugMessages.push([Date.now(), ["Getting execution context"]]);
|
|
474
|
-
}
|
|
475
|
-
const contextId = await getContextId();
|
|
476
|
-
if (contextId === undefined) {
|
|
477
|
-
throw new Error("Execution context not found");
|
|
478
|
-
} else {
|
|
479
|
-
return contextId;
|
|
480
|
-
}
|
|
413
|
+
await waitForPageReadyState({ Page }, state, { options, debugMessages });
|
|
414
|
+
const contextId = await findValidSingleFileContext({ Runtime }, state.contextIds, { options, debugMessages });
|
|
415
|
+
return contextId;
|
|
481
416
|
} finally {
|
|
482
|
-
|
|
417
|
+
removeContextListener();
|
|
418
|
+
await Page.setLifecycleEventsEnabled({ enabled: false });
|
|
483
419
|
}
|
|
420
|
+
}
|
|
484
421
|
|
|
485
|
-
|
|
422
|
+
function setupContextCreatedListener({ Runtime }, state) {
|
|
423
|
+
const EXECUTION_CONTEXT_CREATED_EVENT_TYPE = "executionContextCreated";
|
|
424
|
+
const onContextCreated = ({ params }) => {
|
|
486
425
|
const { context } = params;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
contextIds.unshift(context.id);
|
|
426
|
+
if (context.name === SINGLE_FILE_WORLD_NAME && context.auxData?.frameId === state.topFrameId) {
|
|
427
|
+
state.contextIds.push(context.id);
|
|
490
428
|
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
await Page.setLifecycleEventsEnabled({ enabled: true });
|
|
496
|
-
try {
|
|
497
|
-
await new Promise((resolve, reject) => {
|
|
498
|
-
const LIFE_CYCLE_EVENT = "lifecycleEvent";
|
|
499
|
-
const FRAME_NAVIGATED_EVENT = "frameNavigated";
|
|
500
|
-
Page.addEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
|
|
501
|
-
Page.addEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
|
|
502
|
-
|
|
503
|
-
function onLifecycleEvent({ params }) {
|
|
504
|
-
const { frameId, name } = params;
|
|
505
|
-
if (frameId === topFrameId) {
|
|
506
|
-
if (options.debugMessagesFile) {
|
|
507
|
-
debugMessages.push([Date.now(), ["Detecting lifecycle event", name]]);
|
|
508
|
-
}
|
|
509
|
-
if (name === options.browserWaitUntil || (resolveCallbackTimeout && NETWORK_STATES.indexOf(name) < NETWORK_STATES.indexOf(options.browserWaitUntil))) {
|
|
510
|
-
clearTimeout(resolveCallbackTimeout);
|
|
511
|
-
if (options.debugMessagesFile) {
|
|
512
|
-
debugMessages.push([Date.now(), [`Waiting ${options.browserWaitUntilDelay} ms`]]);
|
|
513
|
-
}
|
|
514
|
-
setTimeout(() => {
|
|
515
|
-
if (options.debugMessagesFile) {
|
|
516
|
-
debugMessages.push([Date.now(), ["Detecting page ready"]]);
|
|
517
|
-
}
|
|
518
|
-
removeListeners();
|
|
519
|
-
resolve();
|
|
520
|
-
}, options.browserWaitUntilDelay);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
429
|
+
};
|
|
430
|
+
Runtime.addEventListener(EXECUTION_CONTEXT_CREATED_EVENT_TYPE, onContextCreated);
|
|
431
|
+
return () => Runtime.removeEventListener(EXECUTION_CONTEXT_CREATED_EVENT_TYPE, onContextCreated);
|
|
432
|
+
}
|
|
524
433
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
434
|
+
async function waitForPageReadyState({ Page }, state, { options, debugMessages }) {
|
|
435
|
+
const LIFE_CYCLE_EVENT_TYPE = "lifecycleEvent";
|
|
436
|
+
const FRAME_NAVIGATED_EVENT_TYPE = "frameNavigated";
|
|
437
|
+
await new Promise((resolve, reject) => {
|
|
438
|
+
const timeoutState = { timeoutId: undefined };
|
|
439
|
+
const cleanup = () => {
|
|
440
|
+
Page.removeEventListener(LIFE_CYCLE_EVENT_TYPE, onLifecycleEvent);
|
|
441
|
+
Page.removeEventListener(FRAME_NAVIGATED_EVENT_TYPE, onFrameNavigated);
|
|
442
|
+
};
|
|
443
|
+
const onLifecycleEvent = createLifecycleEventHandler(state, timeoutState, resolve, cleanup, { options, debugMessages });
|
|
444
|
+
const onFrameNavigated = createFrameNavigatedHandler(state, timeoutState, reject, cleanup, { options, debugMessages });
|
|
445
|
+
Page.addEventListener(LIFE_CYCLE_EVENT_TYPE, onLifecycleEvent);
|
|
446
|
+
Page.addEventListener(FRAME_NAVIGATED_EVENT_TYPE, onFrameNavigated);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
540
449
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
450
|
+
function createLifecycleEventHandler(state, timeoutState, resolve, cleanup, { options, debugMessages }) {
|
|
451
|
+
return ({ params }) => {
|
|
452
|
+
const { frameId, name } = params;
|
|
453
|
+
if (frameId === state.topFrameId) {
|
|
454
|
+
logData(["Detecting lifecycle event", name], { options, debugMessages });
|
|
455
|
+
}
|
|
456
|
+
const shouldResolve = name === options.browserWaitUntil ||
|
|
457
|
+
(timeoutState.timeoutId && NETWORK_STATES.indexOf(name) < NETWORK_STATES.indexOf(options.browserWaitUntil));
|
|
458
|
+
if (shouldResolve) {
|
|
459
|
+
clearTimeout(timeoutState.timeoutId);
|
|
460
|
+
logData([`Waiting ${options.browserWaitUntilDelay} ms`], { options, debugMessages });
|
|
461
|
+
setTimeout(() => {
|
|
462
|
+
logData(["Detecting page ready"], { options, debugMessages });
|
|
463
|
+
cleanup();
|
|
464
|
+
resolve();
|
|
465
|
+
}, options.browserWaitUntilDelay);
|
|
548
466
|
}
|
|
549
|
-
}
|
|
467
|
+
};
|
|
468
|
+
};
|
|
550
469
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
470
|
+
function createFrameNavigatedHandler(state, timeoutState, reject, cleanup, { options, debugMessages }) {
|
|
471
|
+
const UNREACHABLE_URL_ERROR_MESSAGE = "Unreachable URL";
|
|
472
|
+
return ({ params }) => {
|
|
473
|
+
const { frame } = params;
|
|
474
|
+
if (!frame.parentId) {
|
|
475
|
+
if (frame.unreachableUrl) {
|
|
476
|
+
clearTimeout(timeoutState.timeoutId);
|
|
477
|
+
cleanup();
|
|
478
|
+
reject(new Error(UNREACHABLE_URL_ERROR_MESSAGE + ": " + frame.unreachableUrl));
|
|
479
|
+
} else {
|
|
480
|
+
logData(["Detecting top frame ID"], { options, debugMessages });
|
|
481
|
+
state.topFrameId = frame.id;
|
|
482
|
+
}
|
|
561
483
|
}
|
|
562
|
-
|
|
563
|
-
|
|
484
|
+
};
|
|
485
|
+
}
|
|
564
486
|
|
|
565
|
-
|
|
487
|
+
async function findValidSingleFileContext({ Runtime }, contextIds, { options, debugMessages }) {
|
|
488
|
+
const CONTEXT_NOT_FOUND_ERROR_MESSAGE = "Execution context not found for SingleFile world";
|
|
489
|
+
const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined'";
|
|
490
|
+
const NO_VALID_CONTEXT_ERROR_MESSAGE = "No valid SingleFile execution context found";
|
|
491
|
+
logData(["Getting execution context"], { options, debugMessages });
|
|
492
|
+
if (!contextIds.length) {
|
|
493
|
+
throw new Error(CONTEXT_NOT_FOUND_ERROR_MESSAGE);
|
|
494
|
+
}
|
|
495
|
+
for (const contextId of contextIds) {
|
|
566
496
|
try {
|
|
567
497
|
const { result } = await Runtime.evaluate({
|
|
568
|
-
expression:
|
|
498
|
+
expression: SINGLE_FILE_DETECTION_TEST,
|
|
569
499
|
contextId
|
|
570
500
|
});
|
|
571
|
-
|
|
501
|
+
if (result.value === true) {
|
|
502
|
+
return contextId;
|
|
503
|
+
}
|
|
572
504
|
} catch {
|
|
573
505
|
// ignored
|
|
574
506
|
}
|
|
575
|
-
return false;
|
|
576
507
|
}
|
|
508
|
+
throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function setupPageDataCapture({ Runtime }, contextId, { options, debugMessages }) {
|
|
512
|
+
return new Promise(resolve => {
|
|
513
|
+
let pageDataResponse = "";
|
|
514
|
+
Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ({ params }) => {
|
|
515
|
+
if (params.name === SET_PAGE_DATA_FUNCTION_NAME) {
|
|
516
|
+
const { payload } = params;
|
|
517
|
+
if (payload.length) {
|
|
518
|
+
pageDataResponse += payload;
|
|
519
|
+
} else {
|
|
520
|
+
logData(["Setting page data"], { options, debugMessages });
|
|
521
|
+
const result = JSON.parse(pageDataResponse);
|
|
522
|
+
if (result.content instanceof Array) {
|
|
523
|
+
result.content = new Uint8Array(result.content);
|
|
524
|
+
}
|
|
525
|
+
resolve(result);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function setupBindings({ Page, Runtime }, contextId, { options, debugMessages }) {
|
|
533
|
+
await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
|
|
534
|
+
if (options.embedScreenshot && options.compressContent) {
|
|
535
|
+
await setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages });
|
|
536
|
+
}
|
|
537
|
+
if (options.embedPdf) {
|
|
538
|
+
await setupPdfCapture({ Page, Runtime }, contextId, { options, debugMessages });
|
|
539
|
+
}
|
|
540
|
+
await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextId: contextId });
|
|
541
|
+
Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, async ({ params }) => {
|
|
542
|
+
if (params.name === FETCH_FUNCTION_NAME) {
|
|
543
|
+
await handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages });
|
|
544
|
+
}
|
|
545
|
+
});
|
|
577
546
|
}
|
|
578
547
|
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
548
|
+
async function setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages }) {
|
|
549
|
+
await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextId: contextId });
|
|
550
|
+
Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, async ({ params }) => {
|
|
551
|
+
if (params.name === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
|
|
552
|
+
logData(["Capturing screenshot"], { options, debugMessages });
|
|
553
|
+
try {
|
|
554
|
+
const screenshotOptions = parseScreenshotOptions(options.embedScreenshotOptions);
|
|
555
|
+
const { data } = await Page.captureScreenshot(screenshotOptions);
|
|
556
|
+
await callBrowserFunction({ Runtime }, contextId, SET_SCREENSHOT_FUNCTION_NAME, [data]);
|
|
557
|
+
} catch {
|
|
558
|
+
await callBrowserFunction({ Runtime }, contextId, SET_SCREENSHOT_FUNCTION_NAME, [""]);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
589
562
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
563
|
+
function parseScreenshotOptions(optionsString) {
|
|
564
|
+
const PNG_FORMAT = "png";
|
|
565
|
+
let screenshotOptions = { captureBeyondViewport: true };
|
|
566
|
+
if (optionsString) {
|
|
567
|
+
try {
|
|
568
|
+
screenshotOptions = JSON.parse(optionsString);
|
|
569
|
+
} catch {
|
|
570
|
+
// ignored
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
screenshotOptions.format = PNG_FORMAT;
|
|
574
|
+
return screenshotOptions;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function setupPdfCapture({ Page, Runtime }, contextId, { options, debugMessages }) {
|
|
579
|
+
await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextId: contextId });
|
|
580
|
+
Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, async ({ params }) => {
|
|
581
|
+
if (params.name !== PRINT_TO_PDF_FUNCTION_NAME) {
|
|
582
|
+
logData(["Printing to PDF", options.embedPdfOptions || ""], { options, debugMessages });
|
|
583
|
+
const pdfOptions = parsePdfOptions(options.embedPdfOptions);
|
|
584
|
+
try {
|
|
585
|
+
const { data } = await Page.printToPDF(pdfOptions);
|
|
586
|
+
await callBrowserFunction({ Runtime }, contextId, SET_PDF_FUNCTION_NAME, [data]);
|
|
587
|
+
} catch {
|
|
588
|
+
await callBrowserFunction({ Runtime }, contextId, SET_PDF_FUNCTION_NAME, [""]);
|
|
589
|
+
}
|
|
594
590
|
}
|
|
595
591
|
});
|
|
592
|
+
|
|
593
|
+
function parsePdfOptions(optionsString) {
|
|
594
|
+
let pdfOptions = {};
|
|
595
|
+
if (optionsString) {
|
|
596
|
+
try {
|
|
597
|
+
pdfOptions = JSON.parse(optionsString);
|
|
598
|
+
} catch {
|
|
599
|
+
// ignored
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return pdfOptions;
|
|
603
|
+
}
|
|
596
604
|
}
|
|
597
605
|
|
|
598
|
-
async function
|
|
599
|
-
|
|
606
|
+
async function handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages }) {
|
|
607
|
+
const { payload } = params;
|
|
608
|
+
const { requestId, url, options: fetchOptions } = JSON.parse(payload);
|
|
609
|
+
logData(["Fetching URL", url], { options, debugMessages });
|
|
600
610
|
try {
|
|
601
|
-
await
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
611
|
+
const response = await fetch(url, fetchOptions);
|
|
612
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
613
|
+
const base64Data = arrayBufferToBase64(arrayBuffer);
|
|
614
|
+
const result = {
|
|
615
|
+
status: response.status,
|
|
616
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
617
|
+
data: base64Data
|
|
618
|
+
};
|
|
619
|
+
await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
const errorResult = {
|
|
622
|
+
error: error.message,
|
|
623
|
+
code: error.code
|
|
624
|
+
};
|
|
625
|
+
await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult]);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
605
628
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
629
|
+
async function callBrowserFunction({ Runtime }, contextId, functionName, args) {
|
|
630
|
+
const serializedArgs = args.map(arg => JSON.stringify(arg)).join(", ");
|
|
631
|
+
await Runtime.evaluate({
|
|
632
|
+
expression: `globalThis.${functionName}(${serializedArgs})`,
|
|
633
|
+
contextId
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function capturePageData({ Runtime }, contextId, { options, debugMessages }) {
|
|
638
|
+
const CAPTURE_TIMEOUT_ERROR_MESSAGE = "Capture timeout";
|
|
639
|
+
const ERROR_SUBTYPE = "error";
|
|
640
|
+
const captureTimeoutAbortController = new AbortController();
|
|
641
|
+
const captureTimeoutAbortSignal = captureTimeoutAbortController.signal;
|
|
642
|
+
if (options.browserWaitDelay) {
|
|
643
|
+
logData([`Waiting ${options.browserWaitDelay} ms`], { options, debugMessages });
|
|
644
|
+
await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
logData(["Capturing page"], { options, debugMessages });
|
|
648
|
+
const captureScript = `(${getPageDataScriptSource.toString()})(${JSON.stringify(options)},${JSON.stringify([
|
|
649
|
+
SET_SCREENSHOT_FUNCTION_NAME,
|
|
650
|
+
SET_PDF_FUNCTION_NAME,
|
|
651
|
+
SET_PAGE_DATA_FUNCTION_NAME,
|
|
652
|
+
CAPTURE_SCREENSHOT_FUNCTION_NAME,
|
|
653
|
+
PRINT_TO_PDF_FUNCTION_NAME
|
|
654
|
+
])})`;
|
|
655
|
+
const { result } = await Promise.race([
|
|
656
|
+
Runtime.evaluate({
|
|
657
|
+
expression: captureScript,
|
|
658
|
+
awaitPromise: true,
|
|
659
|
+
returnByValue: true,
|
|
660
|
+
contextId
|
|
661
|
+
}),
|
|
662
|
+
waitForTimeout(captureTimeoutAbortSignal, options.browserCaptureMaxTime, CAPTURE_TIMEOUT_ERROR_MESSAGE, CAPTURE_TIMEOUT_ERROR)
|
|
663
|
+
]);
|
|
664
|
+
if (result.subtype === ERROR_SUBTYPE) {
|
|
665
|
+
throw new Error(result.description);
|
|
666
|
+
}
|
|
611
667
|
} finally {
|
|
612
|
-
|
|
668
|
+
if (!captureTimeoutAbortSignal.aborted) {
|
|
669
|
+
captureTimeoutAbortController.abort();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function disableCdpDomains({ Console, Network, Page, Runtime }, { options }) {
|
|
675
|
+
await Runtime.disable();
|
|
676
|
+
await Page.disable();
|
|
677
|
+
if (options.httpHeaders) {
|
|
678
|
+
await Network.disable();
|
|
679
|
+
}
|
|
680
|
+
if (options.consoleMessagesFile) {
|
|
681
|
+
await Console.disable();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function finalizePageData(pageDataPromise, { options, consoleMessages, debugMessages, httpInfo }) {
|
|
686
|
+
const pageData = await pageDataPromise;
|
|
687
|
+
logData(["Returning page data"], { options, debugMessages });
|
|
688
|
+
if (options.consoleMessagesFile) {
|
|
689
|
+
pageData.consoleMessages = consoleMessages;
|
|
690
|
+
}
|
|
691
|
+
if (options.debugMessagesFile) {
|
|
692
|
+
pageData.debugMessages = debugMessages;
|
|
693
|
+
}
|
|
694
|
+
Object.assign(pageData, httpInfo);
|
|
695
|
+
if (options.browserWaitEndDelay) {
|
|
696
|
+
logData([`Waiting ${options.browserWaitEndDelay} ms after processing`], { options, debugMessages });
|
|
697
|
+
await new Promise(resolve => setTimeout(resolve, options.browserWaitEndDelay));
|
|
613
698
|
}
|
|
699
|
+
return pageData;
|
|
614
700
|
}
|
|
615
701
|
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
browserOptions.width = options.browserWidth;
|
|
624
|
-
browserOptions.height = options.browserHeight;
|
|
625
|
-
browserOptions.userAgent = options.userAgent;
|
|
626
|
-
browserOptions.httpProxyServer = options.httpProxyServer;
|
|
627
|
-
return browserOptions;
|
|
702
|
+
function attachDebugInfo(error, { options, consoleMessages, debugMessages }) {
|
|
703
|
+
if (options.consoleMessagesFile) {
|
|
704
|
+
error.consoleMessages = consoleMessages;
|
|
705
|
+
}
|
|
706
|
+
if (options.debugMessagesFile) {
|
|
707
|
+
error.debugMessages = debugMessages;
|
|
708
|
+
}
|
|
628
709
|
}
|
|
710
|
+
|
|
711
|
+
function logData(data, { options, debugMessages }) {
|
|
712
|
+
if (options.debugMessagesFile) {
|
|
713
|
+
debugMessages.push([Date.now(), data]);
|
|
714
|
+
}
|
|
715
|
+
}
|