Compare commits

...

3 Commits

Author SHA1 Message Date
tophf
7d094847f6 WIP2 2017-12-09 09:15:06 +03:00
tophf
62053316a2 WIP 2017-12-09 04:43:42 +03:00
tophf
aff4707bf0 use insertCSS in FF, declarativeContent in Chrome 2017-12-09 04:43:41 +03:00
5 changed files with 266 additions and 224 deletions

View File

@ -12,10 +12,11 @@ var browserCommands, contextMenus;
chrome.runtime.onMessage.addListener(onRuntimeMessage); chrome.runtime.onMessage.addListener(onRuntimeMessage);
{ {
const listener = const [listener] = [
URLS.chromeProtectsNTP [webNavigationListenerChrome, CHROME],
? webNavigationListenerChrome [webNavigationListenerFF, FIREFOX],
: webNavigationListener; [webNavigationListener, true],
].find(([, selected]) => selected);
chrome.webNavigation.onBeforeNavigate.addListener(data => chrome.webNavigation.onBeforeNavigate.addListener(data =>
listener(null, data)); listener(null, data));
@ -44,7 +45,6 @@ if (chrome.contextMenus) {
chrome.contextMenus.onClicked.addListener((info, tab) => chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab)); contextMenus[info.menuItemId].click(info, tab));
} }
if (chrome.commands) { if (chrome.commands) {
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
chrome.commands.onCommand.addListener(command => browserCommands[command]()); chrome.commands.onCommand.addListener(command => browserCommands[command]());
@ -81,6 +81,24 @@ prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {}));
browserUIlanguage: chrome.i18n.getUILanguage(), 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 // bind for 60 seconds max and auto-unbind if it's a normal run
chrome.runtime.onInstalled.addListener(onInstall); chrome.runtime.onInstalled.addListener(onInstall);
@ -175,9 +193,37 @@ window.addEventListener('storageReady', function _() {
updateIcon({id: undefined}, {}); updateIcon({id: undefined}, {});
if (FIREFOX) {
queryTabs().then(tabs =>
tabs.forEach(tab => {
if (!tab.width) {
// skip lazy-loaded tabs (width = 0) that seem to start loading on message
return;
}
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;
}
}));
}));
return;
}
const NTP = 'chrome://newtab/'; const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>'; const ALL_URLS = '<all_urls>';
const contentScripts = chrome.runtime.getManifest().content_scripts; const contentScripts = chrome.runtime.getManifest().content_scripts;
contentScripts.push({
js: ['content/apply.js'],
matches: ['<all_urls>'],
run_at: 'document_start',
match_about_blank: true,
all_frames: true
});
// expand * as .*? // expand * as .*?
const wildcardAsRegExp = (s, flags) => new RegExp( const wildcardAsRegExp = (s, flags) => new RegExp(
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
@ -209,13 +255,9 @@ window.addEventListener('storageReady', function _() {
}; };
queryTabs().then(tabs => queryTabs().then(tabs =>
tabs.forEach(tab => { tabs.forEach(tab => tab.width &&
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF contentScripts.forEach(cs =>
if (!FIREFOX || tab.width) { setTimeout(pingCS, 0, cs, tab))));
contentScripts.forEach(cs =>
setTimeout(pingCS, 0, cs, tab));
}
}));
}); });
// ************************************************************************* // *************************************************************************
@ -243,32 +285,60 @@ function webNavigationListener(method, {url, tabId, frameId}) {
function webNavigationListenerChrome(method, data) { function webNavigationListenerChrome(method, data) {
// Chrome 61.0.3161+ doesn't run content scripts on NTP const {tabId, frameId, url} = data;
if ( if (url.startsWith('https://www.google.') && url.includes('/_/chrome/newtab?')) {
!data.url.startsWith('https://www.google.') || // Chrome 61.0.3161+ doesn't run content scripts on NTP
!data.url.includes('/_/chrome/newtab?') 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;
//console.log(method, data);
if (frameId === 0 || url !== 'about:blank') {
if ((!method || method === 'styleApply') &&
styleViaAPI.getFrameUrl(tabId, frameId) !== url) {
styleViaAPI.cache.delete(tabId);
}
styleViaAPI.setFrameUrl(tabId, frameId, url);
webNavigationListener(method, data); webNavigationListener(method, data);
return; return;
} }
getTab(data.tabId).then(tab => { //const frames = styleViaAPI.allFrameUrls.get(tabId);
if (tab.url === 'chrome://newtab/') { //if (Object.keys(frames).length === 1) {
data.url = tab.url; // frames[frameId] = frames['0'];
} // webNavigationListener(method, data);
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);
//});
} }
function webNavUsercssInstallerFF(data) { function webNavUsercssInstallerFF(data) {
const {tabId} = data; const {tabId} = data;
Promise.all([ // we need tab index to open the installer next to the original one
sendMessage({tabId, method: 'ping'}), // and also to skip the double-invocation in FF which assigns tab url later
// we need tab index to open the installer next to the original one getTab(tabId).then(tab => {
// and also to skip the double-invocation in FF which assigns tab url later if (tab.url !== 'about:blank') {
getTab(tabId),
]).then(([pong, tab]) => {
if (pong !== true && tab.url !== 'about:blank') {
usercssHelper.openInstallPage(tab, {direct: true}); usercssHelper.openInstallPage(tab, {direct: true});
} }
}); });
@ -367,10 +437,6 @@ function onRuntimeMessage(request, sender, sendResponseInternal) {
.catch(() => sendResponse(false)); .catch(() => sendResponse(false));
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
case 'styleViaAPI':
styleViaAPI(request, sender);
return;
case 'download': case 'download':
download(request.url) download(request.url)
.then(sendResponse) .then(sendResponse)

View File

@ -1,230 +1,220 @@
/* global getStyles */ /* global getStyles */
'use strict'; 'use strict';
const styleViaAPI = !CHROME && (() => { // eslint-disable-next-line no-var
var styleViaAPI = !CHROME &&
(() => {
const ACTIONS = { const ACTIONS = {
styleApply, styleApply,
styleDeleted, styleDeleted,
styleUpdated, styleUpdated,
styleAdded, styleAdded,
styleReplaceAll, styleReplaceAll: styleApply,
prefChanged, prefChanged,
ping,
}; };
const NOP = Promise.resolve(new Error('NOP')); const NOP = Promise.resolve(new Error('NOP'));
const onError = () => {}; const PONG = Promise.resolve(true);
const onError = () => NOP;
/* <tabId>: Object
<frameId>: Object
url: String, non-enumerable
<styleId>: Array of strings
section code */
const cache = new Map(); const cache = new Map();
const allFrameUrls = new Map();
let observingTabs = false; chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onReplaced.addListener(onTabReplaced);
return (request, sender) => { return {
const action = ACTIONS[request.action]; process,
return !action ? NOP : getFrameUrl,
action(request, sender) setFrameUrl,
.catch(onError) allFrameUrls,
.then(maybeToggleObserver); cache,
}; };
function styleApply({id = null, ignoreUrlCheck}, {tab, frameId, url}) { //region public methods
if (prefs.get('disableAll')) {
function process(request, sender) {
console.log(request.action || request.method, request.prefs || request.styles || request.style, sender.tab, sender.frameId);
const action = ACTIONS[request.action || request.method];
if (!action) {
return NOP; return NOP;
} }
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId); const {tab} = sender;
if (id === null && !ignoreUrlCheck && frameStyles.url === url) { if (!isNaN(sender.frameId)) {
return NOP; const result = action(request, sender);
return result ? result.catch(onError) : NOP;
}
return browser.webNavigation.getAllFrames({tabId: tab.id}).then(frames =>
Promise.all((frames || []).map(({frameId}) =>
(action(request, {tab, frameId}) || NOP).catch(onError)))
).catch(onError);
}
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});
}
}
//endregion
//region actions
function styleApply({styles, disableAll}, sender) {
if (disableAll) {
return;
}
const {tab: {id: tabId}, frameId, url} = sender;
if (!styles || styles === 'DIY') {
return requestStyles({matchUrl: url || getFrameUrl(tabId, frameId)}, sender);
}
const {tabFrames, frameStyles} = getCachedData(tabId, frameId);
const newSorted = getSortedById(styles);
if (!sameArrays(frameStyles, newSorted, sameArrays)) {
tabFrames[frameId] = newSorted;
cache.set(tabId, tabFrames);
return replaceCSS(tabId, frameId, frameStyles, newSorted);
} }
return getStyles({id, matchUrl: url, enabled: true, asHash: true}).then(styles => {
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');
if (!code) {
delete frameStyles[styleId];
continue;
}
if (code === (frameStyles[styleId] || []).join('\n')) {
continue;
}
frameStyles[styleId] = styleSections;
tasks.push(
browser.tabs.insertCSS(tab.id, {
code,
frameId,
runAt: 'document_start',
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);
}
return Promise.all(tasks);
});
} }
function styleDeleted({id}, {tab, frameId}) { function styleDeleted({id}, {tab, frameId}) {
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id); const {frameStyles} = getCachedData(tab.id, frameId);
const code = styleSections.join('\n'); const index = frameStyles.findIndex(item => item.id === id);
if (code && !duplicateCodeExists({frameStyles, id, code})) { if (index >= 0) {
delete frameStyles[id]; const oldStyles = frameStyles.slice();
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles); frameStyles.splice(index, 1);
return removeCSS(tab.id, frameId, code); return replaceCSS(tab.id, frameId, oldStyles, frameStyles);
} else {
return NOP;
} }
} }
function styleUpdated({style}, sender) { function styleUpdated({style}, sender) {
if (!style.enabled) { return (style.enabled ? styleApply : styleDeleted)(style, sender);
return styleDeleted(style, sender);
}
const {tab, frameId} = sender;
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
const code = styleSections.join('\n');
return styleApply(style, sender).then(code && (() => {
if (!duplicateCodeExists({frameStyles, code, id: null})) {
return removeCSS(tab.id, frameId, code);
}
}));
} }
function styleAdded({style}, sender) { function styleAdded({style: {enabled}}, sender) {
return style.enabled ? styleApply(style, sender) : NOP; return enabled && styleApply({}, sender);
}
function styleReplaceAll(request, sender) {
const {tab, frameId} = sender;
const oldStylesCode = getFrameStylesJoined(sender);
return styleApply({ignoreUrlCheck: true}, sender).then(() => {
const newStylesCode = getFrameStylesJoined(sender);
const tasks = oldStylesCode
.filter(code => !newStylesCode.includes(code))
.map(code => removeCSS(tab.id, frameId, code));
return Promise.all(tasks);
});
} }
function prefChanged({prefs}, sender) { function prefChanged({prefs}, sender) {
if ('disableAll' in prefs) { if ('disableAll' in prefs) {
if (!prefs.disableAll) { disableAll(prefs.disableAll, sender);
return styleApply({}, sender); }
} }
function ping() {
return PONG;
}
//endregion
//region action helpers
function disableAll(state, sender) {
if (state) {
const {tab, frameId} = sender; const {tab, frameId} = sender;
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId); const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
if (isEmpty(frameStyles)) {
return NOP;
}
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
const tasks = Object.keys(frameStyles)
.map(id => removeCSS(tab.id, frameId, frameStyles[id].join('\n')));
return Promise.all(tasks);
} else {
return NOP;
}
}
/* utilities */
function maybeToggleObserver() {
let method;
if (!observingTabs && cache.size) {
method = 'addListener';
} else if (observingTabs && !cache.size) {
method = 'removeListener';
} else {
return;
}
observingTabs = !observingTabs;
chrome.webNavigation.onCommitted[method](onNavigationCommitted);
chrome.tabs.onRemoved[method](onTabRemoved);
chrome.tabs.onReplaced[method](onTabReplaced);
}
function onNavigationCommitted({tabId, frameId}) {
if (frameId === 0) {
onTabRemoved(tabId);
return;
}
const tabFrames = cache.get(tabId);
if (frameId in tabFrames) {
delete tabFrames[frameId]; delete tabFrames[frameId];
if (isEmpty(tabFrames)) { return removeCSS(tab.id, frameId, frameStyles);
onTabRemoved(tabId); } else {
} return styleApply({}, sender);
} }
} }
//endregion
//region observer
function onTabRemoved(tabId) { function onTabRemoved(tabId) {
cache.delete(tabId); cache.delete(tabId);
maybeToggleObserver();
} }
function onTabReplaced(addedTabId, removedTabId) { function onTabReplaced(addedTabId, removedTabId) {
onTabRemoved(removedTabId); cache.delete(removedTabId);
} }
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) { //endregion
if (isEmpty(frameStyles)) { //region browser API
delete tabFrames[frameId];
if (isEmpty(tabFrames)) { function replaceCSS(tabId, frameId, oldStyles, newStyles) {
cache.delete(tabId); console.log.apply(null, arguments);
return insertCSS(tabId, frameId, newStyles).then(() =>
removeCSS(tabId, frameId, oldStyles));
}
function insertCSS(tabId, frameId, frameStyles) {
const code = getFrameCode(frameStyles);
return !code ? NOP :
browser.tabs.insertCSS(tabId, {
code,
frameId,
runAt: 'document_start',
matchAboutBlank: true,
}).catch(onError);
}
function removeCSS(tabId, frameId, frameStyles) {
const code = getFrameCode(frameStyles);
return !code ? NOP :
browser.tabs.removeCSS(tabId, {
code,
frameId,
matchAboutBlank: true
}).catch(onError);
}
//endregion
//region utilities
function requestStyles(options, sender) {
options.matchUrl = options.matchUrl || sender.url;
options.enabled = true;
options.asHash = true;
return getStyles(options).then(styles =>
styleApply({styles}, sender));
}
function getSortedById(styleHash) {
const styles = [];
let needsSorting = false;
let prevKey = -1;
for (let k in styleHash) {
k = parseInt(k);
if (!isNaN(k)) {
const sections = styleHash[k].map(({code}) => code);
styles.push(sections);
defineProperty(sections, 'id', k);
needsSorting |= k < prevKey;
prevKey = k;
} }
return true;
} }
return needsSorting ? styles.sort((a, b) => a.id - b.id) : styles;
} }
function getCachedData(tabId, frameId, styleId) { function getCachedData(tabId, frameId, styleId) {
const tabFrames = cache.get(tabId) || {}; const tabFrames = cache.get(tabId) || {};
const frameStyles = tabFrames[frameId] || {}; const frameStyles = tabFrames[frameId] || [];
const styleSections = styleId && frameStyles[styleId] || []; const styleSections = styleId && frameStyles.find(s => s.id === styleId) || [];
return {tabFrames, frameStyles, styleSections}; return {tabFrames, frameStyles, styleSections};
} }
function getFrameStylesJoined({ function getFrameCode(frameStyles) {
tab, // we cache a shallow copy of code from the sections array in order to reuse references
frameId, // in other places whereas the combined string gets garbage-collected
frameStyles = getCachedData(tab.id, frameId).frameStyles, return typeof frameStyles === 'string' ? frameStyles : [].concat(...frameStyles).join('\n');
}) {
return Object.keys(frameStyles).map(id => frameStyles[id].join('\n'));
} }
function duplicateCodeExists({ function defineProperty(obj, name, value) {
tab, return Object.defineProperty(obj, name, {value, configurable: true});
frameId,
frameStyles = getCachedData(tab.id, frameId).frameStyles,
frameStylesCode = {},
id,
code = frameStylesCode[id] || frameStyles[id].join('\n'),
}) {
id = String(id);
for (const styleId in frameStyles) {
if (id !== styleId &&
code === (frameStylesCode[styleId] || frameStyles[styleId].join('\n'))) {
return true;
}
}
} }
function removeCSS(tabId, frameId, code) { function sameArrays(a, b, fn) {
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true}) return a.length === b.length && a.every((el, i) => fn ? fn(el, b[i]) : el === b[i]);
.catch(onError);
} }
function isEmpty(obj) { //endregion
for (const k in obj) {
return false;
}
return true;
}
})(); })();

