fix USO site installation (#1461)
This commit is contained in:
parent
6995483ec0
commit
685bf1fa3e
|
@ -6,16 +6,9 @@
|
||||||
/* global syncMan */
|
/* global syncMan */
|
||||||
/* global updateMan */
|
/* global updateMan */
|
||||||
/* global usercssMan */
|
/* global usercssMan */
|
||||||
|
/* global usoApi */
|
||||||
/* global uswApi */
|
/* global uswApi */
|
||||||
/* global
|
/* global FIREFOX UA activateTab findExistingTab openURL */ // toolbox.js
|
||||||
FIREFOX
|
|
||||||
UA
|
|
||||||
URLS
|
|
||||||
activateTab
|
|
||||||
download
|
|
||||||
findExistingTab
|
|
||||||
openURL
|
|
||||||
*/ // toolbox.js
|
|
||||||
/* global colorScheme */ // color-scheme.js
|
/* global colorScheme */ // color-scheme.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -42,17 +35,12 @@ addAPI(/** @namespace API */ {
|
||||||
sync: syncMan,
|
sync: syncMan,
|
||||||
updater: updateMan,
|
updater: updateMan,
|
||||||
usercss: usercssMan,
|
usercss: usercssMan,
|
||||||
|
uso: usoApi,
|
||||||
usw: uswApi,
|
usw: uswApi,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
/** @type {BackgroundWorker} */
|
/** @type {BackgroundWorker} */
|
||||||
worker: createWorker({url: '/background/background-worker'}),
|
worker: createWorker({url: '/background/background-worker'}),
|
||||||
|
|
||||||
download(url, opts) {
|
|
||||||
return typeof url === 'string' && url.startsWith(URLS.uso) &&
|
|
||||||
this.sender.url.startsWith(URLS.uso) &&
|
|
||||||
download(url, opts || {});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {string} */
|
/** @returns {string} */
|
||||||
getTabUrlPrefix() {
|
getTabUrlPrefix() {
|
||||||
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||||
|
|
|
@ -139,12 +139,14 @@ const styleMan = (() => {
|
||||||
},
|
},
|
||||||
|
|
||||||
/** @returns {Promise<?StyleObj>} */
|
/** @returns {Promise<?StyleObj>} */
|
||||||
async find(filter) {
|
async find(...filters) {
|
||||||
if (ready.then) await ready;
|
if (ready.then) await ready;
|
||||||
const filterEntries = Object.entries(filter);
|
for (const filter of filters) {
|
||||||
for (const {style} of dataMap.values()) {
|
const filterEntries = Object.entries(filter);
|
||||||
if (filterEntries.every(([key, val]) => style[key] === val)) {
|
for (const {style} of dataMap.values()) {
|
||||||
return style;
|
if (filterEntries.every(([key, val]) => style[key] === val)) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* global API */// msg.js
|
/* global API */// msg.js
|
||||||
/* global RX_META URLS debounce deepMerge download ignoreChromeError */// toolbox.js
|
/* global RX_META URLS debounce deepMerge download ignoreChromeError */// toolbox.js
|
||||||
/* global calcStyleDigest styleJSONseemsValid styleSectionsEqual */ // sections-util.js
|
/* global calcStyleDigest styleSectionsEqual */ // sections-util.js
|
||||||
/* global chromeLocal */// storage-util.js
|
/* global chromeLocal */// storage-util.js
|
||||||
/* global compareVersion */// cmpver.js
|
/* global compareVersion */// cmpver.js
|
||||||
/* global db */
|
/* global db */
|
||||||
|
@ -23,6 +23,7 @@ const updateMan = (() => {
|
||||||
ERROR_JSON: 'error: JSON is invalid',
|
ERROR_JSON: 'error: JSON is invalid',
|
||||||
ERROR_VERSION: 'error: version is older than installed style',
|
ERROR_VERSION: 'error: version is older than installed style',
|
||||||
};
|
};
|
||||||
|
const USO_STYLES_API = `${URLS.uso}api/v1/styles/`;
|
||||||
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
|
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
|
||||||
const RX_DATE2VER = new RegExp([
|
const RX_DATE2VER = new RegExp([
|
||||||
/^(\d{4})/,
|
/^(\d{4})/,
|
||||||
|
@ -37,6 +38,7 @@ const updateMan = (() => {
|
||||||
503, // service unavailable
|
503, // service unavailable
|
||||||
429, // too many requests
|
429, // too many requests
|
||||||
];
|
];
|
||||||
|
let usoReferers = 0;
|
||||||
let lastUpdateTime;
|
let lastUpdateTime;
|
||||||
let checkingAll = false;
|
let checkingAll = false;
|
||||||
let logQueue = [];
|
let logQueue = [];
|
||||||
|
@ -113,12 +115,13 @@ const updateMan = (() => {
|
||||||
save,
|
save,
|
||||||
} = opts;
|
} = opts;
|
||||||
if (!id) id = style.id;
|
if (!id) id = style.id;
|
||||||
const ucd = style.usercssData;
|
const {md5Url} = style;
|
||||||
|
let {usercssData: ucd, updateUrl} = style;
|
||||||
let res, state;
|
let res, state;
|
||||||
try {
|
try {
|
||||||
await checkIfEdited();
|
await checkIfEdited();
|
||||||
res = {
|
res = {
|
||||||
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
|
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
|
||||||
updated: true,
|
updated: true,
|
||||||
};
|
};
|
||||||
state = STATES.UPDATED;
|
state = STATES.UPDATED;
|
||||||
|
@ -142,76 +145,45 @@ const updateMan = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUSO() {
|
async function updateUSO() {
|
||||||
const url = URLS.makeUsoArchiveCodeUrl(style.md5Url.match(/\d+/)[0]);
|
const md5 = await tryDownload(md5Url);
|
||||||
const req = await tryDownload(url, RH_ETAG).catch(() => null);
|
|
||||||
if (req) {
|
|
||||||
return updateToUSOArchive(url, req);
|
|
||||||
}
|
|
||||||
const md5 = await tryDownload(style.md5Url);
|
|
||||||
if (!md5 || md5.length !== 32) {
|
if (!md5 || md5.length !== 32) {
|
||||||
return Promise.reject(STATES.ERROR_MD5);
|
return Promise.reject(STATES.ERROR_MD5);
|
||||||
}
|
}
|
||||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||||
return Promise.reject(STATES.SAME_MD5);
|
return Promise.reject(STATES.SAME_MD5);
|
||||||
}
|
}
|
||||||
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
let varsUrl = '';
|
||||||
if (!styleJSONseemsValid(json)) {
|
if (!ucd) {
|
||||||
return Promise.reject(STATES.ERROR_JSON);
|
ucd = {};
|
||||||
|
varsUrl = updateUrl;
|
||||||
|
updateUrl = style.updateUrl = `${USO_STYLES_API}${md5Url.match(/\/(\d+)/)[1]}`;
|
||||||
|
}
|
||||||
|
usoSpooferStart();
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||||
|
json = await updateUsercss(json.css) ||
|
||||||
|
(await API.uso.toUsercss(json)).style;
|
||||||
|
if (varsUrl) await API.uso.useVarsUrl(json, varsUrl);
|
||||||
|
} finally {
|
||||||
|
usoSpooferStop();
|
||||||
}
|
}
|
||||||
// USO may not provide a correctly updated originalMd5 (#555)
|
// USO may not provide a correctly updated originalMd5 (#555)
|
||||||
json.originalMd5 = md5;
|
json.originalMd5 = md5;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateToUSOArchive(url, req) {
|
async function updateUsercss(css) {
|
||||||
const m2 = await getUsoEmbeddedMeta(req.response);
|
|
||||||
if (m2) {
|
|
||||||
url = m2.updateUrl;
|
|
||||||
req = await tryDownload(url, RH_ETAG);
|
|
||||||
}
|
|
||||||
const json = await API.usercss.buildMeta({
|
|
||||||
id,
|
|
||||||
etag: req.headers.etag,
|
|
||||||
md5Url: null,
|
|
||||||
originalMd5: null,
|
|
||||||
sourceCode: req.response,
|
|
||||||
updateUrl: url,
|
|
||||||
url: URLS.extractUsoArchiveInstallUrl(url),
|
|
||||||
});
|
|
||||||
const varUrlValues = style.updateUrl.split('?')[1];
|
|
||||||
const varData = json.usercssData.vars;
|
|
||||||
if (varUrlValues && varData) {
|
|
||||||
const IK = 'ik-';
|
|
||||||
const IK_LEN = IK.length;
|
|
||||||
for (let [key, val] of new URLSearchParams(varUrlValues)) {
|
|
||||||
if (!key.startsWith(IK)) continue;
|
|
||||||
key = key.slice(IK_LEN);
|
|
||||||
const varDef = varData[key];
|
|
||||||
if (!varDef) continue;
|
|
||||||
if (varDef.options) {
|
|
||||||
let sel = val.startsWith(IK) && getVarOptByName(varDef, val.slice(IK_LEN));
|
|
||||||
if (!sel) {
|
|
||||||
key += '-custom';
|
|
||||||
sel = getVarOptByName(varDef, key + '-dropdown');
|
|
||||||
if (sel) varData[key].value = val;
|
|
||||||
}
|
|
||||||
if (sel) varDef.value = sel.name;
|
|
||||||
} else {
|
|
||||||
varDef.value = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return API.usercss.buildCode(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateUsercss() {
|
|
||||||
let oldVer = ucd.version;
|
let oldVer = ucd.version;
|
||||||
let {etag: oldEtag, updateUrl} = style;
|
let {etag: oldEtag, updateUrl} = style;
|
||||||
const m2 = URLS.extractUsoArchiveId(updateUrl) && await getUsoEmbeddedMeta();
|
const m2 = (css || URLS.extractUsoArchiveId(updateUrl)) &&
|
||||||
|
await getUsoEmbeddedMeta(css);
|
||||||
if (m2 && m2.updateUrl) {
|
if (m2 && m2.updateUrl) {
|
||||||
updateUrl = m2.updateUrl;
|
updateUrl = m2.updateUrl;
|
||||||
oldVer = m2.usercssData.version || '0';
|
oldVer = m2.usercssData.version || '0';
|
||||||
oldEtag = '';
|
oldEtag = '';
|
||||||
|
} else if (css) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (oldEtag && oldEtag === await downloadEtag()) {
|
if (oldEtag && oldEtag === await downloadEtag()) {
|
||||||
return Promise.reject(STATES.SAME_CODE);
|
return Promise.reject(STATES.SAME_CODE);
|
||||||
|
@ -284,8 +256,7 @@ const updateMan = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDateFromVer(style) {
|
function getDateFromVer(style) {
|
||||||
const m = URLS.extractUsoArchiveId(style.updateUrl) &&
|
const m = RX_DATE2VER.exec((style.usercssData || {}).version);
|
||||||
style.usercssData.version.match(RX_DATE2VER);
|
|
||||||
if (m) {
|
if (m) {
|
||||||
m[2]--; // month is 0-based in `Date` constructor
|
m[2]--; // month is 0-based in `Date` constructor
|
||||||
return new Date(...m.slice(1)).getTime();
|
return new Date(...m.slice(1)).getTime();
|
||||||
|
@ -294,13 +265,10 @@ const updateMan = (() => {
|
||||||
|
|
||||||
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
|
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
|
||||||
function getUsoEmbeddedMeta(code = style.sourceCode) {
|
function getUsoEmbeddedMeta(code = style.sourceCode) {
|
||||||
const m = code.includes('@updateURL') && code.replace(RX_META, '').match(RX_META);
|
const isRaw = arguments[0];
|
||||||
|
const m = code.includes('@updateURL') && (isRaw ? code : code.replace(RX_META, '')).match(RX_META);
|
||||||
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
|
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVarOptByName(varDef, name) {
|
|
||||||
return varDef.options.find(o => o.name === name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedule() {
|
function schedule() {
|
||||||
|
@ -349,4 +317,32 @@ const updateMan = (() => {
|
||||||
logLastWriteTime = Date.now();
|
logLastWriteTime = Date.now();
|
||||||
logQueue = [];
|
logQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usoSpooferStart() {
|
||||||
|
if (++usoReferers === 1) {
|
||||||
|
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
usoSpoofer,
|
||||||
|
{types: ['xmlhttprequest'], urls: [USO_STYLES_API + '*']},
|
||||||
|
['blocking', 'requestHeaders', chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS]
|
||||||
|
.filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function usoSpooferStop() {
|
||||||
|
if (--usoReferers <= 0) {
|
||||||
|
usoReferers = 0;
|
||||||
|
chrome.webRequest.onBeforeSendHeaders.removeListener(usoSpoofer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {chrome.webRequest.WebResponseHeadersDetails | browser.webRequest._OnBeforeSendHeadersDetails} info */
|
||||||
|
function usoSpoofer(info) {
|
||||||
|
if (info.tabId < 0 && URLS.ownOrigin.startsWith(info.initiator || info.originUrl || '')) {
|
||||||
|
const {requestHeaders: hh} = info;
|
||||||
|
const i = (hh.findIndex(h => /^referer$/i.test(h.name)) + 1 || hh.push({})) - 1;
|
||||||
|
hh[i].name = 'referer';
|
||||||
|
hh[i].value = URLS.uso;
|
||||||
|
return {requestHeaders: hh};
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -12,10 +12,12 @@ const usercssMan = {
|
||||||
name: null,
|
name: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
async assignVars(style, oldStyle) {
|
/** `src` is a style or vars */
|
||||||
|
async assignVars(style, src) {
|
||||||
const meta = style.usercssData;
|
const meta = style.usercssData;
|
||||||
const vars = meta.vars;
|
const meta2 = src.usercssData;
|
||||||
const oldVars = (oldStyle.usercssData || {}).vars;
|
const {vars} = meta;
|
||||||
|
const oldVars = meta2 ? meta2.vars : src;
|
||||||
if (vars && oldVars) {
|
if (vars && oldVars) {
|
||||||
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
||||||
for (const [key, v] of Object.entries(vars)) {
|
for (const [key, v] of Object.entries(vars)) {
|
||||||
|
@ -43,7 +45,7 @@ const usercssMan = {
|
||||||
let log;
|
let log;
|
||||||
if (!metaOnly) {
|
if (!metaOnly) {
|
||||||
if (vars || assignVars) {
|
if (vars || assignVars) {
|
||||||
await usercssMan.assignVars(style, vars ? {usercssData: {vars}} : dup);
|
await usercssMan.assignVars(style, vars || dup);
|
||||||
}
|
}
|
||||||
await usercssMan.buildCode(style);
|
await usercssMan.buildCode(style);
|
||||||
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
|
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
|
||||||
|
@ -137,17 +139,18 @@ const usercssMan = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async install(style) {
|
async install(style, opts) {
|
||||||
return API.styles.install(await usercssMan.parse(style));
|
return API.styles.install(await usercssMan.parse(style, opts));
|
||||||
},
|
},
|
||||||
|
|
||||||
async parse(style) {
|
async parse(style, {dup, vars} = {}) {
|
||||||
style = await usercssMan.buildMeta(style);
|
style = await usercssMan.buildMeta(style);
|
||||||
// preserve style.vars during update
|
// preserve style.vars during update
|
||||||
const dup = await usercssMan.find(style);
|
if (dup || (dup = await usercssMan.find(style))) {
|
||||||
if (dup) {
|
|
||||||
style.id = dup.id;
|
style.id = dup.id;
|
||||||
await usercssMan.assignVars(style, dup);
|
}
|
||||||
|
if (vars || (vars = dup)) {
|
||||||
|
await usercssMan.assignVars(style, vars);
|
||||||
}
|
}
|
||||||
return usercssMan.buildCode(style);
|
return usercssMan.buildCode(style);
|
||||||
},
|
},
|
||||||
|
|
157
background/uso-api.js
Normal file
157
background/uso-api.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
/* global URLS stringAsRegExp */// toolbox.js
|
||||||
|
/* global usercssMan */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const usoApi = {};
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const pingers = {};
|
||||||
|
|
||||||
|
usoApi.pingback = (usoId, delay) => {
|
||||||
|
clearTimeout(pingers[usoId]);
|
||||||
|
delete pingers[usoId];
|
||||||
|
if (delay > 0) {
|
||||||
|
return new Promise(resolve => (pingers[usoId] = setTimeout(ping, delay, usoId, resolve)));
|
||||||
|
} else if (delay !== false) {
|
||||||
|
return ping(usoId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicating USO-Archive format
|
||||||
|
* https://github.com/33kk/uso-archive/blob/flomaster/lib/uso.js
|
||||||
|
* https://github.com/33kk/uso-archive/blob/flomaster/lib/converters.js
|
||||||
|
*/
|
||||||
|
usoApi.toUsercss = async (data, {metaOnly = true, varsUrl} = {}) => {
|
||||||
|
const badKeys = {};
|
||||||
|
const newKeys = [];
|
||||||
|
const descr = JSON.stringify(data.description.trim());
|
||||||
|
const vars = (data.style_settings || []).map(makeVar, {badKeys, newKeys}).join('');
|
||||||
|
const sourceCode = `\
|
||||||
|
/* ==UserStyle==
|
||||||
|
@name ${data.name}
|
||||||
|
@namespace USO Archive
|
||||||
|
@version ${data.updated.replace(/-/g, '').replace(/[T:]/g, '.').slice(0, 14)}
|
||||||
|
@description ${/^"['`]|\\/.test(descr) ? descr : descr.slice(1, -1)}
|
||||||
|
@author ${(data.user || {}).name || '?'}
|
||||||
|
@license ${makeLicense(data.license)}${vars ? '\n@preprocessor uso' + vars : ''}`
|
||||||
|
.replace(/\*\//g, '*\\/') +
|
||||||
|
`==/UserStyle== */\n${newKeys[0] ? useNewKeys(data.css, badKeys) : data.css}`;
|
||||||
|
const {style} = await usercssMan.build({sourceCode, metaOnly});
|
||||||
|
usoApi.useVarsUrl(style, varsUrl);
|
||||||
|
return {style, badKeys, newKeys};
|
||||||
|
};
|
||||||
|
|
||||||
|
usoApi.useVarsUrl = (style, url) => {
|
||||||
|
if (!/\?ik-/.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = {badKeys: {}, newKeys: []};
|
||||||
|
const {vars} = style.usercssData;
|
||||||
|
if (!vars) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let [key, val] of new URLSearchParams(url.split('?')[1])) {
|
||||||
|
if (!key.startsWith('ik-')) continue;
|
||||||
|
key = makeKey(key.slice(3), cfg);
|
||||||
|
const v = vars[key];
|
||||||
|
if (!v) continue;
|
||||||
|
if (v.options) {
|
||||||
|
let sel = val.startsWith('ik-') && optByName(v, makeKey(val.slice(3), cfg));
|
||||||
|
if (!sel) {
|
||||||
|
key += '-custom';
|
||||||
|
sel = optByName(v, key + '-dropdown');
|
||||||
|
if (sel) vars[key].value = val;
|
||||||
|
}
|
||||||
|
if (sel) v.value = sel.name;
|
||||||
|
} else {
|
||||||
|
v.value = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ping(id, resolve) {
|
||||||
|
return fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`)
|
||||||
|
.then(resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeKey(key, {badKeys, newKeys}) {
|
||||||
|
let res = badKeys[key];
|
||||||
|
if (!res) {
|
||||||
|
res = key.replace(/[^-\w]/g, '-');
|
||||||
|
res += newKeys.includes(res) ? '-' : '';
|
||||||
|
if (key !== res) {
|
||||||
|
badKeys[key] = res;
|
||||||
|
newKeys.push(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLicense(s) {
|
||||||
|
return !s ? 'NO-REDISTRIBUTION' :
|
||||||
|
s === 'publicdomain' ? 'CC0-1.0' :
|
||||||
|
s.startsWith('ccby') ? `${s.toUpperCase().match(/(..)/g).join('-')}-4.0` :
|
||||||
|
s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeVar({
|
||||||
|
label,
|
||||||
|
setting_type: type,
|
||||||
|
install_key: ik,
|
||||||
|
style_setting_options: opts,
|
||||||
|
}) {
|
||||||
|
const cfg = this;
|
||||||
|
let value, suffix;
|
||||||
|
ik = makeKey(ik, cfg);
|
||||||
|
label = JSON.stringify(label);
|
||||||
|
switch (type) {
|
||||||
|
|
||||||
|
case 'color':
|
||||||
|
value = opts[0].value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
value = JSON.stringify(opts[0].value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image': {
|
||||||
|
const ikCust = `${ik}-custom`;
|
||||||
|
opts.push({
|
||||||
|
label: 'Custom',
|
||||||
|
install_key: `${ikCust}-dropdown`,
|
||||||
|
value: `/*[[${ikCust}]]*/`,
|
||||||
|
});
|
||||||
|
suffix = `\n@advanced text ${ikCust} ${label.slice(0, -1)} (Custom)" "https://foo.com/123.jpg"`;
|
||||||
|
type = 'dropdown';
|
||||||
|
} // fallthrough
|
||||||
|
|
||||||
|
case 'dropdown':
|
||||||
|
value = '';
|
||||||
|
for (const o of opts) {
|
||||||
|
const def = o.default ? '*' : '';
|
||||||
|
const val = o.value;
|
||||||
|
const s = ` ${makeKey(o.install_key, cfg)} ${JSON.stringify(o.label + def)} <<<EOT${
|
||||||
|
val.includes('\n') ? '\n' : ' '}${val} EOT;\n`;
|
||||||
|
value = def ? s + value : value + s;
|
||||||
|
}
|
||||||
|
value = `{\n${value}}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
value = '"ERROR: unknown type"';
|
||||||
|
}
|
||||||
|
return `\n@advanced ${type} ${ik} ${label} ${value}${suffix || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optByName(v, name) {
|
||||||
|
return v.options.find(o => o.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useNewKeys(css, badKeys) {
|
||||||
|
const rxsKeys = stringAsRegExp(Object.keys(badKeys).join('\n'), '', true).replace(/\n/g, '|');
|
||||||
|
const rxUsoVars = new RegExp(`(/\\*\\[\\[)(${rxsKeys})(?=]]\\*/)`, 'g');
|
||||||
|
return css.replace(rxUsoVars, (s, a, key) => a + badKeys[key]);
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,378 +1,296 @@
|
||||||
/* global API msg */// msg.js
|
/* global API */// msg.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
// eslint-disable-next-line no-unused-expressions
|
||||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
|
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
|
||||||
const styleId = RegExp.$1;
|
if (window.INJECTED_USO === 1) return;
|
||||||
|
window.INJECTED_USO = 1;
|
||||||
|
|
||||||
|
const usoId = RegExp.$1;
|
||||||
|
const USO = 'https://userstyles.org';
|
||||||
|
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
|
||||||
|
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
|
||||||
|
const CLICK = {
|
||||||
|
customize: '.customize_button',
|
||||||
|
install: '#install_style_button',
|
||||||
|
uninstall: '#uninstall_style_button',
|
||||||
|
update: '#update_style_button',
|
||||||
|
};
|
||||||
|
const CLICK_SEL = Object.values(CLICK).join(',');
|
||||||
const pageEventId = `${performance.now()}${Math.random()}`;
|
const pageEventId = `${performance.now()}${Math.random()}`;
|
||||||
|
const contentEventId = pageEventId + ':';
|
||||||
|
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
|
||||||
|
const $ = (sel, base = document) => base.querySelector(sel);
|
||||||
|
const toggleListener = (isOn, ...args) => (isOn ? addEventListener : removeEventListener)(...args);
|
||||||
|
const togglePageListener = isOn => toggleListener(isOn, contentEventId, onPageEvent, true);
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
const mo = new MutationObserver(onMutation);
|
||||||
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
const observeColors = isOn =>
|
||||||
|
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
|
||||||
|
: mo.disconnect();
|
||||||
|
|
||||||
document.addEventListener('stylishInstallChrome', onClick);
|
let style, dup, md5, pageData, badKeys;
|
||||||
document.addEventListener('stylishUpdateChrome', onClick);
|
|
||||||
|
|
||||||
msg.on(onMessage);
|
runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
|
||||||
|
addEventListener(orphanEventId, orphanCheck, true);
|
||||||
|
addEventListener('click', onClick, true);
|
||||||
|
togglePageListener(true);
|
||||||
|
|
||||||
let currentMd5;
|
[md5, dup] = await Promise.all([
|
||||||
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
|
fetch(md5Url).then(r => r.text()),
|
||||||
Promise.all([
|
API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
|
||||||
API.styles.find({md5Url}),
|
.then(sendVarsToPage),
|
||||||
getResource(md5Url),
|
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
|
||||||
onDOMready(),
|
]);
|
||||||
]).then(checkUpdatability);
|
|
||||||
|
|
||||||
document.documentElement.appendChild(
|
if (!dup.id) {
|
||||||
Object.assign(document.createElement('script'), {
|
sendStylishEvent('styleCanBeInstalledChrome');
|
||||||
textContent: `(${inPageContext})('${pageEventId}')`,
|
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
|
||||||
}));
|
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
|
||||||
|
sendStylishEvent('styleCanBeUpdatedChrome');
|
||||||
function onMessage(msg) {
|
} else {
|
||||||
switch (msg.method) {
|
sendStylishEvent('styleAlreadyInstalledChrome');
|
||||||
case 'ping':
|
|
||||||
// orphaned content script check
|
|
||||||
return true;
|
|
||||||
case 'openSettings':
|
|
||||||
openSettings();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
async function onClick(e) {
|
||||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
const el = e.target.closest(CLICK_SEL);
|
||||||
we need to fix this URL using "stylish-update-url" meta key
|
if (!el) return;
|
||||||
*/
|
el.disabled = true;
|
||||||
function getStyleURL() {
|
const {id} = dup;
|
||||||
const textUrl = getMeta('stylish-update-url') || '';
|
try {
|
||||||
const jsonUrl = getMeta('stylish-code-chrome') ||
|
if (el.matches(CLICK.uninstall)) {
|
||||||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
dup = style = false;
|
||||||
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
removeEventListener('change', onChange);
|
||||||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
await API.styles.delete(id);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
function checkUpdatability([installedStyle, md5]) {
|
if (el.matches(CLICK.customize)) {
|
||||||
// TODO: remove the following statement when USO is fixed
|
const isOn = dup && !$('#style-settings');
|
||||||
document.dispatchEvent(new CustomEvent(pageEventId, {
|
toggleListener(isOn, 'change', onChange);
|
||||||
detail: installedStyle && installedStyle.updateUrl,
|
observeColors(isOn);
|
||||||
}));
|
return;
|
||||||
currentMd5 = md5;
|
}
|
||||||
if (!installedStyle) {
|
e.stopPropagation();
|
||||||
sendEvent({type: 'styleCanBeInstalledChrome'});
|
if (!style) await buildStyle();
|
||||||
return;
|
style = dup = await API.usercss.install(style, {
|
||||||
}
|
dup: {id},
|
||||||
const isCustomizable = /\?/.test(installedStyle.updateUrl);
|
vars: getPageVars(),
|
||||||
const md5Url = getMeta('stylish-md5-url');
|
|
||||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
|
||||||
reportUpdatable(isCustomizable || md5 !== installedStyle.originalMd5);
|
|
||||||
} else {
|
|
||||||
getStyleJson().then(json => {
|
|
||||||
reportUpdatable(
|
|
||||||
isCustomizable ||
|
|
||||||
!json ||
|
|
||||||
!styleSectionsEqual(json, installedStyle));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareInstallButton() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const observer = new MutationObserver(check);
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
check();
|
|
||||||
|
|
||||||
function check() {
|
|
||||||
if (document.querySelector('#install_style_button')) {
|
|
||||||
resolve();
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportUpdatable(isUpdatable) {
|
|
||||||
prepareInstallButton().then(() => {
|
|
||||||
sendEvent({
|
|
||||||
type: isUpdatable
|
|
||||||
? 'styleCanBeUpdatedChrome'
|
|
||||||
: 'styleAlreadyInstalledChrome',
|
|
||||||
detail: {
|
|
||||||
updateUrl: installedStyle.updateUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
sendStylishEvent('styleInstalledChrome');
|
||||||
|
API.uso.pingback(id);
|
||||||
|
} catch (e) {
|
||||||
|
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
|
||||||
|
} finally {
|
||||||
|
el.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChange({target: el}) {
|
||||||
function sendEvent(event) {
|
if (dup && el.matches('[name^="ik-"], [type=file]')) {
|
||||||
sendEvent.lastEvent = event;
|
API.usercss.configVars(dup.id, getPageVars());
|
||||||
let {type, detail = null} = event;
|
|
||||||
if (typeof cloneInto !== 'undefined') {
|
|
||||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
|
||||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
|
||||||
detail = cloneInto({detail}, document); /* global cloneInto */
|
|
||||||
} else {
|
|
||||||
detail = {detail};
|
|
||||||
}
|
|
||||||
document.dispatchEvent(new CustomEvent(type, detail));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClick(event) {
|
|
||||||
if (onClick.processing || !orphanCheck()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onClick.processing = true;
|
|
||||||
doInstall()
|
|
||||||
.then(() => {
|
|
||||||
if (!event.type.includes('Update')) {
|
|
||||||
// FIXME: sometimes the button is broken i.e. the button sends
|
|
||||||
// 'install' instead of 'update' event while the style is already
|
|
||||||
// install.
|
|
||||||
// This triggers an incorrect install count but we don't really care.
|
|
||||||
return getResource(getMeta('stylish-install-ping-url-chrome'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
.then(done);
|
|
||||||
function done() {
|
|
||||||
setTimeout(() => {
|
|
||||||
onClick.processing = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doInstall() {
|
function onMutation(mutations) {
|
||||||
let oldStyle;
|
for (const {target: el} of mutations) {
|
||||||
return API.styles.find({
|
if (el.style.display === 'none' &&
|
||||||
md5Url: getMeta('stylish-md5-url') || location.href,
|
/^ik-/.test(el.name) &&
|
||||||
})
|
/^#[\da-f]{6}$/.test(el.value)) {
|
||||||
.then(_oldStyle => {
|
onChange({target: el});
|
||||||
oldStyle = _oldStyle;
|
|
||||||
return oldStyle ?
|
|
||||||
oldStyle.name :
|
|
||||||
getResource(getMeta('stylish-description'));
|
|
||||||
})
|
|
||||||
.then(name => {
|
|
||||||
const props = {};
|
|
||||||
if (oldStyle) {
|
|
||||||
props.id = oldStyle.id;
|
|
||||||
}
|
|
||||||
return saveStyleCode(oldStyle ? 'styleUpdate' : 'styleInstall', name, props);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveStyleCode(message, name, addProps = {}) {
|
|
||||||
const isNew = message === 'styleInstall';
|
|
||||||
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
|
||||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
saveStyleCode.confirmed = true;
|
|
||||||
enableUpdateButton(false);
|
|
||||||
const json = await getStyleJson();
|
|
||||||
if (!json) {
|
|
||||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
|
||||||
'https://github.com/openstyles/stylus/issues/195');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
|
|
||||||
const style = await API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}));
|
|
||||||
if (!isNew && style.updateUrl.includes('?')) {
|
|
||||||
enableUpdateButton(true);
|
|
||||||
} else {
|
|
||||||
sendEvent({type: 'styleInstalledChrome'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableUpdateButton(state) {
|
|
||||||
const important = s => s.replace(/;/g, '!important;');
|
|
||||||
const button = document.getElementById('update_style_button');
|
|
||||||
if (button) {
|
|
||||||
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
|
|
||||||
const icon = button.querySelector('img[src*=".svg"]');
|
|
||||||
if (icon) {
|
|
||||||
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
|
|
||||||
if (state) {
|
|
||||||
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMeta(name) {
|
function onPageEvent(e) {
|
||||||
const e = document.querySelector(`link[rel="${name}"]`);
|
pageData = e.detail;
|
||||||
return e ? e.getAttribute('href') : null;
|
togglePageListener(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResource(url, opts) {
|
async function buildStyle() {
|
||||||
try {
|
if (!pageData) pageData = await (await fetch(apiUrl)).json();
|
||||||
return url.startsWith('#')
|
({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
|
||||||
? document.getElementById(url.slice(1)).textContent
|
Object.assign(style, {
|
||||||
: await API.download(url, opts);
|
md5Url,
|
||||||
} catch (error) {
|
id: dup.id,
|
||||||
alert('Error\n' + error.message);
|
originalMd5: md5,
|
||||||
return Promise.reject(error);
|
updateUrl: apiUrl,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
function getPageVars() {
|
||||||
// instead of "https://update.userstyles.org/#####.md5"
|
const {vars} = (style || dup).usercssData;
|
||||||
async function getStyleJson() {
|
for (const el of document.querySelectorAll('[name^="ik-"]')) {
|
||||||
try {
|
const name = el.name.slice(3); // dropping "ik-"
|
||||||
const style = await getResource(getStyleURL(), {responseType: 'json'});
|
const ik = badKeys[name] || name;
|
||||||
const codeElement = document.getElementById('stylish-code');
|
const v = vars[ik] || false;
|
||||||
if (!style || !Array.isArray(style.sections) || style.sections.length ||
|
const isImage = el.type === 'radio';
|
||||||
codeElement && !codeElement.textContent.trim()) {
|
if (v && (!isImage || el.checked)) {
|
||||||
return style;
|
const val = el.value;
|
||||||
}
|
const isFile = val === 'user-upload';
|
||||||
const code = await getResource(getMeta('stylish-update-url'));
|
if (isImage && (isFile || val === 'user-url')) {
|
||||||
style.sections = (await API.worker.parseMozFormat({code})).sections;
|
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
|
||||||
if (style.md5Url) style.md5Url = style.md5Url.replace('update.update', 'update');
|
const ikCust = `${ik}-custom`;
|
||||||
return style;
|
v.value = `${ikCust}-dropdown`;
|
||||||
} catch (e) {}
|
vars[ikCust].value = isFile ? getFileUriFromPage(el2) : el2.value;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sections are checked in successive order because it matters when many sections
|
|
||||||
* match the same URL and they have rules with the same CSS specificity
|
|
||||||
* @param {Object} a - first style object
|
|
||||||
* @param {Object} b - second style object
|
|
||||||
* @returns {?boolean}
|
|
||||||
*/
|
|
||||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
|
||||||
const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
|
|
||||||
return a && b && a.length === b.length && a.every(sameSection);
|
|
||||||
function sameSection(secA, i) {
|
|
||||||
return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
|
|
||||||
targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
|
|
||||||
}
|
|
||||||
function equalOrEmpty(a, b, type, comparator) {
|
|
||||||
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
|
|
||||||
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
|
|
||||||
return typeA && typeB && comparator(a, b) ||
|
|
||||||
(a == null || typeA && !a.length) &&
|
|
||||||
(b == null || typeB && !b.length);
|
|
||||||
}
|
|
||||||
function arrayMirrors(a, b) {
|
|
||||||
return a.length === b.length &&
|
|
||||||
a.every(el => b.includes(el)) &&
|
|
||||||
b.every(el => a.includes(el));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDOMready() {
|
|
||||||
return document.readyState !== 'loading'
|
|
||||||
? Promise.resolve()
|
|
||||||
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSettings(countdown = 10e3) {
|
|
||||||
const button = document.querySelector('.customize_button');
|
|
||||||
if (button) {
|
|
||||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
||||||
setTimeout(function pollArea(countdown = 2000) {
|
|
||||||
const area = document.getElementById('advancedsettings_area');
|
|
||||||
if (area || countdown < 0) {
|
|
||||||
(area || button).scrollIntoView({behavior: 'smooth', block: area ? 'end' : 'center'});
|
|
||||||
} else {
|
} else {
|
||||||
setTimeout(pollArea, 100, countdown - 100);
|
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
|
||||||
}
|
}
|
||||||
}, 500);
|
}
|
||||||
} else if (countdown > 0) {
|
|
||||||
setTimeout(openSettings, 100, countdown - 100);
|
|
||||||
}
|
}
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileUriFromPage(el) {
|
||||||
|
togglePageListener(true);
|
||||||
|
sendPageEvent(el);
|
||||||
|
return pageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInPage(fn, ...args) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.attachShadow({mode: 'closed'})
|
||||||
|
.appendChild(document.createElement('script'))
|
||||||
|
.textContent = `(${fn})(${JSON.stringify(args).slice(1, -1)})`;
|
||||||
|
document.documentElement.appendChild(div).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPageEvent(data) {
|
||||||
|
dispatchEvent(data instanceof Node
|
||||||
|
? new MouseEvent(pageEventId, {relatedTarget: data})
|
||||||
|
: new CustomEvent(pageEventId, {detail: data}));
|
||||||
|
//* global cloneInto */// WARNING! Firefox requires cloning of an object `detail`
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendStylishEvent(type) {
|
||||||
|
document.dispatchEvent(new Event(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendVarsToPage(style) {
|
||||||
|
if (style) {
|
||||||
|
const vars = (style.usercssData || {}).vars || `${style.updateUrl}`.split('?')[1];
|
||||||
|
if (vars) sendPageEvent('vars:' + JSON.stringify(vars));
|
||||||
|
}
|
||||||
|
return style || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function orphanCheck() {
|
function orphanCheck() {
|
||||||
try {
|
if (chrome.i18n) return true;
|
||||||
if (chrome.i18n.getUILanguage()) {
|
removeEventListener(orphanEventId, orphanCheck, true);
|
||||||
return true;
|
removeEventListener('click', onClick, true);
|
||||||
}
|
removeEventListener('change', onChange);
|
||||||
} catch (e) {}
|
sendPageEvent('quit');
|
||||||
// In Chrome content script is orphaned on an extension update/reload
|
observeColors(false);
|
||||||
// so we need to detach event listeners
|
togglePageListener(false);
|
||||||
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
|
||||||
document.removeEventListener('stylishInstallChrome', onClick);
|
|
||||||
document.removeEventListener('stylishUpdateChrome', onClick);
|
|
||||||
try {
|
|
||||||
msg.off(onMessage);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function inPageContext(eventId) {
|
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
|
||||||
document.currentScript.remove();
|
|
||||||
window.isInstalled = true;
|
window.isInstalled = true;
|
||||||
const origMethods = {
|
const {dispatchEvent, CustomEvent, removeEventListener} = window;
|
||||||
json: Response.prototype.json,
|
const apply = Map.call.bind(Map.apply);
|
||||||
byId: document.getElementById,
|
const CR = chrome.runtime;
|
||||||
|
const {sendMessage} = CR;
|
||||||
|
const RP = Response.prototype;
|
||||||
|
const origJson = RP.json;
|
||||||
|
let done, vars;
|
||||||
|
CR.sendMessage = function (id, msg, opts, cb = opts) {
|
||||||
|
if (id === 'fjnbnpbmkenffdnngjfgmeleoegfcffe' &&
|
||||||
|
msg && msg.type === 'deleteStyle' &&
|
||||||
|
typeof cb === 'function') {
|
||||||
|
cb(true);
|
||||||
|
} else {
|
||||||
|
return sendMessage(...arguments);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let vars;
|
RP.json = async function () {
|
||||||
// USO bug workaround: prevent errors in console after install and busy cursor
|
const res = await apply(origJson, this, arguments);
|
||||||
document.getElementById = id =>
|
try {
|
||||||
origMethods.byId.call(document, id) ||
|
if (!done && this.url === apiUrl) {
|
||||||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
|
RP.json = origJson;
|
||||||
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
|
done = true; // will be used if called by another script that saved our RP.json hook
|
||||||
: null);
|
send(res);
|
||||||
// USO bug workaround: use the actual image data in customized settings
|
setVars(res);
|
||||||
document.addEventListener(eventId, ({detail}) => {
|
}
|
||||||
vars = /\?/.test(detail) && new URL(detail).searchParams;
|
} catch (e) {}
|
||||||
if (!vars) Response.prototype.json = origMethods.json;
|
return res;
|
||||||
}, {once: true});
|
};
|
||||||
Response.prototype.json = async function () {
|
addEventListener(eventId, onCommand, true);
|
||||||
const json = await origMethods.json.apply(this, arguments);
|
function onCommand(e) {
|
||||||
if (vars && json && Array.isArray(json.style_settings)) {
|
if (e.detail === 'quit') {
|
||||||
Response.prototype.json = origMethods.json;
|
removeEventListener(eventId, onCommand, true);
|
||||||
const images = new Map();
|
CR.sendMessage = sendMessage;
|
||||||
for (const ss of json.style_settings) {
|
RP.json = origJson;
|
||||||
let value = vars.get('ik-' + ss.install_key);
|
done = true;
|
||||||
if (!value || !(ss.style_setting_options || [])[0]) {
|
} else if (/^vars:/.test(e.detail)) {
|
||||||
continue;
|
vars = JSON.parse(e.detail.slice(5));
|
||||||
}
|
} else if (e.relatedTarget) {
|
||||||
if (value.startsWith('ik-')) {
|
send(e.relatedTarget.uploadedData);
|
||||||
value = value.replace(/^ik-/, '');
|
}
|
||||||
const def = ss.style_setting_options.find(item => item.default);
|
}
|
||||||
if (!def || def.install_key !== value) {
|
function send(data) {
|
||||||
if (def) def.default = false;
|
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
|
||||||
for (const item of ss.style_setting_options) {
|
}
|
||||||
if (item.install_key === value) {
|
function setVars(json) {
|
||||||
item.default = true;
|
const images = new Map();
|
||||||
break;
|
const isNew = typeof vars === 'object';
|
||||||
}
|
const badKeys = {};
|
||||||
}
|
const newKeys = [];
|
||||||
}
|
const makeKey = ({install_key: key}) => {
|
||||||
} else if (ss.setting_type === 'image') {
|
let res = isNew ? badKeys[key] : key;
|
||||||
let isListed;
|
if (!res) {
|
||||||
for (const opt of ss.style_setting_options) {
|
res = key.replace(/[^-\w]/g, '-');
|
||||||
isListed |= opt.default = (opt.value === value);
|
res += newKeys.includes(res) ? '-' : '';
|
||||||
}
|
if (key !== res) {
|
||||||
images.set(ss.install_key, {url: value, isListed});
|
badKeys[key] = res;
|
||||||
} else {
|
newKeys.push(res);
|
||||||
const item = ss.style_setting_options[0];
|
|
||||||
if (item.value !== value && item.install_key === 'placeholder') {
|
|
||||||
item.value = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (images.size) {
|
return res;
|
||||||
new MutationObserver((_, observer) => {
|
};
|
||||||
if (document.getElementById('style-settings')) {
|
if (!isNew) vars = new URLSearchParams(vars);
|
||||||
observer.disconnect();
|
for (const ss of json.style_settings || []) {
|
||||||
for (const [name, {url, isListed}] of images) {
|
const ik = makeKey(ss);
|
||||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
let value = isNew ? (vars[ik] || {}).value : vars.get('ik-' + ik);
|
||||||
const elUrl = elRadio &&
|
if (value == null || !(ss.style_setting_options || [])[0]) {
|
||||||
document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
continue;
|
||||||
if (elUrl) {
|
}
|
||||||
elRadio.checked = !isListed;
|
if (ss.setting_type === 'image') {
|
||||||
elUrl.value = url;
|
let isListed;
|
||||||
}
|
for (const opt of ss.style_setting_options) {
|
||||||
|
isListed |= opt.default = (opt.value === value);
|
||||||
|
}
|
||||||
|
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
|
||||||
|
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {
|
||||||
|
value = value.replace(/^ik-/, '');
|
||||||
|
const def = ss.style_setting_options.find(item => item.default);
|
||||||
|
if (!def || makeKey(def) !== value) {
|
||||||
|
if (def) def.default = false;
|
||||||
|
for (const item of ss.style_setting_options) {
|
||||||
|
if (makeKey(item) === value) {
|
||||||
|
item.default = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).observe(document, {childList: true, subtree: true});
|
}
|
||||||
|
} else {
|
||||||
|
const item = ss.style_setting_options[0];
|
||||||
|
if (item.value !== value && item.install_key === 'placeholder') {
|
||||||
|
item.value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json;
|
if (!images.size) return;
|
||||||
};
|
new MutationObserver((_, observer) => {
|
||||||
|
if (!document.getElementById('style-settings')) return;
|
||||||
|
observer.disconnect();
|
||||||
|
for (const [name, {url, isListed}] of images) {
|
||||||
|
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||||
|
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||||
|
if (elUrl) {
|
||||||
|
elRadio.checked = !isListed;
|
||||||
|
elUrl.value = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(document, {childList: true, subtree: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,8 @@ function createStyleElement({style, name: nameLC}) {
|
||||||
parts.homepage.href = parts.homepage.title = style.url || '';
|
parts.homepage.href = parts.homepage.title = style.url || '';
|
||||||
parts.infoVer.textContent = ud ? ud.version : '';
|
parts.infoVer.textContent = ud ? ud.version : '';
|
||||||
parts.infoVer.dataset.value = ud ? ud.version : '';
|
parts.infoVer.dataset.value = ud ? ud.version : '';
|
||||||
if (URLS.extractUsoArchiveId(style.updateUrl)) {
|
// USO-raw and USO-archive version is a date for which we show the Age column
|
||||||
|
if (ud && (style.md5Url || URLS.extractUsoArchiveId(style.updateUrl))) {
|
||||||
parts.infoVer.dataset.isDate = '';
|
parts.infoVer.dataset.isDate = '';
|
||||||
} else {
|
} else {
|
||||||
delete parts.infoVer.dataset.isDate;
|
delete parts.infoVer.dataset.isDate;
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
"background/update-manager.js",
|
"background/update-manager.js",
|
||||||
"background/usercss-install-helper.js",
|
"background/usercss-install-helper.js",
|
||||||
"background/usercss-manager.js",
|
"background/usercss-manager.js",
|
||||||
|
"background/uso-api.js",
|
||||||
"background/usw-api.js",
|
"background/usw-api.js",
|
||||||
|
|
||||||
"background/style-manager.js",
|
"background/style-manager.js",
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
* --------------------- Stylus' internally added extras
|
* --------------------- Stylus' internally added extras
|
||||||
* @prop {boolean} installed
|
* @prop {boolean} installed
|
||||||
* @prop {number} installedStyleId
|
* @prop {number} installedStyleId
|
||||||
* @prop {number} pingbackTimer
|
|
||||||
*/
|
*/
|
||||||
/** @type IndexEntry[] */
|
/** @type IndexEntry[] */
|
||||||
let results;
|
let results;
|
||||||
|
@ -149,7 +148,7 @@
|
||||||
restoreScrollPosition();
|
restoreScrollPosition();
|
||||||
const result = results.find(r => r.installedStyleId === id);
|
const result = results.find(r => r.installedStyleId === id);
|
||||||
if (result) {
|
if (result) {
|
||||||
clearTimeout(result.pingbackTimer);
|
API.uso.pingback(result.i, false);
|
||||||
renderActionButtons(result.i, -1);
|
renderActionButtons(result.i, -1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -437,11 +436,7 @@
|
||||||
installButton.disabled = true;
|
installButton.disabled = true;
|
||||||
entry.style.setProperty('pointer-events', 'none', 'important');
|
entry.style.setProperty('pointer-events', 'none', 'important');
|
||||||
delete entry.dataset.error;
|
delete entry.dataset.error;
|
||||||
if (fmt) {
|
if (fmt) API.uso.pingback(id, PINGBACK_DELAY);
|
||||||
// FIXME: move this to background page and create an API like installUSOStyle
|
|
||||||
result.pingbackTimer = setTimeout(download, PINGBACK_DELAY,
|
|
||||||
`${URLS.uso}styles/install/${id}?source=stylish-ch`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUrl = fmt ? URLS.makeUsoArchiveCodeUrl(id) : URLS.makeUswCodeUrl(id);
|
const updateUrl = fmt ? URLS.makeUsoArchiveCodeUrl(id) : URLS.makeUswCodeUrl(id);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user