content-scripts.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /* global bgReady */// common.js
  2. /* global msg */
  3. /* global URLS ignoreChromeError */// toolbox.js
  4. 'use strict';
  5. /*
  6. Reinject content scripts when the extension is reloaded/updated.
  7. Not used in Firefox as it reinjects automatically.
  8. */
  9. bgReady.all.then(() => {
  10. const NTP = 'chrome://newtab/';
  11. const ALL_URLS = '<all_urls>';
  12. const SCRIPTS = chrome.runtime.getManifest().content_scripts;
  13. // expand * as .*?
  14. const wildcardAsRegExp = (s, flags) => new RegExp(
  15. s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
  16. .replace(/\*/g, '.*?'), flags);
  17. for (const cs of SCRIPTS) {
  18. cs.matches = cs.matches.map(m => (
  19. m === ALL_URLS ? m : wildcardAsRegExp(m)
  20. ));
  21. }
  22. const busyTabs = new Set();
  23. let busyTabsTimer;
  24. setTimeout(injectToAllTabs);
  25. function injectToTab({url, tabId, frameId = null}) {
  26. for (const script of SCRIPTS) {
  27. if (
  28. script.matches.some(match =>
  29. (match === ALL_URLS || url.match(match)) &&
  30. (!url.startsWith('chrome') || url === NTP))
  31. ) {
  32. doInject(tabId, frameId, script);
  33. }
  34. }
  35. }
  36. function doInject(tabId, frameId, script) {
  37. const options = frameId === null ? {} : {frameId};
  38. msg.sendTab(tabId, {method: 'ping'}, options)
  39. .catch(() => false)
  40. .then(pong => {
  41. if (pong) {
  42. return;
  43. }
  44. const options = {
  45. runAt: script.run_at,
  46. allFrames: script.all_frames,
  47. matchAboutBlank: script.match_about_blank,
  48. };
  49. if (frameId !== null) {
  50. options.allFrames = false;
  51. options.frameId = frameId;
  52. }
  53. for (const file of script.js) {
  54. chrome.tabs.executeScript(tabId, Object.assign({file}, options), ignoreChromeError);
  55. }
  56. });
  57. }
  58. function injectToAllTabs() {
  59. return browser.tabs.query({}).then(tabs => {
  60. for (const tab of tabs) {
  61. // skip unloaded/discarded/chrome tabs
  62. if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue;
  63. // our content scripts may still be pending injection at browser start so it's too early to ping them
  64. if (tab.status === 'loading') {
  65. trackBusyTab(tab.id, true);
  66. } else {
  67. injectToTab({
  68. url: tab.pendingUrl || tab.url,
  69. tabId: tab.id,
  70. });
  71. }
  72. }
  73. });
  74. }
  75. function toggleBusyTabListeners(state) {
  76. const toggle = state ? 'addListener' : 'removeListener';
  77. chrome.webNavigation.onCompleted[toggle](onBusyTabUpdated);
  78. chrome.webNavigation.onErrorOccurred[toggle](onBusyTabUpdated);
  79. chrome.webNavigation.onTabReplaced[toggle](onBusyTabReplaced);
  80. chrome.tabs.onRemoved[toggle](onBusyTabRemoved);
  81. if (state) {
  82. busyTabsTimer = setTimeout(toggleBusyTabListeners, 15e3, false);
  83. } else {
  84. clearTimeout(busyTabsTimer);
  85. }
  86. }
  87. function trackBusyTab(tabId, state) {
  88. busyTabs[state ? 'add' : 'delete'](tabId);
  89. if (state && busyTabs.size === 1) toggleBusyTabListeners(true);
  90. if (!state && !busyTabs.size) toggleBusyTabListeners(false);
  91. }
  92. function onBusyTabUpdated({error, frameId, tabId, url}) {
  93. if (!frameId && busyTabs.has(tabId)) {
  94. trackBusyTab(tabId, false);
  95. if (url && !error) {
  96. injectToTab({tabId, url});
  97. }
  98. }
  99. }
  100. function onBusyTabReplaced({replacedTabId}) {
  101. trackBusyTab(replacedTabId, false);
  102. }
  103. function onBusyTabRemoved(tabId) {
  104. trackBusyTab(tabId, false);
  105. }
  106. });