1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-05 11:37:01 +02:00

[mv3] Add ability to grant/revoke permissions on all sites

This commit is contained in:
Raymond Hill 2022-09-27 19:51:38 -04:00
parent d4b7169421
commit f652cc9855
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
10 changed files with 225 additions and 148 deletions

View File

@ -16,8 +16,13 @@
<div class="body">
<div id="actions">
<p id="listsOfBlockedHostsPrompt">
<p>
<label id="omnipotenceWidget"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span data-i18n="omnipotenceLabel">_</span></label>
<legend data-i18n="omnipotenceLegend">_</legend>
</p>
<hr>
<p><button id="buttonApply" class="preferred disabled iconified" type="button" data-i18n-title="genericApplyChanges"><span class="fa-icon">check</span><span data-i18n="genericApplyChanges">_</span><span class="hover"></span></button>
<p id="listsOfBlockedHostsPrompt">
</div>
<div>

View File

@ -115,6 +115,14 @@
"message": "Block CSP reports",
"description": "background information: https://github.com/gorhill/uBlock/issues/3150"
},
"omnipotenceLabel": {
"message": "Enable extended filtering on all websites",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"omnipotenceLegend": {
"message": "uBO Lite can apply extended filtering on a given website only after you explicitly grant the extension permissions to modify data on that website. This setting allows you to grant permissions for extended filtering to all websites at once.",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupDefault": {
"message": "Default",
"description": "Header for a ruleset section in 'Filter lists pane'"

View File

@ -5,9 +5,14 @@
body {
margin-bottom: 6rem;
}
legend {
color: var(--ink-3);
font-size: var(--font-size-smaller);
padding: var(--default-gap-xxsmall);
}
#actions {
background-color: var(--surface-1);
padding: 0.5em 0;
padding: 1px 0;
position: sticky;
top: 0;
z-index: 10;

View File

@ -1,5 +1,6 @@
body > div.body {
margin: 0 1em;
max-width: min(600px, 100vw);
}
h2, h3 {
margin: 1em 0;

View File

@ -186,6 +186,9 @@ body.mobile.no-tooltips .toolRibbon .tool {
#toggleGreatPowers {
position: relative;
}
body.hasOmnipotence #toggleGreatPowers {
pointer-events: none;
}
#toggleGreatPowers .badge {
bottom: 4px;
font-size: var(--font-size-xsmall);
@ -202,7 +205,7 @@ body:not(.hasGreatPowers) [data-i18n-title="popupRevokeGreatPowers"],
body.hasGreatPowers [data-i18n-title="popupGrantGreatPowers"] {
display: none;
}
body [data-i18n-title="popupRevokeGreatPowers"] {
body:not(.hasOmnipotence) [data-i18n-title="popupRevokeGreatPowers"] {
fill: var(--popup-power-ink);
}

View File

@ -23,7 +23,7 @@
/******************************************************************************/
import { sendMessage } from './ext.js';
import { browser, sendMessage } from './ext.js';
import { i18n$ } from './i18n.js';
import { dom, qs$, qsa$ } from './dom.js';
import { simpleStorage } from './storage.js';
@ -42,7 +42,7 @@ const renderNumber = function(value) {
/******************************************************************************/
const renderFilterLists = function(soft) {
function renderFilterLists(soft) {
const { enabledRulesets, rulesetDetails } = cachedRulesetData;
const listGroupTemplate = qs$('#templates .groupEntry');
const listEntryTemplate = qs$('#templates .listEntry');
@ -186,11 +186,13 @@ const renderFilterLists = function(soft) {
}
renderWidgets();
};
}
/******************************************************************************/
const renderWidgets = function() {
qs$('#omnipotenceWidget input').checked = cachedRulesetData.hasOmnipotence;
dom.cl.toggle(
qs$('#buttonApply'),
'disabled',
@ -217,7 +219,30 @@ const renderWidgets = function() {
/******************************************************************************/
const hashFromCurrentFromSettings = function() {
async function onOmnipotenceChanged(ev) {
const input = ev.target;
const newState = input.checked;
const oldState = await browser.permissions.contains({
origins: [ '<all_urls>' ]
});
if ( newState === oldState ) { return; }
if ( newState ) {
browser.permissions.request({ origins: [ '<all_urls>' ] });
} else {
browser.permissions.remove({ origins: [ '<all_urls>' ] });
}
}
dom.on(
qs$('#omnipotenceWidget input'),
'change',
ev => { onOmnipotenceChanged(ev); }
);
/******************************************************************************/
function hashFromCurrentFromSettings() {
const hash = [];
const listHash = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
@ -227,7 +252,7 @@ const hashFromCurrentFromSettings = function() {
}
hash.push(listHash.sort().join());
return hash.join();
};
}
self.hasUnsavedData = function() {
return hashFromCurrentFromSettings() !== filteringSettingsHash;
@ -251,7 +276,7 @@ dom.on(
/******************************************************************************/
const applyEnabledRulesets = async function() {
async function applyEnabledRulesets() {
const enabledRulesets = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) === null ) { continue; }
@ -264,13 +289,13 @@ const applyEnabledRulesets = async function() {
});
filteringSettingsHash = hashFromCurrentFromSettings();
};
}
const buttonApplyHandler = async function() {
async function buttonApplyHandler() {
dom.cl.remove(qs$('#buttonApply'), 'enabled');
await applyEnabledRulesets();
renderWidgets();
};
}
dom.on(
qs$('#buttonApply'),
@ -282,13 +307,13 @@ dom.on(
// Collapsing of unused lists.
const mustHideUnusedLists = function(which) {
function mustHideUnusedLists(which) {
const hideAll = hideUnusedSet.has('*');
if ( which === '*' ) { return hideAll; }
return hideUnusedSet.has(which) !== hideAll;
};
}
const toggleHideUnusedLists = function(which) {
function toggleHideUnusedLists(which) {
const doesHideAll = hideUnusedSet.has('*');
let groupSelector;
let mustHide;
@ -325,7 +350,7 @@ const toggleHideUnusedLists = function(which) {
'hideUnusedFilterLists',
Array.from(hideUnusedSet)
);
};
}
dom.on(
qs$('#lists'),

View File

@ -43,7 +43,7 @@ import {
import {
getInjectableCount,
registerInjectable,
registerInjectables,
} from './scripting-manager.js';
import {
@ -113,26 +113,24 @@ async function saveRulesetConfig() {
/******************************************************************************/
async function hasGreatPowers(origin) {
function hasGreatPowers(origin) {
return browser.permissions.contains({
origins: [ `${origin}/*` ],
});
}
function grantGreatPowers(hostname) {
return browser.permissions.request({
origins: [ `*://${hostname}/*` ],
function hasOmnipotence() {
return browser.permissions.contains({
origins: [ '<all_urls>' ],
});
}
function revokeGreatPowers(hostname) {
return browser.permissions.remove({
origins: [ `*://${hostname}/*` ],
});
function onPermissionsAdded(permissions) {
registerInjectables(permissions.origins);
}
function onPermissionsChanged() {
registerInjectable();
function onPermissionsRemoved(permissions) {
registerInjectables(permissions.origins);
}
/******************************************************************************/
@ -145,7 +143,7 @@ function onMessage(request, sender, callback) {
rulesetConfig.enabledRulesets = request.enabledRulesets;
return Promise.all([
saveRulesetConfig(),
registerInjectable(),
registerInjectables(),
]);
}).then(( ) => {
callback();
@ -157,50 +155,40 @@ function onMessage(request, sender, callback) {
Promise.all([
getRulesetDetails(),
dnr.getEnabledRulesets(),
hasOmnipotence(),
]).then(results => {
const [ rulesetDetails, enabledRulesets ] = results;
const [ rulesetDetails, enabledRulesets, hasOmnipotence ] = results;
callback({
enabledRulesets,
rulesetDetails: Array.from(rulesetDetails.values()),
hasOmnipotence,
});
});
return true;
}
case 'grantGreatPowers':
grantGreatPowers(request.hostname).then(granted => {
console.info(`Granted uBOL great powers on ${request.hostname}: ${granted}`);
callback(granted);
});
return true;
case 'popupPanelData': {
Promise.all([
matchesTrustedSiteDirective(request),
hasOmnipotence(),
hasGreatPowers(request.origin),
getEnabledRulesetsStats(),
getInjectableCount(request.origin),
]).then(results => {
callback({
isTrusted: results[0],
hasGreatPowers: results[1],
rulesetDetails: results[2],
injectableCount: results[3],
hasOmnipotence: results[1],
hasGreatPowers: results[2],
rulesetDetails: results[3],
injectableCount: results[4],
});
});
return true;
}
case 'revokeGreatPowers':
revokeGreatPowers(request.hostname).then(removed => {
console.info(`Revoked great powers from uBOL on ${request.hostname}: ${removed}`);
callback(removed);
});
return true;
case 'toggleTrustedSiteDirective': {
toggleTrustedSiteDirective(request).then(response => {
registerInjectable().then(( ) => {
registerInjectables().then(( ) => {
callback(response);
});
});
@ -232,7 +220,7 @@ async function start() {
// Unsure whether the browser remembers correctly registered css/scripts
// after we quit the browser. For now uBOL will check unconditionally at
// launch time whether content css/scripts are properly registered.
registerInjectable();
registerInjectables();
const enabledRulesets = await dnr.getEnabledRulesets();
console.log(`Enabled rulesets: ${enabledRulesets}`);
@ -249,6 +237,6 @@ async function start() {
runtime.onMessage.addListener(onMessage);
browser.permissions.onAdded.addListener(onPermissionsChanged);
browser.permissions.onRemoved.addListener(onPermissionsChanged);
browser.permissions.onAdded.addListener(onPermissionsAdded);
browser.permissions.onRemoved.addListener(onPermissionsRemoved);
})();

View File

@ -157,9 +157,10 @@ dom.on(qs$('#lessButton'), 'click', ( ) => {
/******************************************************************************/
async function grantGreatPowers() {
const granted = await sendMessage({
what: 'grantGreatPowers',
hostname: tabHostname,
if ( tabHostname === '' ) { return; }
const targetHostname = tabHostname.replace(/^www\./, '');
const granted = await browser.permissions.request({
origins: [ `*://*.${targetHostname}/*` ],
});
if ( granted !== true ) { return; }
dom.cl.add(dom.body, 'hasGreatPowers');
@ -167,9 +168,10 @@ async function grantGreatPowers() {
}
async function revokeGreatPowers() {
const removed = await sendMessage({
what: 'revokeGreatPowers',
hostname: tabHostname,
if ( tabHostname === '' ) { return; }
const targetHostname = tabHostname.replace(/^www\./, '');
const removed = await browser.permissions.remove({
origins: [ `*://*.${targetHostname}/*` ],
});
if ( removed !== true ) { return; }
dom.cl.remove(dom.body, 'hasGreatPowers');
@ -212,6 +214,12 @@ async function init() {
popupPanelData.isTrusted === true
);
dom.cl.toggle(
dom.body,
'hasOmnipotence',
popupPanelData.hasOmnipotence === true
);
dom.cl.toggle(
dom.body,
'hasGreatPowers',

View File

@ -29,12 +29,7 @@ import { browser, dnr } from './ext.js';
import { fetchJSON } from './fetch.js';
import { getAllTrustedSiteDirectives } from './trusted-sites.js';
import {
parsedURLromOrigin,
toBroaderHostname,
fidFromFileName,
fnameFromFileId,
} from './utils.js';
import * as ut from './utils.js';
/******************************************************************************/
@ -57,28 +52,6 @@ function getScriptingDetails() {
/******************************************************************************/
const matchesFromHostnames = hostnames => {
const out = [];
for ( const hn of hostnames ) {
if ( hn === '*' ) {
out.push('*://*/*');
} else {
out.push(`*://*.${hn}/*`);
}
}
return out;
};
const hostnamesFromMatches = origins => {
const out = [];
for ( const origin of origins ) {
const match = /^\*:\/\/([^\/]+)\/\*/.exec(origin);
if ( match === null ) { continue; }
out.push(match[1]);
}
return out;
};
const arrayEq = (a, b) => {
if ( a === undefined ) { return b === undefined; }
if ( b === undefined ) { return false; }
@ -96,20 +69,20 @@ const toRegisterable = (fname, entry) => {
id: fname,
};
if ( entry.matches ) {
directive.matches = matchesFromHostnames(entry.matches);
directive.matches = ut.matchesFromHostnames(entry.matches);
} else {
directive.matches = [ '*://*/*' ];
directive.matches = [ '<all_urls>' ];
}
if ( entry.excludeMatches ) {
directive.excludeMatches = matchesFromHostnames(entry.excludeMatches);
directive.excludeMatches = ut.matchesFromHostnames(entry.excludeMatches);
}
directive.js = [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ];
if ( (fidFromFileName(fname) & RUN_AT_BIT) !== 0 ) {
if ( (ut.fidFromFileName(fname) & RUN_AT_BIT) !== 0 ) {
directive.runAt = 'document_end';
} else {
directive.runAt = 'document_start';
}
if ( (fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
if ( (ut.fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
directive.world = 'MAIN';
}
return directive;
@ -122,22 +95,31 @@ const MAIN_WORLD_BIT = 0b01;
const shouldUpdate = (registered, candidate) => {
const matches = candidate.matches &&
matchesFromHostnames(candidate.matches);
ut.matchesFromHostnames(candidate.matches);
if ( arrayEq(registered.matches, matches) === false ) {
return true;
}
const excludeMatches = candidate.excludeMatches &&
matchesFromHostnames(candidate.excludeMatches);
ut.matchesFromHostnames(candidate.excludeMatches);
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
return true;
}
return false;
};
const isTrustedHostname = (trustedSites, hn) => {
if ( trustedSites.size === 0 ) { return false; }
while ( hn ) {
if ( trustedSites.has(hn) ) { return true; }
hn = ut.toBroaderHostname(hn);
}
return false;
};
/******************************************************************************/
async function getInjectableCount(origin) {
const url = parsedURLromOrigin(origin);
const url = ut.parsedURLromOrigin(origin);
if ( url === undefined ) { return 0; }
const [
@ -161,7 +143,7 @@ async function getInjectableCount(origin) {
} else if ( Array.isArray(fids) ) {
total += fids.length;
}
hn = toBroaderHostname(hn);
hn = ut.toBroaderHostname(hn);
}
}
@ -170,49 +152,27 @@ async function getInjectableCount(origin) {
/******************************************************************************/
async function registerInjectable() {
if ( browser.scripting === undefined ) { return false; }
const [
hostnames,
function registerSomeInjectables(args) {
const {
hostnamesSet,
trustedSites,
rulesetIds,
registered,
scriptingDetails,
] = await Promise.all([
browser.permissions.getAll(),
getAllTrustedSiteDirectives(),
dnr.getEnabledRulesets(),
browser.scripting.getRegisteredContentScripts(),
getScriptingDetails(),
]).then(results => {
results[0] = new Set(hostnamesFromMatches(results[0].origins));
results[1] = new Set(results[1]);
return results;
});
} = args;
const toRegister = new Map();
const isTrustedHostname = hn => {
while ( hn ) {
if ( trustedSites.has(hn) ) { return true; }
hn = toBroaderHostname(hn);
}
return false;
};
const toRegisterMap = new Map();
const checkMatches = (details, hn) => {
let fids = details.matches?.get(hn);
if ( fids === undefined ) { return; }
if ( typeof fids === 'number' ) { fids = [ fids ]; }
for ( const fid of fids ) {
const fname = fnameFromFileId(fid);
const existing = toRegister.get(fname);
const fname = ut.fnameFromFileId(fid);
const existing = toRegisterMap.get(fname);
if ( existing ) {
existing.matches.push(hn);
} else {
toRegister.set(fname, { matches: [ hn ] });
toRegisterMap.set(fname, { matches: [ hn ] });
}
}
};
@ -220,47 +180,91 @@ async function registerInjectable() {
for ( const rulesetId of rulesetIds ) {
const details = scriptingDetails.get(rulesetId);
if ( details === undefined ) { continue; }
for ( let hn of hostnames ) {
if ( isTrustedHostname(hn) ) { continue; }
for ( let hn of hostnamesSet ) {
if ( isTrustedHostname(trustedSites, hn) ) { continue; }
while ( hn ) {
checkMatches(details, hn);
hn = toBroaderHostname(hn);
hn = ut.toBroaderHostname(hn);
}
}
}
const checkExcludeMatches = (details, hn) => {
let fids = details.excludeMatches?.get(hn);
if ( fids === undefined ) { return; }
if ( typeof fids === 'number' ) { fids = [ fids ]; }
for ( const fid of fids ) {
const fname = fnameFromFileId(fid);
const existing = toRegister.get(fname);
if ( existing === undefined ) { continue; }
if ( existing.excludeMatches ) {
existing.excludeMatches.push(hn);
} else {
toRegister.set(fname, { excludeMatches: [ hn ] });
}
}
};
return toRegisterMap;
}
function registerAllInjectables(args) {
const {
trustedSites,
rulesetIds,
scriptingDetails,
} = args;
const toRegisterMap = new Map();
for ( const rulesetId of rulesetIds ) {
const details = scriptingDetails.get(rulesetId);
if ( details === undefined ) { continue; }
for ( let hn of hostnames.keys() ) {
while ( hn ) {
checkExcludeMatches(details, hn);
hn = toBroaderHostname(hn);
for ( let [ hn, fids ] of details.matches ) {
if ( isTrustedHostname(trustedSites, hn) ) { continue; }
if ( typeof fids === 'number' ) { fids = [ fids ]; }
for ( const fid of fids ) {
const fname = ut.fnameFromFileId(fid);
const existing = toRegisterMap.get(fname);
if ( existing ) {
existing.matches.push(hn);
} else {
toRegisterMap.set(fname, { matches: [ hn ] });
}
}
}
}
return toRegisterMap;
}
/******************************************************************************/
async function registerInjectables(origins) {
void origins;
if ( browser.scripting === undefined ) { return false; }
const [
hostnamesSet,
trustedSites,
rulesetIds,
scriptingDetails,
registered,
] = await Promise.all([
browser.permissions.getAll(),
getAllTrustedSiteDirectives(),
dnr.getEnabledRulesets(),
getScriptingDetails(),
browser.scripting.getRegisteredContentScripts(),
]).then(results => {
results[0] = new Set(ut.hostnamesFromMatches(results[0].origins));
results[1] = new Set(results[1]);
return results;
});
const toRegisterMap = hostnamesSet.has('*')
? registerAllInjectables({
trustedSites,
rulesetIds,
scriptingDetails,
})
: registerSomeInjectables({
hostnamesSet,
trustedSites,
rulesetIds,
scriptingDetails,
});
const before = new Map(registered.map(entry => [ entry.id, entry ]));
const toAdd = [];
const toUpdate = [];
for ( const [ fname, entry ] of toRegister ) {
for ( const [ fname, entry ] of toRegisterMap ) {
if ( before.has(fname) === false ) {
toAdd.push(toRegisterable(fname, entry));
continue;
@ -272,7 +276,7 @@ async function registerInjectable() {
const toRemove = [];
for ( const fname of before.keys() ) {
if ( toRegister.has(fname) ) { continue; }
if ( toRegisterMap.has(fname) ) { continue; }
toRemove.push(fname);
}
@ -298,5 +302,5 @@ async function registerInjectable() {
export {
getInjectableCount,
registerInjectable
registerInjectables
};

View File

@ -42,6 +42,34 @@ const toBroaderHostname = hn => {
/******************************************************************************/
const matchesFromHostnames = hostnames => {
const out = [];
for ( const hn of hostnames ) {
if ( hn === '*' ) {
out.push('<all_urls>');
} else {
out.push(`*://*.${hn}/*`);
}
}
return out;
};
const hostnamesFromMatches = origins => {
const out = [];
for ( const origin of origins ) {
if ( origin === '<all_urls>' ) {
out.push('*');
continue;
}
const match = /^\*:\/\/(?:\*\.)?([^\/]+)\/\*/.exec(origin);
if ( match === null ) { continue; }
out.push(match[1]);
}
return out;
};
/******************************************************************************/
const fnameFromFileId = fid =>
fid.toString(32).padStart(7, '0');
@ -53,6 +81,8 @@ const fidFromFileName = fname =>
export {
parsedURLromOrigin,
toBroaderHostname,
matchesFromHostnames,
hostnamesFromMatches,
fnameFromFileId,
fidFromFileName,
};