usercss-install-helper.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /* global RX_META URLS download openURL */// toolbox.js
  2. /* global addAPI bgReady */// common.js
  3. /* global tabMan */// msg.js
  4. 'use strict';
  5. bgReady.all.then(() => {
  6. const installCodeCache = {};
  7. addAPI(/** @namespace API */ {
  8. usercss: {
  9. getInstallCode(url) {
  10. // when the installer tab is reloaded after the cache is expired, this will throw intentionally
  11. const {code, timer} = installCodeCache[url];
  12. clearInstallCode(url);
  13. clearTimeout(timer);
  14. return code;
  15. },
  16. },
  17. });
  18. // `glob`: pathname match pattern for webRequest
  19. // `rx`: pathname regex to verify the URL really looks like a raw usercss
  20. const maybeDistro = {
  21. // https://github.com/StylishThemes/GitHub-Dark/raw/master/github-dark.user.css
  22. 'github.com': {
  23. glob: '/*/raw/*',
  24. rx: /^\/[^/]+\/[^/]+\/raw\/[^/]+\/[^/]+?\.user\.(css|styl)$/,
  25. },
  26. // https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/github-dark.user.css
  27. 'raw.githubusercontent.com': {
  28. glob: '/*',
  29. rx: /^(\/[^/]+?){4}\.user\.(css|styl)$/,
  30. },
  31. };
  32. chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
  33. urls: [
  34. URLS.usw + 'api/style/*.user.css',
  35. ...URLS.usoArchiveRaw.map(s => s + 'usercss/*.user.css'),
  36. ...['greasy', 'sleazy'].map(s => `*://${s}fork.org/scripts/*/code/*.user.css`),
  37. ...[].concat(
  38. ...Object.entries(maybeDistro)
  39. .map(([host, {glob}]) => makeUsercssGlobs(host, glob))),
  40. ],
  41. types: ['main_frame'],
  42. }, ['blocking']);
  43. chrome.webRequest.onHeadersReceived.addListener(rememberContentType, {
  44. urls: makeUsercssGlobs('*', '/*'),
  45. types: ['main_frame'],
  46. }, ['responseHeaders']);
  47. tabMan.onUpdate(maybeInstall);
  48. function clearInstallCode(url) {
  49. return delete installCodeCache[url];
  50. }
  51. /** Sites may be using custom types like text/stylus so this coarse filter only excludes html */
  52. function isContentTypeText(type) {
  53. return /^text\/(?!html)/i.test(type);
  54. }
  55. // in Firefox we have to use a content script to read file://
  56. async function loadFromFile(tabId) {
  57. return (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0];
  58. }
  59. async function loadFromUrl(tabId, url) {
  60. return (
  61. url.startsWith('file:') ||
  62. tabMan.get(tabId, isContentTypeText.name) ||
  63. isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
  64. ) && download(url);
  65. }
  66. function makeUsercssGlobs(host, path) {
  67. return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
  68. }
  69. async function maybeInstall({tabId, url, oldUrl = ''}) {
  70. if (url.includes('.user.') &&
  71. /^(https?|file|ftps?):/.test(url) &&
  72. /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
  73. !oldUrl.startsWith(URLS.installUsercss)) {
  74. const inTab = url.startsWith('file:') && !chrome.app;
  75. const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
  76. if (!/^\s*</.test(code) && RX_META.test(code)) {
  77. openInstallerPage(tabId, url, {code, inTab});
  78. }
  79. }
  80. }
  81. /** Faster installation on known distribution sites to avoid flicker of css text */
  82. function maybeInstallFromDistro({tabId, url}) {
  83. const u = new URL(url);
  84. const m = maybeDistro[u.hostname];
  85. if (!m || m.rx.test(u.pathname)) {
  86. openInstallerPage(tabId, url, {});
  87. // Silently suppress navigation.
  88. // Don't redirect to the install URL as it'll flash the text!
  89. return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
  90. }
  91. }
  92. function openInstallerPage(tabId, url, {code, inTab} = {}) {
  93. const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
  94. if (inTab) {
  95. browser.tabs.get(tabId).then(tab =>
  96. openURL({
  97. url: `${newUrl}&tabId=${tabId}`,
  98. active: tab.active,
  99. index: tab.index + 1,
  100. openerTabId: tabId,
  101. currentWindow: null,
  102. }));
  103. } else {
  104. const timer = setTimeout(clearInstallCode, 10e3, url);
  105. installCodeCache[url] = {code, timer};
  106. chrome.tabs.update(tabId, {url: newUrl});
  107. }
  108. }
  109. /** Remember Content-Type to avoid wasting time to re-fetch in loadFromUrl **/
  110. function rememberContentType({tabId, responseHeaders}) {
  111. const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
  112. tabMan.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
  113. }
  114. });