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.",
|
"message": "Update failed - server unreachable.",
|
||||||
"description": "Text that displays when an update check failed because the update server is 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": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "Style is up to date.",
|
"message": "Style is up to date.",
|
||||||
"description": "Text that displays when an update check completed and no update is available"
|
"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 => {
|
getResource(getMeta('stylish-code-chrome')).then(code => {
|
||||||
chrome.runtime.sendMessage(
|
chrome.runtime.sendMessage(
|
||||||
Object.assign(JSON.parse(code), addProps, {method: 'saveStyle'}),
|
Object.assign(JSON.parse(code), addProps, {
|
||||||
|
method: 'saveStyle',
|
||||||
|
reason: 'update',
|
||||||
|
}),
|
||||||
() => sendEvent('styleInstalledChrome')
|
() => sendEvent('styleInstalledChrome')
|
||||||
);
|
);
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
@ -573,6 +573,10 @@ function reportUpdateState(state, style, details) {
|
||||||
details = t('updateCheckFailServerUnreachable');
|
details = t('updateCheckFailServerUnreachable');
|
||||||
} else if (typeof details == 'number') {
|
} else if (typeof details == 'number') {
|
||||||
details = t('updateCheckFailBadResponseCode', [details]);
|
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 =
|
const same =
|
||||||
details == BG.updater.SKIPPED_SAME_MD5 ||
|
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');
|
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
|
||||||
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
|
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
|
||||||
const SLOPPY_REGEXP_PREFIX = '\0';
|
const SLOPPY_REGEXP_PREFIX = '\0';
|
||||||
|
const DIGEST_KEY_PREFIX = 'originalDigest';
|
||||||
|
|
||||||
// Note, only 'var'-declared variables are visible from another extension page
|
// Note, only 'var'-declared variables are visible from another extension page
|
||||||
// eslint-disable-next-line no-var
|
// 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) {
|
function getDatabase(ready, error) {
|
||||||
const dbOpenRequest = window.indexedDB.open('stylish', 2);
|
const dbOpenRequest = window.indexedDB.open('stylish', 2);
|
||||||
|
@ -214,7 +235,7 @@ function saveStyle(style) {
|
||||||
const existed = Boolean(eventGet.target.result);
|
const existed = Boolean(eventGet.target.result);
|
||||||
const oldStyle = Object.assign({}, eventGet.target.result);
|
const oldStyle = Object.assign({}, eventGet.target.result);
|
||||||
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||||
write(Object.assign(oldStyle, style), {existed, codeIsUpdated});
|
write(Object.assign(oldStyle, style), {reason, existed, codeIsUpdated});
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Create
|
// Create
|
||||||
|
@ -226,10 +247,10 @@ function saveStyle(style) {
|
||||||
md5Url: null,
|
md5Url: null,
|
||||||
url: null,
|
url: null,
|
||||||
originalMd5: null,
|
originalMd5: null,
|
||||||
}, style));
|
}, style), {reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
function write(style, {existed, codeIsUpdated} = {}) {
|
function write(style, {reason, existed, codeIsUpdated} = {}) {
|
||||||
style.sections = (style.sections || []).map(section =>
|
style.sections = (style.sections || []).map(section =>
|
||||||
Object.assign({
|
Object.assign({
|
||||||
urls: [],
|
urls: [],
|
||||||
|
@ -248,6 +269,9 @@ function saveStyle(style) {
|
||||||
style, codeIsUpdated, reason,
|
style, codeIsUpdated, reason,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (reason == 'update') {
|
||||||
|
updateStyleDigest(style);
|
||||||
|
}
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -257,6 +281,7 @@ function saveStyle(style) {
|
||||||
|
|
||||||
|
|
||||||
function deleteStyle({id, notify = true}) {
|
function deleteStyle({id, notify = true}) {
|
||||||
|
chrome.storage.local.remove(DIGEST_KEY_PREFIX + id, ignoreChromeError);
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
getDatabase(db => {
|
getDatabase(db => {
|
||||||
const tx = db.transaction(['styles'], 'readwrite');
|
const tx = db.transaction(['styles'], 'readwrite');
|
||||||
|
@ -507,3 +532,32 @@ function getDomains(url) {
|
||||||
}
|
}
|
||||||
return domains;
|
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('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
79
update.js
79
update.js
|
@ -1,4 +1,4 @@
|
||||||
/* globals getStyles, saveStyle, styleSectionsEqual */
|
/* globals getStyles, saveStyle, styleSectionsEqual, getStyleDigests, updateStyleDigest */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -7,6 +7,8 @@ var updater = {
|
||||||
COUNT: 'count',
|
COUNT: 'count',
|
||||||
UPDATED: 'updated',
|
UPDATED: 'updated',
|
||||||
SKIPPED: 'skipped',
|
SKIPPED: 'skipped',
|
||||||
|
SKIPPED_EDITED: 'locally edited',
|
||||||
|
SKIPPED_MAYBE_EDITED: 'maybe locally edited',
|
||||||
SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged',
|
SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||||
SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged',
|
SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||||
SKIPPED_ERROR_MD5: 'error: MD5 is invalid',
|
SKIPPED_ERROR_MD5: 'error: MD5 is invalid',
|
||||||
|
@ -32,33 +34,60 @@ var updater = {
|
||||||
},
|
},
|
||||||
|
|
||||||
checkStyle(style, observe = () => {}, {save = true} = {}) {
|
checkStyle(style, observe = () => {}, {save = true} = {}) {
|
||||||
return download(style.md5Url)
|
let hasDigest;
|
||||||
.then(md5 =>
|
return getStyleDigests(style)
|
||||||
!md5 || md5.length != 32 ? Promise.reject(updater.SKIPPED_ERROR_MD5) :
|
.then(fetchMd5IfNotEdited)
|
||||||
md5 == style.originalMd5 ? Promise.reject(updater.SKIPPED_SAME_MD5) :
|
.then(fetchCodeIfMd5Changed)
|
||||||
style.updateUrl)
|
.then(saveIfUpdated)
|
||||||
.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)
|
|
||||||
.then(saved => observe(updater.UPDATED, saved))
|
.then(saved => observe(updater.UPDATED, saved))
|
||||||
.catch(err => observe(updater.SKIPPED, style, err));
|
.catch(err => observe(updater.SKIPPED, style, err));
|
||||||
},
|
|
||||||
|
|
||||||
styleJSONseemsValid(json) {
|
function fetchMd5IfNotEdited([originalDigest, current]) {
|
||||||
return json
|
hasDigest = Boolean(originalDigest);
|
||||||
&& json.sections
|
if (hasDigest && originalDigest != current) {
|
||||||
&& json.sections.length
|
return Promise.reject(updater.SKIPPED_EDITED);
|
||||||
&& typeof json.sections.every == 'function'
|
}
|
||||||
&& typeof json.sections[0].code == 'string';
|
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() {
|
schedule() {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user