fix USO site installation
This commit is contained in:
parent
079e7e50f1
commit
509cba0dca
|
@ -6,16 +6,9 @@
|
|||
/* global syncMan */
|
||||
/* global updateMan */
|
||||
/* global usercssMan */
|
||||
/* global usoApi */
|
||||
/* global uswApi */
|
||||
/* global
|
||||
FIREFOX
|
||||
UA
|
||||
URLS
|
||||
activateTab
|
||||
download
|
||||
findExistingTab
|
||||
openURL
|
||||
*/ // toolbox.js
|
||||
/* global FIREFOX UA activateTab findExistingTab openURL */ // toolbox.js
|
||||
/* global colorScheme */ // color-scheme.js
|
||||
'use strict';
|
||||
|
||||
|
@ -42,17 +35,12 @@ addAPI(/** @namespace API */ {
|
|||
sync: syncMan,
|
||||
updater: updateMan,
|
||||
usercss: usercssMan,
|
||||
uso: usoApi,
|
||||
usw: uswApi,
|
||||
colorScheme,
|
||||
/** @type {BackgroundWorker} */
|
||||
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} */
|
||||
getTabUrlPrefix() {
|
||||
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global API */// msg.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 compareVersion */// cmpver.js
|
||||
/* global db */
|
||||
|
@ -37,6 +37,7 @@ const updateMan = (() => {
|
|||
503, // service unavailable
|
||||
429, // too many requests
|
||||
];
|
||||
let usoReferers = 0;
|
||||
let lastUpdateTime;
|
||||
let checkingAll = false;
|
||||
let logQueue = [];
|
||||
|
@ -113,12 +114,13 @@ const updateMan = (() => {
|
|||
save,
|
||||
} = opts;
|
||||
if (!id) id = style.id;
|
||||
const ucd = style.usercssData;
|
||||
const {md5Url} = style;
|
||||
let ucd = style.usercssData;
|
||||
let res, state;
|
||||
try {
|
||||
await checkIfEdited();
|
||||
res = {
|
||||
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
|
||||
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
|
||||
updated: true,
|
||||
};
|
||||
state = STATES.UPDATED;
|
||||
|
@ -142,72 +144,40 @@ const updateMan = (() => {
|
|||
}
|
||||
|
||||
async function updateUSO() {
|
||||
const url = URLS.makeUsoArchiveCodeUrl(style.md5Url.match(/\d+/)[0]);
|
||||
const req = await tryDownload(url, RH_ETAG).catch(() => null);
|
||||
if (req) {
|
||||
return updateToUSOArchive(url, req);
|
||||
}
|
||||
const md5 = await tryDownload(style.md5Url);
|
||||
const md5 = await tryDownload(md5Url);
|
||||
if (!md5 || md5.length !== 32) {
|
||||
return Promise.reject(STATES.ERROR_MD5);
|
||||
}
|
||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(STATES.SAME_MD5);
|
||||
}
|
||||
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||
if (!styleJSONseemsValid(json)) {
|
||||
return Promise.reject(STATES.ERROR_JSON);
|
||||
if (!ucd) {
|
||||
ucd = true;
|
||||
style.updateUrl = `${URLS.uso}api/v1/styles/${md5Url.match(/\/(\d+)/)[1]}`;
|
||||
}
|
||||
usoSpooferStart();
|
||||
let json;
|
||||
try {
|
||||
json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||
const m = await getUsoEmbeddedMeta(json.css);
|
||||
if (m) {
|
||||
return updateUsercss(m);
|
||||
}
|
||||
json = (await API.uso.toUsercss(json)).style;
|
||||
} finally {
|
||||
usoSpooferStop();
|
||||
}
|
||||
// USO may not provide a correctly updated originalMd5 (#555)
|
||||
json.originalMd5 = md5;
|
||||
return json;
|
||||
}
|
||||
|
||||
async function updateToUSOArchive(url, req) {
|
||||
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() {
|
||||
async function updateUsercss(m2) {
|
||||
let oldVer = ucd.version;
|
||||
let {etag: oldEtag, updateUrl} = style;
|
||||
const m2 = URLS.extractUsoArchiveId(updateUrl) && await getUsoEmbeddedMeta();
|
||||
if (!m2) {
|
||||
m2 = (md5Url || URLS.extractUsoArchiveId(updateUrl)) && await getUsoEmbeddedMeta();
|
||||
}
|
||||
if (m2 && m2.updateUrl) {
|
||||
updateUrl = m2.updateUrl;
|
||||
oldVer = m2.usercssData.version || '0';
|
||||
|
@ -284,8 +254,7 @@ const updateMan = (() => {
|
|||
}
|
||||
|
||||
function getDateFromVer(style) {
|
||||
const m = URLS.extractUsoArchiveId(style.updateUrl) &&
|
||||
style.usercssData.version.match(RX_DATE2VER);
|
||||
const m = RX_DATE2VER.exec((style.usercssData || {}).version);
|
||||
if (m) {
|
||||
m[2]--; // month is 0-based in `Date` constructor
|
||||
return new Date(...m.slice(1)).getTime();
|
||||
|
@ -294,13 +263,10 @@ const updateMan = (() => {
|
|||
|
||||
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
|
||||
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);
|
||||
}
|
||||
|
||||
function getVarOptByName(varDef, name) {
|
||||
return varDef.options.find(o => o.name === name);
|
||||
}
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
|
@ -349,4 +315,28 @@ const updateMan = (() => {
|
|||
logLastWriteTime = Date.now();
|
||||
logQueue = [];
|
||||
}
|
||||
|
||||
function usoSpooferStart() {
|
||||
if (++usoReferers === 1) {
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
usoSpoofer,
|
||||
{types: ['xmlhttprequest'], urls: [URLS.uso + 'api/*']},
|
||||
['blocking', 'requestHeaders', chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS]
|
||||
.filter(Boolean));
|
||||
}
|
||||
}
|
||||
|
||||
function usoSpooferStop() {
|
||||
if (--usoReferers <= 0) {
|
||||
usoReferers = 0;
|
||||
chrome.webRequest.onBeforeSendHeaders.removeListener(usoSpoofer);
|
||||
}
|
||||
}
|
||||
|
||||
function usoSpoofer({requestHeaders: hh}) {
|
||||
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,
|
||||
}),
|
||||
|
||||
async assignVars(style, oldStyle) {
|
||||
/** `src` is a style or vars */
|
||||
async assignVars(style, src) {
|
||||
const meta = style.usercssData;
|
||||
const vars = meta.vars;
|
||||
const oldVars = (oldStyle.usercssData || {}).vars;
|
||||
const meta2 = src.usercssData;
|
||||
const {vars} = meta;
|
||||
const oldVars = meta2 ? meta2.vars : src;
|
||||
if (vars && oldVars) {
|
||||
// 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)) {
|
||||
|
@ -43,7 +45,7 @@ const usercssMan = {
|
|||
let log;
|
||||
if (!metaOnly) {
|
||||
if (vars || assignVars) {
|
||||
await usercssMan.assignVars(style, vars ? {usercssData: {vars}} : dup);
|
||||
await usercssMan.assignVars(style, vars || dup);
|
||||
}
|
||||
await usercssMan.buildCode(style);
|
||||
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
|
||||
|
@ -137,17 +139,18 @@ const usercssMan = {
|
|||
}
|
||||
},
|
||||
|
||||
async install(style) {
|
||||
return API.styles.install(await usercssMan.parse(style));
|
||||
async install(style, opts) {
|
||||
return API.styles.install(await usercssMan.parse(style, opts));
|
||||
},
|
||||
|
||||
async parse(style) {
|
||||
async parse(style, {dup, vars} = {}) {
|
||||
style = await usercssMan.buildMeta(style);
|
||||
// preserve style.vars during update
|
||||
const dup = await usercssMan.find(style);
|
||||
if (dup) {
|
||||
if (dup || (dup = await usercssMan.find(style))) {
|
||||
style.id = dup.id;
|
||||
await usercssMan.assignVars(style, dup);
|
||||
}
|
||||
if (vars || (vars = dup)) {
|
||||
await usercssMan.assignVars(style, vars);
|
||||
}
|
||||
return usercssMan.buildCode(style);
|
||||
},
|
||||
|
|
108
background/uso-api.js
Normal file
108
background/uso-api.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/* global usercssMan */
|
||||
'use strict';
|
||||
|
||||
/* exported usoApi */
|
||||
const usoApi = {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async toUsercss(data, {metaOnly = true} = {}) {
|
||||
const {user, style_settings: ss = []} = data;
|
||||
const badKeys = {};
|
||||
const newKeys = [];
|
||||
const descr = JSON.stringify(data.description.trim());
|
||||
const vars = ss.map(makeVar).join('').replace(/\*\//g, '*\\/');
|
||||
const sourceCode = `\
|
||||
/* ==UserStyle==
|
||||
@name ${data.name}
|
||||
@namespace USO Archive
|
||||
@version ${data.updated.replace(/-/g, '').replace(/[T:]/g, '.').slice(0, 14)}
|
||||
@description ${descr.includes('\\') ? descr : descr.slice(1, -1)}
|
||||
@author ${user && user.name || '?'}
|
||||
@license ${makeLicense(data.license)}${vars ? '@preprocessor uso' + vars : ''}
|
||||
==/UserStyle== */
|
||||
${newKeys[0] ? useNewKeys(data.css) : data.css}`;
|
||||
const res = await usercssMan.build({sourceCode, metaOnly});
|
||||
return Object.assign(res, {badKeys, newKeys});
|
||||
|
||||
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,
|
||||
}) {
|
||||
let value, suffix;
|
||||
ik = makeKey(ik);
|
||||
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)} ${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 makeKey(key) {
|
||||
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 useNewKeys(css) {
|
||||
const rxsKeys = Object.keys(badKeys)
|
||||
.join('\n')
|
||||
.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&')
|
||||
.replace(/\n/g, '|');
|
||||
const rxUsoVars = new RegExp(`(/\\*\\[\\[)(${rxsKeys})(?=]]\\*/)`, 'g');
|
||||
return css.replace(rxUsoVars, (s, a, key) => a + badKeys[key]);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,378 +1,210 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global API */// msg.js
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
|
||||
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 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 wiretap = isOn => window[`${isOn ? 'add' : 'remove'}EventListener`](contentEventId, onPageEvent, true);
|
||||
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
||||
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
const mo = new MutationObserver(onMutation);
|
||||
const observeColors = isOn =>
|
||||
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
|
||||
: mo.disconnect();
|
||||
|
||||
document.addEventListener('stylishInstallChrome', onClick);
|
||||
document.addEventListener('stylishUpdateChrome', onClick);
|
||||
addEventListener(orphanEventId, orphanCheck, true);
|
||||
addEventListener('click', onClick, true);
|
||||
addEventListener('change', onChange);
|
||||
wiretap(true);
|
||||
|
||||
msg.on(onMessage);
|
||||
|
||||
let currentMd5;
|
||||
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
|
||||
let oldStyle, pageData, style, md5, badKeys;
|
||||
Promise.all([
|
||||
fetch(md5Url).then(r => r.text()),
|
||||
API.styles.find({md5Url}),
|
||||
getResource(md5Url),
|
||||
onDOMready(),
|
||||
]).then(checkUpdatability);
|
||||
|
||||
document.documentElement.appendChild(
|
||||
Object.assign(document.createElement('script'), {
|
||||
textContent: `(${inPageContext})('${pageEventId}')`,
|
||||
}));
|
||||
|
||||
function onMessage(msg) {
|
||||
switch (msg.method) {
|
||||
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
|
||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||
we need to fix this URL using "stylish-update-url" meta key
|
||||
*/
|
||||
function getStyleURL() {
|
||||
const textUrl = getMeta('stylish-update-url') || '';
|
||||
const jsonUrl = getMeta('stylish-code-chrome') ||
|
||||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
||||
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
||||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||
}
|
||||
|
||||
function checkUpdatability([installedStyle, md5]) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent(pageEventId, {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
}));
|
||||
currentMd5 = md5;
|
||||
if (!installedStyle) {
|
||||
sendEvent({type: 'styleCanBeInstalledChrome'});
|
||||
return;
|
||||
}
|
||||
const isCustomizable = /\?/.test(installedStyle.updateUrl);
|
||||
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(() => {
|
||||
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
|
||||
]).then(async res => {
|
||||
md5 = res[0];
|
||||
oldStyle = res[1] ||
|
||||
await API.styles.find({installationUrl: `https://uso.kkx.one/style/${usoId}`}) ||
|
||||
false;
|
||||
const {updateUrl, originalMd5, id} = oldStyle;
|
||||
sendEvent({
|
||||
type: isUpdatable
|
||||
type: !id
|
||||
? 'styleCanBeInstalledChrome'
|
||||
: /\?/.test(updateUrl) || originalMd5 && originalMd5 !== md5
|
||||
? 'styleCanBeUpdatedChrome'
|
||||
: 'styleAlreadyInstalledChrome',
|
||||
detail: {
|
||||
updateUrl: installedStyle.updateUrl,
|
||||
},
|
||||
detail: updateUrl ? {updateUrl} : null,
|
||||
});
|
||||
observeColors(true);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
const args = [pageEventId, contentEventId, usoId, apiUrl];
|
||||
div.attachShadow({mode: 'closed'})
|
||||
.appendChild(document.createElement('script'))
|
||||
.textContent = `(${inPageContext})(${JSON.stringify(args).slice(1, -1)})`;
|
||||
document.documentElement.appendChild(div).remove();
|
||||
}
|
||||
|
||||
|
||||
function sendEvent(event) {
|
||||
sendEvent.lastEvent = event;
|
||||
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()) {
|
||||
async function onClick(e) {
|
||||
const el = e.target.closest('#install_style_button, #update_style_button, #uninstall_style_button');
|
||||
if (!el) return;
|
||||
el.disabled = true;
|
||||
try {
|
||||
const {id} = oldStyle;
|
||||
if (el.id === 'uninstall_style_button') {
|
||||
oldStyle = style = false;
|
||||
removeEventListener('change', onChange);
|
||||
await API.styles.delete(id);
|
||||
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;
|
||||
e.stopPropagation();
|
||||
if (!style) await buildStyle();
|
||||
style = oldStyle = await API.usercss.install(style, {
|
||||
dup: {id},
|
||||
vars: getPageVars(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function doInstall() {
|
||||
let oldStyle;
|
||||
return API.styles.find({
|
||||
md5Url: getMeta('stylish-md5-url') || location.href,
|
||||
})
|
||||
.then(_oldStyle => {
|
||||
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'});
|
||||
fetch(getMeta('stylish-install-ping-url'));
|
||||
} catch (e) {
|
||||
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
|
||||
} finally {
|
||||
el.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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 onChange({target: el}) {
|
||||
if (oldStyle && el.matches('[name^="ik-"], [type=file]')) {
|
||||
API.usercss.configVars(oldStyle.id, getPageVars());
|
||||
}
|
||||
}
|
||||
|
||||
function onMutation(mutations) {
|
||||
for (const {target: el} of mutations) {
|
||||
if (el.tagName === 'INPUT' && el.type === 'text' && /^ik-/.test(el.name) && /^#[\da-f]{6}$/.test(el.value)) {
|
||||
onChange({target: el});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPageEvent(e) {
|
||||
pageData = e.detail;
|
||||
wiretap(false);
|
||||
}
|
||||
|
||||
async function buildStyle() {
|
||||
if (!pageData) pageData = await (await fetch(apiUrl)).json();
|
||||
({style, badKeys} = await API.uso.toUsercss(pageData));
|
||||
Object.assign(style, {
|
||||
md5Url,
|
||||
id: oldStyle.id,
|
||||
originalMd5: md5,
|
||||
updateUrl: apiUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function getPageVars() {
|
||||
const {vars} = (style || oldStyle).usercssData;
|
||||
for (const el of document.querySelectorAll('[name^="ik-"]')) {
|
||||
const name = el.name.slice(3); // dropping "ik-"
|
||||
const ik = badKeys[name] || name;
|
||||
const v = vars[ik] || false;
|
||||
const isImage = el.type === 'radio';
|
||||
if (v && (!isImage || el.checked)) {
|
||||
const val = el.value;
|
||||
const isFile = val === 'user-upload';
|
||||
if (isImage && (isFile || val === 'user-url')) {
|
||||
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
|
||||
const ikCust = `${ik}-custom`;
|
||||
v.value = `${ikCust}-dropdown`;
|
||||
vars[ikCust].value = isFile ? getDataUriFromPage(el2) : el2.value;
|
||||
} else {
|
||||
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function getDataUriFromPage(el) {
|
||||
wiretap(true);
|
||||
dispatchEvent(new MouseEvent(pageEventId, {relatedTarget: el}));
|
||||
return pageData;
|
||||
}
|
||||
|
||||
function sendEvent(e) {
|
||||
/* global cloneInto */// Firefox requires cloning
|
||||
document.dispatchEvent(new CustomEvent(e.type,
|
||||
typeof cloneInto === 'function' ? cloneInto(e, document) : e));
|
||||
}
|
||||
|
||||
function getMeta(name) {
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
async function getResource(url, opts) {
|
||||
try {
|
||||
return url.startsWith('#')
|
||||
? document.getElementById(url.slice(1)).textContent
|
||||
: await API.download(url, opts);
|
||||
} catch (error) {
|
||||
alert('Error\n' + error.message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
||||
// instead of "https://update.userstyles.org/#####.md5"
|
||||
async function getStyleJson() {
|
||||
try {
|
||||
const style = await getResource(getStyleURL(), {responseType: 'json'});
|
||||
const codeElement = document.getElementById('stylish-code');
|
||||
if (!style || !Array.isArray(style.sections) || style.sections.length ||
|
||||
codeElement && !codeElement.textContent.trim()) {
|
||||
return style;
|
||||
}
|
||||
const code = await getResource(getMeta('stylish-update-url'));
|
||||
style.sections = (await API.worker.parseMozFormat({code})).sections;
|
||||
if (style.md5Url) style.md5Url = style.md5Url.replace('update.update', 'update');
|
||||
return style;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
setTimeout(pollArea, 100, countdown - 100);
|
||||
}
|
||||
}, 500);
|
||||
} else if (countdown > 0) {
|
||||
setTimeout(openSettings, 100, countdown - 100);
|
||||
}
|
||||
const e = $(`link[rel="${name}"]`);
|
||||
const url = e && e.getAttribute('href');
|
||||
if (url) return url[0] === '#' ? $(url).textContent : url;
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
try {
|
||||
if (chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
document.removeEventListener('stylishInstallChrome', onClick);
|
||||
document.removeEventListener('stylishUpdateChrome', onClick);
|
||||
try {
|
||||
msg.off(onMessage);
|
||||
} catch (e) {}
|
||||
if (chrome.i18n) return true;
|
||||
removeEventListener(orphanEventId, orphanCheck, true);
|
||||
removeEventListener('click', onClick, true);
|
||||
removeEventListener('change', onChange);
|
||||
dispatchEvent(new CustomEvent(pageEventId, {detail: 'quit'}));
|
||||
observeColors(false);
|
||||
wiretap(false);
|
||||
}
|
||||
})();
|
||||
|
||||
function inPageContext(eventId) {
|
||||
document.currentScript.remove();
|
||||
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
|
||||
window.isInstalled = true;
|
||||
const origMethods = {
|
||||
json: Response.prototype.json,
|
||||
byId: document.getElementById,
|
||||
};
|
||||
let vars;
|
||||
// USO bug workaround: prevent errors in console after install and busy cursor
|
||||
document.getElementById = id =>
|
||||
origMethods.byId.call(document, id) ||
|
||||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
|
||||
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
|
||||
: null);
|
||||
// USO bug workaround: use the actual image data in customized settings
|
||||
document.addEventListener(eventId, ({detail}) => {
|
||||
vars = /\?/.test(detail) && new URL(detail).searchParams;
|
||||
if (!vars) Response.prototype.json = origMethods.json;
|
||||
}, {once: true});
|
||||
Response.prototype.json = async function () {
|
||||
const json = await origMethods.json.apply(this, arguments);
|
||||
if (vars && json && Array.isArray(json.style_settings)) {
|
||||
Response.prototype.json = origMethods.json;
|
||||
const images = new Map();
|
||||
for (const ss of json.style_settings) {
|
||||
let value = vars.get('ik-' + ss.install_key);
|
||||
if (!value || !(ss.style_setting_options || [])[0]) {
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('ik-')) {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const def = ss.style_setting_options.find(item => item.default);
|
||||
if (!def || def.install_key !== value) {
|
||||
if (def) def.default = false;
|
||||
for (const item of ss.style_setting_options) {
|
||||
if (item.install_key === value) {
|
||||
item.default = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ss.setting_type === 'image') {
|
||||
let isListed;
|
||||
for (const opt of ss.style_setting_options) {
|
||||
isListed |= opt.default = (opt.value === value);
|
||||
}
|
||||
images.set(ss.install_key, {url: value, isListed});
|
||||
const {dispatchEvent, CustomEvent, removeEventListener} = window;
|
||||
const apply = Map.call.bind(Map.apply);
|
||||
const CR = chrome.runtime;
|
||||
const {sendMessage} = CR;
|
||||
const RP = Response.prototype;
|
||||
const origJson = RP.json;
|
||||
let done;
|
||||
CR.sendMessage = function (id, msg, opts, cb = opts) {
|
||||
if (id === 'fjnbnpbmkenffdnngjfgmeleoegfcffe' &&
|
||||
msg && msg.type === 'deleteStyle' &&
|
||||
typeof cb === 'function') {
|
||||
cb(true);
|
||||
} else {
|
||||
const item = ss.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key === 'placeholder') {
|
||||
item.value = value;
|
||||
return sendMessage(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (images.size) {
|
||||
new MutationObserver((_, observer) => {
|
||||
if (document.getElementById('style-settings')) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
return json;
|
||||
};
|
||||
RP.json = async function () {
|
||||
const res = await apply(origJson, this, arguments);
|
||||
try {
|
||||
if (!done && this.url === apiUrl) {
|
||||
RP.json = origJson;
|
||||
done = true; // will be used if called by another script that saved our RP.json hook
|
||||
send(res);
|
||||
}
|
||||
} catch (e) {}
|
||||
return res;
|
||||
};
|
||||
addEventListener(eventId, function onCommand(e) {
|
||||
if (e.detail === 'quit') {
|
||||
removeEventListener(eventId, onCommand, true);
|
||||
RP.json = origJson;
|
||||
done = true;
|
||||
} else if (e.relatedTarget) {
|
||||
send(e.relatedTarget.uploadedData);
|
||||
}
|
||||
}, true);
|
||||
function send(data) {
|
||||
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,7 +108,8 @@ function createStyleElement({style, name: nameLC}) {
|
|||
parts.homepage.href = parts.homepage.title = style.url || '';
|
||||
parts.infoVer.textContent = 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 = '';
|
||||
} else {
|
||||
delete parts.infoVer.dataset.isDate;
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"background/update-manager.js",
|
||||
"background/usercss-install-helper.js",
|
||||
"background/usercss-manager.js",
|
||||
"background/uso-api.js",
|
||||
"background/usw-api.js",
|
||||
|
||||
"background/style-manager.js",
|
||||
|
|
Loading…
Reference in New Issue
Block a user