single-file-cli 1.0.0

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.
Files changed (35) hide show
  1. package/.eslintrc.js +43 -0
  2. package/Dockerfile +12 -0
  3. package/README.MD +163 -0
  4. package/args.js +305 -0
  5. package/back-ends/common/scripts.js +66 -0
  6. package/back-ends/extensions/bypass-csp/index.js +36 -0
  7. package/back-ends/extensions/bypass-csp/manifest.json +20 -0
  8. package/back-ends/extensions/disable-web-security/index.js +54 -0
  9. package/back-ends/extensions/disable-web-security/manifest.json +20 -0
  10. package/back-ends/extensions/network-idle/bg.js +61 -0
  11. package/back-ends/extensions/network-idle/content.js +29 -0
  12. package/back-ends/extensions/network-idle/manifest.json +31 -0
  13. package/back-ends/extensions/signed/bypass_csp-0.0.3-an+fx.xpi +0 -0
  14. package/back-ends/extensions/signed/disable_web_security-0.0.3-an+fx.xpi +0 -0
  15. package/back-ends/extensions/signed/network_idle-0.0.2-an+fx.xpi +0 -0
  16. package/back-ends/jsdom.js +175 -0
  17. package/back-ends/playwright-chromium.js +113 -0
  18. package/back-ends/playwright-firefox.js +113 -0
  19. package/back-ends/puppeteer-firefox.js +170 -0
  20. package/back-ends/puppeteer.js +189 -0
  21. package/back-ends/webdriver-chromium.js +168 -0
  22. package/back-ends/webdriver-gecko.js +181 -0
  23. package/build-lib.sh +3 -0
  24. package/lib/single-file-bootstrap.js +1 -0
  25. package/lib/single-file-frames.js +1 -0
  26. package/lib/single-file-hooks-frames.js +1 -0
  27. package/lib/single-file-hooks.js +1 -0
  28. package/lib/single-file-infobar.js +1 -0
  29. package/lib/single-file.js +1 -0
  30. package/package.json +37 -0
  31. package/rollup.config.dev.js +64 -0
  32. package/rollup.config.js +64 -0
  33. package/single-file +81 -0
  34. package/single-file-cli-api.js +324 -0
  35. package/single-file.bat +2 -0
