FF: support private/container tabs

This commit is contained in:
tophf 2018-01-01 20:02:49 +03:00
parent 62e333a0ba
commit 3418ac9cb9
33 changed files with 1054 additions and 859 deletions

View File

@ -13,9 +13,11 @@ globals:
KEEP_CHANNEL_OPEN: false
CHROME: false
FIREFOX: false
VIVALDI: false
OPERA: false
URLS: false
BG: false
API: false
notifyAllTabs: false
sendMessage: false
queryTabs: false
@ -33,10 +35,6 @@ globals:
tryJSONparse: false
debounce: false
deepCopy: false
onBackgroundReady: false
deleteStyleSafe: false
getStylesSafe: false
saveStyleSafe: false
sessionStorageHash: false
download: false
invokeOrPostpone: false
@ -63,6 +61,10 @@ globals:
# prefs.js
prefs: false
setupLivePrefs: false
# storage-util.js
chromeLocal: false
chromeSync: false
LZString: false
rules:
accessor-pairs: [2]

View File

@ -1,9 +1,38 @@
/* global dbExec, getStyles, saveStyle */
/* global handleCssTransitionBug */
/* global usercssHelper openEditor */
/* global styleViaAPI */
/*
global dbExec getStyles saveStyle deleteStyle
global handleCssTransitionBug detectSloppyRegexps
global openEditor
global styleViaAPI
global loadScript
global updater
*/
'use strict';
// eslint-disable-next-line no-var
var API_METHODS = {
getStyles,
saveStyle,
deleteStyle,
download: msg => download(msg.url),
getPrefs: () => prefs.getAll(),
healthCheck: () => dbExec().then(() => true),
detectSloppyRegexps,
openEditor,
updateIcon,
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;
},
};
// eslint-disable-next-line no-var
var browserCommands, contextMenus;
@ -55,9 +84,17 @@ if (!chrome.browserAction ||
window.updateIcon = () => {};
}
const tabIcons = new Map();
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed));
// *************************************************************************
// set the default icon displayed after a tab is created until webNavigation kicks in
prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {}));
prefs.subscribe(['iconset'], () =>
updateIcon({
tab: {id: undefined},
styles: {},
}));
// *************************************************************************
{
@ -160,7 +197,10 @@ if (chrome.contextMenus) {
window.addEventListener('storageReady', function _() {
window.removeEventListener('storageReady', _);
updateIcon({id: undefined}, {});
updateIcon({
tab: {id: undefined},
styles: {},
});
const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>';
@ -223,7 +263,8 @@ function webNavigationListener(method, {url, tabId, frameId}) {
}
// main page frame id is 0
if (frameId === 0) {
updateIcon({id: tabId, url}, styles);
tabIcons.delete(tabId);
updateIcon({tab: {id: tabId, url}, styles});
}
});
}
@ -256,13 +297,13 @@ function webNavUsercssInstallerFF(data) {
getTab(tabId),
]).then(([pong, tab]) => {
if (pong !== true && tab.url !== 'about:blank') {
usercssHelper.openInstallPage(tab, {direct: true});
API_METHODS.installUsercss({direct: true}, {tab});
}
});
}
function updateIcon(tab, styles) {
function updateIcon({tab, styles}) {
if (tab.id < 0) {
return;
}
@ -277,38 +318,44 @@ function updateIcon(tab, styles) {
.then(url => getStyles({matchUrl: url, enabled: true, asHash: true}))
.then(stylesReceived);
function countStyles(styles) {
if (Array.isArray(styles)) return styles.length;
return Object.keys(styles).reduce((sum, id) => sum + !isNaN(Number(id)), 0);
}
function stylesReceived(styles) {
let numStyles = styles.length;
if (numStyles === undefined) {
// for 'styles' asHash:true fake the length by counting numeric ids manually
numStyles = 0;
for (const id of Object.keys(styles)) {
numStyles += id.match(/^\d+$/) ? 1 : 0;
}
}
const numStyles = countStyles(styles);
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : '';
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
const path = 'images/icon/' + iconset;
chrome.browserAction.setIcon({
tabId: tab.id,
path: {
const tabIcon = tabIcons.get(tab.id) || {};
if (tabIcon.iconType !== iconset + postfix) {
tabIcons.set(tab.id, tabIcon);
tabIcon.iconType = iconset + postfix;
const paths = {};
if (FIREFOX || CHROME >= 2883 && !VIVALDI) {
// Material Design 2016 new size is 16px
16: `${path}16${postfix}.png`,
32: `${path}32${postfix}.png`,
paths['16'] = `${path}16${postfix}.png`;
paths['32'] = `${path}32${postfix}.png`;
} else {
// Chromium forks or non-chromium browsers may still use the traditional 19px
19: `${path}19${postfix}.png`,
38: `${path}38${postfix}.png`,
// TODO: add Edge preferred sizes: 20, 25, 30, 40
},
}, () => {
if (chrome.runtime.lastError || tab.id === undefined) {
return;
paths['19'] = `${path}19${postfix}.png`;
paths['38'] = `${path}38${postfix}.png`;
}
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
chrome.browserAction.setIcon({tabId: tab.id, path: paths}, ignoreChromeError);
}
if (tab.id === undefined) return;
let defaultIcon = tabIcons.get(undefined);
if (!defaultIcon) tabIcons.set(undefined, (defaultIcon = {}));
if (defaultIcon.color !== color) {
defaultIcon.color = color;
chrome.browserAction.setBadgeBackgroundColor({color});
}
if (tabIcon.text !== text) {
tabIcon.text = text;
setTimeout(() => {
getTab(tab.id).then(realTab => {
// skip pre-rendered tabs
@ -317,67 +364,31 @@ function updateIcon(tab, styles) {
}
});
});
});
}
}
}
function onRuntimeMessage(request, sender, sendResponseInternal) {
const sendResponse = data => {
// wrap Error object instance as {__ERROR__: message} - will be unwrapped in sendMessage
if (data instanceof Error) {
data = {__ERROR__: data.message};
}
// prevent browser exception bug on sending a response to a closed tab
tryCatch(sendResponseInternal, data);
};
switch (request.method) {
case 'getStyles':
getStyles(request).then(sendResponse);
function onRuntimeMessage(msg, sender, sendResponse) {
const fn = API_METHODS[msg.method];
if (!fn) return;
// wrap 'Error' object instance as {__ERROR__: message},
// which will be unwrapped by sendMessage,
// and prevent exceptions on sending to a closed tab
const respond = data =>
tryCatch(sendResponse,
data instanceof Error ? {__ERROR__: data.message} : data);
const result = fn(msg, sender, respond);
if (result instanceof Promise) {
result
.catch(e => ({__ERROR__: e instanceof Error ? e.message : e}))
.then(respond);
return KEEP_CHANNEL_OPEN;
case 'saveStyle':
saveStyle(request).then(sendResponse);
} else if (result === KEEP_CHANNEL_OPEN) {
return KEEP_CHANNEL_OPEN;
case 'saveUsercss':
usercssHelper.save(request, true).then(sendResponse);
return KEEP_CHANNEL_OPEN;
case 'buildUsercss':
usercssHelper.build(request, true).then(sendResponse);
return KEEP_CHANNEL_OPEN;
case 'healthCheck':
dbExec()
.then(() => sendResponse(true))
.catch(() => sendResponse(false));
return KEEP_CHANNEL_OPEN;
case 'styleViaAPI':
styleViaAPI(request, sender);
return;
case 'download':
download(request.url)
.then(sendResponse)
.catch(() => sendResponse(null));
return KEEP_CHANNEL_OPEN;
case 'openUsercssInstallPage':
usercssHelper.openInstallPage(sender.tab, request).then(sendResponse);
return KEEP_CHANNEL_OPEN;
case 'closeTab':
chrome.tabs.remove(request.tabId || sender.tab.id, () => {
if (chrome.runtime.lastError && request.tabId !== sender.tab.id) {
sendResponse(new Error(chrome.runtime.lastError.message));
}
});
return KEEP_CHANNEL_OPEN;
case 'openEditor':
openEditor(request.id);
return;
} else if (result !== undefined) {
respond(result);
}
}

100
background/search-db.js Normal file
View File

@ -0,0 +1,100 @@
/* global API_METHODS filterStyles cachedStyles */
'use strict';
(() => {
// toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map();
// top-level style properties to be searched
const PARTS = {
name: searchText,
url: searchText,
sourceCode: searchText,
sections: searchSections,
};
/**
* @param params
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
* @param {number[]} [params.ids] - if not specified, all styles are searched
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = ({query, ids}) => {
let rx, words, icase, matchUrl;
query = query.trim();
if (/^url:/i.test(query)) {
matchUrl = query.slice(query.indexOf(':') + 1).trim();
if (matchUrl) {
return filterStyles({matchUrl}).map(style => style.id);
}
}
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
rx = tryRegExp(RegExp.$1, RegExp.$2);
}
if (!rx) {
words = query
.split(/(".*?")|\s+/)
.filter(Boolean)
.map(w => w.startsWith('"') && w.endsWith('"')
? w.slice(1, -1)
: w)
.filter(w => w.length > 1);
words = words.length ? words : [query];
icase = words.some(w => w === lower(w));
}
const results = [];
for (const item of ids || cachedStyles.list) {
const id = isNaN(item) ? item.id : item;
if (!query || words && !words.length) {
results.push(id);
continue;
}
const style = isNaN(item) ? item : cachedStyles.byId.get(item);
if (!style) continue;
for (const part in PARTS) {
const text = style[part];
if (text && PARTS[part](text, rx, words, icase)) {
results.push(id);
break;
}
}
}
if (cache.size) debounce(clearCache, 60e3);
return results;
};
function searchText(text, rx, words, icase) {
if (rx) return rx.test(text);
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
if (words.every(w => text.includes(w))) return true;
text = lower(text);
}
}
function searchSections(sections, rx, words, icase) {
for (const section of sections) {
for (const prop in section) {
const value = section[prop];
if (typeof value === 'string') {
if (searchText(value, rx, words, icase)) return true;
} else if (Array.isArray(value)) {
if (value.some(str => searchText(str, rx, words, icase))) return true;
}
}
}
}
function lower(text) {
let result = cache.get(text);
if (result) return result;
result = text.toLocaleLowerCase();
cache.set(text, result);
return result;
}
function clearCache() {
cache.clear();
}
})();

View File

@ -1,4 +1,4 @@
/* global LZString */
/* global getStyleWithNoCode */
'use strict';
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
@ -29,54 +29,6 @@ var cachedStyles = {
},
};
window.LZString = window.LZString || window.LZStringUnsafe;
// eslint-disable-next-line no-var
var [chromeLocal, chromeSync] = [
chrome.storage.local,
chrome.storage.sync,
].map(storage => {
const wrapper = {
get(options) {
return new Promise(resolve => {
storage.get(options, data => resolve(data));
});
},
set(data) {
return new Promise(resolve => {
storage.set(data, () => resolve(data));
});
},
remove(keyOrKeys) {
return new Promise(resolve => {
storage.remove(keyOrKeys, resolve);
});
},
getValue(key) {
return wrapper.get(key).then(data => data[key]);
},
setValue(key, value) {
return wrapper.set({[key]: value});
},
getLZValue(key) {
return wrapper.getLZValues([key]).then(data => data[key]);
},
getLZValues(keys) {
return wrapper.get(keys).then((data = {}) => {
for (const key of keys) {
const value = data[key];
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
}
return data;
});
},
setLZValue(key, value) {
return wrapper.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
}
};
return wrapper;
});
// eslint-disable-next-line no-var
var dbExec = dbExecIndexedDB;
dbExec.initialized = false;
@ -247,6 +199,7 @@ function filterStyles({
matchUrl = null,
md5Url = null,
asHash = null,
omitCode,
strictRegexp = true, // used by the popup to detect bad regexps
} = {}) {
enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
@ -274,15 +227,15 @@ function filterStyles({
const cacheKey = [enabled, id, matchUrl, md5Url, asHash, strictRegexp].join('\t');
const cached = cachedStyles.filters.get(cacheKey);
let styles;
if (cached) {
cached.hits++;
cached.lastHit = Date.now();
return asHash
styles = asHash
? Object.assign(blankHash, cached.styles)
: cached.styles;
}
return filterStylesInternal({
: cached.styles.slice();
} else {
styles = filterStylesInternal({
enabled,
id,
matchUrl,
@ -292,6 +245,16 @@ function filterStyles({
blankHash,
cacheKey,
});
}
if (!omitCode) return styles;
if (!asHash) return styles.map(getStyleWithNoCode);
for (const id in styles) {
const style = styles[id];
if (style && style.sections) {
styles[id] = getStyleWithNoCode(style);
}
}
return styles;
}
@ -427,6 +390,7 @@ function saveStyle(style) {
md5Url: null,
url: null,
originalMd5: null,
installDate: Date.now(),
}, style);
return write(style);
}
@ -797,3 +761,47 @@ function handleCssTransitionBug({tabId, frameId, url, styles}) {
return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
}
}
/*
According to CSS4 @document specification the entire URL must match.
Stylish-for-Chrome implemented it incorrectly since the very beginning.
We'll detect styles that abuse the bug by finding the sections that
would have been applied by Stylish but not by us as we follow the spec.
Additionally we'll check for invalid regexps.
*/
function detectSloppyRegexps({matchUrl, ids}) {
const results = [];
for (const id of ids) {
const style = cachedStyles.byId.get(id);
if (!style) continue;
// make sure all regexps are compiled
const rxCache = cachedStyles.regexps;
let hasRegExp = false;
for (const section of style.sections) {
for (const regexp of section.regexps) {
hasRegExp = true;
for (let pass = 1; pass <= 2; pass++) {
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
if (!rxCache.has(cacheKey)) {
// according to CSS4 @document specification the entire URL must match
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
// create in the bg context to avoid leaking of "dead objects"
const rx = tryRegExp(anchored);
rxCache.set(cacheKey, rx || false);
}
}
}
}
if (!hasRegExp) continue;
const applied = getApplicableSections({style, matchUrl});
const wannabe = getApplicableSections({style, matchUrl, strictRegexp: false});
results.push({
id,
applied,
skipped: wannabe.length - applied.length,
hasInvalidRegexps: wannabe.some(({regexps}) => regexps.some(rx => !rxCache.has(rx))),
});
}
return results;
}

View File

@ -1,7 +1,7 @@
/* global getStyles */
/* global getStyles API_METHODS */
'use strict';
const styleViaAPI = !CHROME && (() => {
API_METHODS.styleViaAPI = !CHROME && (() => {
const ACTIONS = {
styleApply,
styleDeleted,

View File

@ -1,15 +1,17 @@
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
/* global calcStyleDigest */
/* global usercss semverCompare usercssHelper */
/*
global getStyles saveStyle styleSectionsEqual
global calcStyleDigest cachedStyles getStyleWithNoCode
global usercss semverCompare
global API_METHODS
*/
'use strict';
// eslint-disable-next-line no-var
var updater = {
var updater = (() => {
COUNT: 'count',
const STATES = {
UPDATED: 'updated',
SKIPPED: 'skipped',
DONE: 'done',
// details for SKIPPED status
EDITED: 'locally edited',
@ -20,28 +22,53 @@ var updater = {
ERROR_MD5: 'error: MD5 is invalid',
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
let lastUpdateTime = parseInt(localStorage.lastUpdateTime) || Date.now();
let checkingAll = false;
let logQueue = [];
let logLastWriteTime = 0;
checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
updater.resetInterval();
updater.checkAllStyles.running = true;
API_METHODS.updateCheckAll = checkAllStyles;
API_METHODS.updateCheck = checkStyle;
API_METHODS.getUpdaterStates = () => updater.STATES;
prefs.subscribe(['updateInterval'], schedule);
schedule();
return {checkAllStyles, checkStyle, STATES};
function checkAllStyles({
save = true,
ignoreDigest,
observe,
} = {}) {
resetInterval();
checkingAll = true;
const port = observe && chrome.runtime.connect({name: 'updater'});
return getStyles({}).then(styles => {
styles = styles.filter(style => style.updateUrl);
observer(updater.COUNT, styles.length);
updater.log('');
updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
return Promise.all(
styles.map(style =>
updater.checkStyle({style, observer, save, ignoreDigest})));
checkStyle({style, port, save, ignoreDigest})));
}).then(() => {
observer(updater.DONE);
updater.log('');
updater.checkAllStyles.running = false;
if (port) port.postMessage({done: true});
if (port) port.disconnect();
log('');
checkingAll = false;
});
},
}
checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
function checkStyle({
id,
style = cachedStyles.byId.get(id),
port,
save = true,
ignoreDigest,
}) {
/*
Original style digests are calculated in these cases:
* style is installed or updated from server
@ -65,29 +92,33 @@ var updater = {
.catch(reportFailure);
function reportSuccess(saved) {
observer(updater.UPDATED, saved);
updater.log(updater.UPDATED + ` #${style.id} ${style.name}`);
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
const info = {updated: true, style: saved};
if (port) port.postMessage(info);
return info;
}
function reportFailure(err) {
observer(updater.SKIPPED, style, err);
err = err === 0 ? 'server unreachable' : err;
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
function reportFailure(error) {
error = error === 0 ? 'server unreachable' : error;
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`);
const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info);
return info;
}
function checkIfEdited(digest) {
if (style.originalDigest && style.originalDigest !== digest) {
return Promise.reject(updater.EDITED);
return Promise.reject(STATES.EDITED);
}
}
function maybeUpdateUSO() {
return download(style.md5Url).then(md5 => {
if (!md5 || md5.length !== 32) {
return Promise.reject(updater.ERROR_MD5);
return Promise.reject(STATES.ERROR_MD5);
}
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(updater.SAME_MD5);
return Promise.reject(STATES.SAME_MD5);
}
return download(style.updateUrl)
.then(text => tryJSONparse(text));
@ -104,14 +135,14 @@ var updater = {
case 0:
// re-install is invalid in a soft upgrade
if (!ignoreDigest) {
return Promise.reject(updater.SAME_VERSION);
return Promise.reject(STATES.SAME_VERSION);
} else if (text === style.sourceCode) {
return Promise.reject(updater.SAME_CODE);
return Promise.reject(STATES.SAME_CODE);
}
break;
case 1:
// downgrade is always invalid
return Promise.reject(updater.ERROR_VERSION);
return Promise.reject(STATES.ERROR_VERSION);
}
return usercss.buildCode(json);
});
@ -120,8 +151,9 @@ var updater = {
function maybeSave(json = {}) {
// usercss is already validated while building
if (!json.usercssData && !styleJSONseemsValid(json)) {
return Promise.reject(updater.ERROR_JSON);
return Promise.reject(STATES.ERROR_JSON);
}
json.id = style.id;
json.updateDate = Date.now();
json.reason = 'update';
@ -139,15 +171,16 @@ var updater = {
if (styleSectionsEqual(json, style)) {
// update digest even if save === false as there might be just a space added etc.
saveStyle(Object.assign(json, {reason: 'update-digest'}));
return Promise.reject(updater.SAME_CODE);
} else if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(updater.MAYBE_EDITED);
return Promise.reject(STATES.SAME_CODE);
}
return !save ? json :
json.usercssData
? usercssHelper.save(json)
: saveStyle(json);
if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.MAYBE_EDITED);
}
return save ?
API_METHODS[json.usercssData ? 'saveUsercss' : 'saveStyle'](json) :
json;
}
function styleJSONseemsValid(json) {
@ -157,49 +190,47 @@ var updater = {
&& typeof json.sections.every === 'function'
&& typeof json.sections[0].code === 'string';
}
},
}
schedule() {
function schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval) {
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
const elapsed = Math.max(0, Date.now() - lastUpdateTime);
debounce(checkAllStyles, Math.max(10e3, interval - elapsed));
} else {
debounce.unregister(updater.checkAllStyles);
debounce.unregister(checkAllStyles);
}
},
resetInterval() {
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
updater.schedule();
},
log: (() => {
let queue = [];
let lastWriteTime = 0;
return text => {
queue.push({text, time: new Date().toLocaleString()});
debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0);
};
function flushQueue() {
chromeLocal.getValue('updateLog').then((lines = []) => {
const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
if (!queue[0].text) {
queue.shift();
if (lines[lines.length - 1]) {
lines.push('');
}
function resetInterval() {
localStorage.lastUpdateTime = lastUpdateTime = Date.now();
schedule();
}
function log(text) {
logQueue.push({text, time: new Date().toLocaleString()});
debounce(flushQueue, text && checkingAll ? 1000 : 0);
}
function flushQueue(stored) {
if (!stored) {
chrome.storage.local.get('updateLog', flushQueue);
return;
}
const lines = stored.lines || [];
const time = Date.now() - logLastWriteTime > 11e3 ?
logQueue[0].time + ' ' :
'';
if (!logQueue[0].text) {
logQueue.shift();
if (lines[lines.length - 1]) lines.push('');
}
lines.splice(0, lines.length - 1000);
lines.push(time + queue[0].text);
lines.push(...queue.slice(1).map(item => item.text));
chromeLocal.setValue('updateLog', lines);
lastWriteTime = Date.now();
queue = [];
});
}
})(),
};
lines.push(time + (logQueue[0] && logQueue[0].text || ''));
lines.push(...logQueue.slice(1).map(item => item.text));
updater.schedule();
prefs.subscribe(['updateInterval'], updater.schedule);
chrome.storage.local.set({updateLog: lines});
logLastWriteTime = Date.now();
logQueue = [];
}
})();

View File

@ -1,8 +1,11 @@
/* global usercss saveStyle getStyles chromeLocal */
/* global API_METHODS usercss saveStyle getStyles chromeLocal cachedStyles */
'use strict';
// eslint-disable-next-line no-var
var usercssHelper = (() => {
(() => {
API_METHODS.saveUsercss = save;
API_METHODS.buildUsercss = build;
API_METHODS.installUsercss = install;
const TEMP_CODE_PREFIX = 'tempUsercssCode';
const TEMP_CODE_CLEANUP_DELAY = 60e3;
@ -48,31 +51,25 @@ var usercssHelper = (() => {
return usercss.buildCode(style);
}
function wrapReject(pending) {
return pending
.catch(err => new Error(Array.isArray(err) ? err.join('\n') : err.message || String(err)));
}
// Parse the source and find the duplication
function build({sourceCode, checkDup = false}, noReject) {
const pending = buildMeta({sourceCode})
function build({sourceCode, checkDup = false}) {
return buildMeta({sourceCode})
.then(style => Promise.all([
buildCode(style),
checkDup && findDup(style)
]))
.then(([style, dup]) => ({style, dup}));
return noReject ? wrapReject(pending) : pending;
}
function save(style, noReject) {
const pending = buildMeta(style)
function save(style) {
if (!style.sourceCode) {
style.sourceCode = cachedStyles.byId.get(style.id).sourceCode;
}
return buildMeta(style)
.then(assignVars)
.then(buildCode)
.then(saveStyle);
return noReject ? wrapReject(pending) : pending;
function assignVars(style) {
if (style.reason === 'config' && style.id) {
return style;
@ -105,11 +102,12 @@ var usercssHelper = (() => {
);
}
function openInstallPage(tab, {url = tab.url, direct, downloaded} = {}) {
function install({url, direct, downloaded}, {tab}) {
url = url || tab.url;
if (direct && !downloaded) {
prefetchCodeForInstallation(tab.id, url);
}
return wrapReject(openURL({
return openURL({
url: '/install-usercss.html' +
'?updateUrl=' + encodeURIComponent(url) +
'&tabId=' + tab.id +
@ -117,7 +115,7 @@ var usercssHelper = (() => {
index: tab.index + 1,
openerTabId: tab.id,
currentWindow: null,
}));
});
}
function prefetchCodeForInstallation(tabId, url) {
@ -131,6 +129,4 @@ var usercssHelper = (() => {
setTimeout(() => chromeLocal.remove(key), TEMP_CODE_CLEANUP_DELAY);
});
}
return {build, save, findDup, openInstallPage};
})();

View File

@ -48,8 +48,8 @@
asHash: true,
}, options);
// On own pages we request the styles directly to minimize delay and flicker
if (typeof getStylesSafe === 'function') {
getStylesSafe(request).then(callback);
if (typeof API === 'function') {
API.getStyles(request).then(callback);
} else {
chrome.runtime.sendMessage(request, callback);
}

View File

@ -97,7 +97,7 @@ function initUsercssInstall() {
});
});
chrome.runtime.sendMessage({
method: 'openUsercssInstallPage',
method: 'installUsercss',
url: location.href,
}, r => r && r.__ERROR__ && alert(r.__ERROR__));
}

View File

@ -198,7 +198,14 @@
if (url.startsWith('#')) {
resolve(document.getElementById(url.slice(1)).textContent);
} else {
chrome.runtime.sendMessage({method: 'download', url}, resolve);
chrome.runtime.sendMessage({method: 'download', url}, result => {
const error = result && result.__ERROR__;
if (error) {
alert('Error' + (error ? '\n' + error : ''));
} else {
resolve(result);
}
});
}
});
}

View File

@ -23,6 +23,7 @@
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/moz-parser.js"></script>
<script src="js/storage-util.js"></script>
<script src="content/apply.js"></script>
<script src="edit/lint.js"></script>
<script src="edit/util.js"></script>

View File

@ -44,7 +44,7 @@ Promise.all([
if (usercss) {
editor = createSourceEditor(style);
} else {
initWithSectionStyle({style});
initWithSectionStyle(style);
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
}
});
@ -155,14 +155,17 @@ function onRuntimeMessage(request) {
request.reason !== 'editSave' &&
request.reason !== 'config') {
// code-less style from notifyAllTabs
if ((request.style.sections[0] || {}).code === null) {
request.style = BG.cachedStyles.byId.get(request.style.id);
}
if (isUsercss(request.style)) {
editor.replaceStyle(request.style, request.codeIsUpdated);
const {sections, id} = request.style;
((sections[0] || {}).code === null
? API.getStyles({id})
: Promise.resolve([request.style])
).then(([style]) => {
if (isUsercss(style)) {
editor.replaceStyle(style, request.codeIsUpdated);
} else {
initWithSectionStyle(request);
initWithSectionStyle(style, request.codeIsUpdated);
}
});
}
break;
case 'styleDeleted':
@ -228,7 +231,7 @@ function initStyleData() {
)
],
});
return getStylesSafe({id: id || -1})
return API.getStyles({id: id || -1})
.then(([style = createEmptyStyle()]) => {
styleId = style.id;
if (styleId) sessionStorage.justEditedStyleId = styleId;
@ -344,7 +347,7 @@ function save() {
return;
}
saveStyleSafe({
API.saveStyle({
id: styleId,
name: $('#name').value.trim(),
enabled: $('#enabled').checked,

View File

@ -121,12 +121,12 @@ var linterConfig = {
config = this.fallbackToDefaults(config);
const linter = linterConfig.getName();
this[linter] = config;
BG.chromeSync.setLZValue(this.storageName[linter], config);
chromeSync.setLZValue(this.storageName[linter], config);
return config;
},
loadAll() {
return BG.chromeSync.getLZValues([
return chromeSync.getLZValues([
'editorCSSLintConfig',
'editorStylelintConfig',
]).then(data => {
@ -167,10 +167,8 @@ var linterConfig = {
},
init() {
if (!linterConfig.init.pending) {
linterConfig.init.pending = linterConfig.loadAll();
}
return linterConfig.init.pending;
if (!this.init.pending) this.init.pending = this.loadAll();
return this.init.pending;
}
};

View File

@ -8,7 +8,7 @@ global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
*/
'use strict';
function initWithSectionStyle({style, codeIsUpdated}) {
function initWithSectionStyle(style, codeIsUpdated) {
$('#name').value = style.name || '';
$('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || '';

View File

@ -106,7 +106,7 @@ function createSourceEditor(style) {
`.replace(/^\s+/gm, '');
dirty.clear('sourceGeneration');
style.sourceCode = '';
BG.chromeSync.getLZValue('usercssTemplate').then(code => {
chromeSync.getLZValue('usercssTemplate').then(code => {
style.sourceCode = code || DEFAULT_CODE;
cm.startOperation();
cm.setValue(style.sourceCode);
@ -216,8 +216,8 @@ function createSourceEditor(style) {
return;
}
const code = cm.getValue();
return onBackgroundReady()
.then(() => BG.usercssHelper.save({
return (
API.saveUsercss({
reason: 'editSave',
id: style.id,
enabled: style.enabled,
@ -228,8 +228,8 @@ function createSourceEditor(style) {
.catch(err => {
if (err.message === t('styleMissingMeta', 'name')) {
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
BG.chromeSync.setLZValue('usercssTemplate', code)
.then(() => BG.chromeSync.getLZValue('usercssTemplate'))
chromeSync.setLZValue('usercssTemplate', code)
.then(() => chromeSync.getLZValue('usercssTemplate'))
.then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving'))));
return;
}

View File

@ -200,7 +200,7 @@
if (!liveReload && !prefs.get('openEditInWindow')) {
chrome.tabs.update({url: '/edit.html?id=' + style.id});
} else {
BG.openEditor(style.id);
API.openEditor({id: style.id});
if (!liveReload) {
closeCurrentTab();
}
@ -212,8 +212,8 @@
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
cm.refresh();
BG.usercssHelper.build(BG.deepCopy({sourceCode, checkDup: true}))
.then(r => init(deepCopy(r)))
API.buildUsercss({sourceCode, checkDup: true})
.then(r => init(r instanceof Object ? r : deepCopy(r)))
.catch(err => {
$('.header').classList.add('meta-init-error');
showError(err);
@ -222,7 +222,7 @@
function buildWarning(err) {
const contents = Array.isArray(err) ?
$create('pre', err.join('\n')) :
[$create('pre', err.join('\n'))] :
[err && err.message || err || 'Unknown error'];
if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index);
@ -283,8 +283,8 @@
data.version,
]))
).then(ok => ok &&
BG.usercssHelper.save(BG.deepCopy(Object.assign(style, dup && {reason: 'update'})))
.then(r => install(deepCopy(r)))
API.saveUsercss(Object.assign(style, dup && {reason: 'update'}))
.then(r => install(r instanceof Object ? r : deepCopy(r)))
.catch(err => messageBox.alert(t('styleInstallFailed', err)))
);
};

View File

@ -64,7 +64,8 @@ onDOMready().then(() => {
if (!chrome.app && chrome.windows) {
// die if unable to access BG directly
chrome.windows.getCurrent(wnd => {
if (!BG && wnd.incognito) {
if (!BG && wnd.incognito &&
!location.pathname.includes('popup.html')) {
// private windows can't get bg page
location.href = '/msgbox/dysfunctional.html';
throw 0;

View File

@ -1,14 +1,18 @@
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
/* global FIREFOX: true */
/*
global BG: true
global FIREFOX: true
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 = CHROME && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi');
const ANDROID = !chrome.windows;
let FIREFOX = !CHROME && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
let FIREFOX = !chrome.app && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
if (!CHROME && !chrome.browserAction.openPopup) {
// in FF pre-57 legacy addons can override useragent so we assume the worst
@ -65,13 +69,14 @@ if (!BG || BG !== window) {
document.documentElement.classList.add('firefox');
} else if (OPERA) {
document.documentElement.classList.add('opera');
} else if (chrome.app && navigator.userAgent.includes('Vivaldi')) {
document.documentElement.classList.add('vivaldi');
} else {
if (VIVALDI) document.documentElement.classList.add('vivaldi');
}
// TODO: remove once our manifest's minimum_chrome_version is 50+
// Chrome 49 doesn't report own extension pages in webNavigation apparently
if (CHROME && CHROME < 2661) {
getActiveTab().then(BG.updateIcon);
getActiveTab().then(tab =>
window.API.updateIcon({tab}));
}
}
@ -82,6 +87,60 @@ if (FIREFOX_NO_DOM_STORAGE) {
Object.defineProperty(window, 'sessionStorage', {value: {}});
}
// eslint-disable-next-line no-var
var API = (() => {
return new Proxy(() => {}, {
get: (target, name) =>
name === 'remoteCall' ?
remoteCall :
arg => invokeBG(name, arg),
});
function remoteCall(name, arg, remoteWindow) {
let thing = window[name] || window.API_METHODS[name];
if (typeof thing === 'function') {
thing = thing(arg);
}
if (!thing || typeof thing !== 'object') {
return thing;
} else if (thing instanceof Promise) {
return thing.then(product => remoteWindow.deepCopy(product));
} else {
return remoteWindow.deepCopy(thing);
}
}
function invokeBG(name, arg = {}) {
if (BG && (name in BG || name in BG.API_METHODS)) {
const call = BG !== window ?
BG.API.remoteCall(name, BG.deepCopy(arg), window) :
remoteCall(name, arg, BG);
return Promise.resolve(call);
}
if (BG && BG.getStyles) {
throw new Error('Bad API method', name, arg);
}
if (FIREFOX) {
arg.method = name;
return sendMessage(arg);
}
return onBackgroundReady().then(() => invokeBG(name, arg));
}
function onBackgroundReady() {
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
sendMessage({method: 'healthCheck'}, health => {
if (health !== undefined) {
BG = chrome.extension.getBackgroundPage();
resolve();
} else {
setTimeout(ping, 0, resolve);
}
});
});
}
})();
function notifyAllTabs(msg) {
const originalMessage = msg;
@ -99,6 +158,12 @@ function notifyAllTabs(msg) {
const affectsIcon = affectsAll || msg.affects.icon;
const affectsPopup = affectsAll || msg.affects.popup;
const affectsSelf = affectsPopup || msg.prefs;
// notify background page and all open popups
if (affectsSelf) {
msg.tabId = undefined;
sendMessage(msg, ignoreChromeError);
}
// notify tabs
if (affectsTabs || affectsIcon) {
const notifyTab = tab => {
// own pages will be notified via runtime.sendMessage later
@ -109,8 +174,9 @@ function notifyAllTabs(msg) {
msg.tabId = tab.id;
sendMessage(msg, ignoreChromeError);
}
if (affectsIcon && BG) {
BG.updateIcon(tab);
if (affectsIcon) {
// eslint-disable-next-line no-use-before-define
debounce(API.updateIcon, 0, {tab});
}
};
// list all tabs including chrome-extension:// which can be ours
@ -132,11 +198,6 @@ function notifyAllTabs(msg) {
if (typeof applyOnMessage !== 'undefined') {
applyOnMessage(originalMessage);
}
// notify background page and all open popups
if (affectsSelf) {
msg.tabId = undefined;
sendMessage(msg, ignoreChromeError);
}
}
@ -294,10 +355,9 @@ function ignoreChromeError() {
function getStyleWithNoCode(style) {
const stripped = Object.assign({}, style, {sections: []});
for (const section of style.sections) {
stripped.sections.push(Object.assign({}, section, {code: null}));
}
const stripped = deepCopy(style);
for (const section of stripped.sections) section.code = null;
stripped.sourceCode = null;
return stripped;
}
@ -343,31 +403,23 @@ const debounce = Object.assign((fn, delay, ...args) => {
function deepCopy(obj) {
return obj !== null && obj !== undefined && typeof obj === 'object'
? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj)
: obj;
}
function deepMerge(target, ...args) {
const isArray = typeof target.slice === 'function';
for (const obj of args) {
if (isArray && obj !== null && obj !== undefined) {
for (const element of obj) {
target.push(deepCopy(element));
if (!obj || typeof obj !== 'object') return obj;
// N.B. a copy should be an explicitly literal
if (Array.isArray(obj)) {
const copy = [];
for (const v of obj) {
copy.push(!v || typeof v !== 'object' ? v : deepCopy(v));
}
continue;
return copy;
}
const copy = {};
const hasOwnProperty = Object.prototype.hasOwnProperty;
for (const k in obj) {
const value = obj[k];
if (k in target && typeof value === 'object' && value !== null) {
deepMerge(target[k], value);
} else {
target[k] = deepCopy(value);
if (!hasOwnProperty.call(obj, k)) continue;
const v = obj[k];
copy[k] = !v || typeof v !== 'object' ? v : deepCopy(v);
}
}
}
return target;
return copy;
}
@ -390,51 +442,6 @@ function sessionStorageHash(name) {
}
function onBackgroundReady() {
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
sendMessage({method: 'healthCheck'}, health => {
if (health !== undefined) {
BG = chrome.extension.getBackgroundPage();
resolve();
} else {
setTimeout(ping, 0, resolve);
}
});
});
}
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
function getStylesSafe(options) {
return onBackgroundReady()
.then(() => BG.getStyles(options));
}
function saveStyleSafe(style) {
return onBackgroundReady()
.then(() => BG.saveStyle(BG.deepCopy(style)))
.then(savedStyle => {
if (style.notify === false) {
handleUpdate(savedStyle, style);
}
return savedStyle;
});
}
function deleteStyleSafe({id, notify = true} = {}) {
return onBackgroundReady()
.then(() => BG.deleteStyle({id, notify}))
.then(() => {
if (!notify) {
handleDelete(id);
}
return id;
});
}
function download(url, {
method = url.includes('?') ? 'POST' : 'GET',
body = url.includes('?') ? url.slice(url.indexOf('?')) : null,
@ -489,7 +496,7 @@ function invokeOrPostpone(isInvoke, fn, ...args) {
}
function openEditor(id) {
function openEditor({id}) {
let url = '/edit.html';
if (id) {
url += `?id=${id}`;

View File

@ -148,18 +148,14 @@ var prefs = new function Prefs() {
values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value);
const hasChanged = !equal(value, oldValue);
if (!fromBroadcast) {
if (BG && BG !== window) {
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync});
} else {
if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) {
localStorage[key] = typeof defaults[key] === 'object'
? JSON.stringify(value)
: value;
if (broadcast && hasChanged) {
}
if (!fromBroadcast && broadcast && hasChanged) {
this.broadcast(key, value, {sync});
}
}
}
if (hasChanged) {
const specific = onChange.specific.get(key);
if (typeof specific === 'function') {
@ -175,8 +171,6 @@ var prefs = new function Prefs() {
}
},
remove: key => this.set(key, undefined),
reset: key => this.set(key, deepCopy(defaults[key])),
broadcast(key, value, {sync = true} = {}) {
@ -226,9 +220,20 @@ var prefs = new function Prefs() {
},
});
// Unlike sync, HTML5 localStorage is ready at browser startup
// so we'll mirror the prefs to avoid using the wrong defaults
// during the startup phase
{
const importFromBG = () =>
API.getPrefs().then(prefs => {
const props = {};
for (const id in prefs) {
const value = prefs[id];
values[id] = value;
props[id] = {value: deepCopy(value)};
}
Object.defineProperties(this.readOnlyValues, props);
});
// Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
// so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
const importFromLocalStorage = () => {
for (const key in defaults) {
const defaultValue = defaults[key];
let value = localStorage[key];
@ -247,6 +252,7 @@ var prefs = new function Prefs() {
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
value = BG.localStorage[key];
value = value === undefined ? defaultValue : value;
localStorage[key] = value;
} else {
value = defaultValue;
}
@ -258,31 +264,20 @@ var prefs = new function Prefs() {
defineReadonlyProperty(this.readOnlyValues, key, value);
}
}
if (!BG || BG === window) {
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
const importFromSync = (synced = {}) => {
for (const key in defaults) {
if (key in synced) {
this.set(key, synced[key], {sync: false});
}
}
return Promise.resolve();
};
getSync().get('settings', ({settings} = {}) => importFromSync(settings));
(FIREFOX_NO_DOM_STORAGE && !BG ? importFromBG() : importFromLocalStorage()).then(() => {
if (BG && BG !== window) return;
if (BG === window) {
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
getSync().get('settings', data => importFromSync.call(this, data.settings));
}
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && 'settings' in changes) {
const synced = changes.settings.newValue;
if (synced) {
importFromSync(synced);
} else {
// user manually deleted our settings, we'll recreate them
getSync().set({'settings': values});
}
importFromSync.call(this, changes.settings.newValue);
}
});
});
}
// any access to chrome API takes time due to initialization of bindings
@ -350,6 +345,14 @@ var prefs = new function Prefs() {
};
}
function importFromSync(synced = {}) {
for (const key in defaults) {
if (key in synced) {
this.set(key, synced[key], {sync: false});
}
}
}
function defineReadonlyProperty(obj, key, value) {
const copy = deepCopy(value);
if (typeof copy === 'object') {

99
js/storage-util.js Normal file
View File

@ -0,0 +1,99 @@
/* global LZString loadScript */
'use strict';
// eslint-disable-next-line no-var
var [chromeLocal, chromeSync] = [
chrome.storage.local,
chrome.storage.sync,
].map(storage => {
const wrapper = {
get(options) {
return new Promise(resolve => {
storage.get(options, data => resolve(data));
});
},
set(data) {
return new Promise(resolve => {
storage.set(data, () => resolve(data));
});
},
remove(keyOrKeys) {
return new Promise(resolve => {
storage.remove(keyOrKeys, resolve);
});
},
getValue(key) {
return wrapper.get(key).then(data => data[key]);
},
setValue(key, value) {
return wrapper.set({[key]: value});
},
loadLZStringScript() {
return Promise.resolve(
window.LZString ||
loadScript('/vendor/lz-string/lz-string-unsafe.js').then(() => {
window.LZString = window.LZStringUnsafe;
}));
},
getLZValue(key) {
return wrapper.getLZValues([key]).then(data => data[key]);
},
getLZValues(keys) {
return Promise.all([
wrapper.get(keys),
wrapper.loadLZStringScript(),
]).then(([data = {}]) => {
for (const key of keys) {
const value = data[key];
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
}
return data;
});
},
setLZValue(key, value) {
return wrapper.loadLZStringScript().then(() =>
wrapper.set({
[key]: LZString.compressToUTF16(JSON.stringify(value)),
}));
}
};
return wrapper;
});
function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
}
if (a.length !== b.length) {
return false;
}
// order of sections should be identical to account for the case of multiple
// sections matching the same URL because the order of rules is part of cascading
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
function propertiesEqual(secA, secB) {
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
return false;
}
}
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
}
function equalOrEmpty(a, b, telltale, comparator) {
const typeA = a && typeof a[telltale] === 'function';
const typeB = b && typeof b[telltale] === 'function';
return (
(a === null || a === undefined || (typeA && !a.length)) &&
(b === null || b === undefined || (typeB && !b.length))
) || typeA && typeB && a.length === b.length && comparator(a, b);
}
function arrayMirrors(array1, array2) {
return (
array1.every(el => array2.includes(el)) &&
array2.every(el => array1.includes(el))
);
}
}

View File

@ -150,13 +150,18 @@
<script src="js/prefs.js"></script>
<script src="content/apply.js"></script>
<script src="js/localization.js"></script>
<script src="js/storage-util.js"></script>
<script src="manage/filters.js"></script>
<script src="manage/updater-ui.js"></script>
<script src="manage/object-diff.js"></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="manage/config-dialog.js"></script>
<script src="manage/sort.js"></script>
<script src="manage/manage.js"></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js" async></script>
<script src="manage/config-dialog.js" async></script>
<script src="manage/updater-ui.js" async></script>
<script src="manage/object-diff.js" async></script>
<script src="manage/import-export.js" async></script>
<script src="msgbox/msgbox.js" async></script>
<script src="manage/incremental-search.js" async></script>
</head>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
@ -358,10 +363,6 @@
<div id="installed"></div>
<script src="manage/import-export.js"></script>
<script src="msgbox/msgbox.js"></script>
<script src="manage/incremental-search.js" async></script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
<symbol id="svg-icon-checked" viewBox="0 0 1000 1000">
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>

View File

@ -107,7 +107,7 @@ function configDialog(style) {
buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose');
}
function save({anyChangeIsDirty = false} = {}) {
function save({anyChangeIsDirty = false} = {}, bgStyle) {
if (saving) {
debounce(save, 0, ...arguments);
return;
@ -116,11 +116,18 @@ function configDialog(style) {
!vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) {
return;
}
if (!bgStyle) {
API.getStyles({id: style.id, omitCode: !BG})
.then(([bgStyle]) => save({anyChangeIsDirty}, bgStyle || {}));
return;
}
style = style.sections ? Object.assign({}, style) : style;
style.enabled = true;
style.reason = 'config';
style.sourceCode = null;
style.sections = null;
const styleVars = style.usercssData.vars;
const bgStyle = BG.cachedStyles.byId.get(style.id);
const bgVars = bgStyle && (bgStyle.usercssData || {}).vars || {};
const bgVars = (bgStyle.usercssData || {}).vars || {};
const invalid = [];
let numValid = 0;
for (const va of vars) {
@ -164,9 +171,9 @@ function configDialog(style) {
return;
}
saving = true;
return BG.usercssHelper.save(BG.deepCopy(style))
return API.saveUsercss(style)
.then(saved => {
varsInitial = getInitialValues(deepCopy(saved.usercssData.vars));
varsInitial = getInitialValues(saved.usercssData.vars);
vars.forEach(va => onchange({target: va.input, justSaved: true}));
renderValues();
updateButtons();

View File

@ -29,7 +29,7 @@ HTMLSelectElement.prototype.adjustWidth = function () {
parent.replaceChild(this, singleSelect);
};
onDOMready().then(onBackgroundReady).then(() => {
onDOMready().then(() => {
$('#search').oninput = searchStyles;
if (urlFilterParam) {
$('#search').value = 'url:' + urlFilterParam;
@ -169,14 +169,17 @@ function filterAndAppend({entry, container}) {
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
entry.classList.add('hidden');
}
} else if ($('#search').value.trim()) {
searchStyles({immediately: true, container});
}
reapplyFilter(container);
}
function reapplyFilter(container = installed) {
function reapplyFilter(container = installed, alreadySearched) {
if (!alreadySearched && $('#search').value.trim()) {
searchStyles({immediately: true, container})
.then(() => reapplyFilter(container, true));
return;
}
// A: show
let toHide = [];
let toUnhide = [];
@ -189,9 +192,6 @@ function reapplyFilter(container = installed) {
if (toUnhide instanceof DocumentFragment) {
installed.appendChild(toUnhide);
return;
} else if (toUnhide.length && $('#search').value.trim()) {
searchStyles({immediately: true, container: toUnhide});
filterContainer({hide: false});
}
// filtering needed or a single-element job from handleUpdate()
for (const entry of toUnhide.children || toUnhide) {
@ -251,16 +251,12 @@ function reapplyFilter(container = installed) {
function showFiltersStats() {
if (!BG.cachedStyles.list) {
debounce(showFiltersStats, 100);
return;
}
const active = filtersSelector.hide !== '';
$('#filters summary').classList.toggle('active', active);
$('#reset-filters').disabled = !active;
const numTotal = BG.cachedStyles.list.length;
const numTotal = installed.children.length;
const numHidden = installed.getElementsByClassName('entry hidden').length;
const numShown = Math.min(numTotal - numHidden, installed.children.length);
const numShown = numTotal - numHidden;
if (filtersSelector.numShown !== numShown ||
filtersSelector.numTotal !== numTotal) {
filtersSelector.numShown = numShown;
@ -273,45 +269,26 @@ function showFiltersStats() {
function searchStyles({immediately, container}) {
const searchElement = $('#search');
const value = searchElement.value.trim();
const urlMode = /^\s*url:/i.test(value);
const query = urlMode
? value.replace(/^\s*url:/i, '')
: value.toLocaleLowerCase();
if (query === searchElement.lastValue && !immediately && !container) {
const el = $('#search');
const query = el.value.trim();
if (query === el.lastValue && !immediately && !container) {
return;
}
if (!immediately) {
debounce(searchStyles, 150, {immediately: true});
return;
}
searchElement.lastValue = query;
el.lastValue = query;
const rx = query.startsWith('/') && query.indexOf('/', 1) > 0 &&
tryRegExp(...(value.match(/^\s*\/(.*?)\/([gimsuy]*)\s*$/) || []).slice(1));
const words = rx ? null :
query.startsWith('"') && query.endsWith('"') ? [value.trim().slice(1, -1)] :
query.split(/\s+/).filter(s => s.length > 1);
if (words && !words.length) {
words.push(query);
}
const entries = container && container.children || container || installed.children;
const siteStyleIds = urlMode &&
new Set(BG.filterStyles({matchUrl: query}).map(style => style.id));
return API.searchDB({
query,
ids: [...entries].map(el => el.styleId),
}).then(ids => {
ids = new Set(ids);
let needsRefilter = false;
for (const entry of entries) {
let isMatching = !query || words && !words.length;
if (!isMatching) {
const style = urlMode ? siteStyleIds.has(entry.styleId) :
BG.cachedStyles.byId.get(entry.styleId) || {};
isMatching = Boolean(style && (
urlMode ||
isMatchingText(style.name) ||
style.url && isMatchingText(style.url) ||
style.sourceCode && isMatchingText(style.sourceCode) ||
isMatchingStyle(style)));
}
const isMatching = ids.has(entry.styleId);
if (entry.classList.contains('not-matching') !== !isMatching) {
entry.classList.toggle('not-matching', !isMatching);
needsRefilter = true;
@ -320,40 +297,6 @@ function searchStyles({immediately, container}) {
if (needsRefilter && !container) {
filterOnChange({forceRefilter: true});
}
return;
function isMatchingStyle(style) {
for (const section of style.sections) {
for (const prop in section) {
const value = section[prop];
switch (typeof value) {
case 'string':
if (isMatchingText(value)) {
return true;
}
break;
case 'object':
for (const str of value) {
if (isMatchingText(str)) {
return true;
}
}
break;
}
}
}
}
function isMatchingText(text) {
if (rx) {
return rx.test(text);
}
for (let pass = 1; pass <= 2; pass++) {
if (words.every(word => text.includes(word))) {
return true;
}
text = text.toLocaleLowerCase();
}
return false;
}
return container;
});
}

View File

@ -1,4 +1,4 @@
/* global messageBox, handleUpdate, applyOnMessage */
/* global messageBox handleUpdate applyOnMessage styleSectionsEqual */
'use strict';
const STYLISH_DUMP_FILE_EXT = '.txt';
@ -41,7 +41,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
importFromString(text) :
getOwnTab().then(tab => {
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
return BG.usercssHelper.openInstallPage(tab, {direct: true})
return API.installUsercss({direct: true}, {tab})
.then(() => URL.revokeObjectURL(tab.url));
})
).then(numStyles => {
@ -56,17 +56,17 @@ function importFromFile({fileTypeFilter, file} = {}) {
}
function importFromString(jsonString) {
if (!BG) {
onBackgroundReady().then(() => importFromString(jsonString));
function importFromString(jsonString, oldStyles) {
if (!oldStyles) {
API.getStyles().then(styles => importFromString(jsonString, styles));
return;
}
// create objects in background context
const json = BG.tryJSONparse(jsonString) || [];
const json = tryJSONparse(jsonString) || [];
if (typeof json.slice !== 'function') {
json.length = 0;
}
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
const oldStylesById = new Map(
oldStyles.map(style => [style.id, style]));
const oldStylesByName = json.length && new Map(
oldStyles.map(style => [style.name.trim(), style]));
@ -94,7 +94,7 @@ function importFromString(jsonString) {
const info = analyze(item);
if (info) {
// using saveStyle directly since json was parsed in background page context
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
return API.saveStyle(Object.assign(item, SAVE_OPTIONS))
.then(style => account({style, info, resolve}));
}
}
@ -110,7 +110,7 @@ function importFromString(jsonString) {
return;
}
item.name = item.name.trim();
const byId = BG.cachedStyles.byId.get(item.id);
const byId = oldStylesById.get(item.id);
const byName = oldStylesByName.get(item.name);
oldStylesByName.delete(item.name);
let oldStyle;
@ -129,7 +129,7 @@ function importFromString(jsonString) {
const metaEqual = oldStyleKeys &&
oldStyleKeys.length === Object.keys(item).length &&
oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]);
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
if (metaEqual && codeEqual) {
stats.unchanged.names.push(oldStyle.name);
stats.unchanged.ids.push(oldStyle.id);
@ -237,10 +237,10 @@ function importFromString(jsonString) {
return;
}
const id = newIds[index++];
deleteStyleSafe({id, notify: false}).then(id => {
API.deleteStyle({id, notify: false}).then(id => {
const oldStyle = oldStylesById.get(id);
if (oldStyle) {
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
API.saveStyle(Object.assign(oldStyle, SAVE_OPTIONS))
.then(undoNextId);
} else {
undoNextId();
@ -293,7 +293,7 @@ function importFromString(jsonString) {
chrome.webNavigation.getAllFrames({tabId}, frames => {
frames = frames && frames[0] ? frames : [{frameId: 0}];
frames.forEach(({frameId}) =>
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
API.getStyles({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
const message = {method: 'styleReplaceAll', tabId, frameId, styles};
if (tab.id === ownTab.id) {
applyOnMessage(message);
@ -301,7 +301,7 @@ function importFromString(jsonString) {
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
}
if (frameId === 0) {
setTimeout(BG.updateIcon, 0, tab, styles);
setTimeout(API.updateIcon, 0, tab, styles);
}
}));
if (resolve) {
@ -314,7 +314,7 @@ function importFromString(jsonString) {
$('#file-all-styles').onclick = () => {
getStylesSafe().then(styles => {
API.getStyles().then(styles => {
const text = JSON.stringify(styles, null, '\t');
const blob = new Blob([text], {type: 'application/json'});
const objectURL = URL.createObjectURL(blob);

View File

@ -1,9 +1,11 @@
/* global messageBox, getStyleWithNoCode, retranslateCSS */
/* global filtersSelector, filterAndAppend */
/* global checkUpdate, handleUpdateInstalled */
/* global objectDiff */
/* global configDialog */
/* global sorter */
/*
global messageBox getStyleWithNoCode retranslateCSS
global filtersSelector filterAndAppend urlFilterParam
global checkUpdate handleUpdateInstalled
global objectDiff
global configDialog
global sorter
*/
'use strict';
let installed;
@ -30,14 +32,13 @@ const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const handleEvent = {};
Promise.all([
getStylesSafe(),
API.getStyles({omitCode: !BG}),
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
onDOMready().then(initGlobalEvents),
]).then(([styles]) => {
showStyles(styles);
]).then(args => {
showStyles(...args);
});
dieOnNullBackground();
chrome.runtime.onMessage.addListener(onRuntimeMessage);
function onRuntimeMessage(msg) {
@ -107,7 +108,7 @@ function initGlobalEvents() {
}
function showStyles(styles = []) {
function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({
styles: styles.map(style => ({
style,
@ -137,7 +138,13 @@ function showStyles(styles = []) {
// eslint-disable-next-line no-unmodified-loop-condition
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
) {
renderBin.appendChild(createStyleElement(sorted[index++]));
const info = sorted[index++];
const entry = createStyleElement(info);
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
entry.classList.add('not-matching');
rendered--;
}
renderBin.appendChild(entry);
}
filterAndAppend({container: renderBin});
if (index < sorted.length) {
@ -277,7 +284,7 @@ function createStyleTargetsElement({entry, style, iconsOnly}) {
function recreateStyleTargets({styles, iconsOnly = false} = {}) {
Promise.resolve(styles || getStylesSafe()).then(styles => {
Promise.resolve(styles || API.getStyles()).then(styles => {
for (const style of styles) {
const entry = $(ENTRY_ID_PREFIX + style.id);
if (entry) {
@ -391,7 +398,7 @@ Object.assign(handleEvent, {
},
toggle(event, entry) {
saveStyleSafe({
API.saveStyle({
id: entry.styleId,
enabled: this.matches('.enable') || this.checked,
});
@ -399,39 +406,30 @@ Object.assign(handleEvent, {
check(event, entry) {
event.preventDefault();
checkUpdate(entry);
checkUpdate(entry, {single: true});
},
update(event, entry) {
event.preventDefault();
const request = Object.assign(entry.updatedCode, {
id: entry.styleId,
reason: 'update',
});
if (entry.updatedCode.usercssData) {
onBackgroundReady()
.then(() => BG.usercssHelper.save(request));
} else {
// update everything but name
request.name = null;
saveStyleSafe(request);
}
const json = entry.updatedCode;
json.id = entry.styleId;
json.reason = 'update';
API[json.usercssData ? 'saveUsercss' : 'saveStyle'](json);
},
delete(event, entry) {
event.preventDefault();
const id = entry.styleId;
const {name} = BG.cachedStyles.byId.get(id) || {};
animateElement(entry);
messageBox({
title: t('deleteStyleConfirm'),
contents: name,
contents: entry.styleMeta.name,
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
.then(({button}) => {
if (button === 0) {
deleteStyleSafe({id});
API.deleteStyle({id});
}
});
},
@ -525,7 +523,7 @@ function handleUpdate(style, {reason, method} = {}) {
sorter.update();
if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry);
scrollElementIntoView(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
}
function handleToggledOrCodeOnly() {
@ -606,7 +604,7 @@ function switchUI({styleOnly} = {}) {
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
installed.textContent = '';
getStylesSafe().then(showStyles);
API.getStyles().then(showStyles);
return;
}
if (changed.targets) {
@ -645,28 +643,3 @@ function usePrefsDuringPageLoad() {
}
$$('#header select').forEach(el => el.adjustWidth());
}
// TODO: remove when these bugs are fixed in FF
function dieOnNullBackground() {
if (!FIREFOX || BG) {
return;
}
sendMessage({method: 'healthCheck'}, health => {
if (health && !chrome.extension.getBackgroundPage()) {
onDOMready().then(() => {
sendMessage({method: 'getStyles'}, showStyles);
messageBox({
title: 'Stylus',
className: 'danger center',
contents: t('dysfunctionalBackgroundConnection'),
onshow: () => {
$('#message-box-close-icon').remove();
window.removeEventListener('keydown', messageBox.listeners.key, true);
}
});
document.documentElement.style.pointerEvents = 'none';
});
}
});
}

View File

@ -129,7 +129,7 @@ const sorter = (() => {
styles: current.map(entry => ({
entry,
name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name,
style: BG.cachedStyles.byId.get(entry.styleId),
style: entry.styleMeta,
}))
});
if (current.some((entry, index) => entry !== sorted[index].entry)) {

View File

@ -29,41 +29,51 @@ function applyUpdateAll() {
function checkUpdateAll() {
document.body.classList.add('update-in-progress');
$('#check-all-updates').disabled = true;
$('#check-all-updates-force').classList.add('hidden');
$('#apply-all-updates').classList.add('hidden');
$('#update-all-no-updates').classList.add('hidden');
const btnCheck = $('#check-all-updates');
const btnCheckForce = $('#check-all-updates-force');
const btnApply = $('#apply-all-updates');
const noUpdates = $('#update-all-no-updates');
btnCheck.disabled = true;
btnCheckForce.classList.add('hidden');
btnApply.classList.add('hidden');
noUpdates.classList.add('hidden');
const ignoreDigest = this && this.id === 'check-all-updates-force';
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
.map(el => checkUpdate(el, {single: false}));
.map(checkUpdate);
let total = 0;
let checked = 0;
let skippedEdited = 0;
let updated = 0;
BG.updater.checkAllStyles({observer, save: false, ignoreDigest}).then(done);
chrome.runtime.onConnect.addListener(function onConnect(port) {
if (port.name !== 'updater') return;
port.onMessage.addListener(observer);
chrome.runtime.onConnect.removeListener(onConnect);
});
function observer(state, value, details) {
switch (state) {
case BG.updater.COUNT:
total = value;
break;
case BG.updater.UPDATED:
API.updateCheckAll({
save: false,
observe: true,
ignoreDigest,
}).then(done);
function observer(info) {
if ('count' in info) {
total = info.count;
}
if (info.updated) {
if (++updated === 1) {
$('#apply-all-updates').disabled = true;
$('#apply-all-updates').classList.remove('hidden');
btnApply.disabled = true;
btnApply.classList.remove('hidden');
}
$('#apply-all-updates').dataset.value = updated;
// fallthrough
case BG.updater.SKIPPED:
btnApply.dataset.value = updated;
}
if (info.updated || info.error) {
checked++;
if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) {
skippedEdited++;
}
reportUpdateState(state, value, details);
break;
skippedEdited += [info.STATES.EDITED, info.STATES.MAYBE_EDITED].includes(info.error);
reportUpdateState(info);
}
const progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth;
@ -72,35 +82,34 @@ function checkUpdateAll() {
function done() {
document.body.classList.remove('update-in-progress');
$('#check-all-updates').disabled = total === 0;
$('#apply-all-updates').disabled = false;
btnCheck.disabled = total === 0;
btnApply.disabled = false;
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
if (!updated) {
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0;
$('#update-all-no-updates').classList.remove('hidden');
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0);
noUpdates.dataset.skippedEdited = skippedEdited > 0;
noUpdates.classList.remove('hidden');
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
}
}
}
function checkUpdate(entry, {single = true} = {}) {
function checkUpdate(entry, {single} = {}) {
$('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = '';
if (single) {
BG.updater.checkStyle({
API.updateCheck({
save: false,
id: entry.styleId,
ignoreDigest: entry.classList.contains('update-problem'),
style: BG.cachedStyles.byId.get(entry.styleId),
observer: reportUpdateState,
});
}).then(reportUpdateState);
}
entry.classList.remove('checking-update', 'no-update', 'update-problem');
entry.classList.add('checking-update');
}
function reportUpdateState(state, style, details) {
function reportUpdateState({updated, style, error, STATES}) {
const entry = $(ENTRY_ID_PREFIX + style.id);
const newClasses = new Map([
/*
@ -117,34 +126,29 @@ function reportUpdateState(state, style, details) {
['no-update', 0],
['update-problem', 0],
]);
switch (state) {
case BG.updater.UPDATED:
if (updated) {
newClasses.set('can-update', true);
entry.updatedCode = style;
$('.update-note', entry).textContent = '';
$('#only-updates').classList.remove('hidden');
break;
case BG.updater.SKIPPED: {
if (entry.classList.contains('can-update')) {
break;
}
} else if (!entry.classList.contains('can-update')) {
const same = (
details === BG.updater.SAME_MD5 ||
details === BG.updater.SAME_CODE ||
details === BG.updater.SAME_VERSION
error === STATES.SAME_MD5 ||
error === STATES.SAME_CODE ||
error === STATES.SAME_VERSION
);
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
entry.dataset.details = details;
if (!details) {
details = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
} else if (typeof details === 'number') {
details = t('updateCheckFailBadResponseCode', [details]) + '\n' + style.updateUrl;
} else if (details === BG.updater.EDITED) {
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (details === BG.updater.MAYBE_EDITED) {
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
const edited = error === STATES.EDITED || error === STATES.MAYBE_EDITED;
entry.dataset.error = error;
if (!error) {
error = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
} else if (typeof error === 'number') {
error = t('updateCheckFailBadResponseCode', [error]) + '\n' + style.updateUrl;
} else if (error === STATES.EDITED) {
error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (error === STATES.MAYBE_EDITED) {
error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
}
const message = same ? t('updateCheckSucceededNoUpdate') : details;
const message = same ? t('updateCheckSucceededNoUpdate') : error;
newClasses.set('no-update', true);
newClasses.set('update-problem', !same);
$('.update-note', entry).textContent = message;
@ -155,16 +159,22 @@ function reportUpdateState(state, style, details) {
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
}
}
}
// construct a new className:
// 1. add all truthy newClasses
// 2. remove falsy newClasses
// 3. keep existing classes otherwise
const classes = new Map([...entry.classList.values()].map(cls => [cls, true]));
[...newClasses.entries()].forEach(([cls, newState]) => classes.set(cls, newState));
const className = [...classes.entries()].filter(([, state]) => state).map(([cls]) => cls).join(' ');
if (className !== entry.className) entry.className = className;
for (const [cls, newState] of newClasses.entries()) {
classes.set(cls, newState);
}
const className = [...classes.entries()]
.map(([cls, state]) => state && cls)
.filter(Boolean)
.join(' ');
if (className !== entry.className) {
entry.className = className;
}
if (filtersSelector.hide) {
filterAndAppend({entry});
@ -200,7 +210,10 @@ function showUpdateHistory(event) {
const log = $create('.update-history-log');
let logText, scroller, toggler;
let deleted = false;
BG.chromeLocal.getValue('updateLog').then((lines = []) => {
Promise.all([
chromeLocal.getValue('updateLog'),
API.getUpdaterStates(),
]).then(([lines = [], states]) => {
logText = lines.join('\n');
messageBox({
title: t('updateCheckHistory'),
@ -227,6 +240,13 @@ function showUpdateHistory(event) {
t('manageOnlyUpdates'),
]));
toggler.rxRemoveNOP = new RegExp(
'^[^#]*(' +
Object.keys(states)
.filter(k => k.startsWith('SAME_'))
.map(k => states[k])
.join('|') +
').*\r?\n', 'gm');
toggler.onchange();
}),
});
@ -242,26 +262,17 @@ function showUpdateHistory(event) {
return;
}
const scrollRatio = calcScrollRatio();
const rxRemoveNOP = this.checked && new RegExp([
'^[^#]*(',
Object.keys(BG.updater)
.filter(k => k.startsWith('SAME_'))
.map(k => stringAsRegExp(BG.updater[k]))
.map(rx => rx.source)
.join('|'),
').*\r?\n',
].join(''), 'gm');
log.textContent = !this.checked ? logText : logText.replace(rxRemoveNOP, '');
log.textContent = !this.checked ? logText : logText.replace(this.rxRemoveNOP, '');
if (Math.abs(scrollRatio - calcScrollRatio()) > .1) {
scroller.scrollTop = scrollRatio * scroller.scrollHeight - scroller.clientHeight;
}
}
function deleteHistory() {
if (deleted) {
BG.chromeLocal.setValue('updateLog', logText.split('\n'));
chromeLocal.setValue('updateLog', logText.split('\n'));
setTimeout(scrollToBottom);
} else {
BG.chromeLocal.remove('updateLog');
chromeLocal.remove('updateLog');
log.textContent = '';
}
deleted = !deleted;

View File

@ -21,17 +21,18 @@
"background": {
"scripts": [
"js/messaging.js",
"vendor/lz-string/lz-string-unsafe.js",
"js/color-parser.js",
"js/usercss.js",
"js/storage-util.js",
"background/storage.js",
"background/usercss-helper.js",
"js/prefs.js",
"js/script-loader.js",
"js/color-parser.js",
"js/usercss.js",
"background/background.js",
"vendor/node-semver/semver.js",
"background/usercss-helper.js",
"background/style-via-api.js",
"background/update.js"
"background/search-db.js",
"background/update.js",
"vendor/node-semver/semver.js"
]
},
"commands": {

View File

@ -62,23 +62,26 @@ function checkUpdates() {
let checked = 0;
let updated = 0;
const maxWidth = $('#update-progress').parentElement.clientWidth;
BG.updater.checkAllStyles({observer});
function observer(state, value) {
switch (state) {
case BG.updater.COUNT:
total = value;
chrome.runtime.onConnect.addListener(function onConnect(port) {
if (port.name !== 'updater') return;
port.onMessage.addListener(observer);
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({observe: true});
function observer(info) {
if ('count' in info) {
total = info.count;
document.body.classList.add('update-in-progress');
break;
case BG.updater.UPDATED:
} else if (info.updated) {
updated++;
// fallthrough
case BG.updater.SKIPPED:
checked++;
break;
case BG.updater.DONE:
} else if (info.error) {
checked++;
} else if (info.done) {
document.body.classList.remove('update-in-progress');
return;
}
$('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
$('#updates-installed').dataset.value = updated || '';

View File

@ -161,6 +161,8 @@
<script src="popup/popup.js"></script>
<script src="popup/search-results.js"></script>
<script src="popup/hotkeys.js"></script>
<script src="js/script-loader.js" async></script>
<script src="js/storage-util.js" async></script>
</head>
<body id="stylus-popup">

View File

@ -101,11 +101,15 @@ var hotkeys = (() => {
entry = typeof entry === 'string' ? $('#' + entry) : entry;
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
results.push(entry.id);
task = task.then(() => saveStyleSafe({
task = task.then(() => API.saveStyle({
id: entry.styleId,
enabled: enable,
notify: false,
}));
})).then(() => {
entry.classList.toggle('enabled', enable);
entry.classList.toggle('disabled', !enable);
$('.checker', entry).checked = enable;
});
}
}
if (results.length) {
@ -115,7 +119,7 @@ var hotkeys = (() => {
}
function refreshAllTabs() {
getStylesSafe({matchUrl: location.href, enabled: true, asHash: true})
API.getStyles({matchUrl: location.href, enabled: true, asHash: true})
.then(styles => applyOnMessage({method: 'styleReplaceAll', styles}));
queryTabs().then(tabs =>
tabs.forEach(tab => (!FIREFOX || tab.width) &&
@ -127,11 +131,11 @@ var hotkeys = (() => {
chrome.webNavigation.getAllFrames({tabId}, frames => {
frames = frames && frames[0] ? frames : [{frameId: 0}];
frames.forEach(({frameId}) =>
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
API.getStyles({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
const message = {method: 'styleReplaceAll', tabId, frameId, styles};
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
if (frameId === 0) {
setTimeout(BG.updateIcon, 0, tab, styles);
setTimeout(API.updateIcon, 0, {tab, styles});
}
}));
ignoreChromeError();

View File

@ -15,16 +15,15 @@ getActiveTab().then(tab =>
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
? getTabRealURLFirefox(tab)
: getTabRealURL(tab)
).then(url => {
tabURL = URLS.supported(url) ? url : '';
Promise.all([
tabURL && getStylesSafe({matchUrl: tabURL}),
onDOMready().then(() => {
initPopup(tabURL);
).then(url => Promise.all([
(tabURL = URLS.supported(url) ? url : '') &&
API.getStyles({
matchUrl: tabURL,
omitCode: !BG,
}),
]).then(([styles]) => {
onDOMready().then(initPopup),
])).then(([styles]) => {
showStyles(styles);
});
});
chrome.runtime.onMessage.addListener(onRuntimeMessage);
@ -33,9 +32,7 @@ function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleAdded':
case 'styleUpdated':
// notifyAllTabs sets msg.style's code to null so we have to get the actual style
// because we analyze its code in detectSloppyRegexps
handleUpdate(BG.cachedStyles.byId.get(msg.style.id));
handleUpdate(msg.style);
break;
case 'styleDeleted':
handleDelete(msg.id);
@ -76,7 +73,7 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
}
function initPopup(url) {
function initPopup() {
installed = $('#installed');
setPopupWidth();
@ -108,7 +105,7 @@ function initPopup(url) {
installed);
}
if (!url) {
if (!tabURL) {
document.body.classList.add('blocked');
document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
return;
@ -153,10 +150,10 @@ function initPopup(url) {
// For this URL
const urlLink = template.writeStyle.cloneNode(true);
Object.assign(urlLink, {
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
title: `url-prefix("${url}")`,
href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL),
title: `url-prefix("${tabURL}")`,
textContent: prefs.get('popup.breadcrumbs.usePath')
? new URL(url).pathname.slice(1)
? new URL(tabURL).pathname.slice(1)
// this&nbsp;URL
: t('writeStyleForURL').replace(/ /g, '\u00a0'),
onclick: handleEvent.openLink,
@ -170,7 +167,7 @@ function initPopup(url) {
matchTargets.appendChild(urlLink);
// For domain
const domains = BG.getDomains(url);
const domains = getDomains(tabURL);
for (const domain of domains) {
const numParts = domain.length - domain.replace(/\./g, '').length + 1;
// Don't include TLD
@ -193,6 +190,19 @@ function initPopup(url) {
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
}
writeStyle.appendChild(matchWrapper);
function getDomains(url) {
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
if (!d || url.startsWith('file:')) {
return [];
}
const domains = [d];
while (d.indexOf('.') !== -1) {
d = d.substring(d.indexOf('.') + 1);
domains.push(d);
}
return domains;
}
}
@ -213,23 +223,19 @@ function showStyles(styles) {
: a.name.localeCompare(b.name)
));
let postponeDetect = false;
const t0 = performance.now();
const container = document.createDocumentFragment();
for (const style of styles) {
createStyleElement({style, container, postponeDetect});
postponeDetect = postponeDetect || performance.now() - t0 > 100;
}
styles.forEach(style => createStyleElement({style, container}));
installed.appendChild(container);
setTimeout(detectSloppyRegexps, 100, styles);
getStylesSafe({matchUrl: tabURL, strictRegexp: false})
.then(unscreenedStyles => {
for (const unscreened of unscreenedStyles) {
if (!styles.includes(unscreened)) {
postponeDetect = postponeDetect || performance.now() - t0 > 100;
createStyleElement({
style: Object.assign({appliedSections: [], postponeDetect}, unscreened),
});
API.getStyles({
matchUrl: tabURL,
strictRegexp: false,
omitCode: true,
}).then(unscreenedStyles => {
for (const style of unscreenedStyles) {
if (!styles.find(({id}) => id === style.id)) {
createStyleElement({style, check: true});
}
}
window.dispatchEvent(new Event('showStyles:done'));
@ -239,8 +245,8 @@ function showStyles(styles) {
function createStyleElement({
style,
check = false,
container = installed,
postponeDetect,
}) {
const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
@ -294,7 +300,7 @@ function createStyleElement({
$('.delete', entry).onclick = handleEvent.delete;
$('.configure', entry).onclick = handleEvent.configure;
invokeOrPostpone(!postponeDetect, detectSloppyRegexps, {entry, style});
if (check) detectSloppyRegexps([style]);
const oldElement = $(ENTRY_ID_PREFIX + style.id);
if (oldElement) {
@ -316,23 +322,24 @@ Object.assign(handleEvent, {
},
name(event) {
this.checkbox.click();
this.checkbox.dispatchEvent(new MouseEvent('click'));
event.preventDefault();
},
toggle(event) {
saveStyleSafe({
API.saveStyle({
id: handleEvent.getClickedStyleId(event),
enabled: this.type === 'checkbox' ? this.checked : this.matches('.enable'),
enabled: this.matches('.enable') || this.checked,
});
},
delete(event) {
const id = handleEvent.getClickedStyleId(event);
const entry = handleEvent.getClickedStyleElement(event);
const id = entry.styleId;
const box = $('#confirm');
box.dataset.display = true;
box.style.cssText = '';
$('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name;
$('b', box).textContent = $('.style-name', entry).textContent;
$('[data-cmd="ok"]', box).focus();
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
@ -350,18 +357,14 @@ Object.assign(handleEvent, {
className: 'lights-on',
onComplete: () => (box.dataset.display = false),
});
if (ok) {
deleteStyleSafe({id}).then(() => {
handleDelete(id);
});
}
if (ok) API.deleteStyle({id});
}
},
configure(event) {
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
if (styleIsUsercss) {
getStylesSafe({id: styleId}).then(([style]) => {
API.getStyles({id: styleId}).then(([style]) => {
hotkeys.setState(false);
configDialog(deepCopy(style)).then(() => {
hotkeys.setState(true);
@ -456,15 +459,22 @@ Object.assign(handleEvent, {
function handleUpdate(style) {
if ($(ENTRY_ID_PREFIX + style.id)) {
createStyleElement({style});
createStyleElement({style, check: true});
return;
}
if (!tabURL) return;
// Add an entry when a new style for the current url is installed
if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
API.getStyles({
matchUrl: tabURL,
stopOnFirst: true,
omitCode: true,
}).then(([style]) => {
if (style) {
document.body.classList.remove('blocked');
$$.remove('.blocked-info, #no-styles');
createStyleElement({style});
createStyleElement({style, check: true});
}
});
}
@ -476,58 +486,28 @@ function handleDelete(id) {
}
/*
According to CSS4 @document specification the entire URL must match.
Stylish-for-Chrome implemented it incorrectly since the very beginning.
We'll detect styles that abuse the bug by finding the sections that
would have been applied by Stylish but not by us as we follow the spec.
Additionally we'll check for invalid regexps.
*/
function detectSloppyRegexps({entry, style}) {
// make sure all regexps are compiled
const rxCache = BG.cachedStyles.regexps;
let hasRegExp = false;
for (const section of style.sections) {
for (const regexp of section.regexps) {
hasRegExp = true;
for (let pass = 1; pass <= 2; pass++) {
const cacheKey = pass === 1 ? regexp : BG.SLOPPY_REGEXP_PREFIX + regexp;
if (!rxCache.has(cacheKey)) {
// according to CSS4 @document specification the entire URL must match
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
// create in the bg context to avoid leaking of "dead objects"
const rx = BG.tryRegExp(anchored);
rxCache.set(cacheKey, rx || false);
}
}
}
}
if (!hasRegExp) {
return;
}
const {
appliedSections =
BG.getApplicableSections({style, matchUrl: tabURL}),
wannabeSections =
BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
} = style;
entry.hasInvalidRegexps = wannabeSections.some(section =>
section.regexps.some(rx => !rxCache.has(rx)));
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
if (!appliedSections.length) {
function detectSloppyRegexps(styles) {
API.detectSloppyRegexps({
matchUrl: tabURL,
ids: styles.map(({id}) => id),
}).then(results => {
for (const {id, applied, skipped, hasInvalidRegexps} of results) {
const entry = $(ENTRY_ID_PREFIX + id);
if (!entry) continue;
if (!applied) {
entry.classList.add('not-applied');
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
}
if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
if (skipped || hasInvalidRegexps) {
entry.classList.toggle('regexp-partial', Boolean(skipped));
entry.classList.toggle('regexp-invalid', Boolean(hasInvalidRegexps));
const indicator = template.regexpProblemIndicator.cloneNode(true);
indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
indicator.appendChild(document.createTextNode(entry.skipped || '!'));
indicator.onclick = handleEvent.indicator;
$('.main-controls', entry).appendChild(indicator);
}
}
});
}

View File

@ -131,7 +131,7 @@ window.addEventListener('showStyles:done', function _() {
if (result) {
result.installed = false;
result.installedStyleId = -1;
BG.clearTimeout(result.pingbackTimer);
(BG || window).clearTimeout(result.pingbackTimer);
renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
}
});
@ -280,7 +280,7 @@ window.addEventListener('showStyles:done', function _() {
return;
}
const md5Url = UPDATE_URL.replace('%', result.id);
getStylesSafe({md5Url}).then(([installedStyle]) => {
API.getStyles({md5Url}).then(([installedStyle]) => {
if (installedStyle) {
totalResults = Math.max(0, totalResults - 1);
} else {
@ -522,7 +522,7 @@ window.addEventListener('showStyles:done', function _() {
event.stopPropagation();
const entry = this.closest('.search-result');
saveScrollPosition(entry);
deleteStyleSafe({id: entry._result.installedStyleId})
API.deleteStyle({id: entry._result.installedStyleId})
.then(restoreScrollPosition);
}
@ -550,11 +550,11 @@ window.addEventListener('showStyles:done', function _() {
style.updateUrl += settings.length ? '?' : '';
// show a 'style installed' tooltip in the manager
style.reason = 'install';
return saveStyleSafe(style);
return API.saveStyle(style);
})
.catch(reason => {
const usoId = result.id;
console.debug('install:saveStyleSafe(usoID:', usoId, ') => [ERROR]: ', reason);
console.debug('install:saveStyle(usoID:', usoId, ') => [ERROR]: ', reason);
error('Error while downloading usoID:' + usoId + '\nReason: ' + reason);
})
.then(() => {
@ -574,7 +574,8 @@ window.addEventListener('showStyles:done', function _() {
}
function pingback(result) {
result.pingbackTimer = BG.setTimeout(BG.download, PINGBACK_DELAY,
const wnd = BG || window;
result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY,
BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
}
@ -721,9 +722,10 @@ window.addEventListener('showStyles:done', function _() {
function readCache(id) {
const key = CACHE_PREFIX + id;
return BG.chromeLocal.getValue(key).then(item => {
return chromeLocal.getValue(key).then(item => {
if (!cacheItemExpired(item)) {
return tryJSONparse(BG.LZString.decompressFromUTF16(item.payload));
return chromeLocal.loadLZStringScript().then(() =>
tryJSONparse(LZString.decompressFromUTF16(item.payload)));
} else if (item) {
chrome.storage.local.remove(key);
}
@ -741,10 +743,11 @@ window.addEventListener('showStyles:done', function _() {
return data;
} else {
debounce(cleanupCache, CACHE_CLEANUP_THROTTLE);
return BG.chromeLocal.setValue(CACHE_PREFIX + data.id, {
payload: BG.LZString.compressToUTF16(JSON.stringify(data)),
return chromeLocal.loadLZStringScript().then(() =>
chromeLocal.setValue(CACHE_PREFIX + data.id, {
payload: LZString.compressToUTF16(JSON.stringify(data)),
date: Date.now(),
}).then(() => data);
})).then(() => data);
}
}