download.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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, document, URL, Blob, MouseEvent, setTimeout, open, navigator, File, setInterval, clearInterval */
  24. import * as yabson from "./../../lib/yabson/yabson.js";
  25. import * as ui from "./../../ui/content/content-ui.js";
  26. import { getSharePageBar, setLabels } from "./../../ui/common/common-content-ui.js";
  27. const MAX_CONTENT_SIZE = 16 * (1024 * 1024);
  28. let EMBEDDED_IMAGE_BUTTON_MESSAGE, SHARE_PAGE_BUTTON_MESSAGE, SHARE_SELECTION_BUTTON_MESSAGE, ERROR_TITLE_MESSAGE;
  29. try {
  30. EMBEDDED_IMAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelEmbeddedImageButton");
  31. SHARE_PAGE_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelSharePageButton");
  32. SHARE_SELECTION_BUTTON_MESSAGE = browser.i18n.getMessage("topPanelShareSelectionButton");
  33. ERROR_TITLE_MESSAGE = browser.i18n.getMessage("topPanelError");
  34. } catch (error) {
  35. // ignored
  36. }
  37. let sharePageBar;
  38. setLabels({
  39. EMBEDDED_IMAGE_BUTTON_MESSAGE,
  40. SHARE_PAGE_BUTTON_MESSAGE,
  41. SHARE_SELECTION_BUTTON_MESSAGE,
  42. ERROR_TITLE_MESSAGE
  43. });
  44. export {
  45. downloadPage,
  46. downloadPageForeground
  47. };
  48. async function downloadPage(pageData, options) {
  49. if (options.includeBOM) {
  50. pageData.content = "\ufeff" + pageData.content;
  51. }
  52. const embeddedImage = options.embeddedImage;
  53. const message = {
  54. method: "downloads.download",
  55. taskId: options.taskId,
  56. insertTextBody: options.insertTextBody,
  57. confirmFilename: options.confirmFilename,
  58. filenameConflictAction: options.filenameConflictAction,
  59. filename: pageData.filename,
  60. mimeType: pageData.mimeType,
  61. saveToClipboard: options.saveToClipboard,
  62. saveToGDrive: options.saveToGDrive,
  63. saveToDropbox: options.saveToDropbox,
  64. saveWithWebDAV: options.saveWithWebDAV,
  65. webDAVURL: options.webDAVURL,
  66. webDAVUser: options.webDAVUser,
  67. webDAVPassword: options.webDAVPassword,
  68. saveToGitHub: options.saveToGitHub,
  69. githubToken: options.githubToken,
  70. githubUser: options.githubUser,
  71. githubRepository: options.githubRepository,
  72. githubBranch: options.githubBranch,
  73. saveWithCompanion: options.saveWithCompanion,
  74. forceWebAuthFlow: options.forceWebAuthFlow,
  75. filenameReplacementCharacter: options.filenameReplacementCharacter,
  76. openEditor: options.openEditor,
  77. openSavedPage: options.openSavedPage,
  78. compressHTML: options.compressHTML,
  79. backgroundSave: options.backgroundSave,
  80. bookmarkId: options.bookmarkId,
  81. replaceBookmarkURL: options.replaceBookmarkURL,
  82. applySystemTheme: options.applySystemTheme,
  83. defaultEditorMode: options.defaultEditorMode,
  84. includeInfobar: options.includeInfobar,
  85. openInfobar: options.openInfobar,
  86. warnUnsavedPage: options.warnUnsavedPage,
  87. createRootDirectory: options.createRootDirectory,
  88. selfExtractingArchive: options.selfExtractingArchive,
  89. embeddedImage: embeddedImage ? Array.from(embeddedImage) : null,
  90. preventAppendedData: options.preventAppendedData,
  91. extractDataFromPage: options.extractDataFromPage,
  92. insertCanonicalLink: options.insertCanonicalLink,
  93. insertMetaNoIndex: options.insertMetaNoIndex,
  94. insertMetaCSP: options.insertMetaCSP,
  95. password: options.password,
  96. compressContent: options.compressContent,
  97. foregroundSave: options.foregroundSave,
  98. sharePage: options.sharePage,
  99. saveToRestFormApi: options.saveToRestFormApi,
  100. saveToRestFormApiUrl: options.saveToRestFormApiUrl,
  101. saveToRestFormApiFileFieldName: options.saveToRestFormApiFileFieldName,
  102. saveToRestFormApiUrlFieldName: options.saveToRestFormApiUrlFieldName,
  103. saveToRestFormApiToken: options.saveToRestFormApiToken,
  104. saveToS3: options.saveToS3,
  105. S3Domain: options.S3Domain,
  106. S3Region: options.S3Region,
  107. S3Bucket: options.S3Bucket,
  108. S3AccessKey: options.S3AccessKey,
  109. S3SecretKey: options.S3SecretKey
  110. };
  111. const pingInterval = setInterval(() => {
  112. browser.runtime.sendMessage({ method: "ping" }).then(() => { });
  113. }, 15000);
  114. if (options.compressContent) {
  115. if ((!options.backgroundSave || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi || options.saveToS3) && options.confirmFilename && !options.openEditor) {
  116. pageData.filename = ui.prompt("Save as", pageData.filename);
  117. }
  118. if (pageData.filename) {
  119. const blob = new Blob([await yabson.serialize(pageData)], { type: pageData.mimeType });
  120. const blobURL = URL.createObjectURL(blob);
  121. message.filename = pageData.filename;
  122. message.blobURL = blobURL;
  123. const result = await browser.runtime.sendMessage(message);
  124. URL.revokeObjectURL(blobURL);
  125. if (result.error) {
  126. message.embeddedImage = embeddedImage;
  127. message.blobURL = null;
  128. message.pageData = pageData;
  129. const serializer = yabson.getSerializer(message);
  130. for await (const chunk of serializer) {
  131. await browser.runtime.sendMessage({
  132. method: "downloads.download",
  133. compressContent: true,
  134. data: Array.from(chunk)
  135. });
  136. }
  137. await browser.runtime.sendMessage({
  138. method: "downloads.download",
  139. compressContent: true,
  140. mimeType: pageData.mimeType
  141. });
  142. }
  143. if (options.backgroundSave) {
  144. await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId });
  145. }
  146. } else {
  147. browser.runtime.sendMessage({ method: "downloads.cancel" });
  148. browser.runtime.sendMessage({ method: "ui.processCancelled" });
  149. }
  150. } else {
  151. if ((options.backgroundSave && !options.sharePage) || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi || options.saveToS3) {
  152. let filename = pageData.filename;
  153. if ((options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV || options.saveToDropbox || options.saveToRestFormApi || options.saveToS3) && options.confirmFilename && !options.openEditor) {
  154. filename = ui.prompt("Save as", pageData.filename);
  155. }
  156. if (filename) {
  157. message.filename = pageData.filename = filename;
  158. const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: pageData.mimeType }));
  159. message.blobURL = blobURL;
  160. const result = await browser.runtime.sendMessage(message);
  161. URL.revokeObjectURL(blobURL);
  162. if (result.error) {
  163. message.blobURL = null;
  164. for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < pageData.content.length; blockIndex++) {
  165. message.truncated = pageData.content.length > MAX_CONTENT_SIZE;
  166. if (message.truncated) {
  167. message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > pageData.content.length;
  168. message.content = pageData.content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
  169. } else {
  170. message.content = pageData.content;
  171. }
  172. await browser.runtime.sendMessage(message);
  173. }
  174. }
  175. } else {
  176. browser.runtime.sendMessage({ method: "downloads.cancel" });
  177. browser.runtime.sendMessage({ method: "ui.processCancelled" });
  178. }
  179. } else {
  180. if (options.saveToClipboard) {
  181. saveToClipboard(pageData);
  182. } else {
  183. await downloadPageForeground(pageData, options);
  184. }
  185. if (options.openSavedPage) {
  186. open(URL.createObjectURL(new Blob([pageData.content], { type: pageData.mimeType })));
  187. }
  188. browser.runtime.sendMessage({ method: "ui.processEnd" });
  189. }
  190. await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId, hash: pageData.hash, woleetKey: options.woleetKey });
  191. }
  192. clearInterval(pingInterval);
  193. }
  194. async function downloadPageForeground(pageData, options) {
  195. if (Array.isArray(pageData.content)) {
  196. pageData.content = new Uint8Array(pageData.content);
  197. }
  198. if (options.sharePage && navigator.share) {
  199. await sharePage(pageData, options);
  200. } else {
  201. let filename = pageData.filename;
  202. if (options.confirmFilename) {
  203. filename = ui.prompt("Save as", pageData.filename);
  204. if (filename) {
  205. pageData.filename = filename;
  206. } else {
  207. browser.runtime.sendMessage({ method: "downloads.cancel" });
  208. browser.runtime.sendMessage({ method: "ui.processCancelled" });
  209. }
  210. }
  211. if (filename) {
  212. const link = document.createElement("a");
  213. link.download = pageData.filename;
  214. link.href = URL.createObjectURL(new Blob([pageData.content], { type: pageData.mimeType }));
  215. link.dispatchEvent(new MouseEvent("click"));
  216. return new Promise(resolve => setTimeout(() => { URL.revokeObjectURL(link.href); resolve(); }, 1000));
  217. }
  218. }
  219. }
  220. async function sharePage(pageData, options) {
  221. sharePageBar = getSharePageBar();
  222. const cancelled = await sharePageBar.display(options.selected);
  223. if (!cancelled) {
  224. const data = { files: [new File([pageData.content], pageData.filename, { type: pageData.mimeType })] };
  225. try {
  226. await navigator.share(data);
  227. sharePageBar.hide();
  228. } catch (error) {
  229. sharePageBar.hide();
  230. if (error.name === "AbortError") {
  231. await sharePage(pageData, options);
  232. } else {
  233. throw error;
  234. }
  235. }
  236. }
  237. }
  238. function saveToClipboard(pageData) {
  239. const command = "copy";
  240. document.addEventListener(command, listener);
  241. document.execCommand(command);
  242. document.removeEventListener(command, listener);
  243. function listener(event) {
  244. event.clipboardData.setData(pageData.mimeType, pageData.content);
  245. event.clipboardData.setData("text/plain", pageData.content);
  246. event.preventDefault();
  247. }
  248. }