use insertCSS in FF, declarativeContent in Chrome

This commit is contained in:
tophf 2017-11-25 03:19:30 +03:00
parent 03048ea98e
commit 8efce3220a
6 changed files with 161 additions and 81 deletions

View File

@ -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 = '<all_urls>';
const contentScripts = chrome.runtime.getManifest().content_scripts;
if (!FIREFOX) {
contentScripts.push({
js: ['content/apply.js'],
matches: ['<all_urls>'],
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)

View File

@ -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 = () => {};
/* <tabId>: Object
@ -19,18 +22,46 @@ const styleViaAPI = !CHROME && (() => {
<styleId>: 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,

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -16,6 +16,7 @@
"webNavigation",
"contextMenus",
"storage",
"declarativeContent",
"<all_urls>"
],
"background": {
@ -43,13 +44,6 @@
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"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",