diff --git a/background/background.js b/background/background.js index 14619543..826a2481 100644 --- a/background/background.js +++ b/background/background.js @@ -38,13 +38,14 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { // in the foreground thus auto-closing the popup (in Chrome) openURL, + // FIXME: who use this? closeTab: (msg, sender, respond) => { chrome.tabs.remove(msg.tabId || sender.tab.id, () => { if (chrome.runtime.lastError && msg.tabId !== sender.tab.id) { respond(new Error(chrome.runtime.lastError.message)); } }); - return KEEP_CHANNEL_OPEN; + return true; }, optionsCustomizeHotkeys() { @@ -71,6 +72,7 @@ if (FIREFOX) { const frameId = port.sender.frameId; const options = tryJSONparse(port.name.slice(MSG_GET_STYLES_LEN)); port.disconnect(); + // FIXME: getStylesFallback? getStyles(options).then(styles => { if (!styles.length) return; chrome.tabs.executeScript(tabId, { @@ -87,39 +89,28 @@ if (FIREFOX) { }); } -{ - const listener = - URLS.chromeProtectsNTP - ? webNavigationListenerChrome - : webNavigationListener; - - chrome.webNavigation.onBeforeNavigate.addListener(data => - listener(null, data)); - - chrome.webNavigation.onCommitted.addListener(data => - listener('styleApply', data)); - - chrome.webNavigation.onHistoryStateUpdated.addListener(data => - listener('styleReplaceAll', data)); - - chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => - listener('styleReplaceAll', data)); - - if (FIREFOX) { - // FF applies page CSP even to content scripts, https://bugzil.la/1267027 - chrome.webNavigation.onCommitted.addListener(webNavUsercssInstallerFF, { - url: [ - {hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'}, - {hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'}, - ] - }); - // FF misses some about:blank iframes so we inject our content script explicitly - chrome.webNavigation.onDOMContentLoaded.addListener(webNavIframeHelperFF, { - url: [ - {urlEquals: 'about:blank'}, - ] - }); +navigatorUtil.onUrlChange(({tabId, frameId}, type) => { + if (type === 'committed') { + // styles would be updated when content script is injected. + return; } + msg.sendTab(tabId, {method: 'urlChanged'}, {frameId}); +}); + +if (FIREFOX) { + // FF applies page CSP even to content scripts, https://bugzil.la/1267027 + navigatorUtil.onCommitted(webNavUsercssInstallerFF, { + url: [ + {hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'}, + {hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'}, + ] + }); + // FF misses some about:blank iframes so we inject our content script explicitly + navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { + url: [ + {urlEquals: 'about:blank'}, + ] + }); } if (chrome.contextMenus) { @@ -149,6 +140,8 @@ prefs.subscribe(['iconset'], () => styles: {}, })); +chrome.navigator. + // ************************************************************************* chrome.runtime.onInstalled.addListener(({reason}) => { if (reason !== 'update') return; @@ -297,74 +290,7 @@ window.addEventListener('storageReady', function _() { })); }); -// ************************************************************************* -{ - const getStylesForFrame = (msg, sender) => { - const stylesTask = getStyles(msg); - if (!sender || !sender.frameId) return stylesTask; - return Promise.all([ - stylesTask, - getTab(sender.tab.id), - ]).then(([styles, tab]) => { - if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1'); - return styles; - }); - }; - const updateAPI = (_, enabled) => { - window.API_METHODS.getStylesForFrame = enabled ? getStylesForFrame : getStyles; - }; - prefs.subscribe(['exposeIframes'], updateAPI); - updateAPI(null, prefs.readOnlyValues.exposeIframes); -} - -// ************************************************************************* - -function webNavigationListener(method, {url, tabId, frameId}) { - Promise.all([ - getStyles({matchUrl: url, asHash: true}), - frameId && prefs.readOnlyValues.exposeIframes && getTab(tabId), - ]).then(([styles, tab]) => { - if (method && URLS.supported(url) && tabId >= 0) { - if (method === 'styleApply') { - handleCssTransitionBug({tabId, frameId, url, styles}); - } - if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1'); - msg.sendTab( - tabId, - { - method, - // ping own page so it retrieves the styles directly - styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles, - }, - {frameId} - ); - } - // main page frame id is 0 - if (frameId === 0) { - tabIcons.delete(tabId); - updateIcon({tab: {id: tabId, url}, styles}); - } - }); -} - - -function webNavigationListenerChrome(method, data) { - // Chrome 61.0.3161+ doesn't run content scripts on NTP - if ( - !data.url.startsWith('https://www.google.') || - !data.url.includes('/_/chrome/newtab?') - ) { - webNavigationListener(method, data); - return; - } - getTab(data.tabId).then(tab => { - if (tab.url === 'chrome://newtab/') { - data.url = tab.url; - } - webNavigationListener(method, data); - }); -} - +// FIXME: implement exposeIframes in apply.js function webNavUsercssInstallerFF(data) { const {tabId} = data; diff --git a/background/navigator-util.js b/background/navigator-util.js new file mode 100644 index 00000000..568527c1 --- /dev/null +++ b/background/navigator-util.js @@ -0,0 +1,68 @@ +'use strict'; + +const navigatorUtil = (() => { + const handler = { + urlChange: null + }; + let listeners; + const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs)); + return extendNative({onUrlChange}); + + function onUrlChange(fn) { + initUrlChange(); + handler.urlChange.push(fn); + } + + function initUrlChange() { + if (!handler.urlChange) { + return; + } + handler.urlChange = []; + + chrome.webNavigation.onCommitted.addListener(data => + fixNTPUrl(data) + .then(() => executeCallbacks(handler.urlChange, data, 'committed')); + + chrome.webNavigation.onHistoryStateUpdated.addListener(data => + fixNTPUrl(data) + .then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated')); + + chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => + fixNTPUrl(data) + .then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated')); + } + + function fixNTPUrl(data) { + if ( + !CHROME || + !URLS.chromeProtectsNTP || + !data.url.startsWith('https://www.google.') || + !data.url.includes('/_/chrome/newtab?') + ) { + return Promise.resolve(); + } + return tabGet(data.tabId) + .then(tab => { + if (tab.url === 'chrome://newtab/') { + data.url = tab.url; + } + }); + } + + function executeCallbacks(callbacks, data, type) { + for (const cb of callbacks) { + cb(data, type); + } + } + + function extendNative(target) { + return new Proxy(target, { + get: (target, prop) => { + if (target[prop]) { + return target[prop]; + } + return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]); + } + }); + } +})(); diff --git a/background/refresh-all-tabs.js b/background/refresh-all-tabs.js deleted file mode 100644 index c7fcf3bb..00000000 --- a/background/refresh-all-tabs.js +++ /dev/null @@ -1,226 +0,0 @@ -/* -global API_METHODS cachedStyles -global getStyles filterStyles invalidateCache normalizeStyleSections -global updateIcon -*/ -'use strict'; - -(() => { - const previewFromTabs = new Map(); - - /** - * When style id and state is provided, only that style is propagated. - * Otherwise all styles are replaced and the toolbar icon is updated. - * @param {Object} [msg] - * @param {{id:Number, enabled?:Boolean, sections?: (Array|String)}} [msg.style] - - * style to propagate - * @param {Boolean} [msg.codeIsUpdated] - * @returns {Promise} - */ - API_METHODS.refreshAllTabs = (msg = {}) => - Promise.all([ - queryTabs(), - maybeParseUsercss(msg), - getStyles(), - ]).then(([tabs, style]) => - new Promise(resolve => { - if (style) msg.style.sections = normalizeStyleSections(style); - run(tabs, msg, resolve); - })); - - - function run(tabs, msg, resolve) { - const {style, codeIsUpdated, refreshOwnTabs} = msg; - - // the style was updated/saved so we need to remove the old copy of the original style - if (msg.method === 'styleUpdated' && msg.reason !== 'editPreview') { - for (const [tabId, original] of previewFromTabs.entries()) { - if (style.id === original.id) { - previewFromTabs.delete(tabId); - } - } - if (!previewFromTabs.size) { - unregisterTabListeners(); - } - } - - if (!style) { - msg = {method: 'styleReplaceAll'}; - - // live preview puts the code in cachedStyles, saves the original in previewFromTabs, - // and if preview is being disabled, but the style is already deleted, we bail out - } else if (msg.reason === 'editPreview' && !updateCache(msg)) { - return; - - // simple style update: - // * if disabled, apply.js will remove the element - // * if toggled and code is unchanged, apply.js will toggle the element - } else if (!style.enabled || codeIsUpdated === false) { - msg = { - method: 'styleUpdated', - reason: msg.reason, - style: { - id: style.id, - enabled: style.enabled, - }, - codeIsUpdated, - }; - - // live preview normal operation, the new code is already in cachedStyles - } else { - msg.method = 'styleApply'; - msg.style = {id: msg.style.id}; - } - - if (!tabs || !tabs.length) { - resolve(); - return; - } - - const last = tabs[tabs.length - 1]; - for (const tab of tabs) { - if (FIREFOX && !tab.width) continue; - if (refreshOwnTabs === false && tab.url.startsWith(URLS.ownOrigin)) continue; - chrome.webNavigation.getAllFrames({tabId: tab.id}, frames => - refreshFrame(tab, frames, msg, tab === last && resolve)); - } - } - - function refreshFrame(tab, frames, msg, resolve) { - ignoreChromeError(); - if (!frames || !frames.length) { - frames = [{ - frameId: 0, - url: tab.url, - }]; - } - msg.tabId = tab.id; - const styleId = msg.style && msg.style.id; - - for (const frame of frames) { - - const styles = filterStyles({ - matchUrl: getFrameUrl(frame, frames), - asHash: true, - id: styleId, - }); - - msg = Object.assign({}, msg); - msg.frameId = frame.frameId; - - if (msg.method !== 'styleUpdated') { - msg.styles = styles; - } - - if (msg.method === 'styleApply' && !styles.length) { - // remove the style from a previously matching frame - invokeOrPostpone(tab.active, sendMessage, { - method: 'styleUpdated', - reason: 'editPreview', - style: { - id: styleId, - enabled: false, - }, - tabId: tab.id, - frameId: frame.frameId, - }, ignoreChromeError); - } else { - invokeOrPostpone(tab.active, sendMessage, msg, ignoreChromeError); - } - - if (!frame.frameId) { - setTimeout(updateIcon, 0, { - tab, - styles: msg.method === 'styleReplaceAll' ? styles : undefined, - }); - } - } - - if (resolve) resolve(); - } - - - function getFrameUrl(frame, frames) { - while (frame.url === 'about:blank' && frame.frameId > 0) { - const parent = frames.find(f => f.frameId === frame.parentFrameId); - if (!parent) break; - frame.url = parent.url; - frame = parent; - } - return (frame || frames[0]).url; - } - - - function maybeParseUsercss({style}) { - if (style && typeof style.sections === 'string') { - return API_METHODS.parseUsercss({sourceCode: style.sections}); - } - } - - - function updateCache(msg) { - const {style, tabId, restoring} = msg; - const spoofed = !restoring && previewFromTabs.get(tabId); - const original = cachedStyles.byId.get(style.id); - - if (style.sections && !restoring) { - if (!previewFromTabs.size) { - registerTabListeners(); - } - if (!spoofed) { - previewFromTabs.set(tabId, Object.assign({}, original)); - } - - } else { - previewFromTabs.delete(tabId); - if (!previewFromTabs.size) { - unregisterTabListeners(); - } - if (!original) { - return; - } - if (!restoring) { - msg.style = spoofed || original; - } - } - invalidateCache({updated: msg.style}); - return true; - } - - - function registerTabListeners() { - chrome.tabs.onRemoved.addListener(onTabRemoved); - chrome.tabs.onReplaced.addListener(onTabReplaced); - chrome.webNavigation.onCommitted.addListener(onTabNavigated); - } - - - function unregisterTabListeners() { - chrome.tabs.onRemoved.removeListener(onTabRemoved); - chrome.tabs.onReplaced.removeListener(onTabReplaced); - chrome.webNavigation.onCommitted.removeListener(onTabNavigated); - } - - - function onTabRemoved(tabId) { - const style = previewFromTabs.get(tabId); - if (style) { - API_METHODS.refreshAllTabs({ - style, - tabId, - reason: 'editPreview', - restoring: true, - }); - } - } - - - function onTabReplaced(addedTabId, removedTabId) { - onTabRemoved(removedTabId); - } - - - function onTabNavigated({tabId}) { - onTabRemoved(tabId); - } -})(); diff --git a/background/style-manager.js b/background/style-manager.js index a28ac9c7..bcd4af6b 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -154,11 +154,12 @@ const styleManager = (() => { } function ensurePrepared(methods) { - for (const [name, fn] in Object.entries(methods)) { - methods[name] = (...args) => + const prepared = {}; + for (const [name, fn] of Object.entries(methods)) { + prepared[name] = (...args) => preparing.then(() => fn(...args)); } - return methods; + return prepared; } function createNewStyle() { diff --git a/background/style-via-api.js b/background/style-via-api.js index 0213caf1..e895e6be 100644 --- a/background/style-via-api.js +++ b/background/style-via-api.js @@ -38,16 +38,14 @@ API_METHODS.styleViaAPI = !CHROME && (() => { if (id === null && !ignoreUrlCheck && frameStyles.url === url) { return NOP; } - return getStyles({id, matchUrl: url, asHash: true}).then(styles => { + const filter = {enabled: true}; + if (id !== null) { + filter.id = id; + } + return styleManager.getSectionsByUrl(url, filter).then(sections => { const tasks = []; - for (const styleId in styles) { - if (isNaN(parseInt(styleId))) { - continue; - } - // shallow-extract code from the sections array in order to reuse references - // in other places whereas the combined string gets garbage-collected - const styleSections = styles[styleId].map(section => section.code); - const code = styleSections.join('\n'); + for (const section of Object.values(sections)) { + const code = section.code; if (!code) { delete frameStyles[styleId]; continue; @@ -55,7 +53,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => { if (code === (frameStyles[styleId] || []).join('\n')) { continue; } - frameStyles[styleId] = styleSections; + frameStyles[styleId] = [code]; tasks.push( browser.tabs.insertCSS(tab.id, { code, diff --git a/background/update.js b/background/update.js index 8426035e..ceb604ac 100644 --- a/background/update.js +++ b/background/update.js @@ -51,7 +51,7 @@ global API_METHODS checkingAll = true; retrying.clear(); const port = observe && chrome.runtime.connect({name: 'updater'}); - return getStyles({}).then(styles => { + return styleManager.getAllStyles().then(styles => { styles = styles.filter(style => style.updateUrl); if (port) port.postMessage({count: styles.length}); log(''); diff --git a/content/apply.js b/content/apply.js index 3a1c0b75..038da078 100644 --- a/content/apply.js +++ b/content/apply.js @@ -147,6 +147,10 @@ } break; + case 'urlChanged': + // TODO + break; + case 'ping': return true; } diff --git a/js/messaging.js b/js/messaging.js index 8b5fa2ba..4b989b90 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -5,9 +5,6 @@ global onRuntimeMessage applyOnMessage */ 'use strict'; -// keep message channel open for sendResponse in chrome.runtime.onMessage listener -const KEEP_CHANNEL_OPEN = true; - const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]); const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]); const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi'); @@ -72,14 +69,9 @@ const URLS = { ), }; -let BG = chrome.extension.getBackgroundPage(); -if (BG && !BG.getStyles && BG !== window) { - // own page like editor/manage is being loaded on browser startup - // before the background page has been fully initialized; - // it'll be resolved in onBackgroundReady() instead - BG = null; -} -if (!BG || BG !== window) { +const IS_BG = chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window; + +if (!IS_BG) { if (FIREFOX) { document.documentElement.classList.add('firefox'); } else if (OPERA) { @@ -93,8 +85,10 @@ if (!BG || BG !== window) { getActiveTab().then(tab => window.API.updateIcon({tab})); } -} else if (!BG.API_METHODS) { - BG.API_METHODS = {}; +} + +if (IS_BG) { + window.API_METHODS = {}; } const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage); @@ -104,32 +98,6 @@ if (FIREFOX_NO_DOM_STORAGE) { Object.defineProperty(window, 'sessionStorage', {value: {}}); } -function sendMessage(msg, callback) { - /* - Promise mode [default]: - - rejects on receiving {__ERROR__: message} created by background.js::onRuntimeMessage - - automatically suppresses chrome.runtime.lastError because it's autogenerated - by browserAction.setText which lacks a callback param in chrome API - Standard callback mode: - - enabled by passing a second param - */ - const {tabId, frameId} = msg; - const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage; - const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg]; - if (callback) { - fn(...args, callback); - } else { - return new Promise((resolve, reject) => { - fn(...args, r => { - const err = r && r.__ERROR__; - (err ? reject : resolve)(err || r); - ignoreChromeError(); - }); - }); - } -} - - function queryTabs(options = {}) { return new Promise(resolve => chrome.tabs.query(options, tabs => diff --git a/js/prefs.js b/js/prefs.js index ec6392e9..7d6c8e87 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -3,6 +3,7 @@ // eslint-disable-next-line no-var var prefs = new function Prefs() { + const BG = undefined; const defaults = { 'openEditInWindow': false, // new editor opens in a own browser window 'windowPosition': {}, // detached window position diff --git a/manifest.json b/manifest.json index 31840ba8..671946c2 100644 --- a/manifest.json +++ b/manifest.json @@ -36,12 +36,12 @@ "js/cache.js", "background/db.js", "background/style-manager.js", + "background/navigator-util.js", "background/background.js", "background/usercss-helper.js", "background/style-via-api.js", "background/search-db.js", "background/update.js", - "background/refresh-all-tabs.js", "background/openusercss-api.js", "vendor/semver-bundle/semver.js", "vendor-overwrites/colorpicker/colorconverter.js"