diff --git a/.eslintrc b/.eslintrc index b4ebf99a..cc8956c0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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] diff --git a/background/background.js b/background/background.js index 0d0bce79..3d7224df 100644 --- a/background/background.js +++ b/background/background.js @@ -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 = ''; @@ -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); - return KEEP_CHANNEL_OPEN; - - case 'saveStyle': - saveStyle(request).then(sendResponse); - 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; + } +} + + +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; + } else if (result === KEEP_CHANNEL_OPEN) { + return KEEP_CHANNEL_OPEN; + } else if (result !== undefined) { + respond(result); } } diff --git a/background/search-db.js b/background/search-db.js new file mode 100644 index 00000000..9d5bece6 --- /dev/null +++ b/background/search-db.js @@ -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(); + } +})(); diff --git a/background/storage.js b/background/storage.js index 69438d85..f3403356 100644 --- a/background/storage.js +++ b/background/storage.js @@ -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,24 +227,34 @@ 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; + : cached.styles.slice(); + } else { + styles = filterStylesInternal({ + enabled, + id, + matchUrl, + md5Url, + asHash, + strictRegexp, + blankHash, + cacheKey, + }); } - - return filterStylesInternal({ - enabled, - id, - matchUrl, - md5Url, - asHash, - strictRegexp, - 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; +} diff --git a/background/style-via-api.js b/background/style-via-api.js index 8bdde4b9..10962963 100644 --- a/background/style-via-api.js +++ b/background/style-via-api.js @@ -1,7 +1,7 @@ -/* global getStyles */ +/* global getStyles API_METHODS */ 'use strict'; -const styleViaAPI = !CHROME && (() => { +API_METHODS.styleViaAPI = !CHROME && (() => { const ACTIONS = { styleApply, styleDeleted, diff --git a/background/update.js b/background/update.js index b27c28f2..8485ea05 100644 --- a/background/update.js +++ b/background/update.js @@ -1,47 +1,74 @@ -/* 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', - UPDATED: 'updated', - SKIPPED: 'skipped', - DONE: 'done', + const STATES = { + UPDATED: 'updated', + SKIPPED: 'skipped', - // details for SKIPPED status - EDITED: 'locally edited', - MAYBE_EDITED: 'may be locally edited', - SAME_MD5: 'up-to-date: MD5 is unchanged', - SAME_CODE: 'up-to-date: code sections are unchanged', - SAME_VERSION: 'up-to-date: version is unchanged', - ERROR_MD5: 'error: MD5 is invalid', - ERROR_JSON: 'error: JSON is invalid', - ERROR_VERSION: 'error: version is older than installed style', + // details for SKIPPED status + EDITED: 'locally edited', + MAYBE_EDITED: 'may be locally edited', + SAME_MD5: 'up-to-date: MD5 is unchanged', + SAME_CODE: 'up-to-date: code sections are unchanged', + SAME_VERSION: 'up-to-date: version is unchanged', + 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(); - }, + function resetInterval() { + localStorage.lastUpdateTime = lastUpdateTime = Date.now(); + 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(''); - } - } - 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 = []; - }); + 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 + (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 = []; + } +})(); diff --git a/background/usercss-helper.js b/background/usercss-helper.js index ae37ec56..90181cdc 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -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}; })(); diff --git a/content/apply.js b/content/apply.js index e28798f9..f715bfc0 100644 --- a/content/apply.js +++ b/content/apply.js @@ -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); } diff --git a/content/install-hook-usercss.js b/content/install-hook-usercss.js index aa3a4d46..5edd0153 100644 --- a/content/install-hook-usercss.js +++ b/content/install-hook-usercss.js @@ -97,7 +97,7 @@ function initUsercssInstall() { }); }); chrome.runtime.sendMessage({ - method: 'openUsercssInstallPage', + method: 'installUsercss', url: location.href, }, r => r && r.__ERROR__ && alert(r.__ERROR__)); } diff --git a/content/install-hook-userstyles.js b/content/install-hook-userstyles.js index 95cb5e66..99f9c7b5 100644 --- a/content/install-hook-userstyles.js +++ b/content/install-hook-userstyles.js @@ -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); + } + }); } }); } diff --git a/edit.html b/edit.html index 25e1d98e..50b6c57b 100644 --- a/edit.html +++ b/edit.html @@ -23,6 +23,7 @@ + diff --git a/edit/edit.js b/edit/edit.js index 45ee8998..1ce98056 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -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); - } else { - initWithSectionStyle(request); - } + 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(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, diff --git a/edit/lint.js b/edit/lint.js index 8f2170e6..f7323464 100644 --- a/edit/lint.js +++ b/edit/lint.js @@ -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; } }; diff --git a/edit/sections.js b/edit/sections.js index c5380303..0e88d500 100644 --- a/edit/sections.js +++ b/edit/sections.js @@ -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 || ''; diff --git a/edit/source-editor.js b/edit/source-editor.js index f41dac65..8b37352e 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -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; } diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 0f5ff973..3794e434 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -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))) ); }; diff --git a/js/dom.js b/js/dom.js index e7846d66..8872df57 100644 --- a/js/dom.js +++ b/js/dom.js @@ -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; diff --git a/js/messaging.js b/js/messaging.js index e9b11a64..f2a33ee2 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -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)); - } - continue; - } - 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 (!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)); } + return copy; } - return target; + const copy = {}; + const hasOwnProperty = Object.prototype.hasOwnProperty; + for (const k in obj) { + if (!hasOwnProperty.call(obj, k)) continue; + const v = obj[k]; + copy[k] = !v || typeof v !== 'object' ? v : deepCopy(v); + } + 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}`; diff --git a/js/prefs.js b/js/prefs.js index 1dea0510..737055e6 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -148,17 +148,13 @@ 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 { - localStorage[key] = typeof defaults[key] === 'object' - ? JSON.stringify(value) - : value; - if (broadcast && hasChanged) { - this.broadcast(key, value, {sync}); - } - } + if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) { + localStorage[key] = typeof defaults[key] === 'object' + ? JSON.stringify(value) + : value; + } + if (!fromBroadcast && broadcast && hasChanged) { + this.broadcast(key, value, {sync}); } if (hasChanged) { const specific = onChange.specific.get(key); @@ -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,62 +220,63 @@ 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 - for (const key in defaults) { - const defaultValue = defaults[key]; - let value = localStorage[key]; - if (typeof value === 'string') { - switch (typeof defaultValue) { - case 'boolean': - value = value.toLowerCase() === 'true'; - break; - case 'number': - value |= 0; - break; - case 'object': - value = tryJSONparse(value) || defaultValue; - break; - } - } else if (FIREFOX_NO_DOM_STORAGE && BG) { - value = BG.localStorage[key]; - value = value === undefined ? defaultValue : value; - } else { - value = defaultValue; - } - if (BG === window) { - // when in bg page, .set() will write to localStorage - this.set(key, value, {broadcast: false, sync: false}); - } else { - values[key] = value; - defineReadonlyProperty(this.readOnlyValues, key, value); - } - } - - if (!BG || BG === window) { - affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); - - const importFromSync = (synced = {}) => { + { + 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) { - if (key in synced) { - this.set(key, synced[key], {sync: false}); - } - } - }; - - getSync().get('settings', ({settings} = {}) => importFromSync(settings)); - - chrome.storage.onChanged.addListener((changes, area) => { - if (area === 'sync' && 'settings' in changes) { - const synced = changes.settings.newValue; - if (synced) { - importFromSync(synced); + const defaultValue = defaults[key]; + let value = localStorage[key]; + if (typeof value === 'string') { + switch (typeof defaultValue) { + case 'boolean': + value = value.toLowerCase() === 'true'; + break; + case 'number': + value |= 0; + break; + case 'object': + value = tryJSONparse(value) || defaultValue; + break; + } + } else if (FIREFOX_NO_DOM_STORAGE && BG) { + value = BG.localStorage[key]; + value = value === undefined ? defaultValue : value; + localStorage[key] = value; } else { - // user manually deleted our settings, we'll recreate them - getSync().set({'settings': values}); + value = defaultValue; + } + if (BG === window) { + // when in bg page, .set() will write to localStorage + this.set(key, value, {broadcast: false, sync: false}); + } else { + values[key] = value; + defineReadonlyProperty(this.readOnlyValues, key, value); } } + return Promise.resolve(); + }; + (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) { + importFromSync.call(this, changes.settings.newValue); + } + }); }); } @@ -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') { diff --git a/js/storage-util.js b/js/storage-util.js new file mode 100644 index 00000000..8e947a3c --- /dev/null +++ b/js/storage-util.js @@ -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)) + ); + } +} diff --git a/manage.html b/manage.html index 4ca43556..0a0ea0dc 100644 --- a/manage.html +++ b/manage.html @@ -150,13 +150,18 @@ + - - - - + + + + + + + + @@ -358,10 +363,6 @@
- - - - diff --git a/manage/config-dialog.js b/manage/config-dialog.js index 04fe1128..e9f01d95 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -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(); diff --git a/manage/filters.js b/manage/filters.js index 662cfe3a..99facf33 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -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,87 +269,34 @@ 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)); - 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))); - } - if (entry.classList.contains('not-matching') !== !isMatching) { - entry.classList.toggle('not-matching', !isMatching); - needsRefilter = true; - } - } - 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; - } + return API.searchDB({ + query, + ids: [...entries].map(el => el.styleId), + }).then(ids => { + ids = new Set(ids); + let needsRefilter = false; + for (const entry of entries) { + const isMatching = ids.has(entry.styleId); + if (entry.classList.contains('not-matching') !== !isMatching) { + entry.classList.toggle('not-matching', !isMatching); + needsRefilter = true; } } - } - - function isMatchingText(text) { - if (rx) { - return rx.test(text); + if (needsRefilter && !container) { + filterOnChange({forceRefilter: true}); } - for (let pass = 1; pass <= 2; pass++) { - if (words.every(word => text.includes(word))) { - return true; - } - text = text.toLocaleLowerCase(); - } - return false; - } + return container; + }); } diff --git a/manage/import-export.js b/manage/import-export.js index ce615bdb..dbd7fb76 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -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); diff --git a/manage/manage.js b/manage/manage.js index 92018467..03ff7caa 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -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'; - }); - } - }); -} diff --git a/manage/sort.js b/manage/sort.js index ad81c6ce..b409ae6e 100644 --- a/manage/sort.js +++ b/manage/sort.js @@ -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)) { diff --git a/manage/updater-ui.js b/manage/updater-ui.js index 983086fa..0976cb5b 100644 --- a/manage/updater-ui.js +++ b/manage/updater-ui.js @@ -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: - if (++updated === 1) { - $('#apply-all-updates').disabled = true; - $('#apply-all-updates').classList.remove('hidden'); - } - $('#apply-all-updates').dataset.value = updated; - // fallthrough - case BG.updater.SKIPPED: - checked++; - if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) { - skippedEdited++; - } - reportUpdateState(state, value, details); - break; + 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) { + btnApply.disabled = true; + btnApply.classList.remove('hidden'); + } + btnApply.dataset.value = updated; + } + if (info.updated || info.error) { + checked++; + 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,43 +126,37 @@ function reportUpdateState(state, style, details) { ['no-update', 0], ['update-problem', 0], ]); - switch (state) { - case BG.updater.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; - } - const same = ( - details === BG.updater.SAME_MD5 || - details === BG.updater.SAME_CODE || - details === BG.updater.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 message = same ? t('updateCheckSucceededNoUpdate') : details; - newClasses.set('no-update', true); - newClasses.set('update-problem', !same); - $('.update-note', entry).textContent = message; - $('.check-update', entry).title = newUI.enabled ? message : ''; - $('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate'); - if (!document.body.classList.contains('update-in-progress')) { - // this is a single update job so we can decide whether to hide the filter - renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); - } + if (updated) { + newClasses.set('can-update', true); + entry.updatedCode = style; + $('.update-note', entry).textContent = ''; + $('#only-updates').classList.remove('hidden'); + } else if (!entry.classList.contains('can-update')) { + const same = ( + error === STATES.SAME_MD5 || + error === STATES.SAME_CODE || + error === STATES.SAME_VERSION + ); + 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') : error; + newClasses.set('no-update', true); + newClasses.set('update-problem', !same); + $('.update-note', entry).textContent = message; + $('.check-update', entry).title = newUI.enabled ? message : ''; + $('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate'); + if (!document.body.classList.contains('update-in-progress')) { + // this is a single update job so we can decide whether to hide the filter + renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')}); } } @@ -162,9 +165,16 @@ function reportUpdateState(state, style, details) { // 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; diff --git a/manifest.json b/manifest.json index 3e1b7cd7..4a5c65a4 100644 --- a/manifest.json +++ b/manifest.json @@ -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": { diff --git a/options/options.js b/options/options.js index a6e57c62..e28a8326 100644 --- a/options/options.js +++ b/options/options.js @@ -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; - document.body.classList.add('update-in-progress'); - break; - case BG.updater.UPDATED: - updated++; - // fallthrough - case BG.updater.SKIPPED: - checked++; - break; - case BG.updater.DONE: - document.body.classList.remove('update-in-progress'); - return; + 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'); + } else if (info.updated) { + updated++; + checked++; + } else if (info.error) { + checked++; + } else if (info.done) { + document.body.classList.remove('update-in-progress'); } $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px'; $('#updates-installed').dataset.value = updated || ''; diff --git a/popup.html b/popup.html index e2d50968..12f9b0e9 100644 --- a/popup.html +++ b/popup.html @@ -161,6 +161,8 @@ + + diff --git a/popup/hotkeys.js b/popup/hotkeys.js index 8787fd94..3f21b27b 100644 --- a/popup/hotkeys.js +++ b/popup/hotkeys.js @@ -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(); diff --git a/popup/popup.js b/popup/popup.js index f9e6fb8d..cecedc75 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -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(([styles]) => { - showStyles(styles); - }); +).then(url => Promise.all([ + (tabURL = URLS.supported(url) ? url : '') && + API.getStyles({ + matchUrl: tabURL, + omitCode: !BG, + }), + 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 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,34 +223,30 @@ 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')); - }); + } + window.dispatchEvent(new Event('showStyles:done')); + }); } 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) { - document.body.classList.remove('blocked'); - $$.remove('.blocked-info, #no-styles'); - createStyleElement({style}); - } + API.getStyles({ + matchUrl: tabURL, + stopOnFirst: true, + omitCode: true, + }).then(([style]) => { + if (style) { + document.body.classList.remove('blocked'); + $$.remove('.blocked-info, #no-styles'); + 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); - } +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 (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.skipped || '!')); + indicator.onclick = handleEvent.indicator; + $('.main-controls', entry).appendChild(indicator); } } - } - 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) { - 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); - const indicator = template.regexpProblemIndicator.cloneNode(true); - indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!')); - indicator.onclick = handleEvent.indicator; - $('.main-controls', entry).appendChild(indicator); - } + }); } diff --git a/popup/search-results.js b/popup/search-results.js index efd6283a..5f6a0ab8 100755 --- a/popup/search-results.js +++ b/popup/search-results.js @@ -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)), - date: Date.now(), - }).then(() => data); + return chromeLocal.loadLZStringScript().then(() => + chromeLocal.setValue(CACHE_PREFIX + data.id, { + payload: LZString.compressToUTF16(JSON.stringify(data)), + date: Date.now(), + })).then(() => data); } }