Detect and don't update locally edited styles
This commit is contained in:
parent
36667dece1
commit
32ae088c03
|
@ -583,6 +583,18 @@
|
|||
"message": "Update failed - server unreachable.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSkippedLocallyEdited": {
|
||||
"message": "This style was edited locally.",
|
||||
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckSkippedMaybeLocallyEdited": {
|
||||
"message": "This style might have been edited locally.",
|
||||
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
|
||||
},
|
||||
"updateCheckManualUpdateHint": {
|
||||
"message": "Do a one-time manual update on its userstyles.org page (your edits will be lost)",
|
||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Style is up to date.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
|
|
|
@ -104,7 +104,10 @@ function saveStyleCode(message, name, addProps) {
|
|||
}
|
||||
getResource(getMeta('stylish-code-chrome')).then(code => {
|
||||
chrome.runtime.sendMessage(
|
||||
Object.assign(JSON.parse(code), addProps, {method: 'saveStyle'}),
|
||||
Object.assign(JSON.parse(code), addProps, {
|
||||
method: 'saveStyle',
|
||||
reason: 'update',
|
||||
}),
|
||||
() => sendEvent('styleInstalledChrome')
|
||||
);
|
||||
resolve();
|
||||
|
|
|
@ -573,6 +573,10 @@ function reportUpdateState(state, style, details) {
|
|||
details = t('updateCheckFailServerUnreachable');
|
||||
} else if (typeof details == 'number') {
|
||||
details = t('updateCheckFailBadResponseCode', [details]);
|
||||
} else if (details == BG.updater.SKIPPED_EDITED) {
|
||||
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
} else if (details == BG.updater.SKIPPED_MAYBE_EDITED) {
|
||||
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
}
|
||||
const same =
|
||||
details == BG.updater.SKIPPED_SAME_MD5 ||
|
||||
|
|
60
storage.js
60
storage.js
|
@ -5,6 +5,7 @@ const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
|||
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
|
||||
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
|
||||
const SLOPPY_REGEXP_PREFIX = '\0';
|
||||
const DIGEST_KEY_PREFIX = 'originalDigest';
|
||||
|
||||
// Note, only 'var'-declared variables are visible from another extension page
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -21,6 +22,26 @@ var cachedStyles = {
|
|||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var chromeLocal = {
|
||||
get(options) {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.local.get(options, data => resolve(data));
|
||||
});
|
||||
},
|
||||
set(data) {
|
||||
return new Promise(resolve => {
|
||||
chrome.storage.local.set(data, () => resolve(data));
|
||||
});
|
||||
},
|
||||
getValue(key) {
|
||||
return chromeLocal.get(key).then(data => data[key]);
|
||||
},
|
||||
setValue(key, value) {
|
||||
return chromeLocal.set({[key]: value});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function getDatabase(ready, error) {
|
||||
const dbOpenRequest = window.indexedDB.open('stylish', 2);
|
||||
|
@ -214,7 +235,7 @@ function saveStyle(style) {
|
|||
const existed = Boolean(eventGet.target.result);
|
||||
const oldStyle = Object.assign({}, eventGet.target.result);
|
||||
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||
write(Object.assign(oldStyle, style), {existed, codeIsUpdated});
|
||||
write(Object.assign(oldStyle, style), {reason, existed, codeIsUpdated});
|
||||
};
|
||||
} else {
|
||||
// Create
|
||||
|
@ -226,10 +247,10 @@ function saveStyle(style) {
|
|||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
}, style));
|
||||
}, style), {reason});
|
||||
}
|
||||
|
||||
function write(style, {existed, codeIsUpdated} = {}) {
|
||||
function write(style, {reason, existed, codeIsUpdated} = {}) {
|
||||
style.sections = (style.sections || []).map(section =>
|
||||
Object.assign({
|
||||
urls: [],
|
||||
|
@ -248,6 +269,9 @@ function saveStyle(style) {
|
|||
style, codeIsUpdated, reason,
|
||||
});
|
||||
}
|
||||
if (reason == 'update') {
|
||||
updateStyleDigest(style);
|
||||
}
|
||||
resolve(style);
|
||||
};
|
||||
}
|
||||
|
@ -257,6 +281,7 @@ function saveStyle(style) {
|
|||
|
||||
|
||||
function deleteStyle({id, notify = true}) {
|
||||
chrome.storage.local.remove(DIGEST_KEY_PREFIX + id, ignoreChromeError);
|
||||
return new Promise(resolve =>
|
||||
getDatabase(db => {
|
||||
const tx = db.transaction(['styles'], 'readwrite');
|
||||
|
@ -507,3 +532,32 @@ function getDomains(url) {
|
|||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
|
||||
function getStyleDigests(style) {
|
||||
return Promise.all([
|
||||
chromeLocal.getValue(DIGEST_KEY_PREFIX + style.id),
|
||||
calcStyleDigest(style),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
function updateStyleDigest(style) {
|
||||
calcStyleDigest(style).then(digest =>
|
||||
chromeLocal.set({[DIGEST_KEY_PREFIX + style.id]: digest}));
|
||||
}
|
||||
|
||||
|
||||
function calcStyleDigest({sections}) {
|
||||
const text = new TextEncoder('utf-8').encode(JSON.stringify(sections));
|
||||
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||
function hex(buffer) {
|
||||
const parts = [];
|
||||
const PAD8 = '00000000';
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < view.byteLength; i += 4) {
|
||||
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
}
|
||||
|
|
69
update.js
69
update.js
|
@ -1,4 +1,4 @@
|
|||
/* globals getStyles, saveStyle, styleSectionsEqual */
|
||||
/* globals getStyles, saveStyle, styleSectionsEqual, getStyleDigests, updateStyleDigest */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -7,6 +7,8 @@ var updater = {
|
|||
COUNT: 'count',
|
||||
UPDATED: 'updated',
|
||||
SKIPPED: 'skipped',
|
||||
SKIPPED_EDITED: 'locally edited',
|
||||
SKIPPED_MAYBE_EDITED: 'maybe locally edited',
|
||||
SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||
SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||
SKIPPED_ERROR_MD5: 'error: MD5 is invalid',
|
||||
|
@ -32,33 +34,60 @@ var updater = {
|
|||
},
|
||||
|
||||
checkStyle(style, observe = () => {}, {save = true} = {}) {
|
||||
return download(style.md5Url)
|
||||
.then(md5 =>
|
||||
!md5 || md5.length != 32 ? Promise.reject(updater.SKIPPED_ERROR_MD5) :
|
||||
md5 == style.originalMd5 ? Promise.reject(updater.SKIPPED_SAME_MD5) :
|
||||
style.updateUrl)
|
||||
.then(download)
|
||||
.then(text => tryJSONparse(text))
|
||||
.then(json =>
|
||||
!updater.styleJSONseemsValid(json) ? Promise.reject(updater.SKIPPED_ERROR_JSON) :
|
||||
styleSectionsEqual(json, style) ? Promise.reject(updater.SKIPPED_SAME_CODE) :
|
||||
// keep the local name as it could've been customized by the user
|
||||
Object.assign(json, {
|
||||
id: style.id,
|
||||
name: null,
|
||||
reason: 'update',
|
||||
}))
|
||||
.then(json => save ? saveStyle(json) : json)
|
||||
let hasDigest;
|
||||
return getStyleDigests(style)
|
||||
.then(fetchMd5IfNotEdited)
|
||||
.then(fetchCodeIfMd5Changed)
|
||||
.then(saveIfUpdated)
|
||||
.then(saved => observe(updater.UPDATED, saved))
|
||||
.catch(err => observe(updater.SKIPPED, style, err));
|
||||
},
|
||||
|
||||
styleJSONseemsValid(json) {
|
||||
function fetchMd5IfNotEdited([originalDigest, current]) {
|
||||
hasDigest = Boolean(originalDigest);
|
||||
if (hasDigest && originalDigest != current) {
|
||||
return Promise.reject(updater.SKIPPED_EDITED);
|
||||
}
|
||||
return download(style.md5Url);
|
||||
}
|
||||
|
||||
function fetchCodeIfMd5Changed(md5) {
|
||||
if (!md5 || md5.length != 32) {
|
||||
return Promise.reject(updater.SKIPPED_ERROR_MD5);
|
||||
}
|
||||
if (md5 == style.originalMd5 && hasDigest) {
|
||||
return Promise.reject(updater.SKIPPED_SAME_MD5);
|
||||
}
|
||||
return download(style.updateUrl);
|
||||
}
|
||||
|
||||
function saveIfUpdated(text) {
|
||||
const json = tryJSONparse(text);
|
||||
if (!styleJSONseemsValid(json)) {
|
||||
return Promise.reject(updater.SKIPPED_ERROR_JSON);
|
||||
}
|
||||
json.id = style.id;
|
||||
if (styleSectionsEqual(json, style)) {
|
||||
if (!hasDigest) {
|
||||
updateStyleDigest(json);
|
||||
}
|
||||
return Promise.reject(updater.SKIPPED_SAME_CODE);
|
||||
} else if (!hasDigest) {
|
||||
return Promise.reject(updater.SKIPPED_MAYBE_EDITED);
|
||||
}
|
||||
return !save ? json :
|
||||
saveStyle(Object.assign(json, {
|
||||
name: null, // keep local name customizations
|
||||
reason: 'update',
|
||||
}));
|
||||
}
|
||||
|
||||
function styleJSONseemsValid(json) {
|
||||
return json
|
||||
&& json.sections
|
||||
&& json.sections.length
|
||||
&& typeof json.sections.every == 'function'
|
||||
&& typeof json.sections[0].code == 'string';
|
||||
}
|
||||
},
|
||||
|
||||
schedule() {
|
||||
|
|
Loading…
Reference in New Issue
Block a user