Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e5e21c0a15 | ||
|
8efce3220a | ||
|
03048ea98e |
|
@ -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});
|
||||
}
|
||||
});
|
||||
|
@ -312,11 +372,13 @@ function updateIcon(tab, styles) {
|
|||
}
|
||||
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
getTab(tab.id).then(realTab => {
|
||||
// skip pre-rendered tabs
|
||||
if (realTab.index >= 0) {
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
}
|
||||
setTimeout(() => {
|
||||
getTab(tab.id).then(realTab => {
|
||||
// skip pre-rendered tabs
|
||||
if (realTab.index >= 0) {
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -355,10 +417,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)
|
||||
|
@ -371,7 +429,7 @@ function onRuntimeMessage(request, sender, sendResponseInternal) {
|
|||
|
||||
case 'closeTab':
|
||||
chrome.tabs.remove(request.tabId || sender.tab.id, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
if (chrome.runtime.lastError && request.tabId !== sender.tab.id) {
|
||||
sendResponse(new Error(chrome.runtime.lastError.message));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
@ -408,49 +415,42 @@ function deleteStyleSafe({id, notify = true} = {}) {
|
|||
}
|
||||
|
||||
|
||||
function download(url) {
|
||||
function download(url, {
|
||||
method = url.includes('?') ? 'POST' : 'GET',
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body = url.includes('?') ? url.slice(url.indexOf('?')) : null,
|
||||
timeout = 10e3,
|
||||
requiredStatusCode = 200,
|
||||
} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
url = new URL(url);
|
||||
const TIMEOUT = 10000;
|
||||
const options = {
|
||||
method: url.search ? 'POST' : 'GET',
|
||||
body: url.search ? url.search.slice(1) : null,
|
||||
headers: {
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
};
|
||||
if (url.protocol === 'file:' && FIREFOX) {
|
||||
// https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
|
||||
options.mode = 'same-origin';
|
||||
// FIXME: add FetchController when it is available.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FetchController/abort
|
||||
let timer;
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Fetch URL timeout: ${url.href}`));
|
||||
}, timeout);
|
||||
fetch(url.href, {mode: 'same-origin'})
|
||||
.then(r => {
|
||||
clearTimeout(timer);
|
||||
if (r.status !== 200) {
|
||||
throw r.status;
|
||||
}
|
||||
return r.text();
|
||||
return r.status === 200 ? r.text() : Promise.reject(r.status);
|
||||
})
|
||||
.then(resolve, reject);
|
||||
timer = setTimeout(
|
||||
() => reject(new Error(`Fetch URL timeout: ${url.href}`)),
|
||||
TIMEOUT
|
||||
);
|
||||
.catch(reject)
|
||||
.then(resolve);
|
||||
return;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = TIMEOUT;
|
||||
xhr.onload = () => (xhr.status === 200 || url.protocol === 'file:'
|
||||
? resolve(xhr.responseText)
|
||||
: reject(xhr.status));
|
||||
xhr.timeout = timeout;
|
||||
xhr.onload = () => (
|
||||
!requiredStatusCode || xhr.status === requiredStatusCode || url.protocol === 'file:' ?
|
||||
resolve(xhr.responseText) :
|
||||
reject(xhr.status));
|
||||
xhr.onerror = reject;
|
||||
xhr.open(options.method, url.href, true);
|
||||
for (const key of Object.keys(options.headers)) {
|
||||
xhr.setRequestHeader(key, options.headers[key]);
|
||||
}
|
||||
xhr.send(options.body);
|
||||
xhr.open(method, url.href, true);
|
||||
Object.keys(headers || {}).forEach(name => xhr.setRequestHeader(name, headers[name]));
|
||||
xhr.send(body);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -299,7 +299,7 @@ var prefs = new function Prefs() {
|
|||
|
||||
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
||||
function getSync() {
|
||||
if ('sync' in chrome.storage) {
|
||||
if ('sync' in chrome.storage && !chrome.runtime.id.includes('@temporary')) {
|
||||
return chrome.storage.sync;
|
||||
}
|
||||
const crappyStorage = {};
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue
Block a user