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