From 8efce3220a4591021c2acb51ec5f6d6b1d0a0d9a Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 25 Nov 2017 03:19:30 +0300 Subject: [PATCH] use insertCSS in FF, declarativeContent in Chrome --- background/background.js | 110 ++++++++++++++++++++++------- background/style-via-api.js | 100 ++++++++++++++++++-------- content/apply.js | 15 ---- install-usercss/install-usercss.js | 2 +- js/messaging.js | 7 ++ manifest.json | 8 +-- 6 files changed, 161 insertions(+), 81 deletions(-) diff --git a/background/background.js b/background/background.js index c8530050..af8127ee 100644 --- a/background/background.js +++ b/background/background.js @@ -12,10 +12,11 @@ var browserCommands, contextMenus; chrome.runtime.onMessage.addListener(onRuntimeMessage); { - const listener = - URLS.chromeProtectsNTP - ? webNavigationListenerChrome - : webNavigationListener; + const [listener] = [ + [webNavigationListenerChrome, CHROME], + [webNavigationListenerFF, FIREFOX], + [webNavigationListener, true], + ].find(([, selected]) => selected); chrome.webNavigation.onBeforeNavigate.addListener(data => listener(null, data)); @@ -44,7 +45,6 @@ if (chrome.contextMenus) { chrome.contextMenus.onClicked.addListener((info, tab) => contextMenus[info.menuItemId].click(info, tab)); } - if (chrome.commands) { // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 chrome.commands.onCommand.addListener(command => browserCommands[command]()); @@ -81,6 +81,24 @@ prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {})); browserUIlanguage: chrome.i18n.getUILanguage(), }); } + if (!FIREFOX && chrome.declarativeContent) { + chrome.declarativeContent.onPageChanged.removeRules(null, () => { + chrome.declarativeContent.onPageChanged.addRules([{ + conditions: [ + new chrome.declarativeContent.PageStateMatcher({ + pageUrl: {urlContains: ':'}, + }) + ], + actions: [ + new chrome.declarativeContent.RequestContentScript({ + js: ['/content/apply.js'], + allFrames: true, + matchAboutBlank: true, + }), + ], + }]); + }); + } }; // bind for 60 seconds max and auto-unbind if it's a normal run chrome.runtime.onInstalled.addListener(onInstall); @@ -168,6 +186,15 @@ window.addEventListener('storageReady', function _() { const NTP = 'chrome://newtab/'; const ALL_URLS = ''; const contentScripts = chrome.runtime.getManifest().content_scripts; + if (!FIREFOX) { + contentScripts.push({ + js: ['content/apply.js'], + matches: [''], + run_at: 'document_start', + match_about_blank: true, + all_frames: true + }); + } // expand * as .*? const wildcardAsRegExp = (s, flags) => new RegExp( s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') @@ -200,8 +227,18 @@ window.addEventListener('storageReady', function _() { queryTabs().then(tabs => tabs.forEach(tab => { - // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF - if (!FIREFOX || tab.width) { + if (FIREFOX) { + const tabId = tab.id; + const frameUrls = {'0': tab.url}; + styleViaAPI.allFrameUrls.set(tabId, frameUrls); + chrome.webNavigation.getAllFrames({tabId}, frames => frames && + frames.forEach(({frameId, parentFrameId, url}) => { + if (frameId) { + frameUrls[frameId] = url === 'about:blank' ? frameUrls[parentFrameId] : url; + } + })); + } else if (tab.width) { + // skip lazy-loaded aka unloaded tabs that seem to start loading on message contentScripts.forEach(cs => setTimeout(pingCS, 0, cs, tab)); } @@ -233,18 +270,44 @@ function webNavigationListener(method, {url, tabId, frameId}) { 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?') - ) { + const {tabId, frameId, url} = data; + if (url.startsWith('https://www.google.') && url.includes('/_/chrome/newtab?')) { + // Chrome 61.0.3161+ doesn't run content scripts on NTP + getTab(tabId).then(tab => { + data.url = tab.url === 'chrome://newtab/' ? tab.url : url; + webNavigationListener(method, data); + }); + } else { + webNavigationListener(method, data); + // chrome.declarativeContent doesn't inject scripts in about:blank iframes + if (method && frameId && url === 'about:blank') { + chrome.tabs.executeScript(tabId, { + file: '/content/apply.js', + runAt: 'document_start', + matchAboutBlank: true, + frameId, + }, ignoreChromeError); + } + } +} + + +function webNavigationListenerFF(method, data) { + const {tabId, frameId, url} = data; + if (url !== 'about:blank' || !frameId) { + styleViaAPI.setFrameUrl(tabId, frameId, url); webNavigationListener(method, data); return; } - getTab(data.tabId).then(tab => { - if (tab.url === 'chrome://newtab/') { - data.url = tab.url; - } + const frames = styleViaAPI.allFrameUrls.get(tabId); + if (Object.keys(frames).length === 1) { + frames[frameId] = frames['0']; + webNavigationListener(method, data); + return; + } + chrome.webNavigation.getFrame({tabId, frameId}, info => { + const hasParent = !chrome.runtime.lastError && info.parentFrameId >= 0; + frames[frameId] = hasParent ? frames[info.parentFrameId] : url; webNavigationListener(method, data); }); } @@ -252,13 +315,10 @@ function webNavigationListenerChrome(method, data) { function webNavUsercssInstallerFF(data) { const {tabId} = data; - Promise.all([ - sendMessage({tabId, method: 'ping'}), - // we need tab index to open the installer next to the original one - // and also to skip the double-invocation in FF which assigns tab url later - getTab(tabId), - ]).then(([pong, tab]) => { - if (pong !== true && tab.url !== 'about:blank') { + // we need tab index to open the installer next to the original one + // and also to skip the double-invocation in FF which assigns tab url later + getTab(tabId).then(tab => { + if (tab.url !== 'about:blank') { usercssHelper.openInstallPage(tab, {direct: true}); } }); @@ -355,10 +415,6 @@ function onRuntimeMessage(request, sender, sendResponseInternal) { .catch(() => sendResponse(false)); return KEEP_CHANNEL_OPEN; - case 'styleViaAPI': - styleViaAPI(request, sender); - return; - case 'download': download(request.url) .then(sendResponse) diff --git a/background/style-via-api.js b/background/style-via-api.js index 8bdde4b9..7640daed 100644 --- a/background/style-via-api.js +++ b/background/style-via-api.js @@ -1,7 +1,8 @@ /* global getStyles */ 'use strict'; -const styleViaAPI = !CHROME && (() => { +// eslint-disable-next-line no-var +var styleViaAPI = !CHROME && (() => { const ACTIONS = { styleApply, styleDeleted, @@ -9,8 +10,10 @@ const styleViaAPI = !CHROME && (() => { styleAdded, styleReplaceAll, prefChanged, + ping, }; const NOP = Promise.resolve(new Error('NOP')); + const PONG = Promise.resolve(true); const onError = () => {}; /* : Object @@ -19,18 +22,46 @@ const styleViaAPI = !CHROME && (() => { : Array of strings section code */ const cache = new Map(); + const allFrameUrls = new Map(); let observingTabs = false; - return (request, sender) => { - const action = ACTIONS[request.action]; - return !action ? NOP : - action(request, sender) - .catch(onError) - .then(maybeToggleObserver); + return { + process, + getFrameUrl, + setFrameUrl, + allFrameUrls, + cache, }; - function styleApply({id = null, ignoreUrlCheck}, {tab, frameId, url}) { + function process(request, sender) { + const action = ACTIONS[request.action || request.method]; + if (!action) { + return NOP; + } + const {frameId, tab: {id: tabId}} = sender; + if (isNaN(frameId)) { + const frameIds = Object.keys(allFrameUrls.get(tabId) || {}); + if (frameIds.length > 1) { + return Promise.all( + frameIds.map(frameId => + process(request, Object.assign({}, sender, {frameId: Number(frameId)})))); + } + sender.frameId = 0; + } + return action(request, sender) + .catch(onError) + .then(maybeToggleObserver); + } + + function styleApply({ + id = null, + ignoreUrlCheck, + }, { + tab, + frameId, + url = getFrameUrl(tab.id, frameId), + }) { if (prefs.get('disableAll')) { return NOP; } @@ -64,22 +95,20 @@ const styleViaAPI = !CHROME && (() => { matchAboutBlank: true, }).catch(onError)); } - if (!removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles)) { - Object.defineProperty(frameStyles, 'url', {value: url, configurable: true}); - tabFrames[frameId] = frameStyles; - cache.set(tab.id, tabFrames); - } + Object.defineProperty(frameStyles, 'url', {value: url, configurable: true}); + tabFrames[frameId] = frameStyles; + cache.set(tab.id, tabFrames); return Promise.all(tasks); }); } function styleDeleted({id}, {tab, frameId}) { - const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id); + const {frameStyles, styleSections} = getCachedData(tab.id, frameId, id); const code = styleSections.join('\n'); if (code && !duplicateCodeExists({frameStyles, id, code})) { - delete frameStyles[id]; - removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles); - return removeCSS(tab.id, frameId, code); + return removeCSS(tab.id, frameId, code).then(() => { + delete frameStyles[id]; + }); } else { return NOP; } @@ -125,7 +154,7 @@ const styleViaAPI = !CHROME && (() => { if (isEmpty(frameStyles)) { return NOP; } - removeFrameIfEmpty(tab.id, frameId, tabFrames, {}); + delete tabFrames[frameId]; const tasks = Object.keys(frameStyles) .map(id => removeCSS(tab.id, frameId, frameStyles[id].join('\n'))); return Promise.all(tasks); @@ -134,21 +163,26 @@ const styleViaAPI = !CHROME && (() => { } } + function ping() { + return PONG; + } + /* utilities */ - function maybeToggleObserver() { + function maybeToggleObserver(passthru) { let method; if (!observingTabs && cache.size) { method = 'addListener'; } else if (observingTabs && !cache.size) { method = 'removeListener'; } else { - return; + return passthru; } observingTabs = !observingTabs; chrome.webNavigation.onCommitted[method](onNavigationCommitted); chrome.tabs.onRemoved[method](onTabRemoved); chrome.tabs.onReplaced[method](onTabReplaced); + return passthru; } function onNavigationCommitted({tabId, frameId}) { @@ -157,7 +191,7 @@ const styleViaAPI = !CHROME && (() => { return; } const tabFrames = cache.get(tabId); - if (frameId in tabFrames) { + if (tabFrames && frameId in tabFrames) { delete tabFrames[frameId]; if (isEmpty(tabFrames)) { onTabRemoved(tabId); @@ -174,16 +208,6 @@ const styleViaAPI = !CHROME && (() => { onTabRemoved(removedTabId); } - function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) { - if (isEmpty(frameStyles)) { - delete tabFrames[frameId]; - if (isEmpty(tabFrames)) { - cache.delete(tabId); - } - return true; - } - } - function getCachedData(tabId, frameId, styleId) { const tabFrames = cache.get(tabId) || {}; const frameStyles = tabFrames[frameId] || {}; @@ -191,6 +215,20 @@ const styleViaAPI = !CHROME && (() => { return {tabFrames, frameStyles, styleSections}; } + function getFrameUrl(tabId, frameId = 0) { + const frameUrls = allFrameUrls.get(tabId); + return frameUrls && frameUrls[frameId] || ''; + } + + function setFrameUrl(tabId, frameId, url) { + const frameUrls = allFrameUrls.get(tabId); + if (frameUrls) { + frameUrls[frameId] = url; + } else { + allFrameUrls.set(tabId, {[frameId]: url}); + } + } + function getFrameStylesJoined({ tab, frameId, diff --git a/content/apply.js b/content/apply.js index e2f52124..372d5261 100644 --- a/content/apply.js +++ b/content/apply.js @@ -23,10 +23,6 @@ } function requestStyles(options, callback = applyStyles) { - if (!chrome.app && document instanceof XMLDocument) { - chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'}); - return; - } var matchUrl = location.href; if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { // dynamic about: and javascript: iframes don't have an URL yet @@ -63,17 +59,6 @@ return; } - if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') { - request.action = request.method; - request.method = 'styleViaAPI'; - request.styles = null; - if (request.style) { - request.style.sections = null; - } - chrome.runtime.sendMessage(request); - return; - } - switch (request.method) { case 'styleDeleted': removeStyle(request); diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index ede62830..51502fad 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -110,7 +110,7 @@ $('.header').classList.add('meta-init'); $('.header').classList.remove('meta-init-error'); - setTimeout(() => $('.lds-spinner').remove(), 1000); + setTimeout(() => $('.lds-spinner') && $('.lds-spinner').remove(), 1000); showError(''); requestAnimationFrame(adjustCodeHeight); diff --git a/js/messaging.js b/js/messaging.js index d78e64de..5dff03be 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -144,6 +144,13 @@ function sendMessage(msg, callback) { - enabled by passing a second param */ const {tabId, frameId} = msg; + if (tabId >= 0 && FIREFOX) { + // FF: reroute all tabs messages to styleViaAPI + const msgInBG = BG === window ? msg : BG.deepCopy(msg); + const sender = {tab: {id: tabId}, frameId}; + const task = BG.styleViaAPI.process(msgInBG, sender); + return callback ? task.then(callback) : task; + } const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage; const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg]; if (callback) { diff --git a/manifest.json b/manifest.json index a54c9035..e992222c 100644 --- a/manifest.json +++ b/manifest.json @@ -16,6 +16,7 @@ "webNavigation", "contextMenus", "storage", + "declarativeContent", "" ], "background": { @@ -43,13 +44,6 @@ } }, "content_scripts": [ - { - "matches": [""], - "run_at": "document_start", - "all_frames": true, - "match_about_blank": true, - "js": ["content/apply.js"] - }, { "matches": ["http://userstyles.org/*", "https://userstyles.org/*"], "run_at": "document_start",