View File

@ -23,10 +23,6 @@
} }
function requestStyles(options, callback = applyStyles) { function requestStyles(options, callback = applyStyles) {
if (!chrome.app && document instanceof XMLDocument) {
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
return;
}
var matchUrl = location.href; var matchUrl = location.href;
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
// dynamic about: and javascript: iframes don't have an URL yet // dynamic about: and javascript: iframes don't have an URL yet
@ -63,17 +59,6 @@
return; 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) { switch (request.method) {
case 'styleDeleted': case 'styleDeleted':
removeStyle(request); removeStyle(request);

View File

@ -150,6 +150,13 @@ function sendMessage(msg, callback) {
- enabled by passing a second param - enabled by passing a second param
*/ */
const {tabId, frameId} = msg; const {tabId, frameId} = msg;
if (tabId >= 0 && FIREFOX) {
// FF: reroute all tabs messages to styleViaAPI
const msgForBG = BG === window ? msg : BG.deepCopy(msg);
const sender = {tab: {id: tabId}, frameId};
const task = BG.styleViaAPI.process(msgForBG, sender);
return callback ? task.then(callback) : task;
}
const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage; const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage;
const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg]; const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg];
if (callback) { if (callback) {

View File

@ -16,6 +16,7 @@
"webNavigation", "webNavigation",
"contextMenus", "contextMenus",
"storage", "storage",
"declarativeContent",
"<all_urls>" "<all_urls>"
], ],
"background": { "background": {
@ -43,13 +44,6 @@
} }
}, },
"content_scripts": [ "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/*"], "matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
"run_at": "document_start", "run_at": "document_start",