From 0325dcdcb4a5994b138fe56999f5a1ca43d41104 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 14 Oct 2023 13:41:49 -0400 Subject: [PATCH] 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. --- platform/chromium/manifest.json | 12 ++++ src/css/3p-filters.css | 4 ++ src/css/themes/default.css | 3 + src/js/3p-filters.js | 14 +++-- src/js/dashboard.js | 10 +++- src/js/messaging.js | 15 +++++ src/js/scriptlets/updater.js | 99 +++++++++++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 src/js/scriptlets/updater.js diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index c48776edc..28ceec5d0 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -62,6 +62,18 @@ ], "run_at": "document_idle", "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'", diff --git a/src/css/3p-filters.css b/src/css/3p-filters.css index 5749b3fbb..014dd202b 100644 --- a/src/css/3p-filters.css +++ b/src/css/3p-filters.css @@ -204,6 +204,10 @@ body.working #actions button { #lists .listEntry.checked.cached:not(.obsolete) > .detailbar .iconbar .cache { 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 { color: var(--info2-ink); fill: var(--info2-ink); diff --git a/src/css/themes/default.css b/src/css/themes/default.css index 44609817e..c37bd28f0 100644 --- a/src/css/themes/default.css +++ b/src/css/themes/default.css @@ -35,6 +35,7 @@ --green-40: 84 255 189; --green-50: 63 225 176; --green-60: 42 195 162; + --green-65: 21 165 149; --green-70: 0 135 135; --green-80: 0 94 94; --ink-10: 57 52 115; @@ -239,6 +240,8 @@ --dashboard-tab-focus-surface-rgb: var(--primary-90); --dashboard-highlight-surface-rgb: var(--primary-90); + --dashboard-happy-green: rgb(var(--green-65)); + /* popup panel */ --popup-cell-cname-ink: #0054d7; /* h260 S:100 Luv:40 */; --popup-cell-label-mixed-surface: #c29100; /* TODO: fix */ diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index edc223b67..ffef2fcf4 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -29,6 +29,7 @@ import { dom, qs$, qsa$ } from './dom.js'; const lastUpdateTemplateString = i18n$('3pLastUpdate'); const obsoleteTemplateString = i18n$('3pExternalListObsolete'); const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m; +const recentlyUpdated = 1 * 60 * 60 * 1000; // 1 hour let listsetDetails = {}; @@ -154,6 +155,8 @@ const renderFilterLists = ( ) => { if ( asset.cached === true ) { dom.cl.add(listEntry, 'cached'); dom.attr(qs$(listEntry, ':scope > .detailbar .status.cache'), 'title', lastUpdateString); + const timeSinceLastUpdate = Date.now() - asset.writeTime; + dom.cl.toggle(listEntry, 'recent', timeSinceLastUpdate < recentlyUpdated); } else { dom.cl.remove(listEntry, 'cached'); } @@ -308,7 +311,7 @@ const updateAssetStatus = details => { dom.attr(qs$(listEntry, '.status.cache'), 'title', lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(Date.now())) ); - + dom.cl.add(listEntry, 'recent'); } updateAncestorListNodes(listEntry, ancestor => { updateListNode(ancestor); @@ -413,7 +416,8 @@ const updateListNode = listNode => { let totalFilterCount = 0; let isCached = false; let isObsolete = false; - let writeTime = 0; + let latestWriteTime = 0; + let oldestWriteTime = Number.MAX_SAFE_INTEGER; for ( const listLeaf of checkedListLeaves ) { const listkey = listLeaf.dataset.key; const listDetails = listsetDetails.available[listkey]; @@ -422,7 +426,8 @@ const updateListNode = listNode => { const assetCache = listsetDetails.cache[listkey] || {}; isCached = isCached || dom.cl.has(listLeaf, 'cached'); 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(qs$(listNode, ':scope > .detailbar .checkbox'), @@ -449,8 +454,9 @@ const updateListNode = listNode => { dom.cl.toggle(listNode, 'obsolete', isObsolete); if ( isCached ) { 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 ) { dom.cl.add(listNode, 'isDefault'); diff --git a/src/js/dashboard.js b/src/js/dashboard.js index a88675957..31ab2e930 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -149,10 +149,18 @@ if ( self.location.hash.slice(1) === 'no-dashboard.html' ) { dom.on('.tabButton', 'click', onTabClickHandler); // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event - dom.on(window, 'beforeunload', ( ) => { + dom.on(self, 'beforeunload', ( ) => { if ( discardUnsavedData(true) ) { return; } event.preventDefault(); 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); + }); + } })(); diff --git a/src/js/messaging.js b/src/js/messaging.js index 3ad22bbcf..053fad433 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -2057,6 +2057,21 @@ const onMessage = function(request, sender, callback) { }); 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: return vAPI.messaging.UNHANDLED; } diff --git a/src/js/scriptlets/updater.js b/src/js/scriptlets/updater.js new file mode 100644 index 000000000..06f9be435 --- /dev/null +++ b/src/js/scriptlets/updater.js @@ -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;