stylus/background/storage.js

848 lines
24 KiB
JavaScript
Raw Normal View History

2018-01-04 13:43:54 +00:00
/* global getStyleWithNoCode styleSectionsEqual */
'use strict';
const RX_NAMESPACE = /\s*(@namespace\s+(?:\S+\s+)?url\(http:\/\/.*?\);)\s*/g;
const RX_CHARSET = /\s*@charset\s+(['"]).*?\1\s*;\s*/g;
2018-01-30 15:45:29 +00:00
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
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
},
};
// eslint-disable-next-line no-var
var dbExec = dbExecIndexedDB;
2017-09-03 18:25:19 +00:00
dbExec.initialized = false;
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
// which, once detected on the first run, is remembered in chrome.storage.local
// for reliablility and in localStorage for fast synchronous access
// (FF may block localStorage depending on its privacy options)
do {
2017-09-03 18:25:19 +00:00
const done = () => {
cachedStyles.mutex.inProgress = false;
2017-09-03 18:25:19 +00:00
getStyles().then(() => {
dbExec.initialized = true;
window.dispatchEvent(new Event('storageReady'));
});
};
const fallback = () => {
dbExec = dbExecChromeStorage;
chromeLocal.set({dbInChromeStorage: true});
localStorage.dbInChromeStorage = 'true';
ignoreChromeError();
2017-09-03 18:25:19 +00:00
done();
};
const fallbackSet = localStorage.dbInChromeStorage;
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
fallback();
break;
} else if (fallbackSet === 'false') {
2017-09-03 18:25:19 +00:00
done();
break;
}
chromeLocal.get('dbInChromeStorage')
.then(data =>
data && data.dbInChromeStorage && Promise.reject())
.then(() =>
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
Promise.reject())
.then(({target}) => (
(target.result || [])[0] ?
Promise.reject('ok') :
2017-09-03 18:56:46 +00:00
dbExecIndexedDB('put', {id: -1})))
.then(() =>
dbExecIndexedDB('get', -1))
.then(({target}) => (
(target.result || {}).id === -1 ?
2017-09-03 18:56:46 +00:00
dbExecIndexedDB('delete', -1) :
Promise.reject()))
2017-09-03 18:56:46 +00:00
.then(() =>
Promise.reject('ok'))
.catch(result => {
if (result === 'ok') {
chromeLocal.set({dbInChromeStorage: false});
localStorage.dbInChromeStorage = 'false';
2017-09-03 18:25:19 +00:00
done();
} else {
fallback();
}
});
} while (0);
function dbExecIndexedDB(method, ...args) {
2017-04-25 21:48:27 +00:00
return new Promise((resolve, reject) => {
Object.assign(indexedDB.open('stylish', 2), {
onsuccess(event) {
const database = event.target.result;
if (!method) {
resolve(database);
} else {
const transaction = database.transaction(['styles'], 'readwrite');
const store = transaction.objectStore('styles');
Object.assign(store[method](...args), {
2017-04-25 21:48:27 +00:00
onsuccess: event => resolve(event, store, transaction, database),
onerror: reject,
});
}
},
onerror(event) {
console.warn(event.target.error || event.target.errorCode);
2017-04-25 21:48:27 +00:00
reject(event);
},
onupgradeneeded(event) {
2017-07-16 18:02:00 +00:00
if (event.oldVersion === 0) {
2017-04-25 21:48:27 +00:00
event.target.result.createObjectStore('styles', {
keyPath: 'id',
autoIncrement: true,
});
}
},
});
});
}
2017-03-17 22:50:35 +00:00
function dbExecChromeStorage(method, data) {
const STYLE_KEY_PREFIX = 'style-';
switch (method) {
case 'get':
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
.then(result => ({target: {result}}));
case 'put':
if (!data.id) {
return getStyles().then(() => {
data.id = 1;
for (const style of cachedStyles.list) {
data.id = Math.max(data.id, style.id + 1);
}
return dbExecChromeStorage('put', data);
});
}
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
case 'delete':
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
case 'getAll':
return chromeLocal.get(null).then(storage => {
const styles = [];
for (const key in storage) {
if (key.startsWith(STYLE_KEY_PREFIX) &&
Number(key.substr(STYLE_KEY_PREFIX.length))) {
styles.push(storage[key]);
}
}
return {target: {result: styles}};
});
}
return Promise.reject();
}
2017-04-25 21:48:27 +00:00
function getStyles(options) {
if (cachedStyles.list) {
2017-04-25 21:48:27 +00:00
return Promise.resolve(filterStyles(options));
}
if (cachedStyles.mutex.inProgress) {
2017-04-25 21:48:27 +00:00
return new Promise(resolve => {
cachedStyles.mutex.onDone.push({options, resolve});
});
}
cachedStyles.mutex.inProgress = true;
2017-04-25 21:48:27 +00:00
return dbExec('getAll').then(event => {
cachedStyles.list = event.target.result || [];
cachedStyles.list.forEach(fixUsoMd5Issue);
2017-04-25 21:48:27 +00:00
cachedStyles.byId.clear();
for (const style of cachedStyles.list) {
cachedStyles.byId.set(style.id, style);
2017-12-03 17:32:50 +00:00
if (!style.name) {
style.name = 'ID: ' + style.id;
}
2017-04-25 21:48:27 +00:00
}
2017-04-25 21:48:27 +00:00
cachedStyles.mutex.inProgress = false;
for (const {options, resolve} of cachedStyles.mutex.onDone) {
resolve(filterStyles(options));
}
cachedStyles.mutex.onDone = [];
return filterStyles(options);
});
2015-01-30 17:07:24 +00:00
}
2017-03-17 22:50:35 +00:00
function filterStyles({
2017-04-19 16:13:11 +00:00
enabled = null,
id = null,
matchUrl = null,
md5Url = null,
asHash = null,
2018-01-01 17:02:49 +00:00
omitCode,
strictRegexp = true, // used by the popup to detect bad regexps
} = {}) {
if (id) id = Number(id);
if (asHash) enabled = true;
2017-07-16 18:02:00 +00:00
if (
enabled === null &&
id === null &&
matchUrl === null &&
md5Url === null &&
2017-07-16 18:02:00 +00:00
asHash !== true
) {
return cachedStyles.list;
}
if (matchUrl && !URLS.supported(matchUrl)) {
return asHash ? {length: 0} : [];
}
const blankHash = asHash && {
length: 0,
disableAll: prefs.get('disableAll'),
exposeIframes: prefs.get('exposeIframes'),
};
// 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);
2018-01-01 17:02:49 +00:00
let styles;
if (cached) {
cached.hits++;
cached.lastHit = Date.now();
2018-01-01 17:02:49 +00:00
styles = asHash
? Object.assign(blankHash, cached.styles)
2018-01-01 17:02:49 +00:00
: cached.styles.slice();
} else {
styles = filterStylesInternal({
enabled,
id,
matchUrl,
md5Url,
asHash,
strictRegexp,
blankHash,
cacheKey,
});
}
if (!omitCode) return styles;
if (!asHash) return styles.map(getStyleWithNoCode);
for (const id in styles) {
2018-01-10 18:54:59 +00:00
const sections = styles[id];
if (Array.isArray(sections)) {
styles[id] = getStyleWithNoCode({sections}).sections;
2018-01-01 17:02:49 +00:00
}
}
return styles;
}
// The md5Url provided by USO includes a duplicate "update" subdomain (see #523),
// This fixes any already installed styles containing this error
function fixUsoMd5Issue(style) {
if (style && style.md5Url && style.md5Url.includes('update.update.userstyles')) {
style.md5Url = style.md5Url.replace('update.update.userstyles', 'update.userstyles');
}
}
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,
}) {
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 styles = id === null
? cachedStyles.list
: [cachedStyles.byId.get(id)];
if (!styles[0]) {
// may happen when users [accidentally] reopen an old URL
// of edit.html with a non-existent style id parameter
return asHash ? blankHash : [];
}
const filtered = asHash ? {length: 0} : [];
const needSections = asHash || matchUrl !== null;
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
2017-07-14 08:46:10 +00:00
let style;
for (let i = 0; (style = styles[i]); i++) {
2017-07-16 18:02:00 +00:00
if ((enabled === null || style.enabled === enabled)
&& (md5Url === null || style.md5Url === md5Url)
2017-07-16 18:02:00 +00:00
&& (id === null || style.id === id)) {
const sections = needSections &&
getApplicableSections({
style,
matchUrl,
strictRegexp,
stopOnFirst: !asHash,
skipUrlCheck: true,
matchUrlBase,
});
if (asHash) {
if (sections.length) {
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;
2015-01-30 17:07:24 +00:00
}
2017-03-17 22:50:35 +00:00
function saveStyle(style) {
const id = Number(style.id) || null;
2017-04-25 21:48:27 +00:00
const reason = style.reason;
const notify = style.notify !== false;
delete style.method;
delete style.reason;
delete style.notify;
if (!style.name) {
delete style.name;
}
2017-07-14 08:46:10 +00:00
let existed;
let codeIsUpdated;
Add: install styles from *.user.css file Fix: handle dup name+namespace Fix: eslint eqeqeq Fix: trim @name's spaces Add: check update for userstyle Add: build CSS variable Fix: only check dup when id is not provided Refactor: userStyle2json -> userstyle.json Add: style for input Add: config dialog Fix: preserve config during update Fix: onchange doesn't fire on keyboard enter event Fix: remove empty file Add: validator. Metas must stay in the same line Add: warn the user if installation failed Fix: add some delay before starting installation Add: open the editor after first installation Fix: add openEditor to globals Fix: i18n Add: preprocessor. Move userstyle.build to background page. Fix: remove unused global Fix: preserved unknown prop in saveStyleSource() like saveStyle() Add: edit userstyle source Fix: load preprocessor dynamically Fix: load content script dynamically Fix: buildCode is async function Fix: drop Object.entries Fix: style.sections is undefined Fix: don't hide the name input but disable it Fix: query the style before installation Revert: changes to editor, editor.html Refactor: use term `usercss` instead of `userstyle` Fix: don't show homepage action for usercss Refactor: move script-loader to js/ Refactor: pull out mozParser Fix: code style Fix: we don't need to build meta anymore Fix: use saveUsercss instead of saveStyle to get responsed error Fix: last is undefined, load script error Fix: switch to moz-format Fix: drop injectContentScript. Move usercss check into install-user-css Fix: response -> respond Fix: globals -> global Fix: queryUsercss -> filterUsercss Fix: add processUsercss function Fix: only open editor for usercss Fix: remove findupUsercss fixme Fix: globals -> global Fix: globals -> global Fix: global pollution Revert: update.js Refactor: checkStyle Add: support usercss Fix: no need to getURL in background page Fix: merget semver.js into usercss.js Fix: drop all_urls in match pattern Fix: drop respondWithError Move stylus -> stylus-lang Add stylus-lang/readme Fix: use include_globs Fix: global pollution
2017-08-05 16:49:25 +00:00
fixUsoMd5Issue(style);
return maybeCalcDigest()
2017-09-16 01:24:50 +00:00
.then(maybeImportFix)
.then(decide);
function maybeCalcDigest() {
if (['install', 'update', 'update-digest'].includes(reason)) {
2017-09-16 01:24:50 +00:00
return calcStyleDigest(style).then(digest => {
style.originalDigest = digest;
});
}
return Promise.resolve();
}
2017-09-16 01:24:50 +00:00
function maybeImportFix() {
if (reason === 'import') {
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
delete style.styleDigest; // TODO: remove in the future
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
delete style.originalDigest;
}
}
}
function decide() {
if (id !== null) {
// Update or create
style.id = id;
return dbExec('get', id).then((event, store) => {
const oldStyle = event.target.result;
existed = Boolean(oldStyle);
2017-07-16 18:02:00 +00:00
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
return style;
}
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
2017-11-26 17:47:23 +00:00
style = Object.assign({installDate: Date.now()}, oldStyle, style);
return write(style, store);
});
} else {
// Create
delete style.id;
style = Object.assign({
// Set optional things if they're undefined
enabled: true,
updateUrl: null,
md5Url: null,
url: null,
originalMd5: null,
2018-01-01 17:02:49 +00:00
installDate: Date.now(),
}, style);
return write(style);
}
2017-04-25 21:48:27 +00:00
}
function write(style, store) {
style.sections = normalizeStyleSections(style);
if (store) {
return new Promise(resolve => {
store.put(style).onsuccess = event => resolve(done(event));
});
} else {
return dbExec('put', style).then(done);
}
}
function done(event) {
2017-07-16 18:02:00 +00:00
if (reason === 'update-digest') {
return style;
}
2017-04-25 21:48:27 +00:00
style.id = style.id || event.target.result;
invalidateCache(existed ? {updated: style} : {added: style});
if (notify) {
notifyAllTabs({
method: existed ? 'styleUpdated' : 'styleAdded',
style, codeIsUpdated, reason,
});
}
return style;
}
2015-01-30 17:07:24 +00:00
}
2017-03-17 22:50:35 +00:00
function deleteStyle({id, notify = true}) {
2017-04-25 21:48:27 +00:00
id = Number(id);
return dbExec('delete', id).then(() => {
invalidateCache({deletedId: id});
if (notify) {
notifyAllTabs({method: 'styleDeleted', id});
}
return id;
});
2015-01-30 17:07:24 +00:00
}
2017-03-17 22:50:35 +00:00
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],
// 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")
}) {
if (!skipUrlCheck && !URLS.supported(matchUrl)) {
return [];
}
2017-03-26 07:19:47 +00:00
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;
2017-03-26 07:19:47 +00:00
}
}
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++) {
2017-07-16 18:02:00 +00:00
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
let rx = cachedStyles.regexps.get(cacheKey);
2017-07-16 18:02:00 +00:00
if (rx === false) {
// invalid regexp
break;
}
if (!rx) {
2017-07-16 18:02:00 +00:00
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;
2016-03-07 02:27:17 +00:00
}
2017-03-17 22:50:35 +00:00
2017-04-13 16:44:43 +00:00
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,
});
if (sections.length) {
styles[id] = sections;
} else {
delete styles[id];
styles.length--;
}
}
}
}
function cleanupCachedFilters({force = false} = {}) {
if (!force) {
2017-04-13 16:44:43 +00:00
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) {
2017-08-26 04:57:52 +00:00
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
if (!d || url.startsWith('file:')) {
return [];
}
const domains = [d];
2017-07-16 18:02:00 +00:00
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 => ({
code: section.code || '',
urls: section.urls || [],
urlPrefixes: section.urlPrefixes || [],
domains: section.domains || [],
regexps: section.regexps || [],
}));
}
function calcStyleDigest(style) {
2017-09-16 01:24:50 +00:00
const jsonString = style.usercssData ?
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
const text = new TextEncoder('utf-8').encode(jsonString);
return crypto.subtle.digest('SHA-1', text).then(hex);
function hex(buffer) {
const parts = [];
const PAD8 = '00000000';
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
}
return parts.join('');
}
}
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() {
2017-09-29 23:32:43 +00:00
const options = {
frameId,
code: CSS_TRANSITION_SUPPRESSOR,
matchAboutBlank: true,
2017-09-29 23:32:43 +00:00
};
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));
}
}
2018-01-01 17:02:49 +00:00
/*
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;
}