@@ -0,0 +1,170 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global singlefile, infobar, require, exports */
25
+
26
+ const puppeteer = require("puppeteer-core");
27
+ const scripts = require("./common/scripts.js");
28
+
29
+ const EXECUTION_CONTEXT_DESTROYED_ERROR = "Execution context was destroyed";
30
+ const NETWORK_IDLE_STATE = "networkidle0";
31
+
32
+ let browser;
33
+
34
+ exports.initialize = async options => {
35
+ browser = await puppeteer.launch(getBrowserOptions(options));
36
+ };
37
+
38
+ exports.getPageData = async options => {
39
+ let page;
40
+ try {
41
+ page = await browser.newPage();
42
+ await setPageOptions(page, options);
43
+ return await getPageData(browser, page, options);
44
+ } finally {
45
+ if (page) {
46
+ await page.close();
47
+ }
48
+ }
49
+ };
50
+
51
+ exports.closeBrowser = () => {
52
+ if (browser) {
53
+ return browser.close();
54
+ }
55
+ };
56
+
57
+ function getBrowserOptions(options) {
58
+ const browserOptions = {};
59
+ if (options.browserHeadless !== undefined) {
60
+ browserOptions.headless = options.browserHeadless && !options.browserDebug;
61
+ }
62
+ browserOptions.args = options.browserArgs ? JSON.parse(options.browserArgs) : [];
63
+ if (options.browserExecutablePath) {
64
+ browserOptions.executablePath = options.browserExecutablePath || "firefox";
65
+ }
66
+ browserOptions.product = "firefox";
67
+ return browserOptions;
68
+ }
69
+
70
+ async function setPageOptions(page, options) {
71
+ if (options.browserWidth && options.browserHeight) {
72
+ await page.setViewport({
73
+ width: options.browserWidth,
74
+ height: options.browserHeight
75
+ });
76
+ }
77
+ if ((options.browserBypassCSP === undefined || options.browserBypassCSP) && page.setBypassCSP) {
78
+ try {
79
+ await page.setBypassCSP(true);
80
+ } catch (error) {
81
+ // ignored
82
+ }
83
+ }
84
+ if (options.httpHeaders) {
85
+ try {
86
+ await page.setExtraHTTPHeaders(options.httpHeaders);
87
+ } catch (error) {
88
+ // ignored
89
+ }
90
+ }
91
+ if (options.browserCookies && options.browserCookies.length) {
92
+ await page.setCookie(...options.browserCookies);
93
+ }
94
+ if (options.emulateMediaFeatures) {
95
+ try {
96
+ await page.emulateMediaFeatures(options.emulateMediaFeatures);
97
+ } catch (error) {
98
+ // ignored
99
+ }
100
+ }
101
+ }
102
+
103
+ async function getPageData(browser, page, options) {
104
+ const injectedScript = await scripts.get(options);
105
+ await page.evaluateOnNewDocument(injectedScript);
106
+ if (options.browserDebug) {
107
+ await page.waitForTimeout(3000);
108
+ }
109
+ await pageGoto(page, options);
110
+ try {
111
+ await page.evaluate(injectedScript);
112
+ if (options.browserWaitDelay) {
113
+ await page.waitForTimeout(options.browserWaitDelay);
114
+ }
115
+ return await page.evaluate(async options => {
116
+ const pageData = await singlefile.getPageData(options);
117
+ if (options.includeInfobar) {
118
+ await infobar.includeScript(pageData);
119
+ }
120
+ return pageData;
121
+ }, options);
122
+ } catch (error) {
123
+ if (error.message && error.message.includes(EXECUTION_CONTEXT_DESTROYED_ERROR)) {
124
+ const pageData = await handleJSRedirect(browser, options);
125
+ if (pageData) {
126
+ return pageData;
127
+ } else {
128
+ throw error;
129
+ }
130
+ } else if (error.name != "TimeoutError") {
131
+ throw error;
132
+ }
133
+ }
134
+ }
135
+
136
+ async function handleJSRedirect(browser, options) {
137
+ const pages = await browser.pages();
138
+ const page = pages[1] || pages[0];
139
+ try {
140
+ await pageGoto(page, options);
141
+ } catch (error) {
142
+ if (error.name != "TimeoutError") {
143
+ throw error;
144
+ }
145
+ }
146
+ const url = page.url();
147
+ if (url != options.url) {
148
+ options.url = url;
149
+ await browser.close();
150
+ return exports.getPageData(options);
151
+ }
152
+ }
153
+
154
+ async function pageGoto(page, options) {
155
+ try {
156
+ await page.goto(options.url, {
157
+ timeout: options.browserLoadMaxTime || 0,
158
+ waitUntil: options.browserWaitUntil || NETWORK_IDLE_STATE
159
+ });
160
+ } catch (error) {
161
+ if (error.message.includes("Unknown waitUntil condition")) {
162
+ await page.goto(options.url, {
163
+ timeout: options.browserLoadMaxTime || 0,
164
+ waitUntil: "load"
165
+ });
166
+ } else {
167
+ throw error;
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,189 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global singlefile, infobar, require, exports */
25
+
26
+ const puppeteer = require("puppeteer-core");
27
+ const scripts = require("./common/scripts.js");
28
+
29
+ const EXECUTION_CONTEXT_DESTROYED_ERROR = "Execution context was destroyed";
30
+ const NETWORK_IDLE_STATE = "networkidle0";
31
+ const NETWORK_STATES = ["networkidle0", "networkidle2", "load", "domcontentloaded"];
32
+
33
+ let browser;
34
+
35
+ exports.initialize = async options => {
36
+ if (options.browserServer) {
37
+ browser = await puppeteer.connect({ browserWSEndpoint: options.browserServer });
38
+ } else {
39
+ browser = await puppeteer.launch(getBrowserOptions(options));
40
+ }
41
+ return browser;
42
+ };
43
+
44
+ exports.getPageData = async (options, page) => {
45
+ const privatePage = !page;
46
+ try {
47
+ if (privatePage) {
48
+ page = await browser.newPage();
49
+ }
50
+ await setPageOptions(page, options);
51
+ return await getPageData(browser, page, options);
52
+ } finally {
53
+ if (privatePage) {
54
+ await page.close();
55
+ }
56
+ }
57
+ };
58
+
59
+ exports.closeBrowser = () => {
60
+ if (browser) {
61
+ return browser.close();
62
+ }
63
+ };
64
+
65
+ function getBrowserOptions(options = {}) {
66
+ const browserOptions = {};
67
+ if (options.browserHeadless !== undefined) {
68
+ browserOptions.headless = options.browserHeadless && !options.browserDebug;
69
+ }
70
+ browserOptions.args = options.browserArgs ? JSON.parse(options.browserArgs) : [];
71
+ if (options.browserDisableWebSecurity === undefined || options.browserDisableWebSecurity) {
72
+ browserOptions.args.push("--disable-web-security");
73
+ }
74
+ browserOptions.args.push("--no-pings");
75
+ if (!options.browserStartMinimized && options.browserDebug) {
76
+ browserOptions.args.push("--auto-open-devtools-for-tabs");
77
+ }
78
+ if (options.browserWidth && options.browserHeight) {
79
+ browserOptions.args.push("--window-size=" + options.browserWidth + "," + options.browserHeight);
80
+ }
81
+ browserOptions.executablePath = options.browserExecutablePath || "chrome";
82
+ if (options.userAgent) {
83
+ browserOptions.args.push("--user-agent=" + options.userAgent);
84
+ }
85
+ return browserOptions;
86
+ }
87
+
88
+ async function setPageOptions(page, options) {
89
+ if (options.browserWidth && options.browserHeight) {
90
+ await page.setViewport({
91
+ width: options.browserWidth,
92
+ height: options.browserHeight
93
+ });
94
+ }
95
+ if (options.browserBypassCSP === undefined || options.browserBypassCSP) {
96
+ await page.setBypassCSP(true);
97
+ }
98
+ if (options.httpHeaders) {
99
+ page.setExtraHTTPHeaders(options.httpHeaders);
100
+ }
101
+ if (options.browserStartMinimized) {
102
+ const session = await page.target().createCDPSession();
103
+ const { windowId } = await session.send("Browser.getWindowForTarget");
104
+ await session.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "minimized" } });
105
+ }
106
+ if (options.browserCookies && options.browserCookies.length) {
107
+ await page.setCookie(...options.browserCookies);
108
+ }
109
+ if (options.emulateMediaFeatures) {
110
+ await page.emulateMediaFeatures(options.emulateMediaFeatures);
111
+ }
112
+ }
113
+
114
+ async function getPageData(browser, page, options) {
115
+ const injectedScript = await scripts.get(options);
116
+ await page.evaluateOnNewDocument(injectedScript);
117
+ if (options.browserDebug) {
118
+ await page.waitForTimeout(3000);
119
+ }
120
+ try {
121
+ await pageGoto(page, options);
122
+ } catch (error) {
123
+ if (options.browserWaitUntilFallback && error.name == "TimeoutError") {
124
+ const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
125
+ if (browserWaitUntil) {
126
+ options.browserWaitUntil = browserWaitUntil;
127
+ return getPageData(browser, page, options);
128
+ } else {
129
+ throw error;
130
+ }
131
+ } else if (error.name != "TimeoutError") {
132
+ throw error;
133
+ }
134
+ }
135
+ try {
136
+ if (options.browserWaitDelay) {
137
+ await page.waitForTimeout(options.browserWaitDelay);
138
+ }
139
+ return await page.evaluate(async options => {
140
+ const pageData = await singlefile.getPageData(options);
141
+ if (options.includeInfobar) {
142
+ await infobar.includeScript(pageData);
143
+ }
144
+ return pageData;
145
+ }, options);
146
+ } catch (error) {
147
+ if (error.message && error.message.includes(EXECUTION_CONTEXT_DESTROYED_ERROR)) {
148
+ const pageData = await handleJSRedirect(browser, options);
149
+ if (pageData) {
150
+ return pageData;
151
+ } else {
152
+ throw error;
153
+ }
154
+ } else {
155
+ throw error;
156
+ }
157
+ }
158
+ }
159
+
160
+ async function handleJSRedirect(browser, options) {
161
+ const pages = await browser.pages();
162
+ const page = pages[1] || pages[0];
163
+ try {
164
+ await pageGoto(page, options);
165
+ } catch (error) {
166
+ if (error.name != "TimeoutError") {
167
+ throw error;
168
+ }
169
+ }
170
+ const url = page.url();
171
+ if (url != options.url) {
172
+ options.url = url;
173
+ await browser.close();
174
+ return exports.getPageData(options);
175
+ }
176
+ }
177
+
178
+ async function pageGoto(page, options) {
179
+ const loadOptions = {
180
+ timeout: options.browserLoadMaxTime || 0,
181
+ waitUntil: options.browserWaitUntil || NETWORK_IDLE_STATE
182
+ };
183
+ if (options.content) {
184
+ await page.goto(options.url, { waitUntil: "domcontentloaded" });
185
+ await page.setContent(options.content, loadOptions);
186
+ } else {
187
+ await page.goto(options.url, loadOptions);
188
+ }
189
+ }
@@ -0,0 +1,168 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global require, exports, process, setTimeout, clearTimeout, Buffer */
25
+
26
+ const chrome = require("selenium-webdriver/chrome");
27
+ const { Builder } = require("selenium-webdriver");
28
+
29
+ exports.initialize = async () => { };
30
+
31
+ exports.getPageData = async options => {
32
+ let driver;
33
+ try {
34
+ const builder = new Builder();
35
+ builder.setChromeOptions(getBrowserOptions(options));
36
+ driver = builder.forBrowser("chrome").build();
37
+ return await getPageData(driver, options);
38
+ } finally {
39
+ if (driver) {
40
+ driver.quit();
41
+ }
42
+ }
43
+ };
44
+
45
+ exports.closeBrowser = () => { };
46
+
47
+ function getBrowserOptions(options) {
48
+ const chromeOptions = new chrome.Options();
49
+ const optionHeadless = (options.browserHeadless === undefined || options.browserHeadless) && !options.browserDebug;
50
+ if (optionHeadless) {
51
+ chromeOptions.headless();
52
+ }
53
+ if (options.browserExecutablePath) {
54
+ chromeOptions.setChromeBinaryPath(options.browserExecutablePath);
55
+ }
56
+ if (options.webDriverExecutablePath) {
57
+ process.env["PATH"] += ";" + options.webDriverExecutablePath.replace(/chromedriver(\.exe)?$/, "");
58
+ }
59
+ if (options.browserArgs) {
60
+ const args = JSON.parse(options.browserArgs);
61
+ args.forEach(argument => chromeOptions.addArguments(argument));
62
+ }
63
+ if (options.browserDisableWebSecurity === undefined || options.browserDisableWebSecurity) {
64
+ chromeOptions.addArguments("--disable-web-security");
65
+ }
66
+ chromeOptions.addArguments("--no-pings");
67
+ if (!optionHeadless) {
68
+ if (options.browserDebug) {
69
+ chromeOptions.addArguments("--auto-open-devtools-for-tabs");
70
+ }
71
+ const extensions = [];
72
+ if (options.browserBypassCSP === undefined || options.browserBypassCSP) {
73
+ extensions.push(encode(require.resolve("./extensions/signed/bypass_csp-0.0.3-an+fx.xpi")));
74
+ }
75
+ if (options.browserWaitUntil === undefined || options.browserWaitUntil == "networkidle0" || options.browserWaitUntil == "networkidle2") {
76
+ extensions.push(encode(require.resolve("./extensions/signed/network_idle-0.0.2-an+fx.xpi")));
77
+ }
78
+ chromeOptions.addExtensions(extensions);
79
+ }
80
+ if (options.userAgent) {
81
+ chromeOptions.addArguments("--user-agent=" + JSON.stringify(options.userAgent));
82
+ }
83
+ if (options.browserMobileEmulation) {
84
+ chromeOptions.setMobileEmulation({
85
+ deviceName: options.browserMobileEmulation
86
+ });
87
+ }
88
+ return chromeOptions;
89
+ }
90
+
91
+ async function getPageData(driver, options) {
92
+ const optionHeadless = (options.browserHeadless === undefined || options.browserHeadless) && !options.browserDebug;
93
+ driver.manage().setTimeouts({ script: options.browserLoadMaxTime, pageLoad: options.browserLoadMaxTime, implicit: options.browserLoadMaxTime });
94
+ if (options.browserWidth && options.browserHeight) {
95
+ const window = driver.manage().window();
96
+ if (window.setRect) {
97
+ window.setRect(options.browserHeight, options.browserWidth);
98
+ } else if (window.setSize) {
99
+ window.setSize(options.browserWidth, options.browserHeight);
100
+ }
101
+ }
102
+ const scripts = await require("./common/scripts.js").get(options);
103
+ if (options.browserDebug) {
104
+ // await driver.sleep(3000);
105
+ }
106
+ await driver.get(options.url);
107
+ if (options.browserCookies) {
108
+ await Promise.all(options.browserCookies.map(cookie => {
109
+ if (cookie.expires) {
110
+ cookie.expiry = cookie.expires;
111
+ delete cookie.expires;
112
+ }
113
+ return driver.manage().addCookie(cookie);
114
+ }));
115
+ await driver.get(options.url);
116
+ }
117
+ await driver.executeScript(scripts);
118
+ if (options.browserWaitUntil != "domcontentloaded") {
119
+ let scriptPromise;
120
+ if (!optionHeadless && (options.browserWaitUntil === undefined || options.browserWaitUntil == "networkidle0")) {
121
+ scriptPromise = driver.executeAsyncScript("addEventListener(\"single-file-network-idle-0\", () => arguments[0](), true)");
122
+ } else if (!optionHeadless && options.browserWaitUntil == "networkidle2") {
123
+ scriptPromise = driver.executeAsyncScript("addEventListener(\"single-file-network-idle-2\", () => arguments[0](), true)");
124
+ } else if (optionHeadless || options.browserWaitUntil == "load") {
125
+ scriptPromise = driver.executeAsyncScript("if (document.readyState == \"loading\" || document.readyState == \"interactive\") { addEventListener(\"load\", () => arguments[0]()) } else { arguments[0](); }");
126
+ }
127
+ let cancelTimeout;
128
+ const timeoutPromise = new Promise(resolve => {
129
+ const timeoutId = setTimeout(resolve, Math.max(0, options.browserLoadMaxTime - 5000));
130
+ cancelTimeout = () => {
131
+ clearTimeout(timeoutId);
132
+ resolve();
133
+ };
134
+ });
135
+ await Promise.race([scriptPromise, timeoutPromise]);
136
+ cancelTimeout();
137
+ }
138
+ if (options.browserWaitDelay) {
139
+ await driver.sleep(options.browserWaitDelay);
140
+ }
141
+ const result = await driver.executeAsyncScript(getPageDataScript(), options);
142
+ if (result.error) {
143
+ throw result.error;
144
+ } else {
145
+ return result.pageData;
146
+ }
147
+ }
148
+
149
+ function encode(file) {
150
+ return new Buffer.from(require("fs").readFileSync(file)).toString("base64");
151
+ }
152
+
153
+ function getPageDataScript() {
154
+ return `
155
+ const [options, callback] = arguments;
156
+ getPageData()
157
+ .then(pageData => callback({ pageData }))
158
+ .catch(error => callback({ error: error && error.toString() }));
159
+
160
+ async function getPageData() {
161
+ const pageData = await singlefile.getPageData(options);
162
+ if (options.includeInfobar) {
163
+ await infobar.includeScript(pageData);
164
+ }
165
+ return pageData;
166
+ }
167
+ `;
168
+ }
@@ -0,0 +1,181 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global require, exports, process, setTimeout, clearTimeout */
25
+
26
+ const firefox = require("selenium-webdriver/firefox");
27
+ const { Builder, By, Key } = require("selenium-webdriver");
28
+
29
+ exports.initialize = async () => { };
30
+
31
+ exports.getPageData = async options => {
32
+ let driver;
33
+ try {
34
+ const builder = new Builder().withCapabilities({ "pageLoadStrategy": "none" });
35
+ builder.setFirefoxOptions(getBrowserOptions(options));
36
+ driver = builder.forBrowser("firefox").build();
37
+ return await getPageData(driver, options);
38
+ } finally {
39
+ if (driver) {
40
+ driver.quit();
41
+ }
42
+ }
43
+ };
44
+
45
+ exports.closeBrowser = () => { };
46
+
47
+ function getBrowserOptions(options) {
48
+ const firefoxOptions = new firefox.Options().setBinary(firefox.Channel.NIGHTLY);
49
+ if ((options.browserHeadless === undefined || options.browserHeadless) && !options.browserDebug) {
50
+ process.env["MOZ_HEADLESS"] = "1";
51
+ }
52
+ if (options.browserExecutablePath) {
53
+ firefoxOptions.setBinary(options.browserExecutablePath);
54
+ }
55
+ if (options.webDriverExecutablePath) {
56
+ process.env["PATH"] += ";" + options.webDriverExecutablePath.replace(/geckodriver(\.exe)?$/, "");
57
+ }
58
+ const extensions = [];
59
+ if (options.browserDisableWebSecurity === undefined || options.browserDisableWebSecurity) {
60
+ extensions.push(require.resolve("./extensions/signed/disable_web_security-0.0.3-an+fx.xpi"));
61
+ }
62
+ if (options.browserBypassCSP === undefined || options.browserBypassCSP) {
63
+ extensions.push(require.resolve("./extensions/signed/bypass_csp-0.0.3-an+fx.xpi"));
64
+ }
65
+ if (options.browserWaitUntil === undefined || options.browserWaitUntil == "networkidle0" || options.browserWaitUntil == "networkidle2") {
66
+ extensions.push(require.resolve("./extensions/signed/network_idle-0.0.2-an+fx.xpi"));
67
+ }
68
+ if (extensions.length) {
69
+ firefoxOptions.addExtensions(extensions);
70
+ }
71
+ if (options.browserArgs) {
72
+ const args = JSON.parse(options.browserArgs);
73
+ args.forEach(argument => firefoxOptions.addArguments(argument));
74
+ }
75
+ if (options.userAgent) {
76
+ firefoxOptions.setPreference("general.useragent.override", options.userAgent);
77
+ }
78
+ }
79
+
80
+ async function getPageData(driver, options) {
81
+ driver.manage().setTimeouts({ script: options.browserLoadMaxTime || 0, pageLoad: options.browserLoadMaxTime || 0, implicit: options.browserLoadMaxTime || 0 });
82
+ if (options.browserWidth && options.browserHeight) {
83
+ const window = driver.manage().window();
84
+ if (window.setRect) {
85
+ window.setRect(options.browserHeight, options.browserWidth);
86
+ } else if (window.setSize) {
87
+ window.setSize(options.browserWidth, options.browserHeight);
88
+ }
89
+ }
90
+ let scripts = await require("./common/scripts.js").get(options);
91
+ if (options.browserDebug) {
92
+ await driver.findElement(By.css("html")).sendKeys(Key.SHIFT + Key.F5);
93
+ await driver.sleep(3000);
94
+ }
95
+ scripts = scripts.replace(/globalThis/g, "window");
96
+ await driver.get(options.url);
97
+ if (options.browserCookies) {
98
+ await Promise.all(options.browserCookies.map(cookie => {
99
+ if (cookie.expires) {
100
+ cookie.expiry = cookie.expires;
101
+ }
102
+ return driver.manage().addCookie(cookie);
103
+ }));
104
+ await driver.get("about:blank");
105
+ await driver.get(options.url);
106
+ while (await driver.getCurrentUrl() == "about:blank") {
107
+ // do nothing
108
+ }
109
+ }
110
+ await driver.executeScript(scripts);
111
+ if (options.browserWaitUntil != "domcontentloaded") {
112
+ let scriptPromise;
113
+ /*
114
+ if (options.browserWaitUntil == "networkidle0") {
115
+ scriptPromise = driver.executeAsyncScript("addEventListener(\"single-file-network-idle-0\", () => arguments[0](), true)");
116
+ } else if (options.browserWaitUntil == "networkidle2") {
117
+ scriptPromise = driver.executeAsyncScript("addEventListener(\"single-file-network-idle-2\", () => arguments[0](), true)");
118
+ } else if (options.browserWaitUntil === undefined || options.browserWaitUntil == "load") {
119
+ */
120
+ scriptPromise = driver.executeAsyncScript("if (document.readyState == \"loading\" || document.readyState == \"interactive\") { addEventListener(\"load\", () => arguments[0]()) } else { arguments[0](); }");
121
+ /*
122
+ }
123
+ */
124
+ let cancelTimeout;
125
+ const timeoutPromise = new Promise(resolve => {
126
+ const timeoutId = setTimeout(resolve, Math.max(0, options.browserLoadMaxTime - 5000));
127
+ cancelTimeout = () => {
128
+ clearTimeout(timeoutId);
129
+ resolve();
130
+ };
131
+ });
132
+ await Promise.race([scriptPromise, timeoutPromise]);
133
+ cancelTimeout();
134
+ }
135
+ if (!options.removeFrames) {
136
+ await executeScriptInFrames(driver, scripts);
137
+ }
138
+ if (options.browserWaitDelay) {
139
+ await driver.sleep(options.browserWaitDelay);
140
+ }
141
+ const result = await driver.executeAsyncScript(getPageDataScript(), options);
142
+ if (result.error) {
143
+ throw result.error;
144
+ } else {
145
+ return result.pageData;
146
+ }
147
+ }
148
+
149
+ async function executeScriptInFrames(driver, scripts) {
150
+ let finished = false, indexFrame = 0;
151
+ while (!finished) {
152
+ try {
153
+ await driver.switchTo().frame(indexFrame);
154
+ } catch (error) {
155
+ finished = true;
156
+ }
157
+ if (!finished) {
158
+ await driver.executeScript(scripts);
159
+ await executeScriptInFrames(driver, scripts);
160
+ indexFrame++;
161
+ await driver.switchTo().parentFrame();
162
+ }
163
+ }
164
+ }
165
+
166
+ function getPageDataScript() {
167
+ return `
168
+ let [options, callback] = arguments;
169
+ getPageData()
170
+ .then(pageData => callback({ pageData }))
171
+ .catch(error => callback({ error: error && error.toString() }));
172
+
173
+ async function getPageData() {
174
+ const pageData = await window.singlefile.getPageData(options);
175
+ if (options.includeInfobar) {
176
+ await infobar.includeScript(pageData);
177
+ }
178
+ return pageData;
179
+ }
180
+ `;
181
+ }