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" "description": "Checkbox to show only locally edited styles"
}, },
"manageOnlyUpdates": { "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" "description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
}, },
"manageNewUI": { "manageNewUI": {
@ -592,7 +592,7 @@
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications" "description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
}, },
"updateCheckManualUpdateHint": { "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" "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
}, },
"updateCheckSucceededNoUpdate": { "updateCheckSucceededNoUpdate": {
@ -600,7 +600,11 @@
"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"
}, },
"updateAllCheckSucceededNoUpdate": { "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" "description": "Text that displays when an update all check completed and no updates are available"
}, },
"updateCompleted": { "updateCompleted": {

View File

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

View File

@ -135,18 +135,26 @@
<fieldset> <fieldset>
<legend id="filters" i18n-text="manageFilters"></legend> <legend id="filters" i18n-text="manageFilters"></legend>
<label> <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> <span i18n-text="manageOnlyEnabled"></span>
</label> </label>
<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> <span i18n-text="manageOnlyEdited"></span>
</label> </label>
<label id="onlyUpdates" class="hidden"> <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> <span i18n-text="manageOnlyUpdates"></span>
</label> </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> </fieldset>
<p> <p>
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button> <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; el.lastValue = value;
} }
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el)); 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, { Object.assign(filtersSelector, {
hide: enabledFilters.map(el => '.entry:not(.hidden)' + el.dataset.filter).join(','), hide: buildFilter(true),
unhide: '.entry.hidden' + enabledFilters.map(el => unhide: buildFilter(false),
(':not(' + el.dataset.filter + ')').replace(/^:not\(:not\((.+?)\)\)$/, '$1')).join(''),
}); });
reapplyFilter(); reapplyFilter();
}, },
@ -505,12 +510,13 @@ function checkUpdateAll() {
let total = 0; let total = 0;
let checked = 0; let checked = 0;
let skippedEdited = 0;
let updated = 0; let updated = 0;
$$('.updatable:not(.can-update)').map(el => checkUpdate(el, {single: false})); $$('.updatable:not(.can-update):not(.update-problem)').map(el => checkUpdate(el, {single: false}));
BG.updater.checkAllStyles(observe, {save: false}).then(done); BG.updater.checkAllStyles({observer, save: false});
function observe(state, value, details) { function observer(state, value, details) {
switch (state) { switch (state) {
case BG.updater.COUNT: case BG.updater.COUNT:
total = value; total = value;
@ -524,37 +530,41 @@ function checkUpdateAll() {
// fallthrough // fallthrough
case BG.updater.SKIPPED: case BG.updater.SKIPPED:
checked++; checked++;
if (details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED) {
skippedEdited++;
}
reportUpdateState(state, value, details); reportUpdateState(state, value, details);
break; 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 progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth; const maxWidth = progress.parentElement.clientWidth;
progress.style.width = Math.round(checked / total * maxWidth) + 'px'; 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} = {}) { function checkUpdate(entry, {single = true} = {}) {
$('.update-note', element).textContent = t('checkingForUpdate'); $('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', element).title = ''; $('.check-update', entry).title = '';
element.classList.remove('checking-update', 'no-update', 'update-problem');
element.classList.add('checking-update');
if (single) { if (single) {
const style = BG.cachedStyles.byId.get(element.styleId); BG.updater.checkStyle({
BG.updater.checkStyle(style, reportUpdateState, {save: false}); 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'); $('#onlyUpdates').classList.remove('hidden');
break; break;
case BG.updater.SKIPPED: { case BG.updater.SKIPPED: {
if (entry.classList.contains('can-update')) {
break;
}
if (!details) { if (!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) { } else if (details == BG.updater.EDITED) {
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); 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'); details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
} }
const same = const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE;
details == BG.updater.SKIPPED_SAME_MD5 ||
details == BG.updater.SKIPPED_SAME_CODE;
const message = same ? t('updateCheckSucceededNoUpdate') : details; const message = same ? t('updateCheckSucceededNoUpdate') : details;
entry.classList.add('no-update'); entry.classList.add('no-update');
entry.classList.toggle('update-problem', !same); entry.classList.toggle('update-problem', !same);
@ -588,7 +599,7 @@ function reportUpdateState(state, style, details) {
$('.check-update', entry).title = newUI.enabled ? message : ''; $('.check-update', entry).title = newUI.enabled ? message : '';
if (!$('#check-all-updates').disabled) { if (!$('#check-all-updates').disabled) {
// this is a single update job so we can decide whether to hide the filter // 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} = {}) { function renderUpdatesOnlyFilter({show, check} = {}) {
const numUpdatable = $$('.can-update').length; const numUpdatable = $$('.can-update').length;
const canUpdate = numUpdatable > 0; const mightUpdate = numUpdatable > 0 || $('.update-problem');
const checkbox = $('#onlyUpdates input'); const checkbox = $('#onlyUpdates input');
show = show !== undefined ? show : canUpdate; show = show !== undefined ? show : mightUpdate;
check = check !== undefined ? show && check : checkbox.checked && canUpdate; check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
$('#onlyUpdates').classList.toggle('hidden', !show); $('#onlyUpdates').classList.toggle('hidden', !show);
checkbox.checked = check; checkbox.checked = check;
@ -611,7 +622,7 @@ function renderUpdatesOnlyFilter({show, check} = {}) {
const btnApply = $('#apply-all-updates'); const btnApply = $('#apply-all-updates');
if (!btnApply.matches('.hidden')) { if (!btnApply.matches('.hidden')) {
if (canUpdate) { if (numUpdatable > 0) {
btnApply.dataset.value = numUpdatable; btnApply.dataset.value = numUpdatable;
} else { } else {
btnApply.classList.add('hidden'); btnApply.classList.add('hidden');

View File

@ -41,16 +41,14 @@ function checkUpdates() {
let total = 0; let total = 0;
let checked = 0; let checked = 0;
let updated = 0; let updated = 0;
const installed = $('#updates-installed'); const maxWidth = $('#update-progress').parentElement.clientWidth;
const progress = $('#update-progress'); BG.updater.checkAllStyles({observer});
const maxWidth = progress.parentElement.clientWidth;
progress.style.width = 0; function observer(state, value) {
installed.dataset.value = '';
document.body.classList.add('update-in-progress');
BG.updater.checkAllStyles((state, value) => {
switch (state) { switch (state) {
case BG.updater.COUNT: case BG.updater.COUNT:
total = value; total = value;
document.body.classList.add('update-in-progress');
break; break;
case BG.updater.UPDATED: case BG.updater.UPDATED:
updated++; updated++;
@ -58,10 +56,11 @@ function checkUpdates() {
case BG.updater.SKIPPED: case BG.updater.SKIPPED:
checked++; checked++;
break; break;
} case BG.updater.DONE:
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
installed.dataset.value = updated || '';
}).then(() => {
document.body.classList.remove('update-in-progress'); 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 tx = db.transaction(['styles'], 'readwrite');
const os = tx.objectStore('styles'); 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 reason = style.reason;
const notify = style.notify !== false; const notify = style.notify !== false;
delete style.method; delete style.method;
@ -227,15 +227,16 @@ function saveStyle(style) {
if (!style.name) { if (!style.name) {
delete style.name; delete style.name;
} }
let existed, codeIsUpdated;
if (id !== null) { if (id !== null) {
// Update or create // Update or create
style.id = id; style.id = id;
os.get(id).onsuccess = eventGet => { os.get(id).onsuccess = eventGet => {
const existed = Boolean(eventGet.target.result); const oldStyle = eventGet.target.result;
const oldStyle = Object.assign({}, eventGet.target.result); existed = Boolean(oldStyle);
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); codeIsUpdated = !existed || style.sections && !styleSectionsEqual(style, oldStyle);
write(Object.assign(oldStyle, style), {reason, existed, codeIsUpdated}); write(Object.assign({}, oldStyle, style));
}; };
} else { } else {
// Create // Create
@ -247,18 +248,11 @@ function saveStyle(style) {
md5Url: null, md5Url: null,
url: null, url: null,
originalMd5: null, originalMd5: null,
}, style), {reason}); }, style));
} }
function write(style, {reason, existed, codeIsUpdated} = {}) { function write(style) {
style.sections = (style.sections || []).map(section => style.sections = normalizeStyleSections(style);
Object.assign({
urls: [],
urlPrefixes: [],
domains: [],
regexps: [],
}, section)
);
os.put(style).onsuccess = event => { os.put(style).onsuccess = event => {
style.id = style.id || event.target.result; style.id = style.id || event.target.result;
invalidateCache(existed ? {updated: style} : {added: style}); invalidateCache(existed ? {updated: style} : {added: style});
@ -271,6 +265,8 @@ function saveStyle(style) {
} }
if (reason == 'update') { if (reason == 'update') {
updateStyleDigest(style); updateStyleDigest(style);
} else if (reason == 'import') {
chrome.storage.local.remove(DIGEST_KEY_PREFIX + style.id, ignoreChromeError);
} }
resolve(style); resolve(style);
}; };
@ -308,10 +304,14 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
for (const section of style.sections) { for (const section of style.sections) {
const {urls, domains, urlPrefixes, regexps, code} = section; const {urls, domains, urlPrefixes, regexps, code} = section;
if ((!urls.length && !urlPrefixes.length && !domains.length && !regexps.length if ((!urls.length && !urlPrefixes.length && !domains.length && !regexps.length
|| urls.length && urls.indexOf(matchUrl) >= 0 || urls.length
|| urlPrefixes.length && arraySomeIsPrefix(urlPrefixes, matchUrl) && urls.indexOf(matchUrl) >= 0
|| domains.length && arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains) || urlPrefixes.length
|| regexps.length && arraySomeMatches(regexps, matchUrl, strictRegexp) && arraySomeIsPrefix(urlPrefixes, matchUrl)
|| domains.length
&& arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|| regexps.length
&& arraySomeMatches(regexps, matchUrl, strictRegexp)
) && !styleCodeEmpty(code)) { ) && !styleCodeEmpty(code)) {
sections.push(section); sections.push(section);
if (stopOnFirst) { 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) { function getStyleDigests(style) {
return Promise.all([ return Promise.all([
chromeLocal.getValue(DIGEST_KEY_PREFIX + style.id), chromeLocal.getValue(DIGEST_KEY_PREFIX + style.id),
@ -548,9 +560,11 @@ function updateStyleDigest(style) {
} }
function calcStyleDigest({sections}) { function calcStyleDigest(style) {
const text = new TextEncoder('utf-8').encode(JSON.stringify(sections)); const jsonString = JSON.stringify(normalizeStyleSections(style));
const text = new TextEncoder('utf-8').encode(jsonString);
return crypto.subtle.digest('SHA-1', text).then(hex); return crypto.subtle.digest('SHA-1', text).then(hex);
function hex(buffer) { function hex(buffer) {
const parts = []; const parts = [];
const PAD8 = '00000000'; const PAD8 = '00000000';

View File

@ -1,4 +1,5 @@
/* globals getStyles, saveStyle, styleSectionsEqual, getStyleDigests, updateStyleDigest */ /* global getStyles, saveStyle, styleSectionsEqual */
/* global getStyleDigests, updateStyleDigest */
'use strict'; 'use strict';
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -7,55 +8,71 @@ 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_CODE: 'up-to-date: code sections are unchanged',
SKIPPED_ERROR_MD5: 'error: MD5 is invalid',
SKIPPED_ERROR_JSON: 'error: JSON is invalid',
DONE: 'done', 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(), lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
checkAllStyles(observe = () => {}, {save = true} = {}) { checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
updater.resetInterval(); updater.resetInterval();
return new Promise(resolve => { return new Promise(resolve => {
getStyles({}, styles => { getStyles({}, styles => {
styles = styles.filter(style => style.updateUrl); styles = styles.filter(style => style.updateUrl);
observe(updater.COUNT, styles.length); observer(updater.COUNT, styles.length);
Promise.all(styles.map(style => Promise.all(styles.map(style =>
updater.checkStyle(style, observe, {save}) updater.checkStyle({style, observer, save, ignoreDigest})
)).then(() => { )).then(() => {
observe(updater.DONE); observer(updater.DONE);
resolve(); resolve();
}); });
}); });
}); });
}, },
checkStyle(style, observe = () => {}, {save = true} = {}) { checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
let hasDigest; 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) return getStyleDigests(style)
.then(fetchMd5IfNotEdited) .then(fetchMd5IfNotEdited)
.then(fetchCodeIfMd5Changed) .then(fetchCodeIfMd5Changed)
.then(saveIfUpdated) .then(saveIfUpdated)
.then(saved => observe(updater.UPDATED, saved)) .then(saved => observer(updater.UPDATED, saved))
.catch(err => observe(updater.SKIPPED, style, err)); .catch(err => observer(updater.SKIPPED, style, err));
function fetchMd5IfNotEdited([originalDigest, current]) { function fetchMd5IfNotEdited([originalDigest, current]) {
hasDigest = Boolean(originalDigest); hasDigest = Boolean(originalDigest);
if (hasDigest && originalDigest != current) { if (hasDigest && !ignoreDigest && originalDigest != current) {
return Promise.reject(updater.SKIPPED_EDITED); return Promise.reject(updater.EDITED);
} }
return download(style.md5Url); return download(style.md5Url);
} }
function fetchCodeIfMd5Changed(md5) { function fetchCodeIfMd5Changed(md5) {
if (!md5 || md5.length != 32) { if (!md5 || md5.length != 32) {
return Promise.reject(updater.SKIPPED_ERROR_MD5); return Promise.reject(updater.ERROR_MD5);
} }
if (md5 == style.originalMd5 && hasDigest) { if (md5 == style.originalMd5 && hasDigest) {
return Promise.reject(updater.SKIPPED_SAME_MD5); return Promise.reject(updater.SAME_MD5);
} }
return download(style.updateUrl); return download(style.updateUrl);
} }
@ -63,16 +80,16 @@ var updater = {
function saveIfUpdated(text) { function saveIfUpdated(text) {
const json = tryJSONparse(text); const json = tryJSONparse(text);
if (!styleJSONseemsValid(json)) { if (!styleJSONseemsValid(json)) {
return Promise.reject(updater.SKIPPED_ERROR_JSON); return Promise.reject(updater.ERROR_JSON);
} }
json.id = style.id; json.id = style.id;
if (styleSectionsEqual(json, style)) { 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); updateStyleDigest(json);
} return Promise.reject(updater.SAME_CODE);
return Promise.reject(updater.SKIPPED_SAME_CODE); } else if (!hasDigest && !ignoreDigest) {
} else if (!hasDigest) { return Promise.reject(updater.MAYBE_EDITED);
return Promise.reject(updater.SKIPPED_MAYBE_EDITED);
} }
return !save ? json : return !save ? json :
saveStyle(Object.assign(json, { saveStyle(Object.assign(json, {