autosave.js 7.7 KB

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