Add ability to update lists through links with specifically crafted URLs

As per discussion with uBO volunteers.

Volunteers offering support for uBO will be able to craft links with
specially formed URLs, which once clicked will cause uBO to automatically
force an update of specified filter lists.

The URL must be crafted as shown in the example below:

https://ublockorigin.github.io/uAssets/update-lists.html?listkeys=ublock-filters,easylist

Where the `listkeys` parameter is a comma-separated list of tokens
corresponding to filter lists. If a token does not match an enabled
filter list, it will be ignored.

The ability to update filter lists through a specially crafted link
is available only on uBO's own support sites:

- https://github.com/uBlockOrigin/
- https://reddit.com/r/uBlockOrigin/
- https://ublockorigin.github.io/

Additionally, a visual cue has been added in the "Filter lists" pane
to easily spot the filter lists which have been recently updated, where
"recently" is currently defined as less than an hour ago.
This commit is contained in:
Raymond Hill 2023-10-14 13:41:49 -04:00
parent 17d30343c5
commit 0325dcdcb4
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
7 changed files with 152 additions and 5 deletions

View File

@ -62,6 +62,18 @@
], ],
"run_at": "document_idle", "run_at": "document_idle",
"all_frames": false "all_frames": false
},
{
"matches": [
"https://github.com/uBlockOrigin/*",
"https://ublockorigin.github.io/*",
"https://*.reddit.com/r/uBlockOrigin/*"
],
"js": [
"/js/scriptlets/updater.js"
],
"run_at": "document_idle",
"all_frames": false
} }
], ],
"content_security_policy": "script-src 'self'; object-src 'self'", "content_security_policy": "script-src 'self'; object-src 'self'",

View File

@ -204,6 +204,10 @@ body.working #actions button {
#lists .listEntry.checked.cached:not(.obsolete) > .detailbar .iconbar .cache { #lists .listEntry.checked.cached:not(.obsolete) > .detailbar .iconbar .cache {
display: inline-flex; display: inline-flex;
} }
#lists .listEntry.cached.recent:not(.obsolete) > .detailbar .iconbar .cache {
color: var(--dashboard-happy-green);
fill: var(--dashboard-happy-green);
}
#lists .iconbar .obsolete { #lists .iconbar .obsolete {
color: var(--info2-ink); color: var(--info2-ink);
fill: var(--info2-ink); fill: var(--info2-ink);

View File

@ -35,6 +35,7 @@
--green-40: 84 255 189; --green-40: 84 255 189;
--green-50: 63 225 176; --green-50: 63 225 176;
--green-60: 42 195 162; --green-60: 42 195 162;
--green-65: 21 165 149;
--green-70: 0 135 135; --green-70: 0 135 135;
--green-80: 0 94 94; --green-80: 0 94 94;
--ink-10: 57 52 115; --ink-10: 57 52 115;
@ -239,6 +240,8 @@
--dashboard-tab-focus-surface-rgb: var(--primary-90); --dashboard-tab-focus-surface-rgb: var(--primary-90);
--dashboard-highlight-surface-rgb: var(--primary-90); --dashboard-highlight-surface-rgb: var(--primary-90);
--dashboard-happy-green: rgb(var(--green-65));
/* popup panel */ /* popup panel */
--popup-cell-cname-ink: #0054d7; /* h260 S:100 Luv:40 */; --popup-cell-cname-ink: #0054d7; /* h260 S:100 Luv:40 */;
--popup-cell-label-mixed-surface: #c29100; /* TODO: fix */ --popup-cell-label-mixed-surface: #c29100; /* TODO: fix */

View File

