diff --git a/background/background.js b/background/background.js index 2dd15b0a..d03250b6 100644 --- a/background/background.js +++ b/background/background.js @@ -1,8 +1,8 @@ /* global download prefs openURL FIREFOX CHROME - URLS ignoreChromeError usercssHelper chromeLocal semverCompare + URLS ignoreChromeError chromeLocal semverCompare styleManager msg navigatorUtil workerUtil contentScripts sync findExistingTab activateTab isTabReplaceable getActiveTab - tabManager */ +*/ 'use strict'; @@ -111,14 +111,6 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => { } }); -tabManager.onUpdate(({tabId, url, oldUrl = ''}) => { - if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) { - usercssHelper.testContents(tabId, url).then(data => { - if (data.code) usercssHelper.openInstallerPage(tabId, url, data); - }); - } -}); - if (FIREFOX) { // FF misses some about:blank iframes so we inject our content script explicitly navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { diff --git a/background/usercss-helper.js b/background/usercss-helper.js index 3f6081f6..00b3a99b 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -1,15 +1,8 @@ -/* global API_METHODS usercss styleManager deepCopy openURL download URLS */ +/* global API_METHODS usercss styleManager deepCopy */ /* exported usercssHelper */ 'use strict'; const usercssHelper = (() => { - const installCodeCache = {}; - const clearInstallCode = url => delete installCodeCache[url]; - const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type')); - // in Firefox we have to use a content script to read file:// - const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed - (tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0])); - API_METHODS.installUsercss = installUsercss; API_METHODS.editSaveUsercss = editSaveUsercss; API_METHODS.configUsercssVars = configUsercssVars; @@ -17,50 +10,6 @@ const usercssHelper = (() => { API_METHODS.buildUsercss = build; API_METHODS.findUsercss = find; - API_METHODS.getUsercssInstallCode = url => { - // when the installer tab is reloaded after the cache is expired, this will throw intentionally - const {code, timer} = installCodeCache[url]; - clearInstallCode(url); - clearTimeout(timer); - return code; - }; - - return { - - testUrl(url) { - return url.includes('.user.') && - /^(https?|file|ftps?):/.test(url) && - /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]); - }, - - /** @return {Promise<{ code:string, inTab:boolean } | false>} */ - testContents(tabId, url) { - const isFile = url.startsWith('file:'); - const inTab = isFile && Boolean(fileLoader); - return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText)) - .then(ok => ok && (inTab ? fileLoader(tabId) : download(url))) - .then(code => /==userstyle==/i.test(code) && {code, inTab}); - }, - - openInstallerPage(tabId, url, {code, inTab} = {}) { - const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; - if (inTab) { - browser.tabs.get(tabId).then(tab => - openURL({ - url: `${newUrl}&tabId=${tabId}`, - active: tab.active, - index: tab.index + 1, - openerTabId: tabId, - currentWindow: null, - })); - } else { - const timer = setTimeout(clearInstallCode, 10e3, url); - installCodeCache[url] = {code, timer}; - chrome.tabs.update(tabId, {url: newUrl}); - } - }, - }; - function buildMeta(style) { if (style.usercssData) { return Promise.resolve(style); diff --git a/background/usercss-install-helper.js b/background/usercss-install-helper.js new file mode 100644 index 00000000..b854564a --- /dev/null +++ b/background/usercss-install-helper.js @@ -0,0 +1,82 @@ +/* global API_METHODS openURL download URLS tabManager */ +'use strict'; + +(() => { + const installCodeCache = {}; + const clearInstallCode = url => delete installCodeCache[url]; + const isContentTypeText = type => /^text\/(css|plain)(;.*?)?$/i.test(type); + + // in Firefox we have to use a content script to read file:// + const fileLoader = !chrome.app && ( + async tabId => + (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]); + + const urlLoader = + async (tabId, url) => ( + url.startsWith('file:') || + tabManager.get(tabId, isContentTypeText.name) || + isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type')) + ) && download(url); + + API_METHODS.getUsercssInstallCode = url => { + // when the installer tab is reloaded after the cache is expired, this will throw intentionally + const {code, timer} = installCodeCache[url]; + clearInstallCode(url); + clearTimeout(timer); + return code; + }; + + // Faster installation on known distribution sites to avoid flicker of css text + chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => { + openInstallerPage(tabId, url, {}); + // Silently suppressing navigation like it never happened + return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url + }, { + urls: [ + URLS.usoArchiveRaw + 'usercss/*.user.css', + '*://greasyfork.org/scripts/*/code/*.user.css', + '*://sleazyfork.org/scripts/*/code/*.user.css', + ], + types: ['main_frame'], + }, ['blocking']); + + // Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow + chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => { + const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type'); + tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined); + }, { + urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','), + types: ['main_frame'], + }, ['responseHeaders']); + + tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => { + if (url.includes('.user.') && + /^(https?|file|ftps?):/.test(url) && + /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) && + !oldUrl.startsWith(URLS.installUsercss)) { + const inTab = url.startsWith('file:') && Boolean(fileLoader); + const code = await (inTab ? fileLoader : urlLoader)(tabId, url); + if (/==userstyle==/i.test(code)) { + openInstallerPage(tabId, url, {code, inTab}); + } + } + }); + + function openInstallerPage(tabId, url, {code, inTab} = {}) { + const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; + if (inTab) { + browser.tabs.get(tabId).then(tab => + openURL({ + url: `${newUrl}&tabId=${tabId}`, + active: tab.active, + index: tab.index + 1, + openerTabId: tabId, + currentWindow: null, + })); + } else { + const timer = setTimeout(clearInstallCode, 10e3, url); + installCodeCache[url] = {code, timer}; + chrome.tabs.update(tabId, {url: newUrl}); + } + } +})(); diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 82bb8b73..4354483c 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -318,7 +318,9 @@ let sequence = null; if (tabId < 0) { getData = DirectDownloader(); - sequence = API.getUsercssInstallCode(initialUrl).catch(getData); + sequence = API.getUsercssInstallCode(initialUrl) + .then(code => code || getData()) + .catch(getData); } else { getData = PortDownloader(); sequence = getData({timer: false}); diff --git a/js/messaging.js b/js/messaging.js index 8c215cac..ec51267c 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -72,6 +72,10 @@ const URLS = { url.startsWith(URLS.usoArchiveRaw) && parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]), + extractGreasyForkId: url => + /^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) && + RegExp.$1, + supported: url => ( url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) || url.startsWith('ftp') || diff --git a/manifest.json b/manifest.json index ad3859f2..a3fc105f 100644 --- a/manifest.json +++ b/manifest.json @@ -51,6 +51,7 @@ "background/icon-manager.js", "background/background.js", "background/usercss-helper.js", + "background/usercss-install-helper.js", "background/style-via-api.js", "background/search-db.js", "background/update.js",