index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import '@/common/browser';
  2. import { getActiveTab, makePause, sendCmd } from '@/common';
  3. import { TIMEOUT_24HOURS, TIMEOUT_MAX, extensionOrigin } from '@/common/consts';
  4. import { deepCopy } from '@/common/object';
  5. import * as tld from '@/common/tld';
  6. import * as sync from './sync';
  7. import { commands } from './utils';
  8. import { getData, getSizes, checkRemove } from './utils/db';
  9. import { initialize } from './utils/init';
  10. import { getOption, hookOptions } from './utils/options';
  11. import './utils/clipboard';
  12. import './utils/hotkeys';
  13. import './utils/icon';
  14. import './utils/notifications';
  15. import './utils/preinject';
  16. import './utils/script';
  17. import './utils/tabs';
  18. import './utils/tab-redirector';
  19. import './utils/tester';
  20. import './utils/update';
  21. hookOptions((changes) => {
  22. if ('autoUpdate' in changes) {
  23. autoUpdate();
  24. }
  25. sendCmd('UpdateOptions', changes);
  26. });
  27. Object.assign(commands, {
  28. async GetData(opts) {
  29. const data = await getData(opts);
  30. data.sync = sync.getStates();
  31. return data;
  32. },
  33. GetSizes: getSizes,
  34. /** @return {Promise<Object>} */
  35. async GetTabDomain() {
  36. const tab = await getActiveTab() || {};
  37. const url = tab.pendingUrl || tab.url || '';
  38. const host = url.match(/^https?:\/\/([^/]+)|$/)[1];
  39. return {
  40. tab,
  41. domain: host && tld.getDomain(host) || host,
  42. };
  43. },
  44. /**
  45. * Timers in content scripts are shared with the web page so it can clear them.
  46. * await sendCmd('SetTimeout', 100) in injected/content
  47. * bridge.call('SetTimeout', 100, cb) in injected/web
  48. */
  49. SetTimeout(ms) {
  50. return ms > 0 && makePause(ms);
  51. },
  52. });
  53. // commands to sync unconditionally regardless of the returned value from the handler
  54. const commandsToSync = [
  55. 'MarkRemoved',
  56. 'Move',
  57. 'ParseScript',
  58. 'RemoveScript',
  59. 'UpdateScriptInfo',
  60. ];
  61. // commands to sync only if the handler returns a truthy value
  62. const commandsToSyncIfTruthy = [
  63. 'CheckRemove',
  64. 'CheckUpdate',
  65. ];
  66. const commandsForSelf = [
  67. // TODO: maybe just add a prefix for all content-exposed commands?
  68. ...commandsToSync,
  69. ...commandsToSyncIfTruthy,
  70. 'ExportZip',
  71. 'GetAllOptions',
  72. 'GetData',
  73. 'GetSizes',
  74. 'GetOptions',
  75. 'SetOptions',
  76. 'SetValueStores',
  77. 'Storage',
  78. ];
  79. async function handleCommandMessage({ cmd, data } = {}, src) {
  80. if (src && src.origin !== extensionOrigin && commandsForSelf.includes(cmd)) {
  81. throw `Command is only allowed in extension context: ${cmd}`;
  82. }
  83. const res = await commands[cmd]?.(data, src);
  84. if (commandsToSync.includes(cmd)
  85. || res && commandsToSyncIfTruthy.includes(cmd)) {
  86. sync.sync();
  87. }
  88. // `undefined` is not transferable, but `null` is
  89. return res ?? null;
  90. }
  91. function autoUpdate() {
  92. const interval = (+getOption('autoUpdate') || 0) * TIMEOUT_24HOURS;
  93. if (!interval) return;
  94. let elapsed = Date.now() - getOption('lastUpdate');
  95. if (elapsed >= interval) {
  96. handleCommandMessage({ cmd: 'CheckUpdate' });
  97. elapsed = 0;
  98. }
  99. clearTimeout(autoUpdate.timer);
  100. autoUpdate.timer = setTimeout(autoUpdate, Math.min(TIMEOUT_MAX, interval - elapsed));
  101. }
  102. initialize(() => {
  103. global.handleCommandMessage = handleCommandMessage;
  104. global.deepCopy = deepCopy;
  105. browser.runtime.onMessage.addListener(
  106. IS_FIREFOX // in FF a rejected Promise value is transferred only if it's an Error object
  107. ? (...args) => handleCommandMessage(...args).catch(e => (
  108. Promise.reject(e instanceof Error ? e : new Error(e))
  109. )) // Didn't use `throw` to avoid interruption in devtools with pause-on-exception enabled.
  110. : handleCommandMessage,
  111. );
  112. setTimeout(autoUpdate, 2e4);
  113. sync.initialize();
  114. checkRemove();
  115. setInterval(checkRemove, TIMEOUT_24HOURS);
  116. const api = global.chrome.declarativeContent;
  117. if (api) {
  118. // Using declarativeContent to run content scripts earlier than document_start
  119. api.onPageChanged.getRules(/* for old Chrome */ null, async ([rule]) => {
  120. const id = rule?.id;
  121. const newId = process.env.INIT_FUNC_NAME;
  122. if (id === newId) {
  123. return;
  124. }
  125. if (id) {
  126. await browser.declarativeContent.onPageChanged.removeRules([id]);
  127. }
  128. api.onPageChanged.addRules([{
  129. id: newId,
  130. conditions: [
  131. new api.PageStateMatcher({
  132. pageUrl: { urlContains: '://' }, // essentially like <all_urls>
  133. }),
  134. ],
  135. actions: [
  136. new api.RequestContentScript({
  137. js: browser.runtime.getManifest().content_scripts[0].js,
  138. // Not using `allFrames:true` as there's no improvement in frames
  139. }),
  140. ],
  141. }]);
  142. });
  143. }
  144. });