convert USO styles to USO-archive on update
This commit is contained in:
parent
272dea01a2
commit
c12d3fc5e3
|
@ -47,7 +47,7 @@ const styleMan = (() => {
|
|||
_id: () => uuidv4(),
|
||||
_rev: () => Date.now(),
|
||||
};
|
||||
const DELETE_IF_NULL = ['id', 'customName'];
|
||||
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
|
||||
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
||||
let ready = init();
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global API */// msg.js
|
||||
/* global URLS debounce stringAsRegExp tryRegExp */// toolbox.js
|
||||
/* global RX_META debounce stringAsRegExp tryRegExp */// toolbox.js
|
||||
/* global addAPI */// common.js
|
||||
'use strict';
|
||||
|
||||
|
@ -10,12 +10,12 @@
|
|||
|
||||
const extractMeta = style =>
|
||||
style.usercssData
|
||||
? (style.sourceCode.match(URLS.rxMETA) || [''])[0]
|
||||
? (style.sourceCode.match(RX_META) || [''])[0]
|
||||
: null;
|
||||
|
||||
const stripMeta = style =>
|
||||
style.usercssData
|
||||
? style.sourceCode.replace(URLS.rxMETA, '')
|
||||
? style.sourceCode.replace(RX_META, '')
|
||||
: null;
|
||||
|
||||
const MODES = Object.assign(Object.create(null), {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* global API */// msg.js
|
||||
/* global URLS debounce download ignoreChromeError */// toolbox.js
|
||||
/* global RX_META URLS debounce download ignoreChromeError */// toolbox.js
|
||||
/* global calcStyleDigest styleJSONseemsValid styleSectionsEqual */ // sections-util.js
|
||||
/* global chromeLocal */// storage-util.js
|
||||
/* global db */
|
||||
/* global prefs */
|
||||
'use strict';
|
||||
|
||||
|
@ -21,7 +22,14 @@ const updateMan = (() => {
|
|||
ERROR_JSON: 'error: JSON is invalid',
|
||||
ERROR_VERSION: 'error: version is older than installed style',
|
||||
};
|
||||
|
||||
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
|
||||
const RX_DATE2VER = new RegExp([
|
||||
/^(\d{4})/,
|
||||
/(1(?:0|[12](?=\d\d))?|[2-9])/, // in ambiguous cases like yyyy123 the month will be 1
|
||||
/([1-2][0-9]?|3[0-1]?|[4-9])/,
|
||||
/\.(0|1[0-9]?|2[0-3]?|[3-9])/,
|
||||
/\.(0|[1-5][0-9]?|[6-9])$/,
|
||||
].map(rx => rx.source).join(''));
|
||||
const ALARM_NAME = 'scheduledUpdate';
|
||||
const MIN_INTERVAL_MS = 60e3;
|
||||
const RETRY_ERRORS = [
|
||||
|
@ -96,13 +104,14 @@ const updateMan = (() => {
|
|||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||
*/
|
||||
async function checkStyle(opts) {
|
||||
let {id} = opts;
|
||||
const {
|
||||
id,
|
||||
style = await API.styles.get(id),
|
||||
ignoreDigest,
|
||||
port,
|
||||
save,
|
||||
} = opts;
|
||||
if (!id) id = style.id;
|
||||
const ucd = style.usercssData;
|
||||
let res, state;
|
||||
try {
|
||||
|
@ -119,7 +128,7 @@ const updateMan = (() => {
|
|||
res = {error, style, STATES};
|
||||
state = `${STATES.SKIPPED} (${error})`;
|
||||
}
|
||||
log(`${state} #${style.id} ${style.customName || style.name}`);
|
||||
log(`${state} #${id} ${style.customName || style.name}`);
|
||||
if (port) port.postMessage(res);
|
||||
return res;
|
||||
|
||||
|
@ -132,6 +141,11 @@ 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);
|
||||
if (!md5 || md5.length !== 32) {
|
||||
return Promise.reject(STATES.ERROR_MD5);
|
||||
|
@ -148,33 +162,82 @@ const updateMan = (() => {
|
|||
return json;
|
||||
}
|
||||
|
||||
async function updateUsercss() {
|
||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||
const url = style.updateUrl;
|
||||
const metaUrl = URLS.extractGreasyForkInstallUrl(url) &&
|
||||
url.replace(/\.user\.css$/, '.meta.css');
|
||||
const text = await tryDownload(metaUrl || url);
|
||||
const json = await API.usercss.buildMeta({sourceCode: text});
|
||||
await require(['/vendor/semver-bundle/semver']); /* global semverCompare */
|
||||
const delta = semverCompare(json.usercssData.version, ucd.version);
|
||||
if (!delta && !ignoreDigest) {
|
||||
// re-install is invalid in a soft upgrade
|
||||
const sameCode = !metaUrl && text === style.sourceCode;
|
||||
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
|
||||
async function updateToUSOArchive(url, req) {
|
||||
// UserCSS metadata may be embedded in the original USO style so let's use its updateURL
|
||||
const [meta2] = req.response.replace(RX_META, '').match(RX_META) || [];
|
||||
if (meta2 && meta2.includes('@updateURL')) {
|
||||
const {updateUrl} = await API.usercss.buildMeta({sourceCode: meta2}).catch(() => ({}));
|
||||
if (updateUrl) {
|
||||
url = updateUrl;
|
||||
req = await tryDownload(url, RH_ETAG);
|
||||
}
|
||||
}
|
||||
if (delta < 0) {
|
||||
// downgrade is always invalid
|
||||
return Promise.reject(STATES.ERROR_VERSION);
|
||||
}
|
||||
if (metaUrl) {
|
||||
json.sourceCode = await tryDownload(url);
|
||||
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() {
|
||||
if (style.etag && style.etag === await downloadEtag()) {
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
}
|
||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||
const {headers: {etag}, response} = await tryDownload(style.updateUrl, RH_ETAG);
|
||||
const json = await API.usercss.buildMeta({sourceCode: response, etag});
|
||||
await require(['/vendor/semver-bundle/semver']); /* global semverCompare */
|
||||
const delta = semverCompare(json.usercssData.version, ucd.version);
|
||||
let err;
|
||||
if (!delta && !ignoreDigest) {
|
||||
// re-install is invalid in a soft upgrade
|
||||
err = response === style.sourceCode ? STATES.SAME_CODE : STATES.SAME_VERSION;
|
||||
}
|
||||
if (delta < 0) {
|
||||
// downgrade is always invalid
|
||||
err = STATES.ERROR_VERSION;
|
||||
}
|
||||
if (err && etag && !style.etag) {
|
||||
// first check of ETAG, gonna write it directly to DB as it's too trivial to sync or announce
|
||||
style.etag = etag;
|
||||
await db.exec('put', style);
|
||||
}
|
||||
return err
|
||||
? Promise.reject(err)
|
||||
: API.usercss.buildCode(json);
|
||||
}
|
||||
|
||||
async function maybeSave(json) {
|
||||
json.id = style.id;
|
||||
json.updateDate = Date.now();
|
||||
json.id = id;
|
||||
json.updateDate = getDateFromVer(json) || Date.now();
|
||||
// keep current state
|
||||
delete json.customName;
|
||||
delete json.enabled;
|
||||
|
@ -206,6 +269,25 @@ const updateMan = (() => {
|
|||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadEtag() {
|
||||
const opts = Object.assign({method: 'head'}, RH_ETAG);
|
||||
const req = await tryDownload(style.updateUrl, opts);
|
||||
return req.headers.etag;
|
||||
}
|
||||
|
||||
function getDateFromVer(style) {
|
||||
const m = style.updateUrl.startsWith(URLS.usoArchiveRaw) &&
|
||||
style.usercssData.version.match(RX_DATE2VER);
|
||||
if (m) {
|
||||
m[2]--; // month is 0-based in `Date` constructor
|
||||
return new Date(...m.slice(1)).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
function getVarOptByName(varDef, name) {
|
||||
return varDef.options.find(o => o.name === name);
|
||||
}
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global URLS download openURL */// toolbox.js
|
||||
/* global RX_META URLS download openURL */// toolbox.js
|
||||
/* global addAPI bgReady */// common.js
|
||||
/* global tabMan */// msg.js
|
||||
'use strict';
|
||||
|
@ -85,7 +85,7 @@ bgReady.all.then(() => {
|
|||
!oldUrl.startsWith(URLS.installUsercss)) {
|
||||
const inTab = url.startsWith('file:') && !chrome.app;
|
||||
const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
|
||||
if (!/^\s*</.test(code) && URLS.rxMETA.test(code)) {
|
||||
if (!/^\s*</.test(code) && RX_META.test(code)) {
|
||||
openInstallerPage(tabId, url, {code, inTab});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global API */// msg.js
|
||||
/* global URLS deepCopy download */// toolbox.js
|
||||
/* global RX_META deepCopy download */// toolbox.js
|
||||
'use strict';
|
||||
|
||||
const usercssMan = {
|
||||
|
@ -15,7 +15,7 @@ const usercssMan = {
|
|||
async assignVars(style, oldStyle) {
|
||||
const meta = style.usercssData;
|
||||
const vars = meta.vars;
|
||||
const oldVars = oldStyle.usercssData.vars;
|
||||
const oldVars = (oldStyle.usercssData || {}).vars;
|
||||
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)) {
|
||||
|
@ -51,7 +51,7 @@ const usercssMan = {
|
|||
|
||||
async buildCode(style) {
|
||||
const {sourceCode: code, usercssData: {vars, preprocessor}} = style;
|
||||
const match = code.match(URLS.rxMETA);
|
||||
const match = code.match(RX_META);
|
||||
const i = match.index;
|
||||
const j = i + match[0].length;
|
||||
const codeNoMeta = code.slice(0, i) + blankOut(code, i, j) + code.slice(j);
|
||||
|
@ -74,7 +74,7 @@ const usercssMan = {
|
|||
enabled: true,
|
||||
sections: [],
|
||||
}, style);
|
||||
const match = code.match(URLS.rxMETA);
|
||||
const match = code.match(RX_META);
|
||||
if (!match) {
|
||||
return Promise.reject(new Error('Could not find metadata.'));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global $ $$ $create $remove messageBoxProxy */// dom.js
|
||||
/* global API */// msg.js
|
||||
/* global CodeMirror */
|
||||
/* global FIREFOX URLS debounce ignoreChromeError sessionStore */// toolbox.js
|
||||
/* global FIREFOX RX_META debounce ignoreChromeError sessionStore */// toolbox.js
|
||||
/* global MozDocMapper clipString helpPopup rerouteHotkeys showCodeMirrorPopup */// util.js
|
||||
/* global createSection */// sections-editor-section.js
|
||||
/* global editor */
|
||||
|
@ -360,7 +360,7 @@ function SectionsEditor() {
|
|||
lockPageUI(true);
|
||||
try {
|
||||
const code = popup.codebox.getValue().trim();
|
||||
if (!URLS.rxMETA.test(code) ||
|
||||
if (!RX_META.test(code) ||
|
||||
!await getPreprocessor(code) ||
|
||||
await messageBoxProxy.confirm(
|
||||
t('importPreprocessor'), 'pre-line',
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
/* global MozDocMapper */// util.js
|
||||
/* global MozSectionFinder */
|
||||
/* global MozSectionWidget */
|
||||
/* global URLS debounce sessionStore */// toolbox.js
|
||||
/* global RX_META debounce sessionStore */// toolbox.js
|
||||
/* global chromeSync */// storage-util.js
|
||||
/* global cmFactory */
|
||||
/* global editor */
|
||||
|
@ -307,7 +307,7 @@ function SourceEditor() {
|
|||
if (_cm !== cm) {
|
||||
return;
|
||||
}
|
||||
const match = text.match(URLS.rxMETA);
|
||||
const match = text.match(RX_META);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
/* exported
|
||||
CHROME_POPUP_BORDER_BUG
|
||||
RX_META
|
||||
capitalize
|
||||
closeCurrentTab
|
||||
deepEqual
|
||||
|
@ -71,8 +72,6 @@ const URLS = {
|
|||
// TODO: remove when "minimum_chrome_version": "61" or higher
|
||||
chromeProtectsNTP: CHROME >= 61,
|
||||
|
||||
rxMETA: /\/\*!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i,
|
||||
|
||||
uso: 'https://userstyles.org/',
|
||||
usoJson: 'https://userstyles.org/styles/chrome/',
|
||||
|
||||
|
@ -86,6 +85,7 @@ const URLS = {
|
|||
const id = URLS.extractUsoArchiveId(url);
|
||||
return id ? `${URLS.usoArchive}?style=${id}` : '';
|
||||
},
|
||||
makeUsoArchiveCodeUrl: id => `${URLS.usoArchiveRaw}usercss/${id}.user.css`,
|
||||
|
||||
extractGreasyForkInstallUrl: url =>
|
||||
/^(https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/\d+)[^/]*\/code\/[^/]*\.user\.css$|$/.exec(url)[1],
|
||||
|
@ -99,6 +99,8 @@ const URLS = {
|
|||
),
|
||||
};
|
||||
|
||||
const RX_META = /\/\*!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
|
||||
|
||||
if (FIREFOX || OPERA || VIVALDI) {
|
||||
document.documentElement.classList.add(
|
||||
FIREFOX && 'firefox' ||
|
||||
|
@ -358,10 +360,11 @@ const sessionStore = new Proxy({}, {
|
|||
* @param {Object} params
|
||||
* @param {String} [params.method]
|
||||
* @param {String|Object} [params.body]
|
||||
* @param {String} [params.responseType] arraybuffer, blob, document, json, text
|
||||
* @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [params.responseType]
|
||||
* @param {Number} [params.requiredStatusCode] resolved when matches, otherwise rejected
|
||||
* @param {Number} [params.timeout] ms
|
||||
* @param {Object} [params.headers] {name: value}
|
||||
* @param {string[]} [params.responseHeaders]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function download(url, {
|
||||
|
@ -372,6 +375,7 @@ function download(url, {
|
|||
timeout = 60e3, // connection timeout, USO is that bad
|
||||
loadTimeout = 2 * 60e3, // data transfer timeout (counted from the first remote response)
|
||||
headers,
|
||||
responseHeaders,
|
||||
} = {}) {
|
||||
/* USO can't handle POST requests for style json and XHR/fetch can't handle super long URL
|
||||
* so we need to collapse all long variables and expand them in the response */
|
||||
|
@ -404,10 +408,20 @@ function download(url, {
|
|||
timer = loadTimeout && setTimeout(onTimeout, loadTimeout);
|
||||
}
|
||||
};
|
||||
xhr.onload = () =>
|
||||
xhr.status === requiredStatusCode || !requiredStatusCode || u.protocol === 'file:'
|
||||
? resolve(expandUsoVars(xhr.response))
|
||||
: reject(xhr.status);
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === requiredStatusCode || !requiredStatusCode || u.protocol === 'file:') {
|
||||
const response = expandUsoVars(xhr.response);
|
||||
if (responseHeaders) {
|
||||
const headers = {};
|
||||
for (const h of responseHeaders) headers[h] = xhr.getResponseHeader(h);
|
||||
resolve({headers, response});
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
} else {
|
||||
reject(xhr.status);
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(xhr.status);
|
||||
xhr.onloadend = () => clearTimeout(timer);
|
||||
xhr.responseType = responseType;
|
||||
|
|
|
@ -140,7 +140,12 @@ function simplifyUsercssVars(vars) {
|
|||
case 'dropdown':
|
||||
case 'image':
|
||||
// TODO: handle customized image
|
||||
value = va.options.find(o => o.name === value).value;
|
||||
for (const opt of va.options) {
|
||||
if (opt.name === value) {
|
||||
value = opt.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
case 'range':
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global API */// msg.js
|
||||
/* global URLS deepEqual isEmptyObj tryJSONparse */// toolbox.js
|
||||
/* global RX_META deepEqual isEmptyObj tryJSONparse */// toolbox.js
|
||||
/* global changeQueue */// manage.js
|
||||
/* global chromeSync */// storage-util.js
|
||||
/* global prefs */
|
||||
|
@ -83,7 +83,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
fReader.onloadend = event => {
|
||||
fileInput.remove();
|
||||
const text = event.target.result;
|
||||
const maybeUsercss = !/^\s*\[/.test(text) && URLS.rxMETA.test(text);
|
||||
const maybeUsercss = !/^\s*\[/.test(text) && RX_META.test(text);
|
||||
if (maybeUsercss) {
|
||||
messageBoxProxy.alert(t('dragDropUsercssTabstrip'));
|
||||
} else {
|
||||
|
|
|
@ -409,7 +409,7 @@
|
|||
result.pingbackTimer = setTimeout(download, PINGBACK_DELAY,
|
||||
`${URLS.uso}styles/install/${id}?source=stylish-ch`);
|
||||
|
||||
const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`;
|
||||
const updateUrl = URLS.makeUsoArchiveCodeUrl(id);
|
||||
try {
|
||||
const sourceCode = await download(updateUrl);
|
||||
const style = await API.usercss.install({sourceCode, updateUrl});
|
||||
|
|
Loading…
Reference in New Issue
Block a user