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 c48776edc35f1..28ceec5d0a2b8 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 5749b3fbba915..014dd202b4f8b 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 44609817e912d..c37bd28f02a2b 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 edc223b675753..ffef2fcf498b2 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 a886759574bc7..31ab2e930a155 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 3ad22bbcf20a4..053fad4336340 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 0000000000000..06f9be4359ace --- /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;