@ -29,6 +29,7 @@ import { dom, qs$, qsa$ } from './dom.js';
const lastUpdateTemplateString = i18n$('3pLastUpdate'); const lastUpdateTemplateString = i18n$('3pLastUpdate');
const obsoleteTemplateString = i18n$('3pExternalListObsolete'); const obsoleteTemplateString = i18n$('3pExternalListObsolete');
const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m; const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m;
const recentlyUpdated = 1 * 60 * 60 * 1000; // 1 hour
let listsetDetails = {}; let listsetDetails = {};
@ -154,6 +155,8 @@ const renderFilterLists = ( ) => {
if ( asset.cached === true ) { if ( asset.cached === true ) {
dom.cl.add(listEntry, 'cached'); dom.cl.add(listEntry, 'cached');
dom.attr(qs$(listEntry, ':scope > .detailbar .status.cache'), 'title', lastUpdateString); dom.attr(qs$(listEntry, ':scope > .detailbar .status.cache'), 'title', lastUpdateString);
const timeSinceLastUpdate = Date.now() - asset.writeTime;
dom.cl.toggle(listEntry, 'recent', timeSinceLastUpdate < recentlyUpdated);
} else { } else {
dom.cl.remove(listEntry, 'cached'); dom.cl.remove(listEntry, 'cached');
} }
@ -308,7 +311,7 @@ const updateAssetStatus = details => {
dom.attr(qs$(listEntry, '.status.cache'), 'title', dom.attr(qs$(listEntry, '.status.cache'), 'title',
lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(Date.now())) lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(Date.now()))
); );
dom.cl.add(listEntry, 'recent');
} }
updateAncestorListNodes(listEntry, ancestor => { updateAncestorListNodes(listEntry, ancestor => {
updateListNode(ancestor); updateListNode(ancestor);
@ -413,7 +416,8 @@ const updateListNode = listNode => {
let totalFilterCount = 0; let totalFilterCount = 0;
let isCached = false; let isCached = false;
let isObsolete = false; let isObsolete = false;
let writeTime = 0; let latestWriteTime = 0;
let oldestWriteTime = Number.MAX_SAFE_INTEGER;
for ( const listLeaf of checkedListLeaves ) { for ( const listLeaf of checkedListLeaves ) {
const listkey = listLeaf.dataset.key; const listkey = listLeaf.dataset.key;
const listDetails = listsetDetails.available[listkey]; const listDetails = listsetDetails.available[listkey];
@ -422,7 +426,8 @@ const updateListNode = listNode => {
const assetCache = listsetDetails.cache[listkey] || {}; const assetCache = listsetDetails.cache[listkey] || {};
isCached = isCached || dom.cl.has(listLeaf, 'cached'); isCached = isCached || dom.cl.has(listLeaf, 'cached');
isObsolete = isObsolete || dom.cl.has(listLeaf, 'obsolete'); isObsolete = isObsolete || dom.cl.has(listLeaf, 'obsolete');
writeTime = Math.max(writeTime, assetCache.writeTime || 0); latestWriteTime = Math.max(latestWriteTime, assetCache.writeTime || 0);
oldestWriteTime = Math.min(oldestWriteTime, assetCache.writeTime || Number.MAX_SAFE_INTEGER);
} }
dom.cl.toggle(listNode, 'checked', checkedListLeaves.length !== 0); dom.cl.toggle(listNode, 'checked', checkedListLeaves.length !== 0);
dom.cl.toggle(qs$(listNode, ':scope > .detailbar .checkbox'), dom.cl.toggle(qs$(listNode, ':scope > .detailbar .checkbox'),
@ -449,8 +454,9 @@ const updateListNode = listNode => {
dom.cl.toggle(listNode, 'obsolete', isObsolete); dom.cl.toggle(listNode, 'obsolete', isObsolete);
if ( isCached ) { if ( isCached ) {
dom.attr(qs$(listNode, ':scope > .detailbar .cache'), 'title', dom.attr(qs$(listNode, ':scope > .detailbar .cache'), 'title',
lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(writeTime)) lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(latestWriteTime))
); );
dom.cl.toggle(listNode, 'recent', (Date.now() - oldestWriteTime) < recentlyUpdated);
} }
if ( qs$(listNode, '.listEntry.isDefault') !== null ) { if ( qs$(listNode, '.listEntry.isDefault') !== null ) {
dom.cl.add(listNode, 'isDefault'); dom.cl.add(listNode, 'isDefault');

View File

@ -149,10 +149,18 @@ if ( self.location.hash.slice(1) === 'no-dashboard.html' ) {
dom.on('.tabButton', 'click', onTabClickHandler); dom.on('.tabButton', 'click', onTabClickHandler);
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
dom.on(window, 'beforeunload', ( ) => { dom.on(self, 'beforeunload', ( ) => {
if ( discardUnsavedData(true) ) { return; } if ( discardUnsavedData(true) ) { return; }
event.preventDefault(); event.preventDefault();
event.returnValue = ''; event.returnValue = '';
}); });
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
dom.on(self, 'hashchange', ( ) => {
const pane = self.location.hash.slice(1);
if ( pane === '' ) { return; }
loadDashboardPanel(pane);
});
} }
})(); })();

View File

@ -2057,6 +2057,21 @@ const onMessage = function(request, sender, callback) {
}); });
break; break;
case 'updateLists':
const listkeys = request.listkeys.split(',').filter(s => s !== '');
if ( listkeys.length === 0 ) { return; }
for ( const listkey of listkeys ) {
io.purge(listkey);
io.remove(`compiled/${listkey}`);
}
µb.scheduleAssetUpdater(0);
µb.openNewTab({
url: 'dashboard.html#3p-filters.html',
select: true,
});
io.updateStart({ delay: 100 });
break;
default: default:
return vAPI.messaging.UNHANDLED; return vAPI.messaging.UNHANDLED;
} }

View File

@ -0,0 +1,99 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* global HTMLDocument */
'use strict';
/******************************************************************************/
// Injected into specific webpages, those which have been pre-selected
// because they are known to contain `https://ublockorigin.github.io/update-lists?` links.
/******************************************************************************/
(( ) => {
// >>>>> start of local scope
/******************************************************************************/
if ( document instanceof HTMLDocument === false ) { return; }
// Maybe uBO has gone away meanwhile.
if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
function updateStockLists(target) {
if ( vAPI instanceof Object === false ) {
document.removeEventListener('click', updateStockLists);
return;
}
try {
const updateURL = new URL(target.href);
if ( updateURL.hostname !== 'ublockorigin.github.io') { return; }
if ( updateURL.pathname !== '/uAssets/update-lists.html') { return; }
const listkeys = updateURL.searchParams.get('listkeys') || '';
if ( listkeys === '' ) { return true; }
vAPI.messaging.send('scriptlets', {
what: 'updateLists',
listkeys,
});
return true;
} catch (_) {
}
}
// https://github.com/easylist/EasyListHebrew/issues/89
// Ensure trusted events only.
document.addEventListener('click', ev => {
if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
const target = ev.target.closest('a');
if ( target instanceof HTMLAnchorElement === false ) { return; }
if ( updateStockLists(target) === true ) {
ev.stopPropagation();
ev.preventDefault();
}
});
/******************************************************************************/
// <<<<< end of local scope
})();
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;