autosave.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 pendingDiscardedTabs = {};
  34. export {
  35. onMessage,
  36. onMessageExternal,
  37. onInit,
  38. isEnabled,
  39. refreshTabs,
  40. onTabRemoved
  41. };
  42. async function onMessage(message, sender) {
  43. if (message.method.endsWith(".init")) {
  44. const [options, autoSaveEnabled] = await Promise.all([config.getOptions(sender.tab.url, true), isEnabled(sender.tab)]);
  45. return { options, autoSaveEnabled };
  46. }
  47. if (message.method.endsWith(".save")) {
  48. const tabId = sender.tab.id;
  49. let resolvePendingDiscardedTab;
  50. pendingDiscardedTabs[tabId] = new Promise(resolve => resolvePendingDiscardedTab = resolve);
  51. if (message.autoSaveDiscard) {
  52. message.tab = sender.tab;
  53. resolvePendingDiscardedTab(message);
  54. if (message.autoSaveUnload) {
  55. await saveContent(message, sender.tab);
  56. }
  57. } else {
  58. await saveContent(message, sender.tab);
  59. }
  60. return {};
  61. }
  62. }
  63. async function onTabRemoved(tabId) {
  64. const pendingDiscardedTab = await pendingDiscardedTabs[tabId];
  65. if (pendingDiscardedTab) {
  66. await saveContent(pendingDiscardedTab, pendingDiscardedTab.tab);
  67. }
  68. }
  69. async function onMessageExternal(message, currentTab) {
  70. if (message.method == "enableAutoSave") {
  71. const allTabsData = await tabsData.get(currentTab.id);
  72. allTabsData[currentTab.id].autoSave = message.enabled;
  73. await tabsData.set(allTabsData);
  74. ui.refreshTab(currentTab);
  75. }
  76. if (message.method == "isAutoSaveEnabled") {
  77. return isEnabled(currentTab);
  78. }
  79. }
  80. async function onInit(tab) {
  81. const [options, autoSaveEnabled] = await Promise.all([config.getOptions(tab.url, true), isEnabled(tab)]);
  82. if (options && ((options.autoSaveLoad || options.autoSaveLoadOrUnload) && autoSaveEnabled)) {
  83. business.saveTabs([tab], { autoSave: true });
  84. }
  85. }
  86. async function isEnabled(tab) {
  87. if (tab) {
  88. const [allTabsData, rule] = await Promise.all([tabsData.get(), config.getRule(tab.url)]);
  89. return Boolean(allTabsData.autoSaveAll ||
  90. (allTabsData.autoSaveUnpinned && !tab.pinned) ||
  91. (allTabsData[tab.id] && allTabsData[tab.id].autoSave)) &&
  92. (!rule || rule.autoSaveProfile != config.DISABLED_PROFILE_NAME);
  93. }
  94. }
  95. async function refreshTabs() {
  96. const allTabs = (await tabs.get({}));
  97. return Promise.all(allTabs.map(async tab => {
  98. const [options, autoSaveEnabled] = await Promise.all([config.getOptions(tab.url, true), isEnabled(tab)]);
  99. try {
  100. await tabs.sendMessage(tab.id, { method: "content.init", autoSaveEnabled, options });
  101. } catch (error) {
  102. // ignored
  103. }
  104. }));
  105. }
  106. async function saveContent(message, tab) {
  107. const tabId = tab.id;
  108. delete pendingDiscardedTabs[tabId];
  109. const options = await config.getOptions(tab.url, true);
  110. if (options) {
  111. ui.onStart(tabId, 1, true);
  112. options.content = message.content;
  113. options.url = message.url;
  114. options.frames = message.frames;
  115. options.canvases = message.canvases;
  116. options.fonts = message.fonts;
  117. options.stylesheets = message.stylesheets;
  118. options.images = message.images;
  119. options.posters = message.posters;
  120. options.usedFonts = message.usedFonts;
  121. options.shadowRoots = message.shadowRoots;
  122. options.imports = message.imports;
  123. options.referrer = message.referrer;
  124. options.updatedResources = message.updatedResources;
  125. options.visitDate = new Date(message.visitDate);
  126. options.backgroundTab = true;
  127. options.autoSave = true;
  128. options.incognito = tab.incognito;
  129. options.tabId = tabId;
  130. options.tabIndex = tab.index;
  131. let pageData;
  132. try {
  133. if (options.autoSaveExternalSave) {
  134. await companion.save(options);
  135. } else {
  136. pageData = await getPageData(options, null, null, { fetch });
  137. if (options.includeInfobar) {
  138. await infobar.includeScript(pageData);
  139. }
  140. const blob = new Blob([pageData.content], { type: "text/html" });
  141. if (options.saveToGDrive) {
  142. await downloads.uploadPage(message.taskId, pageData.filename, blob, options, {});
  143. } else {
  144. pageData.url = URL.createObjectURL(blob);
  145. await downloads.downloadPage(pageData, options);
  146. if (options.openSavedPage) {
  147. const createTabProperties = { active: true, url: URL.createObjectURL(blob) };
  148. const index = tab.index;
  149. try {
  150. await tabs.get({ id: tabId });
  151. createTabProperties.index = index + 1;
  152. } catch (error) {
  153. createTabProperties.index = index;
  154. }
  155. tabs.create(createTabProperties);
  156. }
  157. }
  158. if (pageData.hash) {
  159. await woleet.anchor(pageData.hash);
  160. }
  161. }
  162. } finally {
  163. business.onSaveEnd(message.taskId);
  164. if (pageData && pageData.url) {
  165. URL.revokeObjectURL(pageData.url);
  166. }
  167. ui.onEnd(tabId, true);
  168. }
  169. }
  170. }
  171. function fetch(url) {
  172. return new Promise((resolve, reject) => {
  173. const xhrRequest = new XMLHttpRequest();
  174. xhrRequest.withCredentials = true;
  175. xhrRequest.responseType = "arraybuffer";
  176. xhrRequest.onerror = event => reject(new Error(event.detail));
  177. xhrRequest.onreadystatechange = () => {
  178. if (xhrRequest.readyState == XMLHttpRequest.DONE) {
  179. resolve({
  180. status: xhrRequest.status,
  181. headers: {
  182. get: name => xhrRequest.getResponseHeader(name)
  183. },
  184. arrayBuffer: async () => xhrRequest.response
  185. });
  186. }
  187. };
  188. xhrRequest.open("GET", url, true);
  189. xhrRequest.send();
  190. });
  191. }