2021-01-01 14:27:58 +00:00
|
|
|
/* global API msg */// msg.js
|
2022-01-28 23:54:56 +00:00
|
|
|
/* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
|
|
|
|
/* global bgReady createCache uuidIndex */// common.js
|
2022-08-31 12:15:21 +00:00
|
|
|
/* global calcStyleDigest styleCodeEmpty */// sections-util.js
|
2021-01-01 14:27:58 +00:00
|
|
|
/* global db */
|
|
|
|
/* global prefs */
|
|
|
|
/* global tabMan */
|
2021-03-05 14:25:05 +00:00
|
|
|
/* global usercssMan */
|
2021-12-02 16:49:03 +00:00
|
|
|
/* global colorScheme */
|
2018-11-07 06:09:29 +00:00
|
|
|
'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`
|
2021-01-01 14:27:58 +00:00
|
|
|
to cleanup the temporary code. See livePreview in /edit.
|
2018-11-07 06:09:29 +00:00
|
|
|
*/
|
2020-11-18 11:17:15 +00:00
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
const styleUtil = {};
|
|
|
|
|
|
|
|
/* exported styleMan */
|
2021-01-01 14:27:58 +00:00
|
|
|
const styleMan = (() => {
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
Object.assign(styleUtil, {
|
|
|
|
id2style,
|
|
|
|
handleSave,
|
|
|
|
uuid2style,
|
|
|
|
});
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#region Declarations
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @typedef {{
|
2022-01-28 23:54:56 +00:00
|
|
|
style: StyleObj,
|
|
|
|
preview?: StyleObj,
|
|
|
|
appliesTo: Set<string>,
|
2021-01-01 14:27:58 +00:00
|
|
|
}} StyleMapData */
|
|
|
|
/** @type {Map<number,StyleMapData>} */
|
|
|
|
const dataMap = new Map();
|
|
|
|
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
|
|
|
|
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
|
2018-11-07 06:09:29 +00:00
|
|
|
const cachedStyleForUrl = createCache({
|
2021-01-01 14:27:58 +00:00
|
|
|
onDeleted(url, cache) {
|
2018-11-07 06:09:29 +00:00
|
|
|
for (const section of Object.values(cache.sections)) {
|
2021-01-01 14:27:58 +00:00
|
|
|
const data = id2data(section.id);
|
|
|
|
if (data) data.appliesTo.delete(url);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
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);
|
2021-06-16 17:04:45 +00:00
|
|
|
const uuidv4 = crypto.randomUUID ? crypto.randomUUID.bind(crypto) : (() => {
|
|
|
|
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('');
|
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
const MISSING_PROPS = {
|
|
|
|
name: style => `ID: ${style.id}`,
|
|
|
|
_id: () => uuidv4(),
|
|
|
|
_rev: () => Date.now(),
|
2019-06-11 14:44:32 +00:00
|
|
|
};
|
2021-01-26 13:33:17 +00:00
|
|
|
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
|
2022-01-28 23:54:56 +00:00
|
|
|
const INJ_ORDER = 'injectionOrder';
|
|
|
|
const order = {main: {}, prio: {}};
|
|
|
|
const orderWrap = {
|
|
|
|
id: INJ_ORDER,
|
|
|
|
value: mapObj(order, () => []),
|
|
|
|
_id: `${chrome.runtime.id}-${INJ_ORDER}`,
|
|
|
|
_rev: 0,
|
|
|
|
};
|
|
|
|
uuidIndex.addCustomId(orderWrap, {set: setOrder});
|
2022-08-31 12:15:21 +00:00
|
|
|
|
|
|
|
class MatchQuery {
|
|
|
|
constructor(url) {
|
|
|
|
this.url = url;
|
|
|
|
}
|
|
|
|
get urlWithoutHash() {
|
|
|
|
return this._set('urlWithoutHash', this.url.split('#', 1)[0]);
|
|
|
|
}
|
|
|
|
get urlWithoutParams() {
|
|
|
|
return this._set('urlWithoutParams', this.url.split(/[?#]/, 1)[0]);
|
|
|
|
}
|
|
|
|
get domain() {
|
|
|
|
return this._set('domain', tryURL(this.url).hostname);
|
|
|
|
}
|
|
|
|
get isOwnPage() {
|
|
|
|
return this._set('isOwnPage', this.url.startsWith(URLS.ownOrigin));
|
|
|
|
}
|
|
|
|
_set(name, value) {
|
|
|
|
Object.defineProperty(this, name, {value});
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
2022-02-19 05:46:24 +00:00
|
|
|
let ready = Promise.all([init(), prefs.ready]);
|
2020-10-11 15:12:06 +00:00
|
|
|
|
2022-01-23 09:44:25 +00:00
|
|
|
chrome.runtime.onConnect.addListener(port => {
|
|
|
|
if (port.name === 'livePreview') {
|
|
|
|
handleLivePreview(port);
|
|
|
|
} else if (port.name.startsWith('draft:')) {
|
|
|
|
handleDraft(port);
|
|
|
|
}
|
|
|
|
});
|
2022-02-17 00:10:59 +00:00
|
|
|
colorScheme.onChange(value => {
|
|
|
|
msg.broadcastExtension({method: 'colorScheme', value});
|
|
|
|
for (const {style} of dataMap.values()) {
|
|
|
|
if (colorScheme.SCHEMES.includes(style.preferScheme)) {
|
|
|
|
broadcastStyleUpdated(style, 'colorScheme');
|
2021-12-02 16:49:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region Exports
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
/** @returns {Promise<number>} style id */
|
|
|
|
async delete(id, reason) {
|
|
|
|
if (ready.then) await ready;
|
2022-01-28 23:54:56 +00:00
|
|
|
const {style, appliesTo} = dataMap.get(id);
|
|
|
|
const sync = reason !== 'sync';
|
|
|
|
const uuid = style._id;
|
|
|
|
db.styles.delete(id);
|
|
|
|
if (sync) API.sync.delete(uuid, Date.now());
|
2021-07-30 12:44:06 +00:00
|
|
|
for (const url of appliesTo) {
|
2021-01-01 14:27:58 +00:00
|
|
|
const cache = cachedStyleForUrl.get(url);
|
|
|
|
if (cache) delete cache.sections[id];
|
|
|
|
}
|
|
|
|
dataMap.delete(id);
|
2022-01-28 23:54:56 +00:00
|
|
|
uuidIndex.delete(uuid);
|
|
|
|
mapObj(orderWrap.value, (group, type) => {
|
|
|
|
delete order[type][id];
|
|
|
|
const i = group.indexOf(uuid);
|
|
|
|
if (i >= 0) group.splice(i, 1);
|
|
|
|
});
|
|
|
|
setOrder(orderWrap, {calc: false});
|
2021-07-30 12:44:06 +00:00
|
|
|
if (style._usw && style._usw.token) {
|
|
|
|
// Must be called after the style is deleted from dataMap
|
|
|
|
API.usw.revoke(id);
|
|
|
|
}
|
2022-01-23 09:44:25 +00:00
|
|
|
API.drafts.delete(id);
|
2021-01-01 14:27:58 +00:00
|
|
|
await msg.broadcast({
|
|
|
|
method: 'styleDeleted',
|
|
|
|
style: {id},
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
return id;
|
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleObj>} */
|
|
|
|
async editSave(style) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
style = mergeWithMapped(style);
|
|
|
|
style.updateDate = Date.now();
|
2021-07-30 12:44:06 +00:00
|
|
|
return saveStyle(style, {reason: 'editSave'});
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<?StyleObj>} */
|
2022-08-03 19:37:04 +00:00
|
|
|
async find(...filters) {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (ready.then) await ready;
|
2022-08-03 19:37:04 +00:00
|
|
|
for (const filter of filters) {
|
|
|
|
const filterEntries = Object.entries(filter);
|
|
|
|
for (const {style} of dataMap.values()) {
|
|
|
|
if (filterEntries.every(([key, val]) => style[key] === val)) {
|
|
|
|
return style;
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
2019-11-05 19:30:45 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleObj[]>} */
|
|
|
|
async getAll() {
|
|
|
|
if (ready.then) await ready;
|
2022-01-28 23:54:56 +00:00
|
|
|
return getAllAsArray();
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
/** @returns {Promise<Object<string,StyleObj[]>>}>} */
|
|
|
|
async getAllOrdered(keys) {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (ready.then) await ready;
|
2022-01-29 00:02:45 +00:00
|
|
|
const res = mapObj(orderWrap.value, group => group.map(uuid2style).filter(Boolean));
|
2022-01-28 23:54:56 +00:00
|
|
|
if (res.main.length + res.prio.length < dataMap.size) {
|
|
|
|
for (const {style} of dataMap.values()) {
|
|
|
|
if (!(style.id in order.main) && !(style.id in order.prio)) {
|
|
|
|
res.main.push(style);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return keys
|
|
|
|
? mapObj(res, group => group.map(style => mapObj(style, null, keys)))
|
|
|
|
: res;
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2019-11-05 19:30:45 +00:00
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
getOrder: () => orderWrap.value,
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleSectionsToApply>} */
|
|
|
|
async getSectionsByUrl(url, id, isInitialApply) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
if (isInitialApply && prefs.get('disableAll')) {
|
2022-01-14 12:44:48 +00:00
|
|
|
return {
|
|
|
|
cfg: {
|
|
|
|
disableAll: true,
|
|
|
|
},
|
|
|
|
};
|
2019-11-05 19:30:45 +00:00
|
|
|
}
|
2022-02-10 18:28:47 +00:00
|
|
|
// TODO: enable in FF when it supports sourceURL comment in style elements (also options.html)
|
|
|
|
const {exposeStyleName} = CHROME && prefs.__values;
|
2021-09-24 08:05:55 +00:00
|
|
|
const sender = CHROME && this && this.sender || {};
|
|
|
|
if (sender.frameId === 0) {
|
2021-02-28 15:01:26 +00:00
|
|
|
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
|
|
|
|
so we'll use the real URL reported by webNavigation API.
|
|
|
|
TODO: if FF will do the same, this won't work as is: FF reports onCommitted too late */
|
2021-09-24 08:05:55 +00:00
|
|
|
url = tabMan.get(sender.tab.id, 'url', 0) || url;
|
2021-02-28 15:01:26 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
let cache = cachedStyleForUrl.get(url);
|
|
|
|
if (!cache) {
|
|
|
|
cache = {
|
|
|
|
sections: {},
|
|
|
|
maybeMatch: new Set(),
|
|
|
|
};
|
|
|
|
buildCache(cache, url, dataMap.values());
|
|
|
|
cachedStyleForUrl.set(url, cache);
|
|
|
|
} else if (cache.maybeMatch.size) {
|
|
|
|
buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean));
|
|
|
|
}
|
2022-02-10 18:28:47 +00:00
|
|
|
return Object.assign({cfg: {exposeStyleName, order}},
|
|
|
|
id ? mapObj(cache.sections, null, [id])
|
|
|
|
: cache.sections);
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2019-11-05 19:30:45 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleObj>} */
|
|
|
|
async get(id) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
return id2style(id);
|
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StylesByUrlResult[]>} */
|
|
|
|
async getByUrl(url, id = null) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
// 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 styles = id
|
|
|
|
? [id2style(id)].filter(Boolean)
|
2022-01-28 23:54:56 +00:00
|
|
|
: getAllAsArray();
|
2022-08-31 12:15:21 +00:00
|
|
|
const query = new MatchQuery(url);
|
2021-01-01 14:27:58 +00:00
|
|
|
for (const style of styles) {
|
|
|
|
let excluded = false;
|
2021-12-02 16:49:03 +00:00
|
|
|
let excludedScheme = false;
|
2021-12-08 10:30:16 +00:00
|
|
|
let included = false;
|
2021-01-01 14:27:58 +00:00
|
|
|
let sloppy = false;
|
|
|
|
let sectionMatched = false;
|
|
|
|
const match = urlMatchStyle(query, style);
|
|
|
|
// TODO: enable this when the function starts returning false
|
|
|
|
// if (match === false) {
|
|
|
|
// continue;
|
|
|
|
// }
|
2021-12-08 10:30:16 +00:00
|
|
|
if (match === 'included') {
|
|
|
|
included = true;
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
if (match === 'excluded') {
|
|
|
|
excluded = true;
|
|
|
|
}
|
2021-12-02 16:49:03 +00:00
|
|
|
if (match === 'excludedScheme') {
|
|
|
|
excludedScheme = true;
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
for (const section of style.sections) {
|
2022-08-31 12:15:21 +00:00
|
|
|
const match = urlMatchSection(query, section, true);
|
2021-01-01 14:27:58 +00:00
|
|
|
if (match) {
|
|
|
|
if (match === 'sloppy') {
|
|
|
|
sloppy = true;
|
|
|
|
}
|
|
|
|
sectionMatched = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-12-08 10:30:16 +00:00
|
|
|
if (sectionMatched || included) {
|
|
|
|
result.push(/** @namespace StylesByUrlResult */ {
|
|
|
|
style, excluded, sloppy, excludedScheme, sectionMatched, included});
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
return result;
|
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleObj[]>} */
|
|
|
|
async importMany(items) {
|
|
|
|
if (ready.then) await ready;
|
2021-03-05 14:25:05 +00:00
|
|
|
for (const style of items) {
|
|
|
|
beforeSave(style);
|
|
|
|
if (style.sourceCode && style.usercssData) {
|
|
|
|
await usercssMan.buildCode(style);
|
|
|
|
}
|
|
|
|
}
|
2022-01-28 23:54:56 +00:00
|
|
|
const events = await db.styles.putMany(items);
|
|
|
|
return Promise.all(items.map((item, i) =>
|
|
|
|
handleSave(item, {reason: 'import'}, events[i])
|
|
|
|
));
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleObj>} */
|
|
|
|
async install(style, reason = null) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
reason = reason || dataMap.has(style.id) ? 'update' : 'install';
|
|
|
|
style = mergeWithMapped(style);
|
|
|
|
style.originalDigest = await calcStyleDigest(style);
|
|
|
|
// FIXME: update updateDate? what about usercss config?
|
2021-07-30 12:44:06 +00:00
|
|
|
return saveStyle(style, {reason});
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
save: saveStyle,
|
|
|
|
|
|
|
|
async setOrder(value) {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (ready.then) await ready;
|
2022-01-28 23:54:56 +00:00
|
|
|
return setOrder({value}, {broadcast: true, sync: true});
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2018-11-11 06:04:22 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<number>} style id */
|
|
|
|
async toggle(id, enabled) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
const style = Object.assign({}, id2style(id), {enabled});
|
2021-12-07 04:44:49 +00:00
|
|
|
await saveStyle(style, {reason: 'toggle'});
|
2021-01-01 14:27:58 +00:00
|
|
|
return id;
|
|
|
|
},
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
// using bind() to skip step-into when debugging
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {Promise<StyleObj>} */
|
|
|
|
addExclusion: addIncludeExclude.bind(null, 'exclusions'),
|
|
|
|
/** @returns {Promise<StyleObj>} */
|
|
|
|
addInclusion: addIncludeExclude.bind(null, 'inclusions'),
|
|
|
|
/** @returns {Promise<?StyleObj>} */
|
|
|
|
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
|
|
|
|
/** @returns {Promise<?StyleObj>} */
|
|
|
|
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
|
2021-12-07 04:44:49 +00:00
|
|
|
|
|
|
|
async config(id, prop, value) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
const style = Object.assign({}, id2style(id));
|
2022-02-17 00:10:59 +00:00
|
|
|
const {preview = {}} = dataMap.get(id);
|
|
|
|
style[prop] = preview[prop] = value;
|
2021-12-07 04:44:49 +00:00
|
|
|
return saveStyle(style, {reason: 'config'});
|
|
|
|
},
|
2021-01-01 14:27:58 +00:00
|
|
|
};
|
2019-03-03 22:54:37 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#endregion
|
|
|
|
//#region Implementation
|
|
|
|
|
|
|
|
/** @returns {StyleMapData} */
|
|
|
|
function id2data(id) {
|
|
|
|
return dataMap.get(id);
|
2019-03-03 22:54:37 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {?StyleObj} */
|
|
|
|
function id2style(id) {
|
2022-01-28 23:54:56 +00:00
|
|
|
return (dataMap.get(Number(id)) || {}).style;
|
2019-03-03 22:54:37 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {?StyleObj} */
|
2022-01-28 23:54:56 +00:00
|
|
|
function uuid2style(uuid) {
|
|
|
|
return id2style(uuidIndex.get(uuid));
|
2019-03-03 22:54:37 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {StyleObj} */
|
|
|
|
function createNewStyle() {
|
|
|
|
return /** @namespace StyleObj */ {
|
|
|
|
enabled: true,
|
|
|
|
updateUrl: null,
|
|
|
|
md5Url: null,
|
|
|
|
url: null,
|
|
|
|
originalMd5: null,
|
|
|
|
installDate: Date.now(),
|
|
|
|
};
|
2019-03-03 22:54:37 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {void} */
|
|
|
|
function storeInMap(style) {
|
|
|
|
dataMap.set(style.id, {
|
|
|
|
style,
|
|
|
|
appliesTo: new Set(),
|
|
|
|
});
|
2022-01-28 23:54:56 +00:00
|
|
|
uuidIndex.set(style._id, style.id);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @returns {StyleObj} */
|
|
|
|
function mergeWithMapped(style) {
|
|
|
|
return Object.assign({},
|
|
|
|
id2style(style.id) || createNewStyle(),
|
|
|
|
style);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2022-01-23 09:44:25 +00:00
|
|
|
function handleDraft(port) {
|
|
|
|
const id = port.name.split(':').pop();
|
|
|
|
port.onDisconnect.addListener(() => API.drafts.delete(Number(id) || id));
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
function handleLivePreview(port) {
|
|
|
|
let id;
|
|
|
|
port.onMessage.addListener(style => {
|
|
|
|
if (!id) id = style.id;
|
|
|
|
const data = id2data(id);
|
|
|
|
data.preview = style;
|
|
|
|
broadcastStyleUpdated(style, 'editPreview');
|
|
|
|
});
|
|
|
|
port.onDisconnect.addListener(() => {
|
|
|
|
port = null;
|
|
|
|
if (id) {
|
|
|
|
const data = id2data(id);
|
|
|
|
if (data) {
|
|
|
|
data.preview = null;
|
|
|
|
broadcastStyleUpdated(data.style, 'editPreviewEnd');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2019-11-05 19:30:45 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
async function addIncludeExclude(type, id, rule) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
const style = Object.assign({}, id2style(id));
|
|
|
|
const list = style[type] || (style[type] = []);
|
|
|
|
if (list.includes(rule)) {
|
|
|
|
throw new Error('The rule already exists');
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
style[type] = list.concat([rule]);
|
2021-12-07 04:44:49 +00:00
|
|
|
return saveStyle(style, {reason: 'config'});
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
async function removeIncludeExclude(type, id, rule) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
const style = Object.assign({}, id2style(id));
|
|
|
|
const list = style[type];
|
|
|
|
if (!list || !list.includes(rule)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
style[type] = list.filter(r => r !== rule);
|
2021-12-07 04:44:49 +00:00
|
|
|
return saveStyle(style, {reason: 'config'});
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 04:44:49 +00:00
|
|
|
function broadcastStyleUpdated(style, reason, method = 'styleUpdated') {
|
2021-01-01 14:27:58 +00:00
|
|
|
const {id} = style;
|
|
|
|
const data = id2data(id);
|
2018-11-07 06:09:29 +00:00
|
|
|
const excluded = new Set();
|
|
|
|
const updated = new Set();
|
|
|
|
for (const [url, cache] of cachedStyleForUrl.entries()) {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (!data.appliesTo.has(url)) {
|
|
|
|
cache.maybeMatch.add(id);
|
2018-11-07 06:09:29 +00:00
|
|
|
continue;
|
|
|
|
}
|
2022-08-31 12:15:21 +00:00
|
|
|
const code = getAppliedCode(new MatchQuery(url), style);
|
2021-01-01 14:27:58 +00:00
|
|
|
if (code) {
|
2018-11-07 06:09:29 +00:00
|
|
|
updated.add(url);
|
2022-02-10 18:28:47 +00:00
|
|
|
buildCacheEntry(cache, style, code);
|
2021-01-01 14:27:58 +00:00
|
|
|
} else {
|
|
|
|
excluded.add(url);
|
|
|
|
delete cache.sections[id];
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
data.appliesTo = updated;
|
2018-11-07 06:09:29 +00:00
|
|
|
return msg.broadcast({
|
|
|
|
method,
|
|
|
|
reason,
|
2021-01-01 14:27:58 +00:00
|
|
|
style: {
|
|
|
|
id,
|
|
|
|
md5Url: style.md5Url,
|
|
|
|
enabled: style.enabled,
|
|
|
|
},
|
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) {
|
2021-01-01 14:27:58 +00:00
|
|
|
throw new Error('Style name is empty');
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
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();
|
2021-10-12 17:12:26 +00:00
|
|
|
fixKnownProblems(style);
|
2019-11-05 19:30:45 +00:00
|
|
|
}
|
|
|
|
|
2021-07-30 12:44:06 +00:00
|
|
|
async function saveStyle(style, handlingOptions) {
|
2019-11-05 19:30:45 +00:00
|
|
|
beforeSave(style);
|
2022-01-28 23:54:56 +00:00
|
|
|
const newId = await db.styles.put(style);
|
|
|
|
return handleSave(style, handlingOptions, newId);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
function handleSave(style, {reason, broadcast = true}, id = style.id) {
|
|
|
|
if (style.id == null) style.id = id;
|
|
|
|
const data = id2data(id);
|
2021-01-01 14:27:58 +00:00
|
|
|
const method = data ? 'styleUpdated' : 'styleAdded';
|
|
|
|
if (!data) {
|
|
|
|
storeInMap(style);
|
2018-11-07 06:09:29 +00:00
|
|
|
} else {
|
2021-01-01 14:27:58 +00:00
|
|
|
data.style = style;
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
2022-01-28 23:54:56 +00:00
|
|
|
if (reason !== 'sync') {
|
|
|
|
API.sync.putDoc(style);
|
|
|
|
}
|
2021-12-07 04:44:49 +00:00
|
|
|
if (broadcast) broadcastStyleUpdated(style, reason, method);
|
2021-01-01 14:27:58 +00:00
|
|
|
return style;
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// get styles matching a URL, including sloppy regexps and excluded items.
|
2019-06-11 14:44:32 +00:00
|
|
|
function getAppliedCode(query, data) {
|
2021-12-08 10:30:16 +00:00
|
|
|
const result = urlMatchStyle(query, data);
|
|
|
|
if (result === 'included') {
|
|
|
|
// return all sections
|
|
|
|
return data.sections.map(s => s.code);
|
|
|
|
}
|
|
|
|
if (result !== 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;
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
async function init() {
|
2022-01-29 12:35:01 +00:00
|
|
|
const orderPromise = API.prefsDb.get(orderWrap.id);
|
2022-01-28 23:54:56 +00:00
|
|
|
const styles = await db.styles.getAll() || [];
|
2021-11-14 08:00:12 +00:00
|
|
|
const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean));
|
2021-01-01 14:27:58 +00:00
|
|
|
if (updated.length) {
|
2022-01-28 23:54:56 +00:00
|
|
|
await db.styles.putMany(updated);
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
2022-01-28 23:54:56 +00:00
|
|
|
setOrder(await orderPromise, {store: false});
|
|
|
|
styles.forEach(storeInMap);
|
2021-01-01 14:27:58 +00:00
|
|
|
ready = true;
|
|
|
|
bgReady._resolveStyles();
|
|
|
|
}
|
2019-11-05 19:30:45 +00:00
|
|
|
|
2021-11-14 08:00:12 +00:00
|
|
|
function fixKnownProblems(style, initIndex, initArray) {
|
2021-01-01 14:27:58 +00:00
|
|
|
let res = 0;
|
|
|
|
for (const key in MISSING_PROPS) {
|
|
|
|
if (!style[key]) {
|
|
|
|
style[key] = MISSING_PROPS[key](style);
|
|
|
|
res = 1;
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
2021-07-30 12:44:06 +00:00
|
|
|
/* Upgrade the old way of customizing local names */
|
2021-01-01 14:27:58 +00:00
|
|
|
const {originalName} = style;
|
|
|
|
if (originalName) {
|
|
|
|
if (originalName !== style.name) {
|
|
|
|
style.customName = style.name;
|
|
|
|
style.name = originalName;
|
2020-10-11 15:12:06 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
delete style.originalName;
|
2021-07-30 12:44:06 +00:00
|
|
|
res = 1;
|
2019-11-05 19:30:45 +00:00
|
|
|
}
|
2021-08-22 17:30:09 +00:00
|
|
|
/* wrong homepage url in 1.5.20-1.5.21 due to commit 1e5f118d */
|
|
|
|
for (const key of ['url', 'installationUrl']) {
|
|
|
|
const url = style[key];
|
|
|
|
const fixedUrl = url && url.replace(/([^:]\/)\//, '$1');
|
|
|
|
if (fixedUrl !== url) {
|
|
|
|
res = 1;
|
|
|
|
style[key] = fixedUrl;
|
|
|
|
}
|
|
|
|
}
|
2021-10-12 17:12:26 +00:00
|
|
|
let url;
|
|
|
|
/* USO bug, duplicate "update" subdomain, see #523 */
|
|
|
|
if ((url = style.md5Url) && url.includes('update.update.userstyles')) {
|
|
|
|
res = style.md5Url = url.replace('update.update.userstyles', 'update.userstyles');
|
|
|
|
}
|
|
|
|
/* Default homepage URL for external styles installed from a known distro */
|
2021-10-12 17:26:57 +00:00
|
|
|
if (
|
|
|
|
(!style.url || !style.installationUrl) &&
|
|
|
|
(url = style.updateUrl) &&
|
|
|
|
(url = URLS.extractGreasyForkInstallUrl(url) ||
|
|
|
|
URLS.extractUsoArchiveInstallUrl(url) ||
|
|
|
|
URLS.extractUSwInstallUrl(url)
|
|
|
|
)
|
|
|
|
) {
|
2021-10-12 17:12:26 +00:00
|
|
|
if (!style.url) res = style.url = url;
|
2021-10-12 17:26:57 +00:00
|
|
|
if (!style.installationUrl) res = style.installationUrl = url;
|
2021-10-12 17:12:26 +00:00
|
|
|
}
|
2021-11-14 08:00:12 +00:00
|
|
|
/* @import must precede `vars` that we add at beginning */
|
|
|
|
if (
|
|
|
|
initArray &&
|
|
|
|
!isEmptyObj((style.usercssData || {}).vars) &&
|
|
|
|
style.sections.some(({code}) =>
|
|
|
|
code.startsWith(':root {\n --') &&
|
|
|
|
/@import\s/i.test(code))
|
|
|
|
) {
|
|
|
|
return usercssMan.buildCode(style);
|
|
|
|
}
|
|
|
|
return res && style;
|
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';
|
|
|
|
}
|
2021-12-02 16:49:03 +00:00
|
|
|
if (!colorScheme.shouldIncludeStyle(style)) {
|
|
|
|
return 'excludedScheme';
|
|
|
|
}
|
2021-12-08 10:30:16 +00:00
|
|
|
if (
|
|
|
|
style.inclusions &&
|
|
|
|
style.inclusions.some(r => compileExclusion(r).test(query.urlWithoutParams))
|
|
|
|
) {
|
|
|
|
return 'included';
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-08-31 12:15:21 +00:00
|
|
|
function urlMatchSection(query, section, skipEmptyGlobal) {
|
|
|
|
let dd, ddL, pp, ppL, rr, rrL, uu, uuL;
|
2019-06-11 14:44:32 +00:00
|
|
|
if (
|
2022-08-31 12:15:21 +00:00
|
|
|
(dd = section.domains) && (ddL = dd.length) && dd.some(urlMatchDomain, query) ||
|
|
|
|
(pp = section.urlPrefixes) && (ppL = pp.length) && pp.some(urlMatchPrefix, query) ||
|
|
|
|
/* Per the specification 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 */
|
|
|
|
(uu = section.urls) && (uuL = uu.length) && (
|
|
|
|
uu.includes(query.url) ||
|
|
|
|
uu.includes(query.urlWithoutHash)
|
|
|
|
) ||
|
|
|
|
(rr = section.regexps) && (rrL = rr.length) && rr.some(urlMatchRegexp, query)
|
2019-06-11 14:44:32 +00:00
|
|
|
) {
|
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.
|
|
|
|
*/
|
2022-08-31 12:15:21 +00:00
|
|
|
if (rrL && rr.some(urlMatchRegexpSloppy, query)) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return 'sloppy';
|
|
|
|
}
|
|
|
|
// TODO: check for invalid regexps?
|
2022-08-31 12:15:21 +00:00
|
|
|
return !rrL && !ppL && !uuL && !ddL &&
|
|
|
|
!query.isOwnPage && // We allow only intentionally targeted sections for own pages
|
|
|
|
(!skipEmptyGlobal || !styleCodeEmpty(section.code));
|
|
|
|
}
|
|
|
|
/** @this {MatchQuery} */
|
|
|
|
function urlMatchDomain(d) {
|
|
|
|
const _d = this.domain;
|
|
|
|
return d === _d ||
|
|
|
|
_d[_d.length - d.length - 1] === '.' && _d.endsWith(d);
|
|
|
|
}
|
|
|
|
/** @this {MatchQuery} */
|
|
|
|
function urlMatchPrefix(p) {
|
|
|
|
return p && this.url.startsWith(p);
|
|
|
|
}
|
|
|
|
/** @this {MatchQuery} */
|
|
|
|
function urlMatchRegexp(r) {
|
|
|
|
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
|
|
|
|
compileRe(r).test(this.url);
|
|
|
|
}
|
|
|
|
/** @this {MatchQuery} */
|
|
|
|
function urlMatchRegexpSloppy(r) {
|
|
|
|
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
|
|
|
|
compileSloppyRe(r).test(this.url);
|
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) {
|
2021-01-01 14:27:58 +00:00
|
|
|
return stringAsRegExp(text, '', true)
|
|
|
|
.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
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
function buildCache(cache, url, styleList) {
|
2022-08-31 12:15:21 +00:00
|
|
|
const query = new MatchQuery(url);
|
2021-01-01 14:27:58 +00:00
|
|
|
for (const {style, appliesTo, preview} of styleList) {
|
|
|
|
const code = getAppliedCode(query, preview || style);
|
|
|
|
if (code) {
|
2022-02-10 18:28:47 +00:00
|
|
|
buildCacheEntry(cache, style, code);
|
2021-01-01 14:27:58 +00:00
|
|
|
appliesTo.add(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-10 18:28:47 +00:00
|
|
|
function buildCacheEntry(cache, style, code = style.code) {
|
|
|
|
cache.sections[style.id] = {
|
|
|
|
code,
|
|
|
|
id: style.id,
|
|
|
|
name: style.customName || style.name,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
/** @returns {StyleObj[]} */
|
|
|
|
function getAllAsArray() {
|
|
|
|
return Array.from(dataMap.values(), v => v.style);
|
|
|
|
}
|
|
|
|
|
2020-10-23 05:27:29 +00:00
|
|
|
/** 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 ? '-' : '');
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
async function setOrder(data, {broadcast, calc = true, store = true, sync} = {}) {
|
|
|
|
if (!data || !data.value || deepEqual(data.value, orderWrap.value)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Object.assign(orderWrap, data, sync && {_rev: Date.now()});
|
|
|
|
if (calc) {
|
|
|
|
for (const [type, group] of Object.entries(data.value)) {
|
|
|
|
const dst = order[type] = {};
|
|
|
|
group.forEach((uuid, i) => {
|
|
|
|
const id = uuidIndex.get(uuid);
|
|
|
|
if (id) dst[id] = i;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (broadcast) {
|
|
|
|
msg.broadcast({method: 'styleSort', order});
|
|
|
|
}
|
|
|
|
if (store) {
|
2022-01-29 12:35:01 +00:00
|
|
|
await API.prefsDb.put(orderWrap, orderWrap.id);
|
2022-01-28 23:54:56 +00:00
|
|
|
}
|
|
|
|
if (sync) {
|
|
|
|
API.sync.putDoc(orderWrap);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#endregion
|
2018-11-07 06:09:29 +00:00
|
|
|
})();
|