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.
- package/.eslintrc.js +43 -0
- package/Dockerfile +12 -0
- package/README.MD +163 -0
- package/args.js +305 -0
- package/back-ends/common/scripts.js +66 -0
- package/back-ends/extensions/bypass-csp/index.js +36 -0
- package/back-ends/extensions/bypass-csp/manifest.json +20 -0
- package/back-ends/extensions/disable-web-security/index.js +54 -0
- package/back-ends/extensions/disable-web-security/manifest.json +20 -0
- package/back-ends/extensions/network-idle/bg.js +61 -0
- package/back-ends/extensions/network-idle/content.js +29 -0
- package/back-ends/extensions/network-idle/manifest.json +31 -0
- package/back-ends/extensions/signed/bypass_csp-0.0.3-an+fx.xpi +0 -0
- package/back-ends/extensions/signed/disable_web_security-0.0.3-an+fx.xpi +0 -0
- package/back-ends/extensions/signed/network_idle-0.0.2-an+fx.xpi +0 -0
- package/back-ends/jsdom.js +175 -0
- package/back-ends/playwright-chromium.js +113 -0
- package/back-ends/playwright-firefox.js +113 -0
- package/back-ends/puppeteer-firefox.js +170 -0
- package/back-ends/puppeteer.js +189 -0
- package/back-ends/webdriver-chromium.js +168 -0
- package/back-ends/webdriver-gecko.js +181 -0
- package/build-lib.sh +3 -0
- package/lib/single-file-bootstrap.js +1 -0
- package/lib/single-file-frames.js +1 -0
- package/lib/single-file-hooks-frames.js +1 -0
- package/lib/single-file-hooks.js +1 -0
- package/lib/single-file-infobar.js +1 -0
- package/lib/single-file.js +1 -0
- package/package.json +37 -0
- package/rollup.config.dev.js +64 -0
- package/rollup.config.js +64 -0
- package/single-file +81 -0
- package/single-file-cli-api.js +324 -0
- 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
|
+
}
|