downloads.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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. /* global browser, Blob, URL, document, fetch, btoa, AbortController */
  24. import * as config from "./config.js";
  25. import * as bookmarks from "./bookmarks.js";
  26. import * as companion from "./companion.js";
  27. import * as business from "./business.js";
  28. import * as editor from "./editor.js";
  29. import { launchWebAuthFlow, extractAuthCode } from "./tabs-util.js";
  30. import * as ui from "./../../ui/bg/index.js";
  31. import * as woleet from "./../../lib/woleet/woleet.js";
  32. import { GDrive } from "./../../lib/gdrive/gdrive.js";
  33. import { pushGitHub } from "./../../lib/github/github.js";
  34. import { download } from "./download-util.js";
  35. const partialContents = new Map();
  36. const MIMETYPE_HTML = "text/html";
  37. const GDRIVE_CLIENT_ID = "207618107333-h1220p1oasj3050kr5r416661adm091a.apps.googleusercontent.com"; // 3pj2pmelhnl4sf3rpctghs9cean3q8nj
  38. const GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt"; // "000000000000000000000000";
  39. const SCOPES = ["https://www.googleapis.com/auth/drive.file"];
  40. const CONFLICT_ACTION_SKIP = "skip";
  41. const CONFLICT_ACTION_UNIQUIFY = "uniquify";
  42. const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
  43. const manifest = browser.runtime.getManifest();
  44. const requestPermissionIdentity = manifest.optional_permissions && manifest.optional_permissions.includes("identity");
  45. const gDrive = new GDrive(GDRIVE_CLIENT_ID, GDRIVE_CLIENT_KEY, SCOPES);
  46. export {
  47. onMessage,
  48. downloadPage,
  49. saveToGDrive,
  50. saveToGitHub,
  51. saveWithWebDAV
  52. };
  53. async function onMessage(message, sender) {
  54. if (message.method.endsWith(".download")) {
  55. return downloadTabPage(message, sender.tab);
  56. }
  57. if (message.method.endsWith(".disableGDrive")) {
  58. const authInfo = await config.getAuthInfo();
  59. config.removeAuthInfo();
  60. await gDrive.revokeAuthToken(authInfo && (authInfo.accessToken || authInfo.revokableAccessToken));
  61. return {};
  62. }
  63. if (message.method.endsWith(".end")) {
  64. if (message.hash) {
  65. try {
  66. await woleet.anchor(message.hash, message.woleetKey);
  67. } catch (error) {
  68. ui.onError(sender.tab.id, error.message, error.link);
  69. }
  70. }
  71. business.onSaveEnd(message.taskId);
  72. return {};
  73. }
  74. if (message.method.endsWith(".getInfo")) {
  75. return business.getTasksInfo();
  76. }
  77. if (message.method.endsWith(".cancel")) {
  78. business.cancelTask(message.taskId);
  79. return {};
  80. }
  81. if (message.method.endsWith(".cancelAll")) {
  82. business.cancelAllTasks();
  83. return {};
  84. }
  85. if (message.method.endsWith(".saveUrls")) {
  86. business.saveUrls(message.urls);
  87. return {};
  88. }
  89. }
  90. async function downloadTabPage(message, tab) {
  91. let contents;
  92. if (message.truncated) {
  93. contents = partialContents.get(tab.id);
  94. if (!contents) {
  95. contents = [];
  96. partialContents.set(tab.id, contents);
  97. }
  98. contents.push(message.content);
  99. if (message.finished) {
  100. partialContents.delete(tab.id);
  101. }
  102. } else if (message.content) {
  103. contents = [message.content];
  104. }
  105. if (!message.truncated || message.finished) {
  106. if (message.openEditor) {
  107. ui.onEdit(tab.id);
  108. await editor.open({ tabIndex: tab.index + 1, filename: message.filename, content: contents.join("") });
  109. } else {
  110. if (message.saveToClipboard) {
  111. message.content = contents.join("");
  112. saveToClipboard(message);
  113. ui.onEnd(tab.id);
  114. } else {
  115. await downloadContent(contents, tab, tab.incognito, message);
  116. }
  117. }
  118. }
  119. return {};
  120. }
  121. async function downloadContent(contents, tab, incognito, message) {
  122. try {
  123. if (message.saveWithWebDAV) {
  124. await saveWithWebDAV(message.taskId, message.filename, contents.join(""), message.webDAVURL, message.webDAVUser, message.webDAVPassword);
  125. } else if (message.saveToGDrive) {
  126. await (await saveToGDrive(message.taskId, message.filename, new Blob(contents, { type: MIMETYPE_HTML }), {
  127. forceWebAuthFlow: message.forceWebAuthFlow
  128. }, {
  129. onProgress: (offset, size) => ui.onUploadProgress(tab.id, offset, size)
  130. })).uploadPromise;
  131. } else if (message.saveToGitHub) {
  132. await (await saveToGitHub(message.taskId, message.filename, contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch)).pushPromise;
  133. } else if (message.saveWithCompanion) {
  134. await companion.save({
  135. filename: message.filename,
  136. content: message.content,
  137. filenameConflictAction: message.filenameConflictAction
  138. });
  139. } else {
  140. message.url = URL.createObjectURL(new Blob(contents, { type: MIMETYPE_HTML }));
  141. await downloadPage(message, {
  142. confirmFilename: message.confirmFilename,
  143. incognito,
  144. filenameConflictAction: message.filenameConflictAction,
  145. filenameReplacementCharacter: message.filenameReplacementCharacter,
  146. includeInfobar: message.includeInfobar
  147. });
  148. }
  149. ui.onEnd(tab.id);
  150. if (message.openSavedPage) {
  151. const createTabProperties = { active: true, url: URL.createObjectURL(new Blob(contents, { type: MIMETYPE_HTML })) };
  152. if (tab.index != null) {
  153. createTabProperties.index = tab.index + 1;
  154. }
  155. browser.tabs.create(createTabProperties);
  156. }
  157. } catch (error) {
  158. if (!error.message || error.message != "upload_cancelled") {
  159. console.error(error); // eslint-disable-line no-console
  160. ui.onError(tab.id, error.message, error.link);
  161. }
  162. } finally {
  163. if (message.url) {
  164. URL.revokeObjectURL(message.url);
  165. }
  166. }
  167. }
  168. function getRegExp(string) {
  169. return string.replace(REGEXP_ESCAPE, "\\$1");
  170. }
  171. async function getAuthInfo(authOptions, force) {
  172. let authInfo = await config.getAuthInfo();
  173. const options = {
  174. interactive: true,
  175. forceWebAuthFlow: authOptions.forceWebAuthFlow,
  176. requestPermissionIdentity,
  177. launchWebAuthFlow: options => launchWebAuthFlow(options),
  178. extractAuthCode: authURL => extractAuthCode(authURL)
  179. };
  180. gDrive.setAuthInfo(authInfo, options);
  181. if (!authInfo || !authInfo.accessToken || force) {
  182. authInfo = await gDrive.auth(options);
  183. if (authInfo) {
  184. await config.setAuthInfo(authInfo);
  185. } else {
  186. await config.removeAuthInfo();
  187. }
  188. }
  189. return authInfo;
  190. }
  191. async function saveToGitHub(taskId, filename, content, githubToken, githubUser, githubRepository, githubBranch) {
  192. const taskInfo = business.getTaskInfo(taskId);
  193. if (!taskInfo || !taskInfo.cancelled) {
  194. const pushInfo = pushGitHub(githubToken, githubUser, githubRepository, githubBranch, filename, content);
  195. business.setCancelCallback(taskId, pushInfo.cancelPush);
  196. try {
  197. await (await pushInfo).pushPromise;
  198. return pushInfo;
  199. } catch (error) {
  200. throw new Error(error.message + " (GitHub)");
  201. }
  202. }
  203. }
  204. async function saveWithWebDAV(taskId, filename, content, url, username, password) {
  205. const taskInfo = business.getTaskInfo(taskId);
  206. const controller = new AbortController();
  207. const { signal } = controller;
  208. const authorization = "Basic " + btoa(username + ":" + password);
  209. if (!url.endsWith("/")) {
  210. url += "/";
  211. }
  212. if (!taskInfo || !taskInfo.cancelled) {
  213. business.setCancelCallback(taskId, () => controller.abort());
  214. try {
  215. const response = await sendRequest(url + filename, "PUT", content);
  216. if (response.status == 404 && filename.includes("/")) {
  217. const filenameParts = filename.split(/\/+/);
  218. filenameParts.pop();
  219. let path = "";
  220. for (const filenamePart of filenameParts) {
  221. if (filenamePart) {
  222. path += filenamePart;
  223. const response = await sendRequest(url + path, "MKCOL");
  224. if (response.status >= 400) {
  225. throw new Error("Error " + response.status + " (WebDAV)");
  226. }
  227. path += "/";
  228. }
  229. }
  230. return saveWithWebDAV(taskId, filename, content, url, username, password);
  231. } else if (response.status >= 400) {
  232. throw new Error("Error " + response.status + " (WebDAV)");
  233. } else {
  234. return response;
  235. }
  236. } catch (error) {
  237. if (error.name != "AbortError") {
  238. throw new Error(error.message + " (WebDAV)");
  239. }
  240. }
  241. }
  242. function sendRequest(url, method, body) {
  243. const headers = {
  244. "Authorization": authorization
  245. };
  246. if (body) {
  247. headers["Content-Type"] = "text/html";
  248. }
  249. return fetch(url, { method, headers, signal, body, credentials: "omit" });
  250. }
  251. }
  252. async function saveToGDrive(taskId, filename, blob, authOptions, uploadOptions) {
  253. try {
  254. await getAuthInfo(authOptions);
  255. const taskInfo = business.getTaskInfo(taskId);
  256. if (!taskInfo || !taskInfo.cancelled) {
  257. const uploadInfo = await gDrive.upload(filename, blob, uploadOptions);
  258. business.setCancelCallback(taskId, uploadInfo.cancelUpload);
  259. return uploadInfo;
  260. }
  261. }
  262. catch (error) {
  263. if (error.message == "invalid_token") {
  264. let authInfo;
  265. try {
  266. authInfo = await gDrive.refreshAuthToken();
  267. } catch (error) {
  268. if (error.message == "unknown_token") {
  269. authInfo = await getAuthInfo(authOptions, true);
  270. } else {
  271. throw new Error(error.message + " (Google Drive)");
  272. }
  273. }
  274. if (authInfo) {
  275. await config.setAuthInfo(authInfo);
  276. } else {
  277. await config.removeAuthInfo();
  278. }
  279. return await saveToGDrive(taskId, filename, blob, authOptions, uploadOptions);
  280. } else {
  281. throw new Error(error.message + " (Google Drive)");
  282. }
  283. }
  284. }
  285. async function downloadPage(pageData, options) {
  286. const filenameConflictAction = options.filenameConflictAction;
  287. let skipped;
  288. if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
  289. const downloadItems = await browser.downloads.search({
  290. filenameRegex: "(\\\\|/)" + getRegExp(pageData.filename) + "$",
  291. exists: true
  292. });
  293. if (downloadItems.length) {
  294. skipped = true;
  295. } else {
  296. options.filenameConflictAction = CONFLICT_ACTION_UNIQUIFY;
  297. }
  298. }
  299. if (!skipped) {
  300. const downloadInfo = {
  301. url: pageData.url,
  302. saveAs: options.confirmFilename,
  303. filename: pageData.filename,
  304. conflictAction: options.filenameConflictAction
  305. };
  306. if (options.incognito) {
  307. downloadInfo.incognito = true;
  308. }
  309. const downloadData = await download(downloadInfo, options.filenameReplacementCharacter);
  310. if (downloadData.filename && pageData.bookmarkId && pageData.replaceBookmarkURL) {
  311. if (!downloadData.filename.startsWith("file:")) {
  312. if (downloadData.filename.startsWith("/")) {
  313. downloadData.filename = downloadData.filename.substring(1);
  314. }
  315. downloadData.filename = "file:///" + downloadData.filename.replace(/#/g, "%23");
  316. }
  317. await bookmarks.update(pageData.bookmarkId, { url: downloadData.filename });
  318. }
  319. }
  320. }
  321. function saveToClipboard(pageData) {
  322. const command = "copy";
  323. document.addEventListener(command, listener);
  324. document.execCommand(command);
  325. document.removeEventListener(command, listener);
  326. function listener(event) {
  327. event.clipboardData.setData(MIMETYPE_HTML, pageData.content);
  328. event.clipboardData.setData("text/plain", pageData.content);
  329. event.preventDefault();
  330. }
  331. }