background.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /* global dbExec, getStyles, saveStyle, schedule, download */
  2. 'use strict';
  3. // eslint-disable-next-line no-var
  4. var browserCommands, contextMenus;
  5. // *************************************************************************
  6. // preload the DB and report errors
  7. dbExec().catch((...args) => {
  8. args.forEach(arg => 'message' in arg && console.error(arg.message));
  9. });
  10. // *************************************************************************
  11. // register all listeners
  12. chrome.runtime.onMessage.addListener(onRuntimeMessage);
  13. chrome.webNavigation.onBeforeNavigate.addListener(data =>
  14. webNavigationListener(null, data));
  15. chrome.webNavigation.onCommitted.addListener(data =>
  16. webNavigationListener('styleApply', data));
  17. chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
  18. webNavigationListener('styleReplaceAll', data));
  19. chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
  20. webNavigationListener('styleReplaceAll', data));
  21. chrome.tabs.onAttached.addListener((tabId, data) => {
  22. // When an edit page gets attached or detached, remember its state
  23. // so we can do the same to the next one to open.
  24. chrome.tabs.get(tabId, tab => {
  25. if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) {
  26. chrome.windows.get(tab.windowId, {populate: true}, win => {
  27. // If there's only one tab in this window, it's been dragged to new window
  28. prefs.set('openEditInWindow', win.tabs.length == 1);
  29. });
  30. }
  31. });
  32. });
  33. chrome.contextMenus.onClicked.addListener((info, tab) =>
  34. contextMenus[info.menuItemId].click(info, tab));
  35. if ('commands' in chrome) {
  36. // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
  37. chrome.commands.onCommand.addListener(command => browserCommands[command]());
  38. }
  39. // *************************************************************************
  40. {
  41. const onInstall = ({reason}) => {
  42. chrome.runtime.onInstalled.removeListener(onInstall);
  43. const manifest = chrome.runtime.getManifest();
  44. // Open FAQs page once after installation to guide new users.
  45. // Do not display it in development mode.
  46. if (reason == 'install' && manifest.update_url) {
  47. setTimeout(openURL, 100, {
  48. url: `http://add0n.com/stylus.html?version=${manifest.version}&type=install`
  49. });
  50. }
  51. // reset L10N cache on UI language change or update
  52. const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {};
  53. const UIlang = chrome.i18n.getUILanguage();
  54. if (reason == 'update' || browserUIlanguage != UIlang) {
  55. localStorage.L10N = JSON.stringify({
  56. browserUIlanguage: UIlang,
  57. });
  58. }
  59. };
  60. // bind for 60 seconds max and auto-unbind if it's a normal run
  61. chrome.runtime.onInstalled.addListener(onInstall);
  62. setTimeout(onInstall, 60e3, {reason: 'unbindme'});
  63. }
  64. // *************************************************************************
  65. // browser commands
  66. browserCommands = {
  67. openManage() {
  68. openURL({url: '/manage.html'});
  69. },
  70. styleDisableAll(info) {
  71. prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
  72. },
  73. };
  74. // *************************************************************************
  75. // context menus
  76. contextMenus = Object.assign({
  77. 'show-badge': {
  78. title: 'menuShowBadge',
  79. click: info => prefs.set(info.menuItemId, info.checked),
  80. },
  81. 'disableAll': {
  82. title: 'disableAllStyles',
  83. click: browserCommands.styleDisableAll,
  84. },
  85. 'open-manager': {
  86. title: 'openStylesManager',
  87. click: browserCommands.openManage,
  88. },
  89. }, prefs.get('editor.contextDelete') && {
  90. 'editor.contextDelete': {
  91. title: 'editDeleteText',
  92. type: 'normal',
  93. contexts: ['editable'],
  94. documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
  95. click: (info, tab) => {
  96. chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'});
  97. },
  98. }
  99. });
  100. {
  101. const createContextMenus = (ids = Object.keys(contextMenus)) => {
  102. for (const id of ids) {
  103. const item = Object.assign({id}, contextMenus[id]);
  104. const prefValue = prefs.readOnlyValues[id];
  105. item.title = chrome.i18n.getMessage(item.title);
  106. if (!item.type && typeof prefValue == 'boolean') {
  107. item.type = 'checkbox';
  108. item.checked = prefValue;
  109. }
  110. if (!item.contexts) {
  111. item.contexts = ['browser_action'];
  112. }
  113. delete item.click;
  114. chrome.contextMenus.create(item, ignoreChromeError);
  115. }
  116. };
  117. createContextMenus();
  118. prefs.subscribe((id, checked) => {
  119. if (id == 'editor.contextDelete') {
  120. if (checked) {
  121. createContextMenus([id]);
  122. } else {
  123. chrome.contextMenus.remove(id, ignoreChromeError);
  124. }
  125. } else {
  126. chrome.contextMenus.update(id, {checked}, ignoreChromeError);
  127. }
  128. }, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean'));
  129. }
  130. // *************************************************************************
  131. // [re]inject content scripts
  132. {
  133. const NTP = 'chrome://newtab/';
  134. const PING = {method: 'ping'};
  135. const ALL_URLS = '<all_urls>';
  136. const contentScripts = chrome.runtime.getManifest().content_scripts;
  137. // expand * as .*?
  138. const wildcardAsRegExp = (s, flags) => new RegExp(
  139. s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
  140. .replace(/\*/g, '.*?'), flags);
  141. for (const cs of contentScripts) {
  142. cs.matches = cs.matches.map(m => (
  143. m == ALL_URLS ? m : wildcardAsRegExp(m)
  144. ));
  145. }
  146. const injectCS = (cs, tabId) => {
  147. chrome.tabs.executeScript(tabId, {
  148. file: cs.js[0],
  149. runAt: cs.run_at,
  150. allFrames: cs.all_frames,
  151. matchAboutBlank: cs.match_about_blank,
  152. }, ignoreChromeError);
  153. };
  154. const pingCS = (cs, {id, url}) => {
  155. cs.matches.some(match => {
  156. if ((match == ALL_URLS || url.match(match))
  157. && (!url.startsWith('chrome') || url == NTP)) {
  158. chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id));
  159. return true;
  160. }
  161. });
  162. };
  163. chrome.tabs.query({}, tabs =>
  164. tabs.forEach(tab =>
  165. contentScripts.forEach(cs =>
  166. pingCS(cs, tab))));
  167. }
  168. // *************************************************************************
  169. function webNavigationListener(method, {url, tabId, frameId}) {
  170. getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => {
  171. if (method && !url.startsWith('chrome:') && tabId >= 0) {
  172. chrome.tabs.sendMessage(tabId, {
  173. method,
  174. // ping own page so it retrieves the styles directly
  175. styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
  176. }, {
  177. frameId
  178. });
  179. }
  180. // main page frame id is 0
  181. if (frameId == 0) {
  182. updateIcon({id: tabId, url}, styles);
  183. }
  184. });
  185. }
  186. function updateIcon(tab, styles) {
  187. if (tab.id < 0) {
  188. return;
  189. }
  190. if (styles) {
  191. stylesReceived(styles);
  192. return;
  193. }
  194. getTabRealURL(tab)
  195. .then(url => getStyles({matchUrl: url, enabled: true, asHash: true}))
  196. .then(stylesReceived);
  197. function stylesReceived(styles) {
  198. let numStyles = styles.length;
  199. if (numStyles === undefined) {
  200. // for 'styles' asHash:true fake the length by counting numeric ids manually
  201. numStyles = 0;
  202. for (const id of Object.keys(styles)) {
  203. numStyles += id.match(/^\d+$/) ? 1 : 0;
  204. }
  205. }
  206. const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
  207. const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
  208. const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
  209. const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
  210. chrome.browserAction.setIcon({
  211. tabId: tab.id,
  212. path: {
  213. // Material Design 2016 new size is 16px
  214. 16: `images/icon/16${postfix}.png`,
  215. 32: `images/icon/32${postfix}.png`,
  216. // Chromium forks or non-chromium browsers may still use the traditional 19px
  217. 19: `images/icon/19${postfix}.png`,
  218. 38: `images/icon/38${postfix}.png`,
  219. // TODO: add Edge preferred sizes: 20, 25, 30, 40
  220. },
  221. }, () => {
  222. if (chrome.runtime.lastError) {
  223. return;
  224. }
  225. // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
  226. chrome.browserAction.setBadgeBackgroundColor({color});
  227. getTab(tab.id).then(() => {
  228. chrome.browserAction.setBadgeText({text, tabId: tab.id});
  229. });
  230. });
  231. }
  232. }
  233. function onRuntimeMessage(request, sender, sendResponse) {
  234. switch (request.method) {
  235. case 'getStyles':
  236. getStyles(request).then(sendResponse);
  237. return KEEP_CHANNEL_OPEN;
  238. case 'saveStyle':
  239. saveStyle(request).then(sendResponse);
  240. return KEEP_CHANNEL_OPEN;
  241. case 'healthCheck':
  242. dbExec()
  243. .then(() => sendResponse(true))
  244. .catch(() => sendResponse(false));
  245. return KEEP_CHANNEL_OPEN;
  246. case 'download':
  247. download(request.url)
  248. .then(sendResponse)
  249. .catch(() => sendResponse(null));
  250. return KEEP_CHANNEL_OPEN;
  251. case 'schedule':
  252. schedule.entry(request)
  253. .then(() => sendResponse(true))
  254. .catch(() => sendResponse(false));
  255. return KEEP_CHANNEL_OPEN;
  256. }
  257. }