From dc491e9be3ecdf8da516e6653df0030cab104fb9 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 11 Oct 2018 01:22:13 +0800 Subject: [PATCH] Kill old storage, storage-dummy --- background/search-db.js | 2 +- background/storage-dummy.js | 78 ------ background/storage.js | 535 +----------------------------------- background/style-manager.js | 11 +- content/apply.js | 2 + 5 files changed, 15 insertions(+), 613 deletions(-) delete mode 100644 background/storage-dummy.js diff --git a/background/search-db.js b/background/search-db.js index d6aef03d..8cbf838b 100644 --- a/background/search-db.js +++ b/background/search-db.js @@ -1,4 +1,4 @@ -/* global API_METHODS styleManager */ +/* global API_METHODS styleManager tryRegExp debounce */ 'use strict'; (() => { diff --git a/background/storage-dummy.js b/background/storage-dummy.js deleted file mode 100644 index 5a2de9b2..00000000 --- a/background/storage-dummy.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -// eslint-disable-next-line no-unused-expressions -(chrome.runtime.id.includes('@temporary') || !('sync' in chrome.storage)) && (() => { - - const listeners = new Set(); - Object.assign(chrome.storage.onChanged, { - addListener: fn => listeners.add(fn), - hasListener: fn => listeners.has(fn), - removeListener: fn => listeners.delete(fn), - }); - - for (const name of ['local', 'sync']) { - const dummy = tryJSONparse(localStorage['dummyStorage.' + name]) || {}; - chrome.storage[name] = { - get(data, cb) { - let result = {}; - if (data === null) { - result = deepCopy(dummy); - } else if (Array.isArray(data)) { - for (const key of data) { - result[key] = dummy[key]; - } - } else if (typeof data === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - for (const key in data) { - if (hasOwnProperty.call(data, key)) { - const value = dummy[key]; - result[key] = value === undefined ? data[key] : value; - } - } - } else { - result[data] = dummy[data]; - } - if (typeof cb === 'function') cb(result); - }, - set(data, cb) { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const changes = {}; - for (const key in data) { - if (!hasOwnProperty.call(data, key)) continue; - const newValue = data[key]; - changes[key] = {newValue, oldValue: dummy[key]}; - dummy[key] = newValue; - } - localStorage['dummyStorage.' + name] = JSON.stringify(dummy); - if (typeof cb === 'function') cb(); - notify(changes); - }, - remove(keyOrKeys, cb) { - const changes = {}; - for (const key of Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]) { - changes[key] = {oldValue: dummy[key]}; - delete dummy[key]; - } - localStorage['dummyStorage.' + name] = JSON.stringify(dummy); - if (typeof cb === 'function') cb(); - notify(changes); - }, - }; - } - - window.API_METHODS = Object.assign(window.API_METHODS || {}, { - dummyStorageGet: ({data, name}) => new Promise(resolve => chrome.storage[name].get(data, resolve)), - dummyStorageSet: ({data, name}) => new Promise(resolve => chrome.storage[name].set(data, resolve)), - dummyStorageRemove: ({data, name}) => new Promise(resolve => chrome.storage[name].remove(data, resolve)), - }); - - function notify(changes, name) { - for (const fn of listeners.values()) { - fn(changes, name); - } - sendMessage({ - dummyStorageChanges: changes, - dummyStorageName: name, - }, ignoreChromeError); - } -})(); diff --git a/background/storage.js b/background/storage.js index e79c637f..0ca98ca2 100644 --- a/background/storage.js +++ b/background/storage.js @@ -1,427 +1,6 @@ -/* global getStyleWithNoCode styleSectionsEqual */ +/* exported calcStyleDigest detectSloppyRegexps */ 'use strict'; -const RX_NAMESPACE = /\s*(@namespace\s+(?:\S+\s+)?url\(http:\/\/.*?\);)\s*/g; -const RX_CHARSET = /\s*@charset\s+(['"]).*?\1\s*;\s*/g; -const RX_CSS_COMMENTS = /\/\*[\s\S]*?(?:\*\/|$)/g; -// eslint-disable-next-line no-var -var SLOPPY_REGEXP_PREFIX = '\0'; - -// CSS transition bug workaround: since we insert styles asynchronously, -// the browsers, especially Firefox, may apply all transitions on page load -const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }'; -const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/; - -// Note, only 'var'-declared variables are visible from another extension page -// eslint-disable-next-line no-var -var cachedStyles = { - list: null, // array of all styles - byId: new Map(), // all styles indexed by id - filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max - regexps: new Map(), // compiled style regexps - exclusions: new Map(), // compiled exclusion regexps - urlDomains: new Map(), // getDomain() results for 100 last checked urls - needTransitionPatch: new Map(), // FF bug workaround - mutex: { - inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls - // (initially 'true' to prevent rogue getStyles before dbExec.initialized) - onDone: [], // to getStyles() are queued and resolved when the first one finishes - }, -}; - -function filterStyles({ - enabled = null, - id = null, - matchUrl = null, - md5Url = null, - asHash = null, - omitCode, - strictRegexp = true, // used by the popup to detect bad regexps -} = {}) { - if (matchUrl && !URLS.supported(matchUrl)) { - return asHash ? {length: 0} : []; - } - - // make sure to use the same order in updateFiltersCache() - const cacheKey = - enabled + '\t' + - id + '\t' + - matchUrl + '\t' + - md5Url + '\t' + - asHash + '\t' + - strictRegexp; - const cached = cachedStyles.filters.get(cacheKey); - let styles; - if (cached) { - cached.hits++; - cached.lastHit = Date.now(); - styles = asHash - ? Object.assign(blankHash, cached.styles) - : cached.styles.slice(); - } else { - styles = filterStylesInternal({ - enabled, - id, - matchUrl, - md5Url, - asHash, - strictRegexp, - blankHash, - cacheKey, - omitCode, - }); - } - if (!omitCode) return styles; - if (!asHash) return styles.map(getStyleWithNoCode); - for (const id in styles) { - const sections = styles[id]; - if (Array.isArray(sections)) { - styles[id] = getStyleWithNoCode({sections}).sections; - } - } - return styles; -} - - -function filterStylesInternal({ - // js engines don't like big functions (V8 often deoptimized the original filterStyles) - // it also makes sense to extract the less frequently executed code - enabled, - id, - matchUrl, - md5Url, - asHash, - strictRegexp, - blankHash, - cacheKey, - omitCode, -}) { - if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) { - cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl)); - for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) { - const firstKey = cachedStyles.urlDomains.keys().next().value; - cachedStyles.urlDomains.delete(firstKey); - } - } - - const filtered = asHash ? {length: 0} : []; - const needSections = asHash || matchUrl !== null; - const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0]; - let style; - for (let i = 0; (style = styles[i]); i++) { - if ((enabled === null || style.enabled === enabled) - && (md5Url === null || style.md5Url === md5Url) - && (id === null || style.id === id)) { - const sections = needSections && - getApplicableSections({ - style, - matchUrl, - strictRegexp, - stopOnFirst: !asHash, - skipUrlCheck: true, - matchUrlBase, - omitCode, - }); - if (asHash) { - if (sections.length && sections[0] !== '') { - sections.unshift({included: !isPageExcluded(matchUrl, style.exclusions)}); - filtered[style.id] = sections; - filtered.length++; - } - } else if (matchUrl === null || sections.length) { - filtered.push(style); - } - } - } - - cachedStyles.filters.set(cacheKey, { - styles: filtered, - lastHit: Date.now(), - hits: 1, - }); - if (cachedStyles.filters.size > 10000) { - cleanupCachedFilters(); - } - - // a shallow copy is needed because the cache doesn't store options like disableAll - return asHash - ? Object.assign(blankHash, filtered) - : filtered; -} - -function compileExclusionRegexps(exclusions) { - exclusions.forEach(exclusion => { - if (!cachedStyles.exclusions.get(exclusion)) { - cachedStyles.exclusions.set(exclusion, tryRegExp(exclusion) || false); - } - }); -} - -function isPageExcluded(matchUrl, exclusions = {}) { - const keys = Object.keys(exclusions); - if (!keys.length) { - return false; - } - compileExclusionRegexps(keys); - return keys.some(exclude => { - const rx = cachedStyles.exclusions.get(exclude); - return rx && rx.test(matchUrl); - }); -} - - -function getApplicableSections({ - style, - matchUrl, - strictRegexp = true, - // filterStylesInternal() sets the following to avoid recalc on each style: - stopOnFirst, - skipUrlCheck, - matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0], - omitCode, - // as per spec the fragment portion is ignored in @-moz-document: - // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc - // but the spec is outdated and doesn't account for SPA sites - // so we only respect it in case of url("http://exact.url/without/hash") -}) { - const excluded = isPageExcluded(matchUrl, style.exclusions); - if (!skipUrlCheck && !URLS.supported(matchUrl) || omitCode !== false && excluded) { - return []; - } - // Show excluded style in popup - if (excluded) { - return [{}]; - } - const sections = []; - for (const section of style.sections) { - const {urls, domains, urlPrefixes, regexps, code} = section; - const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length; - const isMatching = !isGlobal && ( - urls.length - && (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase)) - || urlPrefixes.length - && arraySomeIsPrefix(urlPrefixes, matchUrl) - || domains.length - && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains) - || regexps.length - && arraySomeMatches(regexps, matchUrl, strictRegexp)); - if (isGlobal && !styleCodeEmpty(code) || isMatching) { - sections.push(section); - if (stopOnFirst) { - break; - } - } - } - return sections; - - function arraySomeIsPrefix(array, string) { - for (const prefix of array) { - if (string.startsWith(prefix)) { - return true; - } - } - return false; - } - - function arraySomeIn(array, haystack) { - for (const el of array) { - if (haystack.indexOf(el) >= 0) { - return true; - } - } - return false; - } - - function arraySomeMatches(array, matchUrl, strictRegexp) { - for (const regexp of array) { - for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) { - const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp; - let rx = cachedStyles.regexps.get(cacheKey); - if (rx === false) { - // invalid regexp - break; - } - if (!rx) { - const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; - rx = tryRegExp(anchored); - cachedStyles.regexps.set(cacheKey, rx || false); - if (!rx) { - // invalid regexp - break; - } - } - if (rx.test(matchUrl)) { - return true; - } - } - } - return false; - } -} - - -function styleCodeEmpty(code) { - // Collect the global section if it's not empty, not comment-only, not namespace-only. - const cmtOpen = code && code.indexOf('/*'); - if (cmtOpen >= 0) { - const cmtCloseLast = code.lastIndexOf('*/'); - if (cmtCloseLast < 0) { - code = code.substr(0, cmtOpen); - } else { - code = code.substr(0, cmtOpen) + - code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') + - code.substr(cmtCloseLast + 2); - } - } - if (!code || !code.trim()) return true; - if (code.includes('@namespace')) code = code.replace(RX_NAMESPACE, '').trim(); - if (code.includes('@charset')) code = code.replace(RX_CHARSET, '').trim(); - return !code; -} - - -function invalidateCache({added, updated, deletedId} = {}) { - if (!cachedStyles.list) return; - const id = added ? added.id : updated ? updated.id : deletedId; - const cached = cachedStyles.byId.get(id); - - if (updated) { - if (cached) { - const isSectionGlobal = section => - !section.urls.length && - !section.urlPrefixes.length && - !section.domains.length && - !section.regexps.length; - const hadOrHasGlobals = cached.sections.some(isSectionGlobal) || - updated.sections.some(isSectionGlobal); - const reenabled = !cached.enabled && updated.enabled; - const equal = !hadOrHasGlobals && - !reenabled && - styleSectionsEqual(updated, cached, {ignoreCode: true}); - Object.assign(cached, updated); - if (equal) { - updateFiltersCache(cached); - } else { - cachedStyles.filters.clear(); - } - cachedStyles.needTransitionPatch.delete(id); - return; - } else { - added = updated; - } - } - - if (added) { - if (!cached) { - cachedStyles.list.push(added); - cachedStyles.byId.set(added.id, added); - cachedStyles.filters.clear(); - cachedStyles.needTransitionPatch.delete(id); - } - return; - } - - if (deletedId !== undefined) { - if (cached) { - const cachedIndex = cachedStyles.list.indexOf(cached); - cachedStyles.list.splice(cachedIndex, 1); - cachedStyles.byId.delete(deletedId); - for (const {styles} of cachedStyles.filters.values()) { - if (Array.isArray(styles)) { - const index = styles.findIndex(({id}) => id === deletedId); - if (index >= 0) styles.splice(index, 1); - } else if (deletedId in styles) { - delete styles[deletedId]; - styles.length--; - } - } - cachedStyles.needTransitionPatch.delete(id); - return; - } - } - - cachedStyles.list = null; - cachedStyles.filters.clear(); - cachedStyles.needTransitionPatch.clear(id); -} - - -function updateFiltersCache(style) { - const {id} = style; - for (const [key, {styles}] of cachedStyles.filters.entries()) { - if (Array.isArray(styles)) { - const index = styles.findIndex(style => style.id === id); - if (index >= 0) styles[index] = Object.assign({}, style); - continue; - } - if (id in styles) { - const [, , matchUrl, , , strictRegexp] = key.split('\t'); - if (!style.enabled) { - delete styles[id]; - styles.length--; - continue; - } - const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0]; - const sections = getApplicableSections({ - style, - matchUrl, - matchUrlBase, - strictRegexp, - skipUrlCheck: true, - omitCode: false - }); - if (sections.length) { - styles[id] = sections; - } else { - delete styles[id]; - styles.length--; - } - } - } -} - - -function cleanupCachedFilters({force = false} = {}) { - if (!force) { - debounce(cleanupCachedFilters, 1000, {force: true}); - return; - } - const size = cachedStyles.filters.size; - const oldestHit = cachedStyles.filters.values().next().value.lastHit; - const now = Date.now(); - const timeSpan = now - oldestHit; - const recencyWeight = 5 / size; - const hitWeight = 1 / 4; // we make ~4 hits per URL - const lastHitWeight = 10; - // delete the oldest 10% - [...cachedStyles.filters.entries()] - .map(([id, v], index) => ({ - id, - weight: - index * recencyWeight + - v.hits * hitWeight + - (v.lastHit - oldestHit) / timeSpan * lastHitWeight, - })) - .sort((a, b) => a.weight - b.weight) - .slice(0, size / 10 + 1) - .forEach(({id}) => cachedStyles.filters.delete(id)); -} - - -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; -} - - function normalizeStyleSections({sections}) { // retain known properties in an arbitrarily predefined order return (sections || []).map(section => ({ @@ -433,7 +12,6 @@ function normalizeStyleSections({sections}) { })); } - function calcStyleDigest(style) { const jsonString = style.usercssData ? style.sourceCode : JSON.stringify(normalizeStyleSections(style)); @@ -451,81 +29,6 @@ function calcStyleDigest(style) { } } - -function handleCssTransitionBug({tabId, frameId, url, styles}) { - for (let id in styles) { - id |= 0; - if (!id) { - continue; - } - let need = cachedStyles.needTransitionPatch.get(id); - if (need === false) { - continue; - } - if (need !== true) { - need = styles[id].some(sectionContainsTransitions); - cachedStyles.needTransitionPatch.set(id, need); - if (!need) { - continue; - } - } - if (FIREFOX && !url.startsWith(URLS.ownOrigin)) { - patchFirefox(); - } else { - styles.needTransitionPatch = true; - } - break; - } - - function patchFirefox() { - const options = { - frameId, - code: CSS_TRANSITION_SUPPRESSOR, - matchAboutBlank: true, - }; - if (FIREFOX >= 53) { - options.cssOrigin = 'user'; - } - browser.tabs.insertCSS(tabId, Object.assign(options, { - runAt: 'document_start', - })).then(() => setTimeout(() => { - browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError); - })).catch(ignoreChromeError); - } - - function sectionContainsTransitions(section) { - let code = section.code; - const firstTransition = code.indexOf('transition'); - if (firstTransition < 0) { - return false; - } - const firstCmt = code.indexOf('/*'); - // check the part before the first comment - if (firstCmt < 0 || firstTransition < firstCmt) { - if (quickCheckAround(code, firstTransition)) { - return true; - } else if (firstCmt < 0) { - return false; - } - } - // check the rest - const lastCmt = code.lastIndexOf('*/'); - if (lastCmt < firstCmt) { - // the comment is unclosed and we already checked the preceding part - return false; - } - let mid = code.slice(firstCmt, lastCmt + 2); - mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, ''); - code = mid + code.slice(lastCmt + 2); - return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code); - } - - function quickCheckAround(code, pos = code.indexOf('transition')) { - 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. @@ -534,39 +37,5 @@ function handleCssTransitionBug({tabId, frameId, url, styles}) { 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, omitCode: false}); - const wannabe = getApplicableSections({style, matchUrl, omitCode: false, strictRegexp: false}); - if (wannabe.length && wannabe[0] !== '') { - results.push({ - id, - applied, - skipped: wannabe.length - applied.length, - hasInvalidRegexps: wannabe.some(({regexps}) => regexps.some(rx => !rxCache.has(rx))), - }); - } - } - return results; + // TODO } diff --git a/background/style-manager.js b/background/style-manager.js index 8b167db7..780359ee 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -344,6 +344,7 @@ const styleManager = (() => { } } + // TODO: report excluded styles and sloppy regexps? function getAppliedCode(url, data) { if (!urlMatchStyle(url, data)) { return; @@ -388,6 +389,7 @@ const styleManager = (() => { } function urlMatchStyle(url, style) { + // TODO: show excluded style in popup? if (style.exclusions && style.exclusions.some(e => compileExclusion(e).test(url))) { return false; } @@ -402,7 +404,14 @@ const styleManager = (() => { if (section.urlPrefixes && section.urlPrefixes.some(p => url.startsWith(p))) { return true; } - if (section.urls && section.urls.includes(getUrlNoHash(url))) { + // as per spec the fragment portion is ignored in @-moz-document: + // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc + // but the spec is outdated and doesn't account for SPA sites + // so we only respect it for `url()` function + if (section.urls && ( + section.urls.includes(url) || + section.urls.includes(getUrlNoHash(url)) + )) { return true; } if (section.regexps && section.regexps.some(r => compileRe(r).test(url))) { diff --git a/content/apply.js b/content/apply.js index faae6303..641b1f6b 100644 --- a/content/apply.js +++ b/content/apply.js @@ -34,6 +34,8 @@ API.getSectionsByUrl(getMatchUrl(), {enabled: true}) .then(result => { const styles = Object.values(result); + // CSS transition bug workaround: since we insert styles asynchronously, + // the browsers, especially Firefox, may apply all transitions on page load if (styles.some(s => s.code.includes('transition'))) { applyTransitionPatch(); }