autosave.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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, infobar, URL, Blob, XMLHttpRequest */
  24. import * as config from "./config.js";
  25. import * as business from "./business.js";
  26. import * as companion from "./companion.js";
  27. import * as downloads from "./downloads.js";
  28. import * as tabsData from "./tabs-data.js";
  29. import * as ui from "./../../ui/bg/index.js";
  30. import { getPageData } from "./../../index.js";
  31. import * as woleet from "./../../lib/woleet/woleet.js";
  32. import { autoSaveIsEnabled } from "./autosave-util.js";
  33. const pendingMessages = {};
  34. const replacedTabIds = {};
  35. export {
  36. onMessage,
  37. onMessageExternal,
  38. onInit,
  39. onTabUpdated,
  40. onTabRemoved,
  41. onTabDiscarded,
  42. onTabReplaced
  43. };
  44. async function onMessage(message, sender) {
  45. if (message.method.endsWith(".init")) {
  46. const [options, autoSaveEnabled] = await Promise.all([config.getOptions(sender.tab.url, true), autoSaveIsEnabled(sender.tab)]);
  47. return { options, autoSaveEnabled, tabId: sender.tab.id, tabIndex: sender.tab.index };
  48. }
  49. if (message.method.endsWith(".save")) {
  50. if (message.autoSaveDiscard || message.autoSaveRemove) {
  51. if (sender.tab) {
  52. message.tab = sender.tab;
  53. pendingMessages[sender.tab.id] = message;
  54. } else if (pendingMessages[message.tabId] &&
  55. ((pendingMessages[message.tabId].removed && message.autoSaveRemove) ||
  56. (pendingMessages[message.tabId].discarded && message.autoSaveDiscard))
  57. ) {
  58. delete pendingMessages[message.tabId];
  59. await saveContent(message, { id: message.tabId, index: message.tabIndex, url: sender.url });
  60. }
  61. if (message.autoSaveUnload) {
  62. delete pendingMessages[message.tabId];
  63. await saveContent(message, sender.tab);
  64. }
  65. } else {
  66. delete pendingMessages[message.tabId];
  67. await saveContent(message, sender.tab);
  68. }
  69. return {};
  70. }
  71. }
  72. function onTabUpdated(tabId) {
  73. delete pendingMessages[tabId];
  74. }
  75. async function onTabRemoved(tabId) {
  76. const message = pendingMessages[tabId];
  77. if (message) {
  78. if (message.autoSaveRemove) {
  79. delete pendingMessages[tabId];
  80. await saveContent(message, message.tab);
  81. }
  82. } else {
  83. pendingMessages[tabId] = { removed: true };
  84. }
  85. }
  86. async function onTabDiscarded(tabId) {
  87. const message = pendingMessages[tabId];
  88. if (message) {
  89. delete pendingMessages[tabId];
  90. await saveContent(message, message.tab);
  91. } else {
  92. pendingMessages[tabId] = { discarded: true };
  93. }
  94. }
  95. async function onTabReplaced(addedTabId, removedTabId) {
  96. if (pendingMessages[removedTabId] && !pendingMessages[addedTabId]) {
  97. pendingMessages[addedTabId] = pendingMessages[removedTabId];
  98. delete pendingMessages[removedTabId];
  99. replacedTabIds[removedTabId] = addedTabId;
  100. }
  101. const allTabsData = await tabsData.get();
  102. if (allTabsData[removedTabId] && !allTabsData[addedTabId]) {
  103. allTabsData[addedTabId] = allTabsData[removedTabId];
  104. delete allTabsData[removedTabId];
  105. await tabsData.set(allTabsData);
  106. }
  107. }
  108. async function onMessageExternal(message, currentTab) {
  109. if (message.method == "enableAutoSave") {
  110. const allTabsData = await tabsData.get(currentTab.id);
  111. allTabsData[currentTab.id].autoSave = message.enabled;
  112. await tabsData.set(allTabsData);
  113. ui.refreshTab(currentTab);
  114. }
  115. if (message.method == "isAutoSaveEnabled") {
  116. return autoSaveIsEnabled(currentTab);
  117. }
  118. }
  119. async function onInit(tab) {
  120. const [options, autoSaveEnabled] = await Promise.all([config.getOptions(tab.url, true), autoSaveIsEnabled(tab)]);
  121. if (options && ((options.autoSaveLoad || options.autoSaveLoadOrUnload) && autoSaveEnabled)) {
  122. business.saveTabs([tab], { autoSave: true });
  123. }
  124. }
  125. async function saveContent(message, tab) {
  126. const tabId = tab.id;
  127. const options = await config.getOptions(tab.url, true);
  128. if (options) {
  129. ui.onStart(tabId, 1, true);
  130. options.content = message.content;
  131. options.url = message.url;
  132. options.frames = message.frames;
  133. options.canvases = message.canvases;
  134. options.fonts = message.fonts;
  135. options.stylesheets = message.stylesheets;
  136. options.images = message.images;
  137. options.posters = message.posters;
  138. options.usedFonts = message.usedFonts;
  139. options.shadowRoots = message.shadowRoots;
  140. options.imports = message.imports;
  141. options.referrer = message.referrer;
  142. options.updatedResources = message.updatedResources;
  143. options.visitDate = new Date(message.visitDate);
  144. options.backgroundTab = true;
  145. options.autoSave = true;
  146. options.incognito = tab.incognito;
  147. options.tabId = tabId;
  148. options.tabIndex = tab.index;
  149. let pageData;
  150. try {
  151. if (options.autoSaveExternalSave) {
  152. await companion.externalSave(options);
  153. } else {
  154. pageData = await getPageData(options, null, null, { fetch });
  155. if (options.includeInfobar) {
  156. await infobar.includeScript(pageData);
  157. }
  158. if (options.saveToGDrive) {
  159. const blob = new Blob([pageData.content], { type: "text/html" });
  160. await (await downloads.saveToGDrive(message.taskId, pageData.filename, blob, options, {})).uploadPromise;
  161. } if (options.saveToGitHub) {
  162. await (await downloads.saveToGitHub(message.taskId, pageData.filename, pageData.content, options.githubToken, options.githubUser, options.githubRepository, options.githubBranch)).pushPromise;
  163. } else if (options.saveWithCompanion) {
  164. await companion.save({
  165. filename: pageData.filename,
  166. content: pageData.content,
  167. filenameConflictAction: pageData.filenameConflictAction
  168. });
  169. } else {
  170. const blob = new Blob([pageData.content], { type: "text/html" });
  171. pageData.url = URL.createObjectURL(blob);
  172. await downloads.downloadPage(pageData, options);
  173. if (options.openSavedPage) {
  174. const createTabProperties = { active: true, url: URL.createObjectURL(blob), windowId: tab.windowId };
  175. const index = tab.index;
  176. try {
  177. await browser.tabs.get(tabId);
  178. createTabProperties.index = index + 1;
  179. } catch (error) {
  180. createTabProperties.index = index;
  181. }
  182. browser.tabs.create(createTabProperties);
  183. }
  184. }
  185. if (pageData.hash) {
  186. await woleet.anchor(pageData.hash, options.woleetKey);
  187. }
  188. }
  189. } finally {
  190. if (message.taskId) {
  191. business.onSaveEnd(message.taskId);
  192. } else if (options.autoClose) {
  193. browser.tabs.remove(replacedTabIds[tabId] || tabId);
  194. delete replacedTabIds[tabId];
  195. }
  196. if (pageData && pageData.url) {
  197. URL.revokeObjectURL(pageData.url);
  198. }
  199. ui.onEnd(tabId, true);
  200. }
  201. }
  202. }
  203. function fetch(url) {
  204. return new Promise((resolve, reject) => {
  205. const xhrRequest = new XMLHttpRequest();
  206. xhrRequest.withCredentials = true;
  207. xhrRequest.responseType = "arraybuffer";
  208. xhrRequest.onerror = event => reject(new Error(event.detail));
  209. xhrRequest.onreadystatechange = () => {
  210. if (xhrRequest.readyState == XMLHttpRequest.DONE) {
  211. resolve({
  212. status: xhrRequest.status,
  213. headers: {
  214. get: name => xhrRequest.getResponseHeader(name)
  215. },
  216. arrayBuffer: async () => xhrRequest.response
  217. });
  218. }
  219. };
  220. xhrRequest.open("GET", url, true);
  221. xhrRequest.send();
  222. });
  223. }