2018-11-07 06:09:29 +00:00
|
|
|
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
|
2020-10-04 10:42:06 +00:00
|
|
|
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
|
2020-10-23 05:27:29 +00:00
|
|
|
getStyleWithNoCode msg prefs sync URLS */
|
2018-11-07 06:09:29 +00:00
|
|
|
/* exported styleManager */
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/*
|
|
|
|
This style manager is a layer between content script and the DB. When a style
|
|
|
|
is added/updated, it broadcast a message to content script and the content
|
|
|
|
script would try to fetch the new code.
|
|
|
|
|
|
|
|
The live preview feature relies on `runtime.connect` and `port.onDisconnect`
|
|
|
|
to cleanup the temporary code. See /edit/live-preview.js.
|
|
|
|
*/
|
2020-11-18 11:17:15 +00:00
|
|
|
|
|
|
|
/** @type {styleManager} */
|
2018-11-07 06:09:29 +00:00
|
|
|
const styleManager = (() => {
|
|
|
|
const preparing = prepare();
|
|
|
|
|
|
|
|
/* styleId => {
|
|
|
|
data: styleData,
|
|
|
|
preview: styleData,
|
|
|
|
appliesTo: Set<url>
|
|
|
|
} */
|
|
|
|
const styles = new Map();
|
2019-11-05 19:30:45 +00:00
|
|
|
const uuidIndex = new Map();
|
2018-11-07 06:09:29 +00:00
|
|
|
|
|
|
|
/* url => {
|
|
|
|
maybeMatch: Set<styleId>,
|
|
|
|
sections: Object<styleId => {
|
|
|
|
id: styleId,
|
|
|
|
code: Array<String>
|
|
|
|
}>
|
|
|
|
} */
|
|
|
|
const cachedStyleForUrl = createCache({
|
|
|
|
onDeleted: (url, cache) => {
|
|
|
|
for (const section of Object.values(cache.sections)) {
|
|
|
|
const style = styles.get(section.id);
|
|
|
|
if (style) {
|
|
|
|
style.appliesTo.delete(url);
|
|
|
|
}
|
|
|
|
}
|
2020-11-18 11:17:15 +00:00
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const BAD_MATCHER = {test: () => false};
|
|
|
|
const compileRe = createCompiler(text => `^(${text})$`);
|
|
|
|
const compileSloppyRe = createCompiler(text => `^${text}$`);
|
2019-06-11 14:44:32 +00:00
|
|
|
const compileExclusion = createCompiler(buildExclusion);
|
|
|
|
|
|
|
|
const DUMMY_URL = {
|
|
|
|
hash: '',
|
|
|
|
host: '',
|
|
|
|
hostname: '',
|
|
|
|
href: '',
|
|
|
|
origin: '',
|
|
|
|
password: '',
|
|
|
|
pathname: '',
|
|
|
|
port: '',
|
|
|
|
protocol: '',
|
|
|
|
search: '',
|
|
|
|
searchParams: new URLSearchParams(),
|
2020-11-18 11:17:15 +00:00
|
|
|
username: '',
|
2019-06-11 14:44:32 +00:00
|
|
|
};
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2020-10-11 15:12:06 +00:00
|
|
|
const DELETE_IF_NULL = ['id', 'customName'];
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
handleLivePreviewConnections();
|
|
|
|
|
2020-11-18 11:17:15 +00:00
|
|
|
return Object.assign(/** @namespace styleManager */{
|
|
|
|
compareRevision,
|
|
|
|
}, ensurePrepared(/** @namespace styleManager */{
|
2018-11-07 06:09:29 +00:00
|
|
|
get,
|
2019-11-05 19:30:45 +00:00
|
|
|
getByUUID,
|
2018-11-07 06:09:29 +00:00
|
|
|
getSectionsByUrl,
|
2019-11-05 19:30:45 +00:00
|
|
|
putByUUID,
|
2018-11-07 06:09:29 +00:00
|
|
|
installStyle,
|
|
|
|
deleteStyle,
|
2019-11-05 19:30:45 +00:00
|
|
|
deleteByUUID,
|
2018-11-07 06:09:29 +00:00
|
|
|
editSave,
|
|
|
|
findStyle,
|
|
|
|
importStyle,
|
2018-11-11 06:04:22 +00:00
|
|
|
importMany,
|
2018-11-07 06:09:29 +00:00
|
|
|
toggleStyle,
|
|
|
|
getAllStyles, // used by import-export
|
|
|
|
getStylesByUrl, // used by popup
|
|
|
|
styleExists,
|
2019-03-03 22:54:37 +00:00
|
|
|
addExclusion,
|
|
|
|
removeExclusion,
|
|
|
|
addInclusion,
|
2020-11-18 11:17:15 +00:00
|
|
|
removeInclusion,
|
2019-11-05 19:30:45 +00:00
|
|
|
}));
|
2018-11-07 06:09:29 +00:00
|
|
|
|
|
|
|
function handleLivePreviewConnections() {
|
|
|
|
chrome.runtime.onConnect.addListener(port => {
|
|
|
|
if (port.name !== 'livePreview') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let id;
|
|
|
|
port.onMessage.addListener(data => {
|
|
|
|
if (!id) {
|
|
|
|
id = data.id;
|
|
|
|
}
|
|
|
|
const style = styles.get(id);
|
|
|
|
style.preview = data;
|
|
|
|
broadcastStyleUpdated(style.preview, 'editPreview');
|
|
|
|
});
|
|
|
|
port.onDisconnect.addListener(() => {
|
|
|
|
port = null;
|
|
|
|
if (id) {
|
|
|
|
const style = styles.get(id);
|
|
|
|
if (!style) {
|
|
|
|
// maybe deleted
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
style.preview = null;
|
|
|
|
broadcastStyleUpdated(style.data, 'editPreviewEnd');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-03-03 22:54:37 +00:00
|
|
|
function escapeRegExp(text) {
|
|
|
|
// https://github.com/lodash/lodash/blob/0843bd46ef805dd03c0c8d804630804f3ba0ca3c/lodash.js#L152
|
|
|
|
return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function get(id, noCode = false) {
|
|
|
|
const data = styles.get(id).data;
|
|
|
|
return noCode ? getStyleWithNoCode(data) : data;
|
|
|
|
}
|
|
|
|
|
2019-11-05 19:30:45 +00:00
|
|
|
function getByUUID(uuid) {
|
|
|
|
const id = uuidIndex.get(uuid);
|
|
|
|
if (id) {
|
|
|
|
return get(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-18 11:17:15 +00:00
|
|
|
function getAllStyles() {
|
|
|
|
return [...styles.values()].map(s => s.data);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2019-11-05 19:30:45 +00:00
|
|
|
function compareRevision(rev1, rev2) {
|
|
|
|
return rev1 - rev2;
|
|
|
|
}
|
|
|
|
|
|
|
|
function putByUUID(doc) {
|
|
|
|
const id = uuidIndex.get(doc._id);
|
|
|
|
if (id) {
|
|
|
|
doc.id = id;
|
|
|
|
} else {
|
|
|
|
delete doc.id;
|
|
|
|
}
|
|
|
|
const oldDoc = id && styles.has(id) && styles.get(id).data;
|
|
|
|
let diff = -1;
|
|
|
|
if (oldDoc) {
|
|
|
|
diff = compareRevision(oldDoc._rev, doc._rev);
|
|
|
|
if (diff > 0) {
|
|
|
|
sync.put(oldDoc._id, oldDoc._rev);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (diff < 0) {
|
|
|
|
return db.exec('put', doc)
|
|
|
|
.then(event => {
|
|
|
|
doc.id = event.target.result;
|
|
|
|
uuidIndex.set(doc._id, doc.id);
|
|
|
|
return handleSave(doc, 'sync');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function toggleStyle(id, enabled) {
|
|
|
|
const style = styles.get(id);
|
|
|
|
const data = Object.assign({}, style.data, {enabled});
|
|
|
|
return saveStyle(data)
|
|
|
|
.then(newData => handleSave(newData, 'toggle', false))
|
|
|
|
.then(() => id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// used by install-hook-userstyles.js
|
|
|
|
function findStyle(filter, noCode = false) {
|
|
|
|
for (const style of styles.values()) {
|
|
|
|
if (filterMatch(filter, style.data)) {
|
|
|
|
return noCode ? getStyleWithNoCode(style.data) : style.data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function styleExists(filter) {
|
|
|
|
return [...styles.values()].some(s => filterMatch(filter, s.data));
|
|
|
|
}
|
|
|
|
|
|
|
|
function filterMatch(filter, target) {
|
|
|
|
for (const key of Object.keys(filter)) {
|
|
|
|
if (filter[key] !== target[key]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function importStyle(data) {
|
|
|
|
// FIXME: is it a good idea to save the data directly?
|
|
|
|
return saveStyle(data)
|
|
|
|
.then(newData => handleSave(newData, 'import'));
|
|
|
|
}
|
|
|
|
|
2018-11-11 06:04:22 +00:00
|
|
|
function importMany(items) {
|
2019-11-05 19:30:45 +00:00
|
|
|
items.forEach(beforeSave);
|
2018-11-11 06:04:22 +00:00
|
|
|
return db.exec('putMany', items)
|
|
|
|
.then(events => {
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
2019-11-05 19:30:45 +00:00
|
|
|
afterSave(items[i], events[i].target.result);
|
2018-11-11 06:04:22 +00:00
|
|
|
}
|
|
|
|
return Promise.all(items.map(i => handleSave(i, 'import')));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function installStyle(data, reason = null) {
|
|
|
|
const style = styles.get(data.id);
|
|
|
|
if (!style) {
|
|
|
|
data = Object.assign(createNewStyle(), data);
|
|
|
|
} else {
|
|
|
|
data = Object.assign({}, style.data, data);
|
|
|
|
}
|
|
|
|
if (!reason) {
|
|
|
|
reason = style ? 'update' : 'install';
|
|
|
|
}
|
2020-10-07 14:59:21 +00:00
|
|
|
let url = !data.url && data.updateUrl;
|
|
|
|
if (url) {
|
|
|
|
const usoId = URLS.extractUsoArchiveId(url);
|
|
|
|
url = usoId && `${URLS.usoArchive}?style=${usoId}` ||
|
|
|
|
URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0];
|
|
|
|
if (url) data.url = data.installationUrl = url;
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
// FIXME: update updateDate? what about usercss config?
|
|
|
|
return calcStyleDigest(data)
|
|
|
|
.then(digest => {
|
|
|
|
data.originalDigest = digest;
|
|
|
|
return saveStyle(data);
|
|
|
|
})
|
|
|
|
.then(newData => handleSave(newData, reason));
|
|
|
|
}
|
|
|
|
|
|
|
|
function editSave(data) {
|
|
|
|
const style = styles.get(data.id);
|
|
|
|
if (style) {
|
|
|
|
data = Object.assign({}, style.data, data);
|
|
|
|
} else {
|
|
|
|
data = Object.assign(createNewStyle(), data);
|
|
|
|
}
|
2019-02-02 13:01:08 +00:00
|
|
|
data.updateDate = Date.now();
|
2018-11-07 06:09:29 +00:00
|
|
|
return saveStyle(data)
|
|
|
|
.then(newData => handleSave(newData, 'editSave'));
|
|
|
|
}
|
|
|
|
|
2019-03-03 22:54:37 +00:00
|
|
|
function addIncludeExclude(id, rule, type) {
|
|
|
|
const data = Object.assign({}, styles.get(id).data);
|
|
|
|
if (!data[type]) {
|
|
|
|
data[type] = [];
|
|
|
|
}
|
|
|
|
if (data[type].includes(rule)) {
|
|
|
|
throw new Error('The rule already exists');
|
|
|
|
}
|
|
|
|
data[type] = data[type].concat([rule]);
|
|
|
|
return saveStyle(data)
|
|
|
|
.then(newData => handleSave(newData, 'styleSettings'));
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeIncludeExclude(id, rule, type) {
|
|
|
|
const data = Object.assign({}, styles.get(id).data);
|
|
|
|
if (!data[type]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!data[type].includes(rule)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
data[type] = data[type].filter(r => r !== rule);
|
2018-11-07 06:09:29 +00:00
|
|
|
return saveStyle(data)
|
2019-03-03 22:54:37 +00:00
|
|
|
.then(newData => handleSave(newData, 'styleSettings'));
|
|
|
|
}
|
|
|
|
|
|
|
|
function addExclusion(id, rule) {
|
|
|
|
return addIncludeExclude(id, rule, 'exclusions');
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeExclusion(id, rule) {
|
|
|
|
return removeIncludeExclude(id, rule, 'exclusions');
|
|
|
|
}
|
|
|
|
|
|
|
|
function addInclusion(id, rule) {
|
|
|
|
return addIncludeExclude(id, rule, 'inclusions');
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeInclusion(id, rule) {
|
|
|
|
return removeIncludeExclude(id, rule, 'inclusions');
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2019-11-05 19:30:45 +00:00
|
|
|
function deleteStyle(id, reason) {
|
2018-11-07 06:09:29 +00:00
|
|
|
const style = styles.get(id);
|
2019-11-05 19:30:45 +00:00
|
|
|
const rev = Date.now();
|
2018-11-07 06:09:29 +00:00
|
|
|
return db.exec('delete', id)
|
|
|
|
.then(() => {
|
2019-11-05 19:30:45 +00:00
|
|
|
if (reason !== 'sync') {
|
|
|
|
sync.delete(style.data._id, rev);
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
for (const url of style.appliesTo) {
|
|
|
|
const cache = cachedStyleForUrl.get(url);
|
|
|
|
if (cache) {
|
|
|
|
delete cache.sections[id];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
styles.delete(id);
|
2019-11-05 19:30:45 +00:00
|
|
|
uuidIndex.delete(style.data._id);
|
2018-11-07 06:09:29 +00:00
|
|
|
return msg.broadcast({
|
|
|
|
method: 'styleDeleted',
|
2020-11-18 11:17:15 +00:00
|
|
|
style: {id},
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
|
|
|
})
|
|
|
|
.then(() => id);
|
|
|
|
}
|
|
|
|
|
2019-11-05 19:30:45 +00:00
|
|
|
function deleteByUUID(_id, rev) {
|
|
|
|
const id = uuidIndex.get(_id);
|
|
|
|
const oldDoc = id && styles.has(id) && styles.get(id).data;
|
|
|
|
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
|
|
|
|
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
|
|
|
|
return deleteStyle(id, 'sync');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function ensurePrepared(methods) {
|
|
|
|
const prepared = {};
|
|
|
|
for (const [name, fn] of Object.entries(methods)) {
|
|
|
|
prepared[name] = (...args) =>
|
|
|
|
preparing.then(() => fn(...args));
|
|
|
|
}
|
|
|
|
return prepared;
|
|
|
|
}
|
|
|
|
|
|
|
|
function createNewStyle() {
|
|
|
|
return {
|
|
|
|
enabled: true,
|
|
|
|
updateUrl: null,
|
|
|
|
md5Url: null,
|
|
|
|
url: null,
|
|
|
|
originalMd5: null,
|
2020-11-18 11:17:15 +00:00
|
|
|
installDate: Date.now(),
|
2018-11-07 06:09:29 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function broadcastStyleUpdated(data, reason, method = 'styleUpdated', codeIsUpdated = true) {
|
|
|
|
const style = styles.get(data.id);
|
|
|
|
const excluded = new Set();
|
|
|
|
const updated = new Set();
|
|
|
|
for (const [url, cache] of cachedStyleForUrl.entries()) {
|
|
|
|
if (!style.appliesTo.has(url)) {
|
|
|
|
cache.maybeMatch.add(data.id);
|
|
|
|
continue;
|
|
|
|
}
|
2019-06-11 14:44:32 +00:00
|
|
|
const code = getAppliedCode(createMatchQuery(url), data);
|
2018-11-07 06:09:29 +00:00
|
|
|
if (!code) {
|
|
|
|
excluded.add(url);
|
|
|
|
delete cache.sections[data.id];
|
|
|
|
} else {
|
|
|
|
updated.add(url);
|
|
|
|
cache.sections[data.id] = {
|
|
|
|
id: data.id,
|
2020-11-18 11:17:15 +00:00
|
|
|
code,
|
2018-11-07 06:09:29 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
style.appliesTo = updated;
|
|
|
|
return msg.broadcast({
|
|
|
|
method,
|
|
|
|
style: {
|
|
|
|
id: data.id,
|
2018-11-15 13:46:52 +00:00
|
|
|
md5Url: data.md5Url,
|
2020-11-18 11:17:15 +00:00
|
|
|
enabled: data.enabled,
|
2018-11-07 06:09:29 +00:00
|
|
|
},
|
|
|
|
reason,
|
2020-11-18 11:17:15 +00:00
|
|
|
codeIsUpdated,
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-11-05 19:30:45 +00:00
|
|
|
function beforeSave(style) {
|
2018-11-07 06:09:29 +00:00
|
|
|
if (!style.name) {
|
|
|
|
throw new Error('style name is empty');
|
|
|
|
}
|
2020-10-11 15:12:06 +00:00
|
|
|
for (const key of DELETE_IF_NULL) {
|
|
|
|
if (style[key] == null) {
|
|
|
|
delete style[key];
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
2019-11-05 19:30:45 +00:00
|
|
|
if (!style._id) {
|
2020-02-20 12:17:15 +00:00
|
|
|
style._id = uuidv4();
|
2019-11-05 19:30:45 +00:00
|
|
|
}
|
|
|
|
style._rev = Date.now();
|
2018-11-07 06:09:29 +00:00
|
|
|
fixUsoMd5Issue(style);
|
2019-11-05 19:30:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function afterSave(style, newId) {
|
|
|
|
if (style.id == null) {
|
|
|
|
style.id = newId;
|
|
|
|
}
|
|
|
|
uuidIndex.set(style._id, style.id);
|
|
|
|
sync.put(style._id, style._rev);
|
|
|
|
}
|
|
|
|
|
|
|
|
function saveStyle(style) {
|
|
|
|
beforeSave(style);
|
2018-11-07 06:09:29 +00:00
|
|
|
return db.exec('put', style)
|
|
|
|
.then(event => {
|
2019-11-05 19:30:45 +00:00
|
|
|
afterSave(style, event.target.result);
|
2018-11-07 06:09:29 +00:00
|
|
|
return style;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleSave(data, reason, codeIsUpdated) {
|
|
|
|
const style = styles.get(data.id);
|
|
|
|
let method;
|
|
|
|
if (!style) {
|
|
|
|
styles.set(data.id, {
|
|
|
|
appliesTo: new Set(),
|
2020-11-18 11:17:15 +00:00
|
|
|
data,
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
|
|
|
method = 'styleAdded';
|
|
|
|
} else {
|
|
|
|
style.data = data;
|
|
|
|
method = 'styleUpdated';
|
|
|
|
}
|
2018-12-26 12:51:21 +00:00
|
|
|
broadcastStyleUpdated(data, reason, method, codeIsUpdated);
|
|
|
|
return data;
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// get styles matching a URL, including sloppy regexps and excluded items.
|
|
|
|
function getStylesByUrl(url, id = null) {
|
|
|
|
// FIXME: do we want to cache this? Who would like to open popup rapidly
|
|
|
|
// or search the DB with the same URL?
|
|
|
|
const result = [];
|
|
|
|
const datas = !id ? [...styles.values()].map(s => s.data) :
|
|
|
|
styles.has(id) ? [styles.get(id).data] : [];
|
2019-06-11 14:44:32 +00:00
|
|
|
const query = createMatchQuery(url);
|
2018-11-07 06:09:29 +00:00
|
|
|
for (const data of datas) {
|
|
|
|
let excluded = false;
|
|
|
|
let sloppy = false;
|
|
|
|
let sectionMatched = false;
|
2019-06-11 14:44:32 +00:00
|
|
|
const match = urlMatchStyle(query, data);
|
2018-11-07 06:09:29 +00:00
|
|
|
// TODO: enable this when the function starts returning false
|
|
|
|
// if (match === false) {
|
|
|
|
// continue;
|
|
|
|
// }
|
|
|
|
if (match === 'excluded') {
|
|
|
|
excluded = true;
|
|
|
|
}
|
|
|
|
for (const section of data.sections) {
|
2020-10-04 10:42:06 +00:00
|
|
|
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
|
2018-11-07 06:09:29 +00:00
|
|
|
continue;
|
|
|
|
}
|
2019-06-11 14:44:32 +00:00
|
|
|
const match = urlMatchSection(query, section);
|
2018-11-07 06:09:29 +00:00
|
|
|
if (match) {
|
|
|
|
if (match === 'sloppy') {
|
|
|
|
sloppy = true;
|
|
|
|
}
|
|
|
|
sectionMatched = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (sectionMatched) {
|
2020-11-18 11:17:15 +00:00
|
|
|
result.push({data, excluded, sloppy});
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-10-22 19:58:11 +00:00
|
|
|
function getSectionsByUrl(url, id, isInitialApply) {
|
2018-11-07 06:09:29 +00:00
|
|
|
let cache = cachedStyleForUrl.get(url);
|
|
|
|
if (!cache) {
|
|
|
|
cache = {
|
|
|
|
sections: {},
|
2020-11-18 11:17:15 +00:00
|
|
|
maybeMatch: new Set(),
|
2018-11-07 06:09:29 +00:00
|
|
|
};
|
|
|
|
buildCache(styles.values());
|
|
|
|
cachedStyleForUrl.set(url, cache);
|
|
|
|
} else if (cache.maybeMatch.size) {
|
|
|
|
buildCache(
|
|
|
|
[...cache.maybeMatch]
|
|
|
|
.filter(i => styles.has(i))
|
|
|
|
.map(i => styles.get(i))
|
|
|
|
);
|
|
|
|
}
|
2020-10-22 19:58:11 +00:00
|
|
|
const res = id
|
|
|
|
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
|
|
|
|
: cache.sections;
|
|
|
|
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
|
|
|
|
return isInitialApply && prefs.get('disableAll')
|
|
|
|
? Object.assign({disableAll: true}, res)
|
|
|
|
: res;
|
2018-11-07 06:09:29 +00:00
|
|
|
|
|
|
|
function buildCache(styleList) {
|
2019-06-11 14:44:32 +00:00
|
|
|
const query = createMatchQuery(url);
|
2018-11-07 06:09:29 +00:00
|
|
|
for (const {appliesTo, data, preview} of styleList) {
|
2019-06-11 14:44:32 +00:00
|
|
|
const code = getAppliedCode(query, preview || data);
|
2018-11-07 06:09:29 +00:00
|
|
|
if (code) {
|
|
|
|
cache.sections[data.id] = {
|
|
|
|
id: data.id,
|
2020-11-18 11:17:15 +00:00
|
|
|
code,
|
2018-11-07 06:09:29 +00:00
|
|
|
};
|
|
|
|
appliesTo.add(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-11 14:44:32 +00:00
|
|
|
function getAppliedCode(query, data) {
|
|
|
|
if (urlMatchStyle(query, data) !== true) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const code = [];
|
|
|
|
for (const section of data.sections) {
|
2019-06-11 14:44:32 +00:00
|
|
|
if (urlMatchSection(query, section) === true && !styleCodeEmpty(section.code)) {
|
2018-11-07 06:09:29 +00:00
|
|
|
code.push(section.code);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return code.length && code;
|
|
|
|
}
|
|
|
|
|
|
|
|
function prepare() {
|
2019-11-05 19:30:45 +00:00
|
|
|
const ADD_MISSING_PROPS = {
|
|
|
|
name: style => `ID: ${style.id}`,
|
2020-02-20 12:17:15 +00:00
|
|
|
_id: () => uuidv4(),
|
2020-11-18 11:17:15 +00:00
|
|
|
_rev: () => Date.now(),
|
2019-11-05 19:30:45 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return db.exec('getAll')
|
|
|
|
.then(event => event.target.result || [])
|
|
|
|
.then(styleList => {
|
|
|
|
// setup missing _id, _rev
|
|
|
|
const updated = [];
|
|
|
|
for (const style of styleList) {
|
|
|
|
if (addMissingProperties(style)) {
|
|
|
|
updated.push(style);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (updated.length) {
|
|
|
|
return db.exec('putMany', updated)
|
|
|
|
.then(() => styleList);
|
|
|
|
}
|
|
|
|
return styleList;
|
|
|
|
})
|
|
|
|
.then(styleList => {
|
|
|
|
for (const style of styleList) {
|
|
|
|
fixUsoMd5Issue(style);
|
|
|
|
styles.set(style.id, {
|
|
|
|
appliesTo: new Set(),
|
2020-11-18 11:17:15 +00:00
|
|
|
data: style,
|
2019-11-05 19:30:45 +00:00
|
|
|
});
|
|
|
|
uuidIndex.set(style._id, style.id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
function addMissingProperties(style) {
|
|
|
|
let touched = false;
|
|
|
|
for (const key in ADD_MISSING_PROPS) {
|
|
|
|
if (!style[key]) {
|
|
|
|
style[key] = ADD_MISSING_PROPS[key](style);
|
|
|
|
touched = true;
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-11 15:12:06 +00:00
|
|
|
// upgrade the old way of customizing local names
|
|
|
|
const {originalName} = style;
|
|
|
|
if (originalName) {
|
|
|
|
touched = true;
|
|
|
|
if (originalName !== style.name) {
|
|
|
|
style.customName = style.name;
|
|
|
|
style.name = originalName;
|
|
|
|
}
|
|
|
|
delete style.originalName;
|
|
|
|
}
|
2019-11-05 19:30:45 +00:00
|
|
|
return touched;
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2019-06-11 14:44:32 +00:00
|
|
|
function urlMatchStyle(query, style) {
|
|
|
|
if (
|
|
|
|
style.exclusions &&
|
|
|
|
style.exclusions.some(e => compileExclusion(e).test(query.urlWithoutParams))
|
|
|
|
) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return 'excluded';
|
|
|
|
}
|
|
|
|
if (!style.enabled) {
|
|
|
|
return 'disabled';
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-06-11 14:44:32 +00:00
|
|
|
function urlMatchSection(query, section) {
|
|
|
|
if (
|
|
|
|
section.domains &&
|
|
|
|
section.domains.some(d => d === query.domain || query.domain.endsWith(`.${d}`))
|
|
|
|
) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
2020-10-03 17:17:59 +00:00
|
|
|
if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// 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 && (
|
2019-06-11 14:44:32 +00:00
|
|
|
section.urls.includes(query.url) ||
|
|
|
|
section.urls.includes(query.urlWithoutHash)
|
2018-11-07 06:09:29 +00:00
|
|
|
)) {
|
|
|
|
return true;
|
|
|
|
}
|
2019-06-11 14:44:32 +00:00
|
|
|
if (section.regexps && section.regexps.some(r => compileRe(r).test(query.url))) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
2019-06-11 14:44:32 +00:00
|
|
|
if (section.regexps && section.regexps.some(r => compileSloppyRe(r).test(query.url))) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return 'sloppy';
|
|
|
|
}
|
|
|
|
// TODO: check for invalid regexps?
|
2020-10-04 10:42:06 +00:00
|
|
|
return styleSectionGlobal(section);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function createCompiler(compile) {
|
|
|
|
// FIXME: FIFO cache doesn't work well here, if we want to match many
|
|
|
|
// regexps more than the cache size, we will never hit the cache because
|
|
|
|
// the first cache is deleted. So we use a simple map but it leaks memory.
|
|
|
|
const cache = new Map();
|
|
|
|
return text => {
|
|
|
|
let re = cache.get(text);
|
|
|
|
if (!re) {
|
|
|
|
re = tryRegExp(compile(text));
|
|
|
|
if (!re) {
|
|
|
|
re = BAD_MATCHER;
|
|
|
|
}
|
|
|
|
cache.set(text, re);
|
|
|
|
}
|
|
|
|
return re;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-06-11 14:44:32 +00:00
|
|
|
function compileGlob(text) {
|
|
|
|
return escapeRegExp(text).replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*');
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2019-06-11 14:44:32 +00:00
|
|
|
function buildExclusion(text) {
|
|
|
|
// match pattern
|
|
|
|
const match = text.match(/^(\*|[\w-]+):\/\/(\*\.)?([\w.]+\/.*)/);
|
|
|
|
if (!match) {
|
|
|
|
return '^' + compileGlob(text) + '$';
|
|
|
|
}
|
|
|
|
return '^' +
|
|
|
|
(match[1] === '*' ? '[\\w-]+' : match[1]) +
|
|
|
|
'://' +
|
|
|
|
(match[2] ? '(?:[\\w.]+\\.)?' : '') +
|
|
|
|
compileGlob(match[3]) +
|
|
|
|
'$';
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
}
|
|
|
|
}
|
2019-06-11 14:44:32 +00:00
|
|
|
|
|
|
|
function createMatchQuery(url) {
|
|
|
|
let urlWithoutHash;
|
|
|
|
let urlWithoutParams;
|
|
|
|
let domain;
|
|
|
|
return {
|
|
|
|
url,
|
|
|
|
get urlWithoutHash() {
|
|
|
|
if (!urlWithoutHash) {
|
|
|
|
urlWithoutHash = url.split('#')[0];
|
|
|
|
}
|
|
|
|
return urlWithoutHash;
|
|
|
|
},
|
|
|
|
get urlWithoutParams() {
|
|
|
|
if (!urlWithoutParams) {
|
|
|
|
const u = createURL(url);
|
|
|
|
urlWithoutParams = u.origin + u.pathname;
|
|
|
|
}
|
|
|
|
return urlWithoutParams;
|
|
|
|
},
|
|
|
|
get domain() {
|
|
|
|
if (!domain) {
|
|
|
|
const u = createURL(url);
|
|
|
|
domain = u.hostname;
|
|
|
|
}
|
|
|
|
return domain;
|
2020-11-18 11:17:15 +00:00
|
|
|
},
|
2019-06-11 14:44:32 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function createURL(url) {
|
|
|
|
try {
|
|
|
|
return new URL(url);
|
|
|
|
} catch (err) {
|
|
|
|
return DUMMY_URL;
|
|
|
|
}
|
|
|
|
}
|
2020-10-23 05:27:29 +00:00
|
|
|
|
|
|
|
function uuidv4() {
|
|
|
|
const seeds = crypto.getRandomValues(new Uint16Array(8));
|
|
|
|
// 00001111-2222-M333-N444-555566667777
|
|
|
|
seeds[3] = seeds[3] & 0x0FFF | 0x4000; // UUID version 4, M = 4
|
|
|
|
seeds[4] = seeds[4] & 0x3FFF | 0x8000; // UUID variant 1, N = 8..0xB
|
|
|
|
return Array.from(seeds, hex4dashed).join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
/** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */
|
|
|
|
function hex4dashed(num, i) {
|
|
|
|
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
})();
|