background.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /* global API msg */// msg.js
  2. /* global addAPI bgReady */// common.js
  3. /* global createWorker */// worker-util.js
  4. /* global prefs */
  5. /* global styleMan */
  6. /* global syncMan */
  7. /* global updateMan */
  8. /* global usercssMan */
  9. /* global uswApi */
  10. /* global
  11. FIREFOX
  12. URLS
  13. activateTab
  14. download
  15. findExistingTab
  16. openURL
  17. */ // toolbox.js
  18. /* global colorScheme */ // color-scheme.js
  19. 'use strict';
  20. //#region API
  21. addAPI(/** @namespace API */ {
  22. /** Temporary storage for data needed elsewhere e.g. in a content script */
  23. data: ((data = {}) => ({
  24. del: key => delete data[key],
  25. get: key => data[key],
  26. has: key => key in data,
  27. pop: key => {
  28. const val = data[key];
  29. delete data[key];
  30. return val;
  31. },
  32. set: (key, val) => {
  33. data[key] = val;
  34. },
  35. }))(),
  36. styles: styleMan,
  37. sync: syncMan,
  38. updater: updateMan,
  39. usercss: usercssMan,
  40. usw: uswApi,
  41. colorScheme,
  42. /** @type {BackgroundWorker} */
  43. worker: createWorker({url: '/background/background-worker'}),
  44. download(url, opts) {
  45. return typeof url === 'string' && url.startsWith(URLS.uso) &&
  46. this.sender.url.startsWith(URLS.uso) &&
  47. download(url, opts || {});
  48. },
  49. /** @returns {string} */
  50. getTabUrlPrefix() {
  51. return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
  52. },
  53. /**
  54. * Opens the editor or activates an existing tab
  55. * @param {{
  56. id?: number
  57. domain?: string
  58. 'url-prefix'?: string
  59. }} params
  60. * @returns {Promise<chrome.tabs.Tab>}
  61. */
  62. async openEditor(params) {
  63. const u = new URL(chrome.runtime.getURL('edit.html'));
  64. u.search = new URLSearchParams(params);
  65. const wnd = prefs.get('openEditInWindow');
  66. const wndPos = wnd && prefs.get('windowPosition');
  67. const wndBase = wnd && prefs.get('openEditInWindow.popup') ? {type: 'popup'} : {};
  68. const ffBug = wnd && FIREFOX; // https://bugzil.la/1271047
  69. const tab = await openURL({
  70. url: `${u}`,
  71. currentWindow: null,
  72. newWindow: wnd && Object.assign(wndBase, !ffBug && wndPos),
  73. });
  74. if (ffBug) await browser.windows.update(tab.windowId, wndPos);
  75. return tab;
  76. },
  77. /** @returns {Promise<chrome.tabs.Tab>} */
  78. async openManage({options = false, search, searchMode} = {}) {
  79. let url = chrome.runtime.getURL('manage.html');
  80. if (search) {
  81. url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
  82. }
  83. if (options) {
  84. url += '#stylus-options';
  85. }
  86. const tab = await findExistingTab({
  87. url,
  88. currentWindow: null,
  89. ignoreHash: true,
  90. ignoreSearch: true,
  91. });
  92. if (tab) {
  93. await activateTab(tab);
  94. if (url !== (tab.pendingUrl || tab.url)) {
  95. await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
  96. }
  97. return tab;
  98. }
  99. return openURL({url, ignoreExisting: true}).then(activateTab); // activateTab unminimizes the window
  100. },
  101. /**
  102. * Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
  103. * when the tab is ready, which is needed in the popup, otherwise another
  104. * extension could force the tab to open in foreground thus auto-closing the
  105. * popup (in Chrome at least) and preventing the sendMessage code from running
  106. * @returns {Promise<chrome.tabs.Tab>}
  107. */
  108. async openURL(opts) {
  109. const tab = await openURL(opts);
  110. if (opts.message) {
  111. await onTabReady(tab);
  112. await msg.sendTab(tab.id, opts.message);
  113. }
  114. return tab;
  115. function onTabReady(tab) {
  116. return new Promise((resolve, reject) =>
  117. setTimeout(function ping(numTries = 10, delay = 100) {
  118. msg.sendTab(tab.id, {method: 'ping'})
  119. .catch(() => false)
  120. .then(pong => pong
  121. ? resolve(tab)
  122. : numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
  123. reject('timeout'));
  124. }));
  125. }
  126. },
  127. prefs: {
  128. getValues: () => prefs.__values, // will be deepCopy'd by apiHandler
  129. set: prefs.set,
  130. },
  131. });
  132. //#endregion
  133. //#region Events
  134. const browserCommands = {
  135. openManage: () => API.openManage(),
  136. openOptions: () => API.openManage({options: true}),
  137. reload: () => chrome.runtime.reload(),
  138. styleDisableAll(info) {
  139. prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
  140. },
  141. };
  142. if (chrome.commands) {
  143. chrome.commands.onCommand.addListener(id => browserCommands[id]());
  144. }
  145. chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
  146. if (reason === 'update') {
  147. const [a, b, c] = (previousVersion || '').split('.');
  148. if (a <= 1 && b <= 5 && c <= 13) { // 1.5.13
  149. require(['/background/remove-unused-storage']);
  150. }
  151. }
  152. });
  153. msg.on((msg, sender) => {
  154. if (msg.method === 'invokeAPI') {
  155. let res = msg.path.reduce((res, name) => res && res[name], API);
  156. if (!res) throw new Error(`Unknown API.${msg.path.join('.')}`);
  157. res = res.apply({msg, sender}, msg.args);
  158. return res === undefined ? null : res;
  159. }
  160. });
  161. //#endregion
  162. Promise.all([
  163. browser.extension.isAllowedFileSchemeAccess()
  164. .then(res => API.data.set('hasFileAccess', res)),
  165. bgReady.styles,
  166. /* These are loaded conditionally.
  167. Each item uses `require` individually so IDE can jump to the source and track usage. */
  168. FIREFOX &&
  169. require(['/background/style-via-api']),
  170. FIREFOX && ((browser.commands || {}).update) &&
  171. require(['/background/browser-cmd-hotkeys']),
  172. !FIREFOX &&
  173. require(['/background/content-scripts']),
  174. chrome.contextMenus &&
  175. require(['/background/context-menus']),
  176. ]).then(() => {
  177. bgReady._resolveAll();
  178. msg.isBgReady = true;
  179. msg.broadcast({method: 'backgroundReady'});
  180. });