updater: add 'ignoreDigest' to force-update on manage page

* saveStyle: retain only known properties in sections[] and normalize their order
* remove styleDigest on import
* shorten detailed status names in updater
* don't autohide update status message
This commit is contained in:
tophf 2017-04-24 16:29:48 +03:00
parent 32ae088c03
commit 7677f0dece
7 changed files with 158 additions and 101 deletions

View File

@ -323,7 +323,7 @@
"description": "Checkbox to show only locally edited styles"
},
"manageOnlyUpdates": {
"message": "Only with updates",
"message": "Only with updates or problems",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
},
"manageNewUI": {
@ -592,7 +592,7 @@
"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)",
"message": "To force an update (and lose your edits) update each style individually.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"updateCheckSucceededNoUpdate": {
@ -600,7 +600,11 @@
"description": "Text that displays when an update check completed and no update is available"
},
"updateAllCheckSucceededNoUpdate": {
"message": "All styles are up to date.",
"message": "No updates found.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"updateAllCheckSucceededSomeEdited": {
"message": "Some updatable styles weren't checked to avoid losing possible local edits.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"updateCompleted": {

View File

@ -294,7 +294,7 @@ summary {
cursor: pointer;
}
.update-problem .check-update svg {
.newUI .update-problem .check-update svg {
fill: #ef6969;
}
@ -500,6 +500,10 @@ input[id^="manage.newUI"] {
opacity: .35;
}
#update-all-no-updates[data-skipped-edited="true"]:after {
content: " __MSG_updateAllCheckSucceededSomeEdited__ __MSG_updateCheckManualUpdateHint__";
}
/* highlight updated/added styles */
.highlight {
animation: highlight 10s cubic-bezier(0,.82,.47,.98);

View File

@ -135,18 +135,26 @@
<fieldset>
<legend id="filters" i18n-text="manageFilters"></legend>
<label>
<input id="manage.onlyEnabled" type="checkbox" data-filter=".disabled">
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<span i18n-text="manageOnlyEnabled"></span>
</label>
<label>
<input id="manage.onlyEdited" type="checkbox" data-filter=".updatable">
<input id="manage.onlyEdited" type="checkbox"
data-filter=":not(.updatable)"
data-filter-hide=".updatable">
<span i18n-text="manageOnlyEdited"></span>
</label>
<label id="onlyUpdates" class="hidden">
<input type="checkbox" data-filter=":not(.can-update)">
<input type="checkbox"
data-filter=".can-update, .update-problem"
data-filter-hide=":not(.updatable), .update-done, .no-update:not(.update-problem)">
<span i18n-text="manageOnlyUpdates"></span>
</label>
<input id="search" type="search" i18n-placeholder="searchStyles" data-filter=".not-matching">
<input id="search" type="search" i18n-placeholder="searchStyles"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
</fieldset>
<p>
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>

View File

@ -365,10 +365,15 @@ Object.assign(handleEvent, {
el.lastValue = value;
}
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
const buildFilter = hide =>
[...enabledFilters.map(el =>
el.dataset[hide ? 'filterHide' : 'filter']
.split(/,\s*/)
.map(s => '.entry' + (hide ? '' : '.hidden') + s))
].join(',');
Object.assign(filtersSelector, {
hide: enabledFilters.map(el => '.entry:not(.hidden)' + el.dataset.filter).join(','),
unhide: '.entry.hidden' + enabledFilters.map(el =>
(':not(' + el.dataset.filter + ')').replace(/^:not\(:not\((.+?)\)\)$/, '$1')).join(''),
hide: buildFilter(true),
unhide: buildFilter(false),
});
reapplyFilter();
},
@ -505,12 +510,13 @@ function checkUpdateAll() {
let total = 0;
let checked = 0;
let skippedEdited = 0;
let updated = 0;
$$('.updatable:not(.can-update)').map(el => checkUpdate(el, {single: false}));
BG.updater.checkAllStyles(observe, {save: false}).then(done);
$$('.updatable:not(.can-update):not(.update-problem)').map(el => checkUpdate(el, {single: false}));
BG.updater.checkAllStyles({observer, save: false});
function observe(state, value, details) {
function observer(state, value, details) {
switch (state) {
case BG.updater.COUNT:
total = value;
@ -524,37 +530,41 @@ function checkUpdateAll() {
// fallthrough
case BG.updater.SKIPPED:
checked++;
if (details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED) {
skippedEdited++;
}
reportUpdateState(state, value, details);
break;
case BG.updater.DONE:
$('#check-all-updates').disabled = false;
$('#apply-all-updates').disabled = false;
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
if (!updated) {
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0;
$('#update-all-no-updates').classList.remove('hidden');
}
return;
}
const progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth;
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
}
function done() {
$('#check-all-updates').disabled = false;
$('#apply-all-updates').disabled = false;
renderUpdatesOnlyFilter({check: updated > 0});
if (!updated) {
$('#update-all-no-updates').classList.remove('hidden');
setTimeout(() => {
$('#update-all-no-updates').classList.add('hidden');
}, 10e3);
}
}
}
function checkUpdate(element, {single = true} = {}) {
$('.update-note', element).textContent = t('checkingForUpdate');
$('.check-update', element).title = '';
element.classList.remove('checking-update', 'no-update', 'update-problem');
element.classList.add('checking-update');
function checkUpdate(entry, {single = true} = {}) {
$('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = '';
if (single) {
const style = BG.cachedStyles.byId.get(element.styleId);
BG.updater.checkStyle(style, reportUpdateState, {save: false});
BG.updater.checkStyle({
save: false,
ignoreDigest: entry.classList.contains('update-problem'),
style: BG.cachedStyles.byId.get(entry.styleId),
observer: reportUpdateState,
});
}
entry.classList.remove('checking-update', 'no-update', 'update-problem');
entry.classList.add('checking-update');
}
@ -569,18 +579,19 @@ function reportUpdateState(state, style, details) {
$('#onlyUpdates').classList.remove('hidden');
break;
case BG.updater.SKIPPED: {
if (entry.classList.contains('can-update')) {
break;
}
if (!details) {
details = t('updateCheckFailServerUnreachable');
} else if (typeof details == 'number') {
details = t('updateCheckFailBadResponseCode', [details]);
} else if (details == BG.updater.SKIPPED_EDITED) {
} else if (details == BG.updater.EDITED) {
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} else if (details == BG.updater.SKIPPED_MAYBE_EDITED) {
} else if (details == BG.updater.MAYBE_EDITED) {
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
}
const same =
details == BG.updater.SKIPPED_SAME_MD5 ||
details == BG.updater.SKIPPED_SAME_CODE;
const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE;
const message = same ? t('updateCheckSucceededNoUpdate') : details;
entry.classList.add('no-update');
entry.classList.toggle('update-problem', !same);
@ -588,7 +599,7 @@ function reportUpdateState(state, style, details) {
$('.check-update', entry).title = newUI.enabled ? message : '';
if (!$('#check-all-updates').disabled) {
// this is a single update job so we can decide whether to hide the filter
$('#onlyUpdates').classList.toggle('hidden', !$('.can-update'));
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
}
}
}
@ -600,10 +611,10 @@ function reportUpdateState(state, style, details) {
function renderUpdatesOnlyFilter({show, check} = {}) {
const numUpdatable = $$('.can-update').length;
const canUpdate = numUpdatable > 0;
const mightUpdate = numUpdatable > 0 || $('.update-problem');
const checkbox = $('#onlyUpdates input');
show = show !== undefined ? show : canUpdate;
check = check !== undefined ? show && check : checkbox.checked && canUpdate;
show = show !== undefined ? show : mightUpdate;
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
$('#onlyUpdates').classList.toggle('hidden', !show);
checkbox.checked = check;
@ -611,7 +622,7 @@ function renderUpdatesOnlyFilter({show, check} = {}) {
const btnApply = $('#apply-all-updates');
if (!btnApply.matches('.hidden')) {
if (canUpdate) {
if (numUpdatable > 0) {
btnApply.dataset.value = numUpdatable;
} else {
btnApply.classList.add('hidden');

View File

@ -41,16 +41,14 @@ function checkUpdates() {
let total = 0;
let checked = 0;
let updated = 0;
const installed = $('#updates-installed');
const progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth;
progress.style.width = 0;
installed.dataset.value = '';
document.body.classList.add('update-in-progress');
BG.updater.checkAllStyles((state, value) => {
const maxWidth = $('#update-progress').parentElement.clientWidth;
BG.updater.checkAllStyles({observer});
function observer(state, value) {
switch (state) {
case BG.updater.COUNT:
total = value;
document.body.classList.add('update-in-progress');
break;
case BG.updater.UPDATED:
updated++;
@ -58,10 +56,11 @@ function checkUpdates() {
case BG.updater.SKIPPED:
checked++;
break;
}
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
installed.dataset.value = updated || '';
}).then(() => {
case BG.updater.DONE:
document.body.classList.remove('update-in-progress');
});
return;
}
$('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
$('#updates-installed').dataset.value = updated || '';
}
}

View File

@ -218,7 +218,7 @@ function saveStyle(style) {
const tx = db.transaction(['styles'], 'readwrite');
const os = tx.objectStore('styles');
const id = style.id !== undefined && style.id !== null ? Number(style.id) : null;
const id = style.id == '0' ? 0 : Number(style.id) || null;
const reason = style.reason;
const notify = style.notify !== false;
delete style.method;
@ -227,15 +227,16 @@ function saveStyle(style) {
if (!style.name) {
delete style.name;
}
let existed, codeIsUpdated;
if (id !== null) {
// Update or create
style.id = id;
os.get(id).onsuccess = eventGet => {
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), {reason, existed, codeIsUpdated});
const oldStyle = eventGet.target.result;
existed = Boolean(oldStyle);
codeIsUpdated = !existed || style.sections && !styleSectionsEqual(style, oldStyle);
write(Object.assign({}, oldStyle, style));
};
} else {
// Create
@ -247,18 +248,11 @@ function saveStyle(style) {
md5Url: null,
url: null,
originalMd5: null,
}, style), {reason});
}, style));
}
function write(style, {reason, existed, codeIsUpdated} = {}) {
style.sections = (style.sections || []).map(section =>
Object.assign({
urls: [],
urlPrefixes: [],
domains: [],
regexps: [],
}, section)
);
function write(style) {
style.sections = normalizeStyleSections(style);
os.put(style).onsuccess = event => {
style.id = style.id || event.target.result;
invalidateCache(existed ? {updated: style} : {added: style});
@ -271,6 +265,8 @@ function saveStyle(style) {
}
if (reason == 'update') {
updateStyleDigest(style);
} else if (reason == 'import') {
chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id, ignoreChromeError);
}
resolve(style);
};
@ -308,10 +304,14 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
for (const section of style.sections) {
const {urls, domains, urlPrefixes, regexps, code} = section;
if ((!urls.length && !urlPrefixes.length && !domains.length && !regexps.length
|| urls.length && urls.indexOf(matchUrl) >= 0
|| urlPrefixes.length && arraySomeIsPrefix(urlPrefixes, matchUrl)
|| domains.length && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|| regexps.length && arraySomeMatches(regexps, matchUrl, strictRegexp)
|| urls.length
&& urls.indexOf(matchUrl) >= 0
|| urlPrefixes.length
&& arraySomeIsPrefix(urlPrefixes, matchUrl)
|| domains.length
&& arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|| regexps.length
&& arraySomeMatches(regexps, matchUrl, strictRegexp)
) && !styleCodeEmpty(code)) {
sections.push(section);
if (stopOnFirst) {
@ -534,6 +534,18 @@ function getDomains(url) {
}
function normalizeStyleSections({sections}) {
// retain known properties in an arbitrarily predefined order
return (sections || []).map(section => ({
code: section.code || '',
urls: section.urls || [],
urlPrefixes: section.urlPrefixes || [],
domains: section.domains || [],
regexps: section.regexps || [],
}));
}
function getStyleDigests(style) {
return Promise.all([
chromeLocal.getValue(DIGEST_KEY_PREFIX + style.id),
@ -548,9 +560,11 @@ function updateStyleDigest(style) {
}
function calcStyleDigest({sections}) {
const text = new TextEncoder('utf-8').encode(JSON.stringify(sections));
function calcStyleDigest(style) {
const jsonString = JSON.stringify(normalizeStyleSections(style));
const text = new TextEncoder('utf-8').encode(jsonString);
return crypto.subtle.digest('SHA-1', text).then(hex);
function hex(buffer) {
const parts = [];
const PAD8 = '00000000';

View File

@ -1,4 +1,5 @@
/* globals getStyles, saveStyle, styleSectionsEqual, getStyleDigests, updateStyleDigest */
/* global getStyles, saveStyle, styleSectionsEqual */
/* global getStyleDigests, updateStyleDigest */
'use strict';
// eslint-disable-next-line no-var
@ -7,55 +8,71 @@ 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',
SKIPPED_ERROR_JSON: 'error: JSON is invalid',
DONE: 'done',
// details for SKIPPED status
EDITED: 'locally edited',
MAYBE_EDITED: 'maybe locally edited',
SAME_MD5: 'up-to-date: MD5 is unchanged',
SAME_CODE: 'up-to-date: code sections are unchanged',
ERROR_MD5: 'error: MD5 is invalid',
ERROR_JSON: 'error: JSON is invalid',
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
checkAllStyles(observe = () => {}, {save = true} = {}) {
checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
updater.resetInterval();
return new Promise(resolve => {
getStyles({}, styles => {
styles = styles.filter(style => style.updateUrl);
observe(updater.COUNT, styles.length);
observer(updater.COUNT, styles.length);
Promise.all(styles.map(style =>
updater.checkStyle(style, observe, {save})
updater.checkStyle({style, observer, save, ignoreDigest})
)).then(() => {
observe(updater.DONE);
observer(updater.DONE);
resolve();
});
});
});
},
checkStyle(style, observe = () => {}, {save = true} = {}) {
checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
let hasDigest;
/*
Original style digests are calculated in these cases:
* style is installed or updated from server
* style is checked for an update and its code is equal to the server code
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
* [ignoreDigest: none/false] style doesn't yet have the original digest
so we compare the code to the server code and if it's the same we save the digest,
otherwise we skip the style and report MAYBE_EDITED status
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
return getStyleDigests(style)
.then(fetchMd5IfNotEdited)
.then(fetchCodeIfMd5Changed)
.then(saveIfUpdated)
.then(saved => observe(updater.UPDATED, saved))
.catch(err => observe(updater.SKIPPED, style, err));
.then(saved => observer(updater.UPDATED, saved))
.catch(err => observer(updater.SKIPPED, style, err));
function fetchMd5IfNotEdited([originalDigest, current]) {
hasDigest = Boolean(originalDigest);
if (hasDigest && originalDigest != current) {
return Promise.reject(updater.SKIPPED_EDITED);
if (hasDigest && !ignoreDigest && originalDigest != current) {
return Promise.reject(updater.EDITED);
}
return download(style.md5Url);
}
function fetchCodeIfMd5Changed(md5) {
if (!md5 || md5.length != 32) {
return Promise.reject(updater.SKIPPED_ERROR_MD5);
return Promise.reject(updater.ERROR_MD5);
}
if (md5 == style.originalMd5 && hasDigest) {
return Promise.reject(updater.SKIPPED_SAME_MD5);
return Promise.reject(updater.SAME_MD5);
}
return download(style.updateUrl);
}
@ -63,16 +80,16 @@ var updater = {
function saveIfUpdated(text) {
const json = tryJSONparse(text);
if (!styleJSONseemsValid(json)) {
return Promise.reject(updater.SKIPPED_ERROR_JSON);
return Promise.reject(updater.ERROR_JSON);
}
json.id = style.id;
if (styleSectionsEqual(json, style)) {
if (!hasDigest) {
// JSONs may have different order of items even if sections are effectively equal
// so we'll update the digest anyway
updateStyleDigest(json);
}
return Promise.reject(updater.SKIPPED_SAME_CODE);
} else if (!hasDigest) {
return Promise.reject(updater.SKIPPED_MAYBE_EDITED);
return Promise.reject(updater.SAME_CODE);
} else if (!hasDigest && !ignoreDigest) {
return Promise.reject(updater.MAYBE_EDITED);
}
return !save ? json :
saveStyle(Object.assign(json, {