[mv3] New approach to toggle toolbar icon not requiring extra permissions

Related issue:
https://github.com/uBlockOrigin/uBOL-home/issues/198#issuecomment-2855135571

As a result, the permissions `tabs` and `webNavigation` have been removed.
This commit is contained in:
Raymond Hill 2025-05-07 08:53:59 -04:00
parent 0ee6e3044d
commit cc2760f4d6
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
8 changed files with 97 additions and 84 deletions

View file

@ -40,9 +40,7 @@
"activeTab",
"declarativeNetRequest",
"scripting",
"storage",
"tabs",
"webNavigation"
"storage"
],
"short_name": "uBO Lite",
"storage": {

View file

@ -19,8 +19,8 @@
Home: https://github.com/gorhill/uBlock
*/
import { matchesFromHostnames, strArrayEq } from './utils.js';
import { browser } from './ext.js';
import { getFilteringModeDetails } from './mode-manager.js';
/******************************************************************************/
@ -28,14 +28,6 @@ let reverseMode = false;
/******************************************************************************/
function toggleToolbarIcon(tabId) {
if ( reverseMode ) {
enableToolbarIcon(tabId);
} else {
disableToolbarIcon(tabId);
}
}
function disableToolbarIcon(tabId) {
const details = {
path: {
@ -68,9 +60,12 @@ function enableToolbarIcon(tabId) {
/******************************************************************************/
function toolbarIconListener(details) {
if ( details.frameId !== 0 ) { return; }
toggleToolbarIcon(details.tabId);
export function toggleToolbarIcon(tabId) {
if ( reverseMode ) {
enableToolbarIcon(tabId);
} else {
disableToolbarIcon(tabId);
}
}
/******************************************************************************/
@ -78,13 +73,8 @@ function toolbarIconListener(details) {
// https://github.com/uBlockOrigin/uBOL-home/issues/198
// Ensure the toolbar icon reflects the no-filtering mode of "trusted sites"
export async function syncToolbarIcon(wakeup) {
const { webNavigation } = browser;
if ( typeof webNavigation !== 'object' ) { return; }
webNavigation.onCommitted.removeListener(toolbarIconListener);
const { none, basic, optimal, complete } = await getFilteringModeDetails();
export async function registerToolbarIconToggler(context) {
const { none, basic, optimal, complete } = context.filteringModeDetails;
const reverseModeAfter = none.delete('all-urls');
const toToggle = reverseModeAfter ?
new Set([ ...basic, ...optimal, ...complete ])
@ -101,20 +91,20 @@ export async function syncToolbarIcon(wakeup) {
if ( toToggle.size === 0 ) { return; }
webNavigation.onCommitted.addListener(toolbarIconListener, {
url: Array.from(toToggle).map(a => ({
originAndPathMatches: `^https?://([^.].*\\.)?${a.replaceAll('.', '\\.')}/`
})),
});
const registered = context.before.get('toolbar-icon');
context.before.delete('toolbar-icon'); // Important!
if ( wakeup === undefined ) { return; }
const directive = {
id: 'toolbar-icon',
js: [ '/js/scripting/toolbar-icon.js' ],
matches: matchesFromHostnames(toToggle),
runAt: 'document_start',
};
// If waking up, update icon for existing tabs
const tabs = await browser.tabs.query({
url: Array.from(toToggle).map(a => `*://*.${a}/*`)
}).catch(( ) => ([]));
for ( const tab of tabs ) {
toggleToolbarIcon(tab.id);
if ( registered === undefined ) {
context.toAdd.push(directive);
} else if ( strArrayEq(registered.matches, directive.matches) === false ) {
context.toRemove.push('toolbar-icon');
context.toAdd.push(directive);
}
}

View file

@ -21,7 +21,6 @@
import {
MODE_BASIC,
MODE_NONE,
MODE_OPTIMAL,
getDefaultFilteringMode,
getFilteringMode,
@ -78,7 +77,7 @@ import {
import { dnr } from './ext-compat.js';
import { registerInjectables } from './scripting-manager.js';
import { syncToolbarIcon } from './action.js';
import { toggleToolbarIcon } from './action.js';
/******************************************************************************/
@ -170,6 +169,14 @@ function onMessage(request, sender, callback) {
return false;
}
case 'toggleToolbarIcon': {
const tabId = sender?.tab?.id ?? false;
if ( tabId ) {
toggleToolbarIcon(tabId);
}
return false;
}
default:
break;
}
@ -313,17 +320,11 @@ function onMessage(request, sender, callback) {
break;
case 'setFilteringMode': {
let trustedSitesChanged = false;
getFilteringMode(request.hostname).then(beforeLevel => {
if ( request.level === beforeLevel ) { return beforeLevel; }
trustedSitesChanged = beforeLevel === MODE_NONE;
return setFilteringMode(request.hostname, request.level);
}).then(afterLevel => {
registerInjectables();
trustedSitesChanged ||= afterLevel === MODE_NONE;
if ( trustedSitesChanged ) {
syncToolbarIcon();
}
callback(afterLevel);
});
return true;
@ -357,7 +358,6 @@ function onMessage(request, sender, callback) {
case 'setTrustedSites':
setTrustedSites(request.hostnames).then(( ) => {
registerInjectables();
syncToolbarIcon(true);
return Promise.all([
getDefaultFilteringMode(),
getTrustedSites(),
@ -485,7 +485,6 @@ async function start() {
await startSession();
}
syncToolbarIcon(process.wakeupRun);
toggleDeveloperMode(rulesetConfig.developerMode);
}

View file

@ -25,6 +25,7 @@ import { browser } from './ext.js';
import { fetchJSON } from './fetch.js';
import { getEnabledRulesetsDetails } from './ruleset-manager.js';
import { getFilteringModeDetails } from './mode-manager.js';
import { registerToolbarIconToggler } from './action.js';
import { ubolLog } from './debug.js';
/******************************************************************************/
@ -53,19 +54,6 @@ function getGenericDetails() {
/******************************************************************************/
// Important: We need to sort the arrays for fast comparison
const arrayEq = (a = [], b = [], sort = true) => {
const alen = a.length;
if ( alen !== b.length ) { return false; }
if ( sort ) { a.sort(); b.sort(); }
for ( let i = 0; i < alen; i++ ) {
if ( a[i] !== b[i] ) { return false; }
}
return true;
};
/******************************************************************************/
const normalizeMatches = matches => {
if ( matches.length <= 1 ) { return; }
if ( matches.includes('<all_urls>') === false ) {
@ -168,9 +156,9 @@ function registerHighGeneric(context, genericDetails) {
// update
if (
arrayEq(registered.css, css, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
ut.strArrayEq(registered.css, css, false) === false ||
ut.strArrayEq(registered.matches, matches) === false ||
ut.strArrayEq(registered.excludeMatches, excludeMatches) === false
) {
context.toRemove.push('css-generichigh');
context.toAdd.push(directive);
@ -231,8 +219,8 @@ function registerGeneric(context, genericDetails) {
if ( registered === undefined ) { // register
context.toAdd.push(directive);
} else if ( // update
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, directive.matches) === false
ut.strArrayEq(registered.js, js, false) === false ||
ut.strArrayEq(registered.matches, directive.matches) === false
) {
context.toRemove.push('css-generic-some');
context.toAdd.push(directive);
@ -257,8 +245,8 @@ function registerGeneric(context, genericDetails) {
if ( registeredAll === undefined ) { // register
context.toAdd.push(directiveAll);
} else if ( // update
arrayEq(registeredAll.js, js, false) === false ||
arrayEq(registeredAll.excludeMatches, directiveAll.excludeMatches) === false
ut.strArrayEq(registeredAll.js, js, false) === false ||
ut.strArrayEq(registeredAll.excludeMatches, directiveAll.excludeMatches) === false
) {
context.toRemove.push('css-generic-all');
context.toAdd.push(directiveAll);
@ -281,8 +269,8 @@ function registerGeneric(context, genericDetails) {
if ( registeredSome === undefined ) { // register
context.toAdd.push(directiveSome);
} else if ( // update
arrayEq(registeredSome.js, js, false) === false ||
arrayEq(registeredSome.matches, directiveSome.matches) === false
ut.strArrayEq(registeredSome.js, js, false) === false ||
ut.strArrayEq(registeredSome.matches, directiveSome.matches) === false
) {
context.toRemove.push('css-generic-some');
context.toAdd.push(directiveSome);
@ -342,9 +330,9 @@ function registerProcedural(context) {
// update
if (
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
ut.strArrayEq(registered.js, js, false) === false ||
ut.strArrayEq(registered.matches, matches) === false ||
ut.strArrayEq(registered.excludeMatches, excludeMatches) === false
) {
context.toRemove.push('css-procedural');
context.toAdd.push(directive);
@ -404,9 +392,9 @@ function registerDeclarative(context) {
// update
if (
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
ut.strArrayEq(registered.js, js, false) === false ||
ut.strArrayEq(registered.matches, matches) === false ||
ut.strArrayEq(registered.excludeMatches, excludeMatches) === false
) {
context.toRemove.push('css-declarative');
context.toAdd.push(directive);
@ -466,9 +454,9 @@ function registerSpecific(context) {
// update
if (
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
ut.strArrayEq(registered.js, js, false) === false ||
ut.strArrayEq(registered.matches, matches) === false ||
ut.strArrayEq(registered.excludeMatches, excludeMatches) === false
) {
context.toRemove.push('css-specific');
context.toAdd.push(directive);
@ -545,8 +533,8 @@ function registerScriptlet(context, scriptletDetails) {
// update
if (
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
ut.strArrayEq(registered.matches, matches) === false ||
ut.strArrayEq(registered.excludeMatches, excludeMatches) === false
) {
context.toRemove.push(id);
context.toAdd.push(directive);
@ -599,6 +587,7 @@ async function registerInjectables() {
registerSpecific(context);
registerGeneric(context, genericDetails);
registerHighGeneric(context, genericDetails);
registerToolbarIconToggler(context);
toRemove.push(...Array.from(before.keys()));

View file

@ -0,0 +1,27 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-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
*/
(function uBOL_toggleToolbarIcon() {
chrome.runtime.sendMessage({
what: 'toggleToolbarIcon',
}).catch(( ) => {
});
})();

View file

@ -182,6 +182,19 @@ async function gotoURL(url, type) {
/******************************************************************************/
// Important: We need to sort the arrays for fast comparison
const strArrayEq = (a = [], b = [], sort = true) => {
const alen = a.length;
if ( alen !== b.length ) { return false; }
if ( sort ) { a.sort(); b.sort(); }
for ( let i = 0; i < alen; i++ ) {
if ( a[i] !== b[i] ) { return false; }
}
return true;
};
/******************************************************************************/
export {
broadcastMessage,
parsedURLromOrigin,
@ -195,4 +208,5 @@ export {
hostnamesFromMatches,
hasBroadHostPermissions,
gotoURL,
strArrayEq,
};

View file

@ -52,9 +52,7 @@
"activeTab",
"declarativeNetRequest",
"scripting",
"storage",
"tabs",
"webNavigation"
"storage"
],
"short_name": "uBO Lite",
"version": "1.0",

View file

@ -40,9 +40,7 @@
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"scripting",
"storage",
"tabs",
"webNavigation"
"storage"
],
"short_name": "uBO Lite",
"version": "1.0",