1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-25 20:02:51 +01:00

[mv3] Introduce per-site filtering modes in lieu of per-site toggle switch

This commit is contained in:
Raymond Hill 2022-10-10 12:28:24 -04:00
parent 5777b672a4
commit 8eb28a446c
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
26 changed files with 1750 additions and 801 deletions

View File

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>uBlock Origin Lite — Filter lists</title>
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/fa-icons.css">
<link rel="stylesheet" type="text/css" href="css/3p-filters.css">
</head>
<body>
<div class="body">
<div id="actions">
<p class="firstRun">
<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 id="listsOfBlockedHostsPrompt">
<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>
</div>
<div>
<div id="lists"></div>
</div>
</div>
<div id="templates">
<div class="groupEntry">
<div class="geDetails"><span class="geName"></span> <span class="geCount"></span></div>
<div class="listEntries"></div>
</div>
<div class="li listEntry">
<label><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><span class="listname forinput"></span> <span class="iconbar"><!--
--><a class="fa-icon support" href="#" target="_blank">home</a><!--
--><a class="fa-icon mustread" href="#" target="_blank">info-circle</a><!--
--></span></span></label>
</div>
</div>
<script src="js/fa-icons.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>
<script src="js/3p-filters.js" type="module"></script>
</body>
</html>

View File

@ -222,5 +222,57 @@
"genericRevert": {
"message": "Revert",
"description": "for generic 'Revert' buttons"
},
"firstRunSectionLabel": {
"message": "Welcome",
"description": "The header text for the welcome message section"
},
"firstRunDescription": {
"message": "You have just installed uBO Lite. You can choose here the default filtering mode to use on all websites.\n\nBy default, <em>Basic</em> mode is selected because it does not require the permission to read and change data. If you trust uBO Lite, you can give it broad permission to read and change data on all websites in order to enable more advanced filtering capabilities for all websites by default.",
"description": "Descriptive text shown at first install time only "
},
"defaultFilteringModeSectionLabel": {
"message": "Default filtering mode",
"description": "The header text for the default filtering mode section"
},
"defaultFilteringModeDescription": {
"message": "The default filtering mode will be overriden by per-website filtering modes. You can adjust the filtering mode on any given website according to whichever mode works best on that website. Each mode has its advantages and disadvantages.",
"description": "This describes the default filtering mode setting"
},
"filteringMode0Name": {
"message": "no filtering",
"description": "Name of blocking mode 0"
},
"filteringMode1Name": {
"message": "basic",
"description": "Name of blocking mode 1"
},
"filteringMode2Name": {
"message": "optimal",
"description": "Name of blocking mode 2"
},
"filteringMode3Name": {
"message": "complete",
"description": "Name of blocking mode 3"
},
"basicFilteringModeDescription": {
"message": "Basic network filtering from selected filter lists.\n\nDoes not require permission to read and change data on websites.",
"description": "This describes the 'basic' filtering mode"
},
"optimalFilteringModeDescription": {
"message": "Advanced network filtering plus specific extended filtering from selected filter lists.\n\nRequires broad permission to read and change data on all websites.",
"description": "This describes the 'optimal' filtering mode"
},
"completeFilteringModeDescription": {
"message": "Advanced network filtering plus specific and generic extended filtering from selected filter lists.\n\nRequires broad permission to read and change data on all websites.\n\nGeneric extended filtering may cause higher webpage resources usage.",
"description": "This describes the 'complete' filtering mode"
},
"behaviorSectionLabel": {
"message": "Behavior",
"description": "The header text for the 'Behavior' section"
},
"autoReloadLabel": {
"message": "Automatically reload page when changing filtering mode",
"description": "Label for a checkbox in the options page"
}
}

View File

@ -1,5 +1,4 @@
body > div.body {
margin: 0 1em;
body {
}
h2, h3 {
margin: 1em 0;

View File

@ -1,9 +1,11 @@
html, body {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100vh;
justify-content: stretch;
padding: 0 1em;
overflow: hidden;
position: relative;
width: 100vw;

View File

@ -0,0 +1,92 @@
.filteringModeSlider {
align-items: center;
display: flex;
height: 60px;
justify-content: center;
position: relative;
width: 240px;
}
.filteringModeButton {
background-color: var(--surface-1);
box-sizing: border-box;
border-radius: 30% 15% / 15% 30%;
height: 100%;
position: absolute;
width: 25%;
z-index: 10;
}
.filteringModeButton > div {
background-color: var(--accent-surface-1);
border: 4px solid var(--accent-surface-1);
border-radius: inherit;
box-sizing: border-box;
height: calc(100% - 2px);
margin: 1px;
width: calc(100% - 2px);
}
.filteringModeSlider.moving .filteringModeButton > div,
.filteringModeButton > div:hover {
filter: brightness(0.9);
}
.filteringModeSlider[data-level="0"] .filteringModeButton > div {
background-color: var(--surface-2);
border-color: var(--surface-2);
}
.filteringModeSlider span[data-level] {
background-color: var(--accent-surface-1);
display: inline-flex;
height: 30%;
margin-left: 1px;
width: 25%;
}
.filteringModeSlider.moving span[data-level] {
pointer-events: none;
}
.filteringModeSlider[data-level="0"] .filteringModeButton {
left: 0;
}
.filteringModeSlider[data-level="1"] .filteringModeButton {
left: 25%;
}
.filteringModeSlider[data-level="2"] .filteringModeButton {
left: 50%;
}
.filteringModeSlider[data-level="3"] .filteringModeButton {
left: 75%;
}
[dir="rtl"] .filteringModeSlider[data-level="0"] .filteringModeButton {
left: 75%;
}
[dir="rtl"] .filteringModeSlider[data-level="1"] .filteringModeButton {
left: 50%;
}
[dir="rtl"] .filteringModeSlider[data-level="2"] .filteringModeButton {
left: 25%;
}
[dir="rtl"] .filteringModeSlider[data-level="3"] .filteringModeButton {
left: 0;
}
.filteringModeSlider[data-level="0"] span[data-level] {
background-color: var(--surface-2);
}
.filteringModeSlider[data-level="1"] span[data-level]:nth-of-type(1) ~ span[data-level] {
background-color: var(--surface-2);
}
.filteringModeSlider[data-level="2"] span[data-level]:nth-of-type(2) ~ span[data-level] {
background-color: var(--surface-2);
}
.filteringModeSlider[data-level]:not(.moving) span[data-level]:hover {
filter: brightness(0.9);
}

View File

@ -47,34 +47,32 @@ hr {
padding: 0;
}
#sticky {
background-color: var(--surface-1);
position: sticky;
top: 0;
z-index: 100;
}
#stickyTools {
align-items: stretch;
#filteringModeText {
background-color: var(--surface-2);
color: var(--ink-3);
display: flex;
justify-content: space-between;
padding: var(--default-gap-xsmall);
text-transform: lowercase;
}
#switch {
color: var(--popup-power-ink);
cursor: pointer;
display: flex;
fill: var(--popup-power-ink);
flex-grow: 1;
font-size: 96px;
justify-content: center;
margin: var(--popup-gap-thin) var(--popup-gap-thin) 0;
padding: 0;
stroke: none;
stroke-width: 64;
#filteringModeText > span:nth-of-type(2) {
display: none;
}
body.off #switch {
fill: var(--surface-1);
stroke: var(--checkbox-ink);
#filteringModeText > span:nth-of-type(2):not(:empty) {
display: inline;
}
#filteringModeText > span:nth-of-type(2):not(:empty)::before {
content: '\2002\2192\2002';
}
[dir="rtl"] #filteringModeText > span:nth-of-type(2):not(:empty)::before {
content: '\2002\2190\2002';
}
.filteringModeSlider {
height: 32px;
margin: 8px;
width: 128px;
}
.rulesetTools {
background-color: transparent;
border: 0;
@ -100,9 +98,7 @@ body.off #switch {
.rulesetTools [id] > svg {
fill: var(--ink-4);
}
body.needReload #refresh,
body.needSave #saveRules,
body.needSave #revertRules {
body.needReload #refresh {
visibility: visible;
}
#hostname {
@ -184,32 +180,6 @@ body.mobile.no-tooltips .toolRibbon .tool {
margin-bottom: 0;
}
#toggleGreatPowers {
position: relative;
}
body.hasOmnipotence #toggleGreatPowers {
pointer-events: none;
}
#toggleGreatPowers .badge {
bottom: 4px;
font-size: var(--font-size-xsmall);
line-height: 1;
pointer-events: none;
position: absolute;
right: 4px;
}
body:not(.hasGreatPowers) [data-i18n-title="popupGrantGreatPowers"],
body.hasGreatPowers [data-i18n-title="popupRevokeGreatPowers"] {
display: flex;
}
body:not(.hasGreatPowers) [data-i18n-title="popupRevokeGreatPowers"],
body.hasGreatPowers [data-i18n-title="popupGrantGreatPowers"] {
display: none;
}
body:not(.hasOmnipotence) [data-i18n-title="popupRevokeGreatPowers"] {
fill: var(--popup-power-ink);
}
#moreOrLess {
column-gap: 0;
display: grid;
@ -281,22 +251,12 @@ body:not([data-section~="b"]) [data-section="b"] {
:root.desktop body {
--popup-gap: calc(var(--font-size) * 0.875);
}
:root.desktop body:not(.off) #switch:hover {
fill: rgb(var(--popup-power-ink-rgb) / 90%);
}
:root.desktop body.off #switch:hover {
stroke: var(--popup-power-ink);
}
:root.desktop .rulesetTools [id]:hover {
background-color: var(--popup-ruleset-tool-surface-hover);
}
:root.desktop .rulesetTools [id]:hover > svg {
fill: var(--ink-2);
}
:root.desktop #firewall {
direction: rtl;
line-height: 1.4;
}
:root.desktop .tool:hover {
background-color: var(--popup-toolbar-surface-hover);
}

View File

@ -10,25 +10,66 @@ legend {
font-size: var(--font-size-smaller);
padding: var(--default-gap-xxsmall);
}
#actions {
background-color: var(--surface-1);
padding: 1px 0;
position: sticky;
top: 0;
z-index: 10;
body .firstRun {
display: none;
}
body.firstRun .firstRun {
background-color: rgb(var(--dashboard-highlight-surface-rgb));
display: block;
padding: 8px;
zoom: 1.1;
}
#buttonUpdate.active {
body > div {
margin: 1em 0;
}
h3 {
margin: 0;
}
p {
white-space: pre-line;
}
#defaultFilteringMode {
display: grid;
gap: 1em;
grid: auto-flow dense / 1fr 1fr 1fr;
}
.filteringModeCard {
border: 1px solid var(--surface-3);
border-radius: 4px;
display: flex;
flex-direction: column;
}
.filteringModeCard:has(.radio > [type="radio"]:checked) {
background-color: var(--surface-0);
}
.filteringModeCard .input.radio ~ [data-i18n] {
text-transform: capitalize;
}
.filteringModeCard span:has(> .input) {
align-items: center;
display: inline-flex;
}
.filteringModeCard > div {
align-items: center;
box-sizing: border-box;
display: flex;
padding: 0.5em;
width: 100%;
}
.filteringModeCard > div:nth-of-type(2) {
justify-content: center;
}
.filteringModeCard > div:nth-of-type(3) {
border-top: 1px solid var(--surface-2);
font-size: var(--font-size-smaller);
white-space: pre-line;
}
.filteringModeSlider {
height: calc(60px / 2);
pointer-events: none;
width: calc(240px / 2);
}
#buttonUpdate.active .fa-icon svg {
animation: spin 1s linear infinite;
transform-origin: 50%;
}
#lists {
margin: 0.5em 0 0 0;
padding: 0;
@ -74,15 +115,8 @@ body.firstRun .firstRun {
.listEntry .listname {
white-space: nowrap;
}
.listEntry.toRemove .checkbox {
visibility: hidden;
}
.listEntry.toRemove .listname {
text-decoration: line-through;
}
.listEntry a,
.listEntry .fa-icon,
.listEntry .counts {
.listEntry .fa-icon {
color: var(--info0-ink);
fill: var(--info0-ink);
display: none;
@ -101,75 +135,15 @@ body.firstRun .firstRun {
.listEntry.support a.support {
display: inline-flex;
}
.listEntry .remove,
.listEntry .unsecure,
.listEntry .failed {
color: var(--info3-ink);
fill: var(--info3-ink);
cursor: pointer;
}
.listEntry.external .remove {
display: inline-flex;
}
.listEntry.mustread a.mustread {
color: var(--info1-ink);
fill: var(--info1-ink);
display: inline-flex;
}
.listEntry .counts {
font-size: smaller;
}
.listEntry.checked .counts {
display: inline-block;
}
.listEntry .status {
cursor: default;
display: none;
}
.listEntry.checked.unsecure .unsecure {
display: inline-flex;
}
.listEntry.failed .failed {
display: inline-flex;
}
.listEntry .cache {
cursor: pointer;
}
.listEntry.checked.cached:not(.obsolete) .cache {
display: inline-flex;
}
.listEntry .obsolete {
color: var(--info2-ink);
fill: var(--info2-ink);
}
body:not(.updating) .listEntry.checked.obsolete .obsolete {
display: inline-flex;
}
.listEntry .updating {
transform-origin: 50%;
}
body.updating .listEntry.checked.obsolete .updating {
animation: spin 1s steps(8) infinite;
display: inline-flex;
}
.listEntry.toImport {
margin: 0.5em 0;
}
.listEntry.toImport textarea {
border: 1px solid #ccc;
box-sizing: border-box;
display: block;
font-size: smaller;
height: 6em;
margin: 0;
resize: vertical;
visibility: hidden;
white-space: pre;
width: 100%;
}
.listEntry.toImport.checked textarea {
visibility: visible;
}
/* touch-screen devices */
:root.mobile .listEntry .fa-icon {
@ -181,7 +155,6 @@ body.updating .listEntry.checked.obsolete .updating {
-webkit-margin-start: 0;
}
:root.mobile .li.listEntry {
/* background-color: var(--bg-1); */
overflow-x: auto;
}
:root.mobile .li.listEntry label > span:not([class]) {

View File

@ -15,7 +15,7 @@
<!-- -------- -->
<div id="dashboard-nav">
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg"></span><!--
--><button class="tabButton" type="button" data-pane="3p-filters.html" data-i18n="3pPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="settings.html" data-i18n="settingsPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="about.html" data-i18n="aboutPageName" tabindex="0"></button>
</div>
<!-- -------- -->

View File

@ -47,15 +47,19 @@ import {
} from './scripting-manager.js';
import {
matchesTrustedSiteDirective,
toggleTrustedSiteDirective,
} from './trusted-sites.js';
getFilteringMode,
setFilteringMode,
getDefaultFilteringMode,
setDefaultFilteringMode,
syncWithDemotedOrigins,
} from './mode-manager.js';
/******************************************************************************/
const rulesetConfig = {
version: '',
enabledRulesets: [],
enabledRulesets: [ 'default' ],
autoReload: 1,
firstRun: false,
};
@ -73,12 +77,25 @@ async function loadRulesetConfig() {
rulesetConfig.firstRun = true;
return;
}
let rawConfig;
try {
rawConfig = JSON.parse(self.atob(configRule.condition.urlFilter));
} catch(ex) {
}
// New format
if ( Array.isArray(rawConfig) ) {
rulesetConfig.version = rawConfig[0];
rulesetConfig.enabledRulesets = rawConfig[1];
rulesetConfig.autoReload = rawConfig[2];
return;
}
// Legacy format. TODO: remove when next new format is widely in use.
const match = /^\|\|(?:example|ubolite)\.invalid\/([^\/]+)\/(?:([^\/]+)\/)?/.exec(
configRule.condition.urlFilter
);
if ( match === null ) { return; }
rulesetConfig.version = match[1];
if ( match[2] ) {
rulesetConfig.enabledRulesets =
@ -97,13 +114,21 @@ async function saveRulesetConfig() {
},
condition: {
urlFilter: '',
initiatorDomains: [
'ubolite.invalid',
],
resourceTypes: [
'main_frame',
],
},
};
}
const version = rulesetConfig.version;
const enabledRulesets = encodeURIComponent(rulesetConfig.enabledRulesets.join(' '));
const urlFilter = `||ubolite.invalid/${version}/${enabledRulesets}/`;
const rawConfig = [
rulesetConfig.version,
rulesetConfig.enabledRulesets,
rulesetConfig.autoReload,
];
const urlFilter = self.btoa(JSON.stringify(rawConfig));
if ( urlFilter === configRule.condition.urlFilter ) { return; }
configRule.condition.urlFilter = urlFilter;
@ -128,18 +153,13 @@ function hasOmnipotence() {
});
}
function onPermissionsAdded(permissions) {
if ( permissions.origins?.includes('<all_urls>') ) {
updateDynamicRules();
}
registerInjectables(permissions.origins);
}
function onPermissionsRemoved(permissions) {
if ( permissions.origins?.includes('<all_urls>') ) {
updateDynamicRules();
}
registerInjectables(permissions.origins);
syncWithDemotedOrigins(permissions.origins).then(( ) => {
registerInjectables(permissions.origins);
});
}
/******************************************************************************/
@ -158,17 +178,22 @@ function onMessage(request, sender, callback) {
return true;
}
case 'getRulesetData': {
case 'getOptionsPageData': {
Promise.all([
getDefaultFilteringMode(),
getRulesetDetails(),
dnr.getEnabledRulesets(),
hasOmnipotence(),
]).then(results => {
const [ rulesetDetails, enabledRulesets, hasOmnipotence ] = results;
const [
defaultFilteringMode,
rulesetDetails,
enabledRulesets,
] = results;
callback({
defaultFilteringMode,
enabledRulesets,
rulesetDetails: Array.from(rulesetDetails.values()),
hasOmnipotence,
autoReload: rulesetConfig.autoReload === 1,
firstRun: rulesetConfig.firstRun,
});
rulesetConfig.firstRun = false;
@ -176,16 +201,24 @@ function onMessage(request, sender, callback) {
return true;
}
case 'setAutoReload':
rulesetConfig.autoReload = request.state ? 1 : 0;
saveRulesetConfig().then(( ) => {
callback();
});
return true;
case 'popupPanelData': {
Promise.all([
matchesTrustedSiteDirective(request),
getFilteringMode(request.hostname),
hasOmnipotence(),
hasGreatPowers(request.origin),
getEnabledRulesetsDetails(),
getInjectableCount(request.origin),
]).then(results => {
callback({
isTrusted: results[0],
level: results[0],
autoReload: rulesetConfig.autoReload === 1,
hasOmnipotence: results[1],
hasGreatPowers: results[2],
rulesetDetails: results[3],
@ -195,17 +228,44 @@ function onMessage(request, sender, callback) {
return true;
}
case 'toggleTrustedSiteDirective': {
toggleTrustedSiteDirective(request).then(response => {
case 'getFilteringMode': {
getFilteringMode(request.hostname).then(actualLevel => {
callback(actualLevel);
});
return true;
}
case 'setFilteringMode': {
getFilteringMode(request.hostname).then(actualLevel => {
if ( request.level === actualLevel ) { return actualLevel; }
return setFilteringMode(request.hostname, request.level);
}).then(actualLevel => {
registerInjectables();
callback(response);
callback(actualLevel);
});
return true;
}
case 'setDefaultFilteringMode': {
getDefaultFilteringMode(
).then(beforeLevel =>
setDefaultFilteringMode(request.level).then(afterLevel =>
({ beforeLevel, afterLevel })
)
).then(({ beforeLevel, afterLevel }) => {
if ( beforeLevel === 1 || afterLevel === 1 ) {
updateDynamicRules();
}
if ( afterLevel !== beforeLevel ) {
registerInjectables();
}
callback(afterLevel);
});
return true;
}
default:
break;
}
}
@ -245,7 +305,6 @@ async function start() {
runtime.onMessage.addListener(onMessage);
browser.permissions.onAdded.addListener(onPermissionsAdded);
browser.permissions.onRemoved.addListener(onPermissionsRemoved);
if ( rulesetConfig.firstRun ) {

View File

@ -115,7 +115,7 @@ if ( self.location.hash.slice(1) === 'no-dashboard.html' ) {
if ( self.location.hash !== '' ) {
pane = self.location.hash.slice(1) || null;
}
loadDashboardPanel(pane !== null ? pane : '3p-filters.html', true);
loadDashboardPanel(pane !== null ? pane : 'settings.html', true);
dom.on(
qs$('#dashboard-nav'),

View File

@ -0,0 +1,366 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-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
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
import { dnr } from './ext.js';
import {
hostnamesFromMatches,
isDescendantHostnameOfIter,
} from './utils.js';
import {
TRUSTED_DIRECTIVE_BASE_RULE_ID,
BLOCKING_MODES_RULE_ID,
getDynamicRules
} from './ruleset-manager.js';
/******************************************************************************/
const pruneDescendantHostnamesFromSet = (hostname, hnSet) => {
for ( const hn of hnSet ) {
if ( hn.endsWith(hostname) === false ) { continue; }
if ( hn === hostname ) { continue; }
if ( hn.at(-hostname.length-1) !== '.' ) { continue; }
hnSet.delete(hn);
}
};
/******************************************************************************/
const eqSets = (setBefore, setAfter) => {
for ( const hn of setAfter ) {
if ( setBefore.has(hn) === false ) { return false; }
}
for ( const hn of setBefore ) {
if ( setAfter.has(hn) === false ) { return false; }
}
return true;
};
/******************************************************************************/
// 0: no blocking => TRUSTED_DIRECTIVE_BASE_RULE_ID / requestDomains
// 1: network => BLOCKING_MODES_RULE_ID / excludedInitiatorDomains
// 2: specific content => BLOCKING_MODES_RULE_ID / excludedRequestDomains
// 3: generic content => BLOCKING_MODES_RULE_ID / initiatorDomains
let filteringModeDetailsPromise;
function getActualFilteringModeDetails() {
if ( filteringModeDetailsPromise !== undefined ) {
return filteringModeDetailsPromise;
}
filteringModeDetailsPromise = Promise.all([
getDynamicRules(),
getAllTrustedSiteDirectives(),
]).then(results => {
const [ dynamicRuleMap, trustedSiteDirectives ] = results;
const details = {
none: new Set(trustedSiteDirectives),
};
const rule = dynamicRuleMap.get(BLOCKING_MODES_RULE_ID);
if ( rule ) {
details.network = new Set(rule.condition.excludedInitiatorDomains);
details.extendedSpecific = new Set(rule.condition.excludedRequestDomains);
details.extendedGeneric = new Set(rule.condition.initiatorDomains);
} else {
details.network = new Set([ 'all-urls' ]);
details.extendedSpecific = new Set();
details.extendedGeneric = new Set();
}
return details;
});
return filteringModeDetailsPromise;
}
/******************************************************************************/
async function getFilteringModeDetails() {
const actualDetails = await getActualFilteringModeDetails();
return {
none: new Set(actualDetails.none),
network: new Set(actualDetails.network),
extendedSpecific: new Set(actualDetails.extendedSpecific),
extendedGeneric: new Set(actualDetails.extendedGeneric),
};
}
/******************************************************************************/
async function setFilteringModeDetails(afterDetails) {
const [ dynamicRuleMap, actualDetails ] = await Promise.all([
getDynamicRules(),
getActualFilteringModeDetails(),
]);
const addRules = [];
const removeRuleIds = [];
if ( eqSets(actualDetails.none, afterDetails.none) === false ) {
actualDetails.none = afterDetails.none;
if ( dynamicRuleMap.has(TRUSTED_DIRECTIVE_BASE_RULE_ID) ) {
removeRuleIds.push(TRUSTED_DIRECTIVE_BASE_RULE_ID);
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID);
}
const rule = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID,
action: { type: 'allowAllRequests' },
condition: {
requestDomains: [],
resourceTypes: [ 'main_frame' ],
},
};
if ( actualDetails.none.size ) {
rule.condition.requestDomains = Array.from(actualDetails.none);
addRules.push(rule);
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule);
}
}
if (
eqSets(actualDetails.network, afterDetails.network) === false ||
eqSets(actualDetails.extendedSpecific, afterDetails.extendedSpecific) === false ||
eqSets(actualDetails.extendedGeneric, afterDetails.extendedGeneric) === false
) {
actualDetails.network = afterDetails.network;
actualDetails.extendedSpecific = afterDetails.extendedSpecific;
actualDetails.extendedGeneric = afterDetails.extendedGeneric;
if ( dynamicRuleMap.has(BLOCKING_MODES_RULE_ID) ) {
removeRuleIds.push(BLOCKING_MODES_RULE_ID);
dynamicRuleMap.delete(BLOCKING_MODES_RULE_ID);
}
const rule = {
id: BLOCKING_MODES_RULE_ID,
action: { type: 'allow' },
condition: {
resourceTypes: [ 'main_frame' ],
urlFilter: '||ubol-blocking-modes.invalid^',
},
};
if ( actualDetails.network.size ) {
rule.condition.excludedInitiatorDomains =
Array.from(actualDetails.network);
}
if ( actualDetails.extendedSpecific.size ) {
rule.condition.excludedRequestDomains =
Array.from(actualDetails.extendedSpecific);
}
if ( actualDetails.extendedGeneric.size ) {
rule.condition.initiatorDomains =
Array.from(actualDetails.extendedGeneric);
}
if (
actualDetails.network.size ||
actualDetails.extendedSpecific.size ||
actualDetails.extendedGeneric.size
) {
addRules.push(rule);
dynamicRuleMap.set(BLOCKING_MODES_RULE_ID, rule);
}
}
if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; }
const updateOptions = {};
if ( addRules.length ) {
updateOptions.addRules = addRules;
}
if ( removeRuleIds.length ) {
updateOptions.removeRuleIds = removeRuleIds;
}
return dnr.updateDynamicRules(updateOptions);
}
/******************************************************************************/
async function getFilteringMode(hostname) {
const filteringModes = await getFilteringModeDetails();
if ( filteringModes.none.has(hostname) ) { return 0; }
if ( filteringModes.network.has(hostname) ) { return 1; }
if ( filteringModes.extendedSpecific.has(hostname) ) { return 2; }
if ( filteringModes.extendedGeneric.has(hostname) ) { return 3; }
return getDefaultFilteringMode();
}
/******************************************************************************/
async function setFilteringMode(hostname, afterLevel) {
if ( hostname === 'all-urls' ) {
return setDefaultFilteringMode(afterLevel);
}
const [
beforeLevel,
defaultLevel,
filteringModes
] = await Promise.all([
getFilteringMode(hostname),
getDefaultFilteringMode(),
getFilteringModeDetails(),
]);
if ( afterLevel === beforeLevel ) { return afterLevel; }
const {
none,
network,
extendedSpecific,
extendedGeneric,
} = filteringModes;
switch ( beforeLevel ) {
case 0:
none.delete(hostname);
break;
case 1:
network.delete(hostname);
break;
case 2:
extendedSpecific.delete(hostname);
break;
case 3:
extendedGeneric.delete(hostname);
break;
}
if ( afterLevel !== defaultLevel ) {
switch ( afterLevel ) {
case 0:
if ( isDescendantHostnameOfIter(hostname, none) === false ) {
filteringModes.none.add(hostname);
pruneDescendantHostnamesFromSet(hostname, none);
}
break;
case 1:
if ( isDescendantHostnameOfIter(hostname, network) === false ) {
filteringModes.network.add(hostname);
pruneDescendantHostnamesFromSet(hostname, network);
}
break;
case 2:
if ( isDescendantHostnameOfIter(hostname, extendedSpecific) === false ) {
filteringModes.extendedSpecific.add(hostname);
pruneDescendantHostnamesFromSet(hostname, extendedSpecific);
}
break;
case 3:
if ( isDescendantHostnameOfIter(hostname, extendedGeneric) === false ) {
filteringModes.extendedGeneric.add(hostname);
pruneDescendantHostnamesFromSet(hostname, extendedGeneric);
}
break;
}
}
await setFilteringModeDetails(filteringModes);
return getFilteringMode(hostname);
}
/******************************************************************************/
async function getDefaultFilteringMode() {
const filteringModes = await getFilteringModeDetails();
if ( filteringModes.none.has('all-urls') ) { return 0; }
if ( filteringModes.network.has('all-urls') ) { return 1; }
if ( filteringModes.extendedSpecific.has('all-urls') ) { return 2; }
if ( filteringModes.extendedGeneric.has('all-urls') ) { return 3; }
return 1;
}
/******************************************************************************/
async function setDefaultFilteringMode(afterLevel) {
const [ beforeLevel, filteringModes ] = await Promise.all([
getDefaultFilteringMode(),
getFilteringModeDetails(),
]);
if ( afterLevel === beforeLevel ) { return afterLevel; }
switch ( afterLevel ) {
case 0:
filteringModes.none.clear();
filteringModes.none.add('all-urls');
break;
case 1:
filteringModes.network.clear();
filteringModes.network.add('all-urls');
break;
case 2:
filteringModes.extendedSpecific.clear();
filteringModes.extendedSpecific.add('all-urls');
break;
case 3:
filteringModes.extendedGeneric.clear();
filteringModes.extendedGeneric.add('all-urls');
break;
}
switch ( beforeLevel ) {
case 0:
filteringModes.none.delete('all-urls');
break;
case 1:
filteringModes.network.delete('all-urls');
break;
case 2:
filteringModes.extendedSpecific.delete('all-urls');
break;
case 3:
filteringModes.extendedGeneric.delete('all-urls');
break;
}
await setFilteringModeDetails(filteringModes);
return getDefaultFilteringMode();
}
/******************************************************************************/
async function syncWithDemotedOrigins(demotedOrigins) {
const demotedHostnames = new Set(hostnamesFromMatches(demotedOrigins));
if ( demotedHostnames.has('all-urls') ) {
await setDefaultFilteringMode(1);
}
const filteringModes = await getFilteringModeDetails();
const { extendedSpecific, extendedGeneric } = filteringModes;
for ( const hn of extendedSpecific ) {
if ( demotedHostnames.has(hn) === false ) { continue; }
extendedSpecific.delete(hn);
}
for ( const hn of extendedGeneric ) {
if ( demotedHostnames.has(hn) === false ) { continue; }
extendedGeneric.delete(hn);
}
return setFilteringModeDetails(filteringModes);
}
/******************************************************************************/
async function getAllTrustedSiteDirectives() {
const dynamicRuleMap = await getDynamicRules();
const rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule === undefined ) { return []; }
return rule.condition.requestDomains;
}
/******************************************************************************/
export {
getFilteringMode,
setFilteringMode,
getDefaultFilteringMode,
setDefaultFilteringMode,
getFilteringModeDetails,
getAllTrustedSiteDirectives,
syncWithDemotedOrigins,
};

View File

@ -32,69 +32,172 @@ import { simpleStorage } from './storage.js';
/******************************************************************************/
let currentTab = {};
const popupPanelData = {};
const currentTab = {};
let tabHostname = '';
/******************************************************************************/
let originalStateHash = '';
function getCurrentStateHash() {
const parts = [
dom.cl.has(dom.body, 'off'),
dom.cl.has(dom.body, 'hasGreatPowers'),
];
return parts.join('\t');
}
function onStateHashChanged() {
dom.cl.toggle(
dom.body,
'needReload',
getCurrentStateHash() !== originalStateHash
);
function normalizedHostname(hn) {
return hn.replace(/^www\./, '');
}
/******************************************************************************/
async function toggleTrustedSiteDirective() {
let url;
try {
url = new URL(currentTab.url);
} catch(ex) {
return;
const BLOCKING_MODE_MAX = 3;
function setFilteringMode(level, commit = false) {
const modeSlider = qs$('.filteringModeSlider');
modeSlider.dataset.level = level;
if ( qs$('.filteringModeSlider.moving') === null ) {
dom.text(
qs$('#filteringModeText > span:nth-of-type(1)'),
i18n$(`filteringMode${level}Name`)
);
}
if ( url instanceof URL === false ) { return; }
if ( commit !== true ) { return; }
commitFilteringMode();
}
const targetTrustedState = dom.cl.has(dom.body, 'off');
const newTrustedState = await sendMessage({
what: 'toggleTrustedSiteDirective',
origin: url.origin,
state: targetTrustedState,
tabId: currentTab.id,
}).catch(( ) =>
targetTrustedState === false
async function commitFilteringMode() {
if ( tabHostname === '' ) { return; }
const targetHostname = normalizedHostname(tabHostname);
const modeSlider = qs$('.filteringModeSlider');
const afterLevel = parseInt(modeSlider.dataset.level, 10);
const beforeLevel = parseInt(modeSlider.dataset.levelBefore, 10);
if ( afterLevel > 1 ) {
let granted = false;
try {
granted = await browser.permissions.request({
origins: [ `*://*.${targetHostname}/*` ],
});
} catch(ex) {
}
if ( granted !== true ) {
setFilteringMode(beforeLevel);
return;
}
}
dom.text(
qs$('#filteringModeText > span:nth-of-type(1)'),
i18n$(`filteringMode${afterLevel}Name`)
);
dom.cl.toggle(dom.body, 'off', newTrustedState === true);
onStateHashChanged();
}
dom.on(qs$('#switch'), 'click', toggleTrustedSiteDirective);
/******************************************************************************/
function reloadTab(ev) {
browser.tabs.reload(currentTab.id, {
bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey,
const actualLevel = await sendMessage({
what: 'setFilteringMode',
hostname: targetHostname,
level: afterLevel,
});
dom.cl.remove(dom.body, 'needReload');
originalStateHash = getCurrentStateHash();
if ( actualLevel !== afterLevel ) {
setFilteringMode(actualLevel);
}
if ( actualLevel !== beforeLevel && popupPanelData.autoReload ) {
browser.tabs.reload(currentTab.id);
}
}
dom.on(qs$('#refresh'), 'click', reloadTab);
{
let mx0 = 0;
let mx1 = 0;
let l0 = 0;
let lMax = 0;
let timer;
const move = ( ) => {
timer = undefined;
const l1 = Math.min(Math.max(l0 + mx1 - mx0, 0), lMax);
let level = Math.floor(l1 * BLOCKING_MODE_MAX / lMax);
if ( qs$('body[dir="rtl"]') !== null ) {
level = 3 - level;
}
const modeSlider = qs$('.filteringModeSlider');
if ( `${level}` === modeSlider.dataset.level ) { return; }
dom.text(
qs$('#filteringModeText > span:nth-of-type(2)'),
i18n$(`filteringMode${level}Name`)
);
setFilteringMode(level);
};
const moveAsync = ev => {
if ( timer !== undefined ) { return; }
mx1 = ev.pageX;
timer = self.requestAnimationFrame(move);
};
const stop = ev => {
if ( ev.button !== 0 ) { return; }
const modeSlider = qs$('.filteringModeSlider');
if ( dom.cl.has(modeSlider, 'moving') === false ) { return; }
dom.cl.remove(modeSlider, 'moving');
self.removeEventListener('mousemove', moveAsync, { capture: true });
self.removeEventListener('mouseup', stop, { capture: true });
dom.text(qs$('#filteringModeText > span:nth-of-type(2)'), '');
commitFilteringMode();
ev.stopPropagation();
ev.preventDefault();
if ( timer !== undefined ) {
self.cancelAnimationFrame(timer);
timer = undefined;
}
};
const startSliding = ev => {
if ( ev.button !== 0 ) { return; }
const modeButton = qs$('.filteringModeButton');
if ( ev.currentTarget !== modeButton ) { return; }
const modeSlider = qs$('.filteringModeSlider');
if ( dom.cl.has(modeSlider, 'moving') ) { return; }
modeSlider.dataset.levelBefore = modeSlider.dataset.level;
mx0 = ev.pageX;
const buttonRect = modeButton.getBoundingClientRect();
l0 = buttonRect.left + buttonRect.width / 2;
const sliderRect = modeSlider.getBoundingClientRect();
lMax = sliderRect.width - buttonRect.width ;
dom.cl.add(modeSlider, 'moving');
self.addEventListener('mousemove', moveAsync, { capture: true });
self.addEventListener('mouseup', stop, { capture: true });
ev.stopPropagation();
ev.preventDefault();
};
dom.on(qs$('.filteringModeButton'), 'mousedown', startSliding);
}
dom.on(
qs$('.filteringModeSlider'),
'click',
'.filteringModeSlider span[data-level]',
ev => {
const modeSlider = qs$('.filteringModeSlider');
modeSlider.dataset.levelBefore = modeSlider.dataset.level;
const span = ev.target;
const level = parseInt(span.dataset.level, 10);
setFilteringMode(level, true);
}
);
dom.on(
qs$('.filteringModeSlider'),
'mouseenter',
'.filteringModeSlider span[data-level]',
ev => {
const span = ev.target;
const level = parseInt(span.dataset.level, 10);
dom.text(
qs$('#filteringModeText > span:nth-of-type(2)'),
i18n$(`filteringMode${level}Name`)
);
}
);
dom.on(
qs$('.filteringModeSlider'),
'mouseleave',
'.filteringModeSlider span[data-level]',
( ) => {
dom.text(qs$('#filteringModeText > span:nth-of-type(2)'), '');
}
);
/******************************************************************************/
@ -156,42 +259,10 @@ dom.on(qs$('#lessButton'), 'click', ( ) => {
/******************************************************************************/
async function grantGreatPowers() {
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');
onStateHashChanged();
}
async function revokeGreatPowers() {
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');
onStateHashChanged();
}
dom.on(qs$('#toggleGreatPowers'), 'click', ( ) => {
if ( dom.cl.has(dom.body, 'hasGreatPowers' ) ) {
revokeGreatPowers();
} else {
grantGreatPowers();
}
});
/******************************************************************************/
async function init() {
const [ tab ] = await browser.tabs.query({ active: true });
if ( tab instanceof Object === false ) { return true; }
currentTab = tab;
Object.assign(currentTab, tab);
let url;
try {
@ -200,37 +271,20 @@ async function init() {
} catch(ex) {
}
let popupPanelData = {};
if ( url !== undefined ) {
popupPanelData = await sendMessage({
const response = await sendMessage({
what: 'popupPanelData',
origin: url.origin,
hostname: normalizedHostname(tabHostname),
});
if ( response instanceof Object ) {
Object.assign(popupPanelData, response);
}
}
dom.cl.toggle(
dom.body,
'off',
popupPanelData.isTrusted === true
);
dom.cl.toggle(
dom.body,
'hasOmnipotence',
popupPanelData.hasOmnipotence === true
);
dom.cl.toggle(
dom.body,
'hasGreatPowers',
popupPanelData.hasGreatPowers === true
);
setFilteringMode(popupPanelData.level);
dom.text(qs$('#hostname'), tabHostname);
dom.text(
qs$('#toggleGreatPowers .badge'),
popupPanelData.injectableCount || ''
);
const parent = qs$('#rulesetStats');
for ( const details of popupPanelData.rulesetDetails || [] ) {
@ -253,8 +307,6 @@ async function init() {
dom.cl.remove(dom.body, 'loading');
originalStateHash = getCurrentStateHash();
return true;
}

View File

@ -36,6 +36,7 @@ const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE;
const REMOVEPARAMS_REALM_START = 2000000;
const REMOVEPARAMS_REALM_END = REMOVEPARAMS_REALM_START + RULE_REALM_SIZE;
const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000;
const BLOCKING_MODES_RULE_ID = TRUSTED_DIRECTIVE_BASE_RULE_ID + 1;
const CURRENT_CONFIG_BASE_RULE_ID = 9000000;
/******************************************************************************/
@ -67,8 +68,8 @@ function getDynamicRules() {
const map = new Map(
rules.map(rule => [ rule.id, rule ])
);
console.log(`Dynamic rule count: ${map.size}`);
console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - map.size}`);
console.info(`Dynamic rule count: ${map.size}`);
console.info(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - map.size}`);
return map;
});
return dynamicRuleMapPromise;
@ -362,8 +363,9 @@ async function getEnabledRulesetsDetails() {
/******************************************************************************/
export {
TRUSTED_DIRECTIVE_BASE_RULE_ID,
BLOCKING_MODES_RULE_ID,
CURRENT_CONFIG_BASE_RULE_ID,
TRUSTED_DIRECTIVE_BASE_RULE_ID,
getRulesetDetails,
getDynamicRules,
enableRulesets,

View File

@ -27,7 +27,8 @@
import { browser, dnr } from './ext.js';
import { fetchJSON } from './fetch.js';
import { getAllTrustedSiteDirectives } from './trusted-sites.js';
import { getFilteringModeDetails } from './mode-manager.js';
import { getEnabledRulesetsDetails } from './ruleset-manager.js';
import * as ut from './utils.js';
@ -51,44 +52,8 @@ function getScriptingDetails() {
/******************************************************************************/
const toRegisterable = (fname, hostnames, trustedSites) => {
const directive = {
id: fname,
allFrames: true,
matchOriginAsFallback: true,
};
if ( hostnames ) {
directive.matches = ut.matchesFromHostnames(hostnames);
} else {
directive.matches = [ '<all_urls>' ];
}
if (
directive.matches.length === 1 &&
directive.matches[0] === '<all_urls>'
) {
directive.excludeMatches = ut.matchesFromHostnames(trustedSites);
}
directive.js = [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ];
if ( (ut.fidFromFileName(fname) & RUN_AT_END_BIT) !== 0 ) {
directive.runAt = 'document_end';
} else {
directive.runAt = 'document_start';
}
if ( (ut.fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
directive.world = 'MAIN';
}
return directive;
};
const RUN_AT_END_BIT = 0b10;
const MAIN_WORLD_BIT = 0b01;
/******************************************************************************/
// Important: We need to sort the arrays for fast comparison
const arrayEq = (a, b) => {
if ( a === undefined ) { return b === undefined; }
if ( b === undefined ) { return false; }
const arrayEq = (a = [], b = []) => {
const alen = a.length;
if ( alen !== b.length ) { return false; }
a.sort(); b.sort();
@ -98,30 +63,129 @@ const arrayEq = (a, b) => {
return true;
};
const shouldUpdate = (registered, afterHostnames, afterExcludeHostnames) => {
if ( afterHostnames.length === 1 && afterHostnames[0] === '*' ) {
const beforeExcludeHostnames = registered.excludeMatches &&
ut.hostnamesFromMatches(registered.excludeMatches) ||
[];
if ( arrayEq(beforeExcludeHostnames, afterExcludeHostnames) === false ) {
return true;
}
/******************************************************************************/
const toRegisterableScript = (context, fname, hostnames) => {
if ( context.before.has(fname) ) {
return toUpdatableScript(context, fname, hostnames);
}
const beforeHostnames = registered.matches &&
ut.hostnamesFromMatches(registered.matches) ||
[];
return arrayEq(beforeHostnames, afterHostnames) === false;
const matches = hostnames
? ut.matchesFromHostnames(hostnames)
: [ '<all_urls>' ];
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
? ut.matchesFromHostnames(context.filteringModeDetails.none)
: [];
const runAt = (ut.fidFromFileName(fname) & RUN_AT_END_BIT) !== 0
? 'document_end'
: 'document_start';
const directive = {
id: fname,
matches,
excludeMatches,
js: [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ],
runAt,
};
if ( (ut.fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
directive.world = 'MAIN';
}
context.toAdd.push(directive);
};
const isTrustedHostname = (trustedSites, hn) => {
if ( trustedSites.size === 0 ) { return false; }
while ( hn ) {
if ( trustedSites.has(hn) ) { return true; }
hn = ut.toBroaderHostname(hn);
const toUpdatableScript = (context, fname, hostnames) => {
const registered = context.before.get(fname);
context.before.delete(fname); // Important!
const directive = { id: fname };
const matches = hostnames
? ut.matchesFromHostnames(hostnames)
: [ '<all_urls>' ];
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
}
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
? ut.matchesFromHostnames(context.filteringModeDetails.none)
: [];
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
}
return false;
};
const RUN_AT_END_BIT = 0b10;
const MAIN_WORLD_BIT = 0b01;
/******************************************************************************/
async function registerGeneric(context, args) {
const { before } = context;
const registered = before.get('css-generic');
before.delete('css-generic'); // Important!
const {
filteringModeDetails,
rulesetsDetails,
} = args;
const js = [];
for ( const details of rulesetsDetails ) {
if ( details.css.generic.count === 0 ) { continue; }
js.push(`/rulesets/js/${details.id}.generic.js`);
}
if ( js.length === 0 ) {
if ( registered !== undefined ) {
context.toRemove.push('css-generic');
}
return;
}
const matches = [];
const excludeMatches = [];
if ( filteringModeDetails.extendedGeneric.has('all-urls') ) {
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
matches.push('<all_urls>');
} else {
matches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric));
}
if ( matches.length === 0 ) {
if ( registered !== undefined ) {
context.toRemove.push('css-generic');
}
return;
}
// register
if ( registered === undefined ) {
context.toAdd.push({
id: 'css-generic',
js,
matches,
excludeMatches,
runAt: 'document_idle',
});
return;
}
// update
const directive = { id: 'css-generic' };
if ( arrayEq(registered.js, js) === false ) {
directive.js = js;
}
if ( arrayEq(registered.matches, matches) === false ) {
directive.matches = matches;
}
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
directive.excludeMatches = excludeMatches;
}
if ( directive.js || directive.matches || directive.excludeMatches ) {
context.toUpdate.push(directive);
}
}
/******************************************************************************/
async function getInjectableCount(origin) {
@ -158,16 +222,27 @@ async function getInjectableCount(origin) {
/******************************************************************************/
function registerSomeInjectables(args) {
function registerSpecific(args) {
const {
hostnamesSet,
trustedSites,
rulesetIds,
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
} = args;
// Combined both specific and generic sets
if (
filteringModeDetails.extendedSpecific.has('all-urls') ||
filteringModeDetails.extendedGeneric.has('all-urls')
) {
return registerAllSpecific(args);
}
const targetHostnames = [
...filteringModeDetails.extendedSpecific,
...filteringModeDetails.extendedGeneric,
];
const toRegisterMap = new Map();
const trustedSitesSet = new Set(trustedSites);
const checkMatches = (hostnamesToFidsMap, hn) => {
let fids = hostnamesToFidsMap.get(hn);
@ -189,11 +264,10 @@ function registerSomeInjectables(args) {
}
};
for ( const rulesetId of rulesetIds ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetId);
for ( const rulesetDetails of rulesetsDetails ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id);
if ( hostnamesToFidsMap === undefined ) { continue; }
for ( let hn of hostnamesSet ) {
if ( isTrustedHostname(trustedSitesSet, hn) ) { continue; }
for ( let hn of targetHostnames ) {
while ( hn ) {
checkMatches(hostnamesToFidsMap, hn);
hn = ut.toBroaderHostname(hn);
@ -204,21 +278,25 @@ function registerSomeInjectables(args) {
return toRegisterMap;
}
function registerAllInjectables(args) {
function registerAllSpecific(args) {
const {
trustedSites,
rulesetIds,
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
} = args;
const toRegisterMap = new Map();
const trustedSitesSet = new Set(trustedSites);
const excludeSet = new Set([
...filteringModeDetails.network,
...filteringModeDetails.none,
]);
for ( const rulesetId of rulesetIds ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetId);
for ( const rulesetDetails of rulesetsDetails ) {
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id);
if ( hostnamesToFidsMap === undefined ) { continue; }
for ( let [ hn, fids ] of hostnamesToFidsMap ) {
if ( isTrustedHostname(trustedSitesSet, hn) ) { continue; }
if ( excludeSet.has(hn) ) { continue; }
if ( ut.isDescendantHostnameOfIter(hn, excludeSet) ) { continue; }
if ( typeof fids === 'number' ) { fids = [ fids ]; }
for ( const fid of fids ) {
const fname = ut.fnameFromFileId(fid);
@ -248,80 +326,65 @@ async function registerInjectables(origins) {
if ( browser.scripting === undefined ) { return false; }
const [
hostnamesSet,
trustedSites,
rulesetIds,
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
registered,
] = await Promise.all([
browser.permissions.getAll(),
getAllTrustedSiteDirectives(),
dnr.getEnabledRulesets(),
getFilteringModeDetails(),
getEnabledRulesetsDetails(),
getScriptingDetails(),
browser.scripting.getRegisteredContentScripts(),
]).then(results => {
results[0] = new Set(ut.hostnamesFromMatches(results[0].origins));
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 = [], toUpdate = [], toRemove = [];
const promises = [];
const context = {
filteringModeDetails,
before,
toAdd,
toUpdate,
toRemove,
};
await registerGeneric(context, { filteringModeDetails, rulesetsDetails, });
const toRegisterMap = registerSpecific({
filteringModeDetails,
rulesetsDetails,
scriptingDetails,
});
const toAdd = [];
const toUpdate = [];
for ( const [ fname, hostnames ] of toRegisterMap ) {
if ( before.has(fname) === false ) {
toAdd.push(toRegisterable(fname, hostnames, trustedSites));
continue;
}
if ( shouldUpdate(before.get(fname), hostnames, trustedSites) ) {
toUpdate.push(toRegisterable(fname, hostnames, trustedSites));
}
toRegisterableScript(context, fname, hostnames);
}
toRemove.push(...Array.from(before.keys()));
const toRemove = [];
for ( const fname of before.keys() ) {
if ( toRegisterMap.has(fname) ) { continue; }
toRemove.push(fname);
}
const todo = [];
if ( toRemove.length !== 0 ) {
console.info(`Unregistered ${toRemove} content (css/js)`);
todo.push(
promises.push(
browser.scripting.unregisterContentScripts({ ids: toRemove })
.catch(reason => { console.info(reason); })
);
}
if ( toAdd.length !== 0 ) {
console.info(`Registered ${toAdd.map(v => v.id)} content (css/js)`);
todo.push(
promises.push(
browser.scripting.registerContentScripts(toAdd)
.catch(reason => { console.info(reason); })
);
}
if ( toUpdate.length !== 0 ) {
console.info(`Updated ${toUpdate.map(v => v.id)} content (css/js)`);
todo.push(
promises.push(
browser.scripting.updateContentScripts(toUpdate)
.catch(reason => { console.info(reason); })
);
}
if ( todo.length === 0 ) { return; }
if ( promises.length === 0 ) { return; }
return Promise.all(todo);
return Promise.all(promises);
}
/******************************************************************************/

View File

@ -32,7 +32,6 @@ import { simpleStorage } from './storage.js';
const rulesetMap = new Map();
let cachedRulesetData = {};
let filteringSettingsHash = '';
let hideUnusedSet = new Set([ 'regions' ]);
/******************************************************************************/
@ -44,7 +43,7 @@ function renderNumber(value) {
/******************************************************************************/
function rulesetStats(rulesetId) {
const canRemoveParams = cachedRulesetData.hasOmnipotence;
const canRemoveParams = cachedRulesetData.defaultFilteringMode > 1;
const rulesetDetails = rulesetMap.get(rulesetId);
if ( rulesetDetails === undefined ) { return; }
const { rules, filters } = rulesetDetails;
@ -202,27 +201,20 @@ function renderFilterLists(soft = false) {
dom.remove(qsa$('#lists .listEntries .listEntry.discard'));
// Compute a hash of the settings so that we can keep track of changes
// affecting the loading of filter lists.
if ( !soft ) {
filteringSettingsHash = hashFromCurrentFromSettings();
}
renderWidgets();
}
/******************************************************************************/
const renderWidgets = function() {
dom.cl.toggle(dom.body, 'firstRun', cachedRulesetData.firstRun === true);
if ( cachedRulesetData.firstRun ) {
dom.cl.add(dom.body, 'firstRun');
}
qs$('#omnipotenceWidget input').checked = cachedRulesetData.hasOmnipotence;
const defaultLevel = cachedRulesetData.defaultFilteringMode;
qs$(`.filteringModeCard input[type="radio"][value="${defaultLevel}"]`).checked = true;
dom.cl.toggle(
qs$('#buttonApply'),
'disabled',
filteringSettingsHash === hashFromCurrentFromSettings()
);
qs$('#autoReload input[type="checkbox"').checked = cachedRulesetData.autoReload;
// Compute total counts
let filterCount = 0;
@ -241,71 +233,54 @@ const renderWidgets = function() {
/******************************************************************************/
async function onOmnipotenceChanged(ev) {
async function onFilteringModeChange(ev) {
const input = ev.target;
const newState = input.checked;
const newLevel = parseInt(input.value, 10);
let granted = false;
const oldState = await browser.permissions.contains({
origins: [ '<all_urls>' ]
});
if ( newState === oldState ) { return; }
let actualState;
if ( newState ) {
actualState = await browser.permissions.request({
switch ( newLevel ) {
case 1: { // Revoke broad permissions
granted = await browser.permissions.remove({
origins: [ '<all_urls>' ]
});
} else {
actualState = await browser.permissions.remove({
origins: [ '<all_urls>' ]
}) !== true;
break;
}
case 2:
case 3: { // Request broad permissions
granted = await browser.permissions.request({
origins: [ '<all_urls>' ]
});
break;
}
default:
break;
}
if ( granted ) {
const actualLevel = await sendMessage({
what: 'setDefaultFilteringMode',
level: newLevel,
});
cachedRulesetData.defaultFilteringMode = actualLevel;
}
cachedRulesetData.hasOmnipotence = actualState;
qs$('#omnipotenceWidget input').checked = actualState;
renderFilterLists(true);
renderWidgets();
}
dom.on(
qs$('#omnipotenceWidget input'),
qs$('#defaultFilteringMode'),
'change',
ev => { onOmnipotenceChanged(ev); }
'.filteringModeCard input[type="radio"]',
ev => { onFilteringModeChange(ev); }
);
/******************************************************************************/
function hashFromCurrentFromSettings() {
const hash = [];
const listHash = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) ) {
listHash.push(dom.attr(liEntry, 'data-listkey'));
}
}
hash.push(listHash.sort().join());
return hash.join();
}
self.hasUnsavedData = function() {
return hashFromCurrentFromSettings() !== filteringSettingsHash;
};
/******************************************************************************/
function onListsetChanged(ev) {
const input = ev.target;
const li = input.closest('.listEntry');
dom.cl.toggle(li, 'checked', input.checked);
renderWidgets();
}
dom.on(
qs$('#lists'),
'change',
'.listEntry input',
onListsetChanged
);
dom.on(qs$('#autoReload input[type="checkbox"'), 'change', ev => {
sendMessage({
what: 'setAutoReload',
state: ev.target.checked,
});
});
/******************************************************************************/
@ -321,20 +296,12 @@ async function applyEnabledRulesets() {
enabledRulesets,
});
filteringSettingsHash = hashFromCurrentFromSettings();
}
async function buttonApplyHandler() {
dom.cl.remove(qs$('#buttonApply'), 'enabled');
await applyEnabledRulesets();
renderWidgets();
}
dom.on(
qs$('#buttonApply'),
'click',
( ) => { buttonApplyHandler(); }
);
dom.on(qs$('#lists'), 'change', '.listEntry input[type="checkbox"]', ( ) => {
applyEnabledRulesets();
});
/******************************************************************************/
@ -406,7 +373,7 @@ simpleStorage.getItem('hideUnusedFilterLists').then(value => {
/******************************************************************************/
sendMessage({
what: 'getRulesetData',
what: 'getOptionsPageData',
}).then(data => {
if ( !data ) { return; }
cachedRulesetData = data;

View File

@ -1,170 +0,0 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-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
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
import { dnr } from './ext.js';
import {
parsedURLromOrigin,
toBroaderHostname,
} from './utils.js';
import {
TRUSTED_DIRECTIVE_BASE_RULE_ID,
getDynamicRules
} from './ruleset-manager.js';
/******************************************************************************/
async function getAllTrustedSiteDirectives() {
const dynamicRuleMap = await getDynamicRules();
const rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule === undefined ) { return []; }
return rule.condition.requestDomains;
}
/******************************************************************************/
async function matchesTrustedSiteDirective(details) {
const hostname =
details.hostname ||
parsedURLromOrigin(details.origin)?.hostname ||
undefined;
if ( hostname === undefined ) { return false; }
const dynamicRuleMap = await getDynamicRules();
const rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule === undefined ) { return false; }
const domainSet = new Set(rule.condition.requestDomains);
let hn = hostname;
while ( hn ) {
if ( domainSet.has(hn) ) { return true; }
hn = toBroaderHostname(hn);
}
return false;
}
/******************************************************************************/
async function addTrustedSiteDirective(details) {
const url = parsedURLromOrigin(details.origin);
if ( url === undefined ) { return false; }
const dynamicRuleMap = await getDynamicRules();
let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule !== undefined ) {
rule.condition.initiatorDomains = undefined;
if ( Array.isArray(rule.condition.requestDomains) === false ) {
rule.condition.requestDomains = [];
}
}
if ( rule === undefined ) {
rule = {
id: TRUSTED_DIRECTIVE_BASE_RULE_ID,
action: {
type: 'allowAllRequests',
},
condition: {
requestDomains: [ url.hostname ],
resourceTypes: [ 'main_frame' ],
},
priority: TRUSTED_DIRECTIVE_BASE_RULE_ID,
};
dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule);
} else if ( rule.condition.requestDomains.includes(url.hostname) === false ) {
rule.condition.requestDomains.push(url.hostname);
}
await dnr.updateDynamicRules({
addRules: [ rule ],
removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ],
});
return true;
}
/******************************************************************************/
async function removeTrustedSiteDirective(details) {
const url = parsedURLromOrigin(details.origin);
if ( url === undefined ) { return false; }
const dynamicRuleMap = await getDynamicRules();
let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID);
if ( rule === undefined ) { return false; }
rule.condition.initiatorDomains = undefined;
if ( Array.isArray(rule.condition.requestDomains) === false ) {
rule.condition.requestDomains = [];
}
const domainSet = new Set(rule.condition.requestDomains);
const beforeCount = domainSet.size;
let hostname = url.hostname;
for (;;) {
domainSet.delete(hostname);
const pos = hostname.indexOf('.');
if ( pos === -1 ) { break; }
hostname = hostname.slice(pos+1);
}
if ( domainSet.size === beforeCount ) { return false; }
if ( domainSet.size === 0 ) {
dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID);
await dnr.updateDynamicRules({
removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ]
});
return false;
}
rule.condition.requestDomains = Array.from(domainSet);
await dnr.updateDynamicRules({
addRules: [ rule ],
removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ],
});
return false;
}
/******************************************************************************/
async function toggleTrustedSiteDirective(details) {
return details.state
? removeTrustedSiteDirective(details)
: addTrustedSiteDirective(details);
}
/******************************************************************************/
export {
getAllTrustedSiteDirectives,
matchesTrustedSiteDirective,
toggleTrustedSiteDirective,
};

View File

@ -42,10 +42,28 @@ const toBroaderHostname = hn => {
/******************************************************************************/
// Is a descendant hostname of b?
const isDescendantHostname = (a, b) => {
if ( b === 'all-urls' ) { return true; }
if ( a.endsWith(b) === false ) { return false; }
if ( a === b ) { return false; }
return a.charCodeAt(a.length - b.length - 1) === 0x2E /* '.' */;
};
const isDescendantHostnameOfIter = (a, iter) => {
for ( const b of iter ) {
if ( isDescendantHostname(a, b) ) { return true; }
}
return false;
};
/******************************************************************************/
const matchesFromHostnames = hostnames => {
const out = [];
for ( const hn of hostnames ) {
if ( hn === '*' ) {
if ( hn === '*' || hn === 'all-urls' ) {
out.length = 0;
out.push('<all_urls>');
break;
@ -59,9 +77,8 @@ const hostnamesFromMatches = origins => {
const out = [];
for ( const origin of origins ) {
if ( origin === '<all_urls>' ) {
out.length = 0;
out.push('*');
break;
out.push('all-urls');
continue;
}
const match = /^\*:\/\/(?:\*\.)?([^\/]+)\/\*/.exec(origin);
if ( match === null ) { continue; }
@ -83,6 +100,8 @@ const fidFromFileName = fname =>
export {
parsedURLromOrigin,
toBroaderHostname,
isDescendantHostname,
isDescendantHostnameOfIter,
matchesFromHostnames,
hostnamesFromMatches,
fnameFromFileId,

View File

@ -8,46 +8,25 @@
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/fa-icons.css">
<link rel="stylesheet" href="css/popup.css">
<link rel="stylesheet" href="css/filtering-mode.css">
<title data-i18n="extName"></title>
</head>
<body class="loading" data-section="">
<div id="main">
<div id="filteringModeText"><span>_</span><span></span></div>
<!-- -------- -->
<div id="sticky">
<div id="stickyTools">
<div class="rulesetTools">
<span id="saveRules" class="fa-icon" data-i18n-title="popupTipSaveRules">lock</span>
<span id="revertRules" class="fa-icon" data-i18n-title="popupTipRevertRules">eraser</span>
</div>
<div id="switch" role="button" aria-label tabindex="0" data-i18n-title="popupPowerSwitchInfo">
<span class="fa-icon"><!--
Power button taken from Font Awesome v4.7.0 by Dave Gandy.
Unlike other FA icons, the power button is inlined here so
that we can use a clip-path in order to ensure that the stroke
does not "bleed" outside the fill area.
--><svg viewBox="0 0 1536 1664">
<defs>
<path id="power-off-path" d="m 1536,896 q 0,156 -61,298 -61,142 -164,245 -103,103 -245,164 -142,61 -298,61 -156,0 -298,-61 Q 328,1542 225,1439 122,1336 61,1194 0,1052 0,896 0,714 80.5,553 161,392 307,283 q 43,-32 95.5,-25 52.5,7 83.5,50 32,42 24.5,94.5 Q 503,455 461,487 363,561 309.5,668 256,775 256,896 q 0,104 40.5,198.5 40.5,94.5 109.5,163.5 69,69 163.5,109.5 94.5,40.5 198.5,40.5 104,0 198.5,-40.5 Q 1061,1327 1130,1258 1199,1189 1239.5,1094.5 1280,1000 1280,896 1280,775 1226.5,668 1173,561 1075,487 1033,455 1025.5,402.5 1018,350 1050,308 q 31,-43 84,-50 53,-7 95,25 146,109 226.5,270 80.5,161 80.5,343 z m -640,-768 0,640 q 0,52 -38,90 -38,38 -90,38 -52,0 -90,-38 -38,-38 -38,-90 l 0,-640 q 0,-52 38,-90 38,-38 90,-38 52,0 90,38 38,38 38,90 z"/>
<clipPath id="power-off-clip"><use href="#power-off-path"/></clipPath>
</defs>
<use href="#power-off-path" clip-path="url(#power-off-clip)"/>
</svg><!--
--></span>
</div>
<div class="rulesetTools">
<span id="refresh" class="fa-icon">refresh</span>
</div>
</div>
<div id="hostname"><span></span>&shy;<span></span></div>
<div class="filteringModeSlider">
<div class="filteringModeButton"><div></div></div>
<span data-level="0"></span>
<span data-level="1"></span>
<span data-level="2"></span>
<span data-level="3"></span>
</div>
<div id="hostname"><span></span>&shy;<span></span></div>
<!-- -------- -->
<div class="toolRibbon pageTools">
<span id="toggleGreatPowers">
<span class="fa-icon tool enabled" data-i18n-title="popupGrantGreatPowers">sun-o<span class="caption"></span></span>
<span class="fa-icon tool enabled" data-i18n-title="popupRevokeGreatPowers">sun<span class="caption"></span></span>
<span class="badge"></span>
</span>
<span></span>
<span></span>
<span></span>
<span></span>

View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>uBlock Origin Lite — Filter lists</title>
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/fa-icons.css">
<link rel="stylesheet" type="text/css" href="css/filtering-mode.css">
<link rel="stylesheet" type="text/css" href="css/settings.css">
</head>
<body>
<div class="firstRun">
<h3 data-i18n="firstRunSectionLabel"></h3>
<p data-i18n="firstRunDescription"></p>
</div>
<div>
<h3 data-i18n="defaultFilteringModeSectionLabel"></h3>
<p data-i18n="defaultFilteringModeDescription"></p>
<div id="defaultFilteringMode">
<label class="filteringModeCard">
<div>
<span><span class="input radio"><input type="radio" name="filteringMode" value="1"><svg viewBox="0 0 24 24"><path d="M 12 0 A 12 12 0 0 0 0 12 A 12 12 0 0 0 12 24 A 12 12 0 0 0 24 12 A 12 12 0 0 0 12 0 z M 12 2.5 A 9.5 9.5 0 0 1 21.5 12 A 9.5 9.5 0 0 1 12 21.5 A 9.5 9.5 0 0 1 2.5 12 A 9.5 9.5 0 0 1 12 2.5 z"/><circle cx="12" cy="12" r="7"/></svg></span><span data-i18n="filteringMode1Name">_</span></span>
</div>
<div>
<div class="filteringModeSlider" data-level="1">
<div class="filteringModeButton"><div></div></div>
<span data-level="0"></span>
<span data-level="1"></span>
<span data-level="2"></span>
<span data-level="3"></span>
</div>
</div>
<div data-i18n="basicFilteringModeDescription"></div>
</label>
<label class="filteringModeCard">
<div>
<span><span class="input radio"><input type="radio" name="filteringMode" value="2"><svg viewBox="0 0 24 24"><path d="M 12 0 A 12 12 0 0 0 0 12 A 12 12 0 0 0 12 24 A 12 12 0 0 0 24 12 A 12 12 0 0 0 12 0 z M 12 2.5 A 9.5 9.5 0 0 1 21.5 12 A 9.5 9.5 0 0 1 12 21.5 A 9.5 9.5 0 0 1 2.5 12 A 9.5 9.5 0 0 1 12 2.5 z"/><circle cx="12" cy="12" r="7"/></svg></span><span data-i18n="filteringMode2Name">_</span></span>
</div>
<div>
<div class="filteringModeSlider" data-level="2">
<div class="filteringModeButton"><div></div></div>
<span data-level="0"></span>
<span data-level="1"></span>
<span data-level="2"></span>
<span data-level="3"></span>
</div>
</div>
<div data-i18n="optimalFilteringModeDescription"></div>
</label>
<label class="filteringModeCard">
<div>
<span><span class="input radio"><input type="radio" name="filteringMode" value="3"><svg viewBox="0 0 24 24"><path d="M 12 0 A 12 12 0 0 0 0 12 A 12 12 0 0 0 12 24 A 12 12 0 0 0 24 12 A 12 12 0 0 0 12 0 z M 12 2.5 A 9.5 9.5 0 0 1 21.5 12 A 9.5 9.5 0 0 1 12 21.5 A 9.5 9.5 0 0 1 2.5 12 A 9.5 9.5 0 0 1 12 2.5 z"/><circle cx="12" cy="12" r="7"/></svg></span><span data-i18n="filteringMode3Name">_</span></span>
</div>
<div>
<div class="filteringModeSlider" data-level="3">
<div class="filteringModeButton"><div></div></div>
<span data-level="0"></span>
<span data-level="1"></span>
<span data-level="2"></span>
<span data-level="3"></span>
</div>
</div>
<div data-i18n="completeFilteringModeDescription"></div>
</label>
</div>
</div>
<div>
<h3 data-i18n="behaviorSectionLabel"></h3>
<p><label id="autoReload" data-i18n="autoReloadLabel"><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>_</label>
</p>
</div>
<div>
<h3>Filter lists</h3>
<div>
<p id="listsOfBlockedHostsPrompt"></p>
</div>
<div>
<div id="lists"></div>
</div>
</div>
<div id="templates">
<div class="groupEntry">
<div class="geDetails"><span class="geName"></span> <span class="geCount"></span></div>
<div class="listEntries"></div>
</div>
<div class="li listEntry">
<label><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><span class="listname forinput"></span> <span class="iconbar"><!--
--><a class="fa-icon support" href="#" target="_blank">home</a><!--
--><a class="fa-icon mustread" href="#" target="_blank">info-circle</a><!--
--></span></span></label>
</div>
</div>
<script src="js/fa-icons.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>
<script src="js/settings.js" type="module"></script>
</body>
</html>

View File

@ -382,15 +382,54 @@ function addScriptingAPIResources(id, hostnames, fid) {
}
}
const toCSSFileId = s => (uidint32(s) & ~0b11) | 0b00;
const toJSFileId = s => (uidint32(s) & ~0b11) | 0b01;
const toProceduralFileId = s => (uidint32(s) & ~0b11) | 0b10;
const toIsolatedStartFileId = s => (uidint32(s) & ~0b11) | 0b00;
const toMainStartFileId = s => (uidint32(s) & ~0b11) | 0b01;
const toIsolatedEndFileId = s => (uidint32(s) & ~0b11) | 0b10;
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`;
/******************************************************************************/
const MAX_COSMETIC_FILTERS_PER_FILE = 128;
async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusions) {
const out = {
count: 0,
exclusionCount: 0,
};
if ( bucketsMap === undefined ) { return out; }
if ( bucketsMap.size === 0 ) { return out; }
const bucketsList = Array.from(bucketsMap);
const count = bucketsList.reduce((a, v) => a += v[1].length, 0);
if ( count === 0 ) { return out; }
out.count = count;
const selectorLists = bucketsList.map(v => [ v[0], v[1].join(',') ]);
const originalScriptletMap = await loadAllSourceScriptlets();
const patchedScriptlet = originalScriptletMap.get('css-generic')
.replace(
'$rulesetId$',
assetDetails.id
).replace(
/\bself\.\$excludeHostnameSet\$/m,
`${JSON.stringify(exclusions, scriptletJsonReplacer)}`
).replace(
/\bself\.\$genericSelectorLists\$/m,
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
);
writeFile(
`${scriptletDir}/${assetDetails.id}.generic.js`,
patchedScriptlet
);
log(`CSS-generic: ${count} plain CSS selectors`);
return out;
}
/******************************************************************************/
const MAX_COSMETIC_FILTERS_PER_FILE = 256;
// This merges selectors which are used by the same hostnames
@ -530,11 +569,11 @@ async function processCosmeticFilters(assetDetails, mapin) {
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
const fid = toCSSFileId(patchedScriptlet);
const fid = toIsolatedStartFileId(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
writeFile(pathFromFileName(fname), patchedScriptlet, {});
writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname);
}
for ( const entry of slice ) {
@ -543,8 +582,8 @@ async function processCosmeticFilters(assetDetails, mapin) {
}
if ( generatedFiles.length !== 0 ) {
log(`CSS-related distinct filters: ${contentArray.length} distinct combined selectors`);
log(`CSS-related injectable files: ${generatedFiles.length}`);
log(`CSS-specific distinct filters: ${contentArray.length} distinct combined selectors`);
log(`CSS-specific injectable files: ${generatedFiles.length}`);
log(`\t${generatedFiles.join(', ')}`);
}
@ -596,11 +635,11 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
const fid = toProceduralFileId(patchedScriptlet);
const fid = toIsolatedEndFileId(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
writeFile(pathFromFileName(fname), patchedScriptlet, {});
writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname);
}
for ( const entry of slice ) {
@ -725,11 +764,11 @@ async function processScriptletFilters(assetDetails, mapin) {
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
// ends-with 1 = scriptlet resource
const fid = toJSFileId(patchedScriptlet);
const fid = toMainStartFileId(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
writeFile(pathFromFileName(fname), patchedScriptlet, {});
writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname);
}
for ( const details of argsDetails.values() ) {
@ -771,8 +810,8 @@ const rulesetFromURLS = async function(assetDetails) {
const declarativeCosmetic = new Map();
const proceduralCosmetic = new Map();
const rejectedCosmetic = [];
if ( results.cosmetic ) {
for ( const [ selector, details ] of results.cosmetic ) {
if ( results.specificCosmetic ) {
for ( const [ selector, details ] of results.specificCosmetic ) {
if ( details.rejected ) {
rejectedCosmetic.push(selector);
continue;
@ -786,7 +825,12 @@ const rulesetFromURLS = async function(assetDetails) {
proceduralCosmetic.set(JSON.stringify(parsed), details);
}
}
const cosmeticStats = await processCosmeticFilters(
const genericCosmeticStats = await processGenericCosmeticFilters(
assetDetails,
results.genericCosmetic,
results.network.generichideExclusions.filter(hn => hn.endsWith('.*') === false)
);
const specificCosmeticStats = await processCosmeticFilters(
assetDetails,
declarativeCosmetic
);
@ -824,7 +868,8 @@ const rulesetFromURLS = async function(assetDetails) {
rejected: netStats.rejected,
},
css: {
specific: cosmeticStats,
generic: genericCosmeticStats,
specific: specificCosmeticStats,
procedural: proceduralStats,
},
scriptlets: {
@ -896,20 +941,36 @@ async function main() {
'ara-0',
'EST-0',
];
// Merge lists which have same target languages
const langToListsMap = new Map();
for ( const [ id, asset ] of Object.entries(assets) ) {
if ( asset.content !== 'filters' ) { continue; }
if ( asset.off !== true ) { continue; }
if ( typeof asset.lang !== 'string' ) { continue; }
if ( excludedLists.includes(id) ) { continue; }
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
let ids = langToListsMap.get(asset.lang);
if ( ids === undefined ) {
langToListsMap.set(asset.lang, ids = []);
}
ids.push(id);
}
for ( const ids of langToListsMap.values() ) {
const urls = [];
for ( const id of ids ) {
const asset = assets[id];
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
urls.push(contentURL);
}
const id = ids[0];
const asset = assets[id];
await rulesetFromURLS({
id: id.toLowerCase(),
lang: asset.lang,
name: asset.title,
enabled: false,
urls: [ contentURL ],
urls,
homeURL: asset.supportURL,
});
}
@ -933,6 +994,13 @@ async function main() {
}
// Handpicked rulesets from abroad
await rulesetFromURLS({
id: 'cname-trackers',
name: 'AdGuard CNAME-cloaked trackers',
enabled: true,
urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ],
homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers',
});
await rulesetFromURLS({
id: 'stevenblack-hosts',
name: 'Steven Black\'s hosts file',

View File

@ -0,0 +1,291 @@
/*******************************************************************************
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
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
/// name css-generic
/******************************************************************************/
// Important!
// Isolate from global scope
(function uBOL_cssGeneric() {
/******************************************************************************/
// $rulesetId$
{
const excludeHostnameSet = new Set(self.$excludeHostnameSet$);
let hn;
try { hn = document.location.hostname; } catch(ex) { }
while ( hn ) {
if ( excludeHostnameSet.has(hn) ) { return; }
const pos = hn.indexOf('.');
if ( pos === -1 ) { break; }
hn = hn.slice(pos+1);
}
excludeHostnameSet.clear();
}
const genericSelectorLists = new Map(self.$genericSelectorLists$);
/******************************************************************************/
const queriedHashes = new Set();
const maxSurveyTimeSlice = 4;
const styleSheetSelectors = [];
const stopAllRatio = 0.95; // To be investigated
let surveyCount = 0;
let surveyMissCount = 0;
let styleSheetTimer;
let processTimer;
let domChangeTimer;
let lastDomChange = Date.now();
/******************************************************************************/
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
const hashFromStr = (type, s) => {
const len = s.length;
const step = len + 7 >>> 3;
let hash = type;
for ( let i = 0; i < len; i += step ) {
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
}
return hash & 0x00FFFFFF;
};
/******************************************************************************/
// Extract all classes/ids: these will be passed to the cosmetic
// filtering engine, and in return we will obtain only the relevant
// CSS selectors.
// https://github.com/gorhill/uBlock/issues/672
// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
// http://jsperf.com/enumerate-classes/6
const uBOL_idFromNode = (node, out) => {
const raw = node.id;
if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
const s = raw.trim();
const hash = hashFromStr(0x23 /* '#' */, s);
if ( queriedHashes.has(hash) ) { return; }
out.push(hash);
queriedHashes.add(hash);
};
// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
// Performance: avoid using Element.classList
const uBOL_classesFromNode = (node, out) => {
const s = node.getAttribute('class');
if ( typeof s !== 'string' ) { return; }
const len = s.length;
for ( let beg = 0, end = 0, token = ''; beg < len; beg += 1 ) {
end = s.indexOf(' ', beg);
if ( end === beg ) { continue; }
if ( end === -1 ) { end = len; }
token = s.slice(beg, end);
beg = end;
const hash = hashFromStr(0x2E /* '.' */, token);
if ( queriedHashes.has(hash) ) { continue; }
out.push(hash);
queriedHashes.add(hash);
}
};
/******************************************************************************/
const pendingNodes = {
nodeLists: [],
buffer: [
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
],
j: 0,
add(nodes) {
if ( nodes.length === 0 ) { return; }
this.nodeLists.push(nodes);
},
next() {
if ( this.nodeLists.length === 0 ) { return 0; }
const maxSurveyBuffer = this.buffer.length;
const nodeLists = this.nodeLists;
let ib = 0;
do {
const nodeList = nodeLists[0];
let j = this.j;
let n = j + maxSurveyBuffer - ib;
if ( n > nodeList.length ) {
n = nodeList.length;
}
for ( let i = j; i < n; i++ ) {
this.buffer[ib++] = nodeList[j++];
}
if ( j !== nodeList.length ) {
this.j = j;
break;
}
this.j = 0;
this.nodeLists.shift();
} while ( ib < maxSurveyBuffer && nodeLists.length !== 0 );
return ib;
},
hasNodes() {
return this.nodeLists.length !== 0;
},
};
/******************************************************************************/
const uBOL_processNodes = ( ) => {
const t0 = Date.now();
const hashes = [];
const nodes = pendingNodes.buffer;
const deadline = t0 + maxSurveyTimeSlice;
let processed = 0;
for (;;) {
const n = pendingNodes.next();
if ( n === 0 ) { break; }
for ( let i = 0; i < n; i++ ) {
const node = nodes[i];
nodes[i] = null;
uBOL_idFromNode(node, hashes);
uBOL_classesFromNode(node, hashes);
}
processed += n;
if ( performance.now() >= deadline ) { break; }
}
for ( const hash of hashes ) {
const selectorList = genericSelectorLists.get(hash);
if ( selectorList === undefined ) { continue; }
styleSheetSelectors.push(selectorList);
genericSelectorLists.delete(hash);
}
surveyCount += 1;
if ( styleSheetSelectors.length === 0 ) {
surveyMissCount += 1;
if (
surveyCount >= 100 &&
(surveyMissCount / surveyCount) >= stopAllRatio
) {
stopAll('too many misses in surveyor');
}
return;
}
if ( styleSheetTimer !== undefined ) { return; }
styleSheetTimer = self.requestAnimationFrame(( ) => {
styleSheetTimer = undefined;
uBOL_injectStyleSheet();
});
};
/******************************************************************************/
const uBOL_processChanges = mutations => {
for ( let i = 0; i < mutations.length; i++ ) {
const mutation = mutations[i];
for ( const added of mutation.addedNodes ) {
if ( added.nodeType !== 1 ) { continue; }
pendingNodes.add([ added ]);
if ( added.firstElementChild === null ) { continue; }
pendingNodes.add(added.querySelectorAll('[id],[class]'));
}
}
if ( pendingNodes.hasNodes() === false ) { return; }
lastDomChange = Date.now();
if ( processTimer !== undefined ) { return; }
processTimer = self.setTimeout(( ) => {
processTimer = undefined;
uBOL_processNodes();
}, 64);
};
/******************************************************************************/
const uBOL_injectStyleSheet = ( ) => {
try {
const sheet = new CSSStyleSheet();
sheet.replace(`@layer{${styleSheetSelectors.join(',')}{display:none!important;}}`);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
} catch(ex) {
}
styleSheetSelectors.length = 0;
};
/******************************************************************************/
pendingNodes.add(document.querySelectorAll('[id],[class]'));
uBOL_processNodes();
let domMutationObserver = new MutationObserver(uBOL_processChanges);
domMutationObserver.observe(document, {
childList: true,
subtree: true,
});
const needDomChangeObserver = ( ) => {
domChangeTimer = undefined;
if ( domMutationObserver === undefined ) { return; }
if ( (Date.now() - lastDomChange) > 20000 ) {
return stopAll('no more DOM changes');
}
domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
};
needDomChangeObserver();
/******************************************************************************/
const stopAll = reason => {
if ( domChangeTimer !== undefined ) {
self.clearTimeout(domChangeTimer);
domChangeTimer = undefined;
}
domMutationObserver.disconnect();
domMutationObserver.takeRecords();
domMutationObserver = undefined;
genericSelectorLists.clear();
queriedHashes.clear();
console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`);
};
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -43,6 +43,10 @@ const hostnamesMap = new Map(self.$hostnamesMap$);
/******************************************************************************/
let proceduralFilterer;
/******************************************************************************/
const addStylesheet = text => {
try {
const sheet = new CSSStyleSheet();
@ -149,11 +153,8 @@ class PSelectorMatchesMediaTask extends PSelectorTask {
this.mql = window.matchMedia(task[1]);
if ( this.mql.media === 'not all' ) { return; }
this.mql.addEventListener('change', ( ) => {
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI === null ) { return; }
const filterer = vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
if ( filterer instanceof Object === false ) { return; }
filterer.onDOMChanged([ null ]);
if ( proceduralFilterer instanceof Object === false ) { return; }
proceduralFilterer.onDOMChanged([ null ]);
});
}
transpose(node, output) {
@ -258,25 +259,10 @@ class PSelectorSpathTask extends PSelectorTask {
this.spath = `:scope ${this.spath.trim()}`;
}
}
qsa(node) {
if ( this.nth === false ) {
return node.querySelectorAll(this.spath);
}
const parent = node.parentElement;
if ( parent === null ) { return; }
let pos = 1;
for (;;) {
node = node.previousElementSibling;
if ( node === null ) { break; }
pos += 1;
}
return parent.querySelectorAll(
`:scope > :nth-child(${pos})${this.spath}`
);
}
transpose(node, output) {
const nodes = this.qsa(node);
if ( nodes === undefined ) { return; }
const nodes = this.nth
? PSelectorSpathTask.qsa(node, this.spath)
: node.querySelectorAll(this.spath);
for ( const node of nodes ) {
output.push(node);
}
@ -344,10 +330,8 @@ class PSelectorWatchAttrs extends PSelectorTask {
}
// TODO: Is it worth trying to re-apply only the current selector?
handler() {
const filterer =
vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
if ( filterer instanceof Object ) {
filterer.onDOMChanged([ null ]);
if ( proceduralFilterer instanceof Object ) {
proceduralFilterer.onDOMChanged([ null ]);
}
}
transpose(node, output) {
@ -420,12 +404,10 @@ class PSelector {
prime(input) {
const root = input || document;
if ( this.selector === '' ) { return [ root ]; }
let selector = this.selector;
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
}
const elems = root.querySelectorAll(selector);
return Array.from(elems);
return Array.from(root.querySelectorAll(this.selector));
}
exec(input) {
let nodes = this.prime(input);
@ -509,7 +491,7 @@ class ProceduralFilterer {
this.onDOMChanged();
}
commitNow() {
uBOL_commitNow() {
//console.time('procedural selectors/dom layout changed');
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
@ -589,7 +571,7 @@ class ProceduralFilterer {
if ( this.timer !== undefined ) { return; }
this.timer = self.requestAnimationFrame(( ) => {
this.timer = undefined;
this.commitNow();
this.uBOL_commitNow();
});
}
}
@ -668,10 +650,8 @@ if ( styleSelectors.length !== 0 ) {
/******************************************************************************/
// Procedural selectors
if ( proceduralSelectors.length !== 0 ) {
const filterer = new ProceduralFilterer(proceduralSelectors);
proceduralFilterer = new ProceduralFilterer(proceduralSelectors);
const observer = new MutationObserver(mutations => {
let domChanged = false;
for ( let i = 0; i < mutations.length && !domChanged; i++ ) {
@ -686,7 +666,7 @@ if ( proceduralSelectors.length !== 0 ) {
}
}
if ( domChanged === false ) { return; }
filterer.onDOMChanged();
proceduralFilterer.onDOMChanged();
});
observer.observe(document, {
childList: true,

View File

@ -185,6 +185,9 @@ section.notice {
position: relative;
width: var(--checkbox-size);
}
label:hover .checkbox:not([disabled]) {
background-color: var(--surface-2);
}
.checkbox > input[type="checkbox"] {
box-sizing: border-box;
height: 100%;
@ -217,6 +220,49 @@ section.notice {
filter: var(--checkbox-disabled-filter);
}
.radio {
--margin-end: calc(var(--font-size) * 0.75);
box-sizing: border-box;
display: inline-flex;
flex-shrink: 0;
height: calc(var(--checkbox-size) + 2px);
margin: 0;
margin-inline-end: var(--margin-end);
-webkit-margin-end: var(--margin-end);
position: relative;
width: calc(var(--checkbox-size) + 2px);
}
.radio > input[type="radio"] {
box-sizing: border-box;
height: 100%;
margin: 0;
min-width: var(--checkbox-size);
opacity: 0;
position: absolute;
width: 100%;
}
.radio > input[type="radio"] + svg {
background-color: transparent;
box-sizing: border-box;
height: 100%;
pointer-events: none;
position: absolute;
width: 100%;
}
.radio > input[type="radio"] + svg > path {
fill: var(--checkbox-ink);
}
.radio > input[type="radio"] + svg > circle {
fill: transparent;
}
label:hover .radio > input[type="radio"]:not(:checked) + svg > circle {
fill: var(--surface-3);
}
.radio > input[type="radio"]:checked + svg > path,
.radio > input[type="radio"]:checked + svg > circle {
fill: var(--checkbox-checked-ink);
}
select {
padding: 2px;
}

View File

@ -240,9 +240,9 @@ const onMessage = function(request, sender, callback) {
isUnsupported(rule)
);
out.push(`+ Unsupported filters (${bad.length}): ${JSON.stringify(bad, replacer, 2)}`);
out.push(`\n+ Cosmetic filters: ${result.cosmetic.length}`);
for ( const details of result.cosmetic ) {
out.push(`+ generichide exclusions (${network.generichideExclusions.length}): ${JSON.stringify(network.generichideExclusions, replacer, 2)}`);
out.push(`+ Cosmetic filters: ${result.specificCosmetic.size}`);
for ( const details of result.specificCosmetic ) {
out.push(` ${JSON.stringify(details)}`);
}

View File

@ -34,6 +34,57 @@ import {
/******************************************************************************/
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
const hashFromStr = (type, s) => {
const len = s.length;
const step = len + 7 >>> 3;
let hash = type;
for ( let i = 0; i < len; i += step ) {
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
}
return hash & 0x00FFFFFF;
};
/******************************************************************************/
// Copied from cosmetic-filter.js for the time being to avoid unwanted
// dependencies
const rePlainSelector = /^[#.][\w\\-]+/;
const rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/;
const reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g;
const keyFromSelector = selector => {
let matches = rePlainSelector.exec(selector);
if ( matches === null ) { return; }
let key = matches[0];
if ( key.indexOf('\\') === -1 ) {
return key;
}
matches = rePlainSelectorEscaped.exec(selector);
if ( matches === null ) { return; }
key = '';
const escaped = matches[0];
let beg = 0;
reEscapeSequence.lastIndex = 0;
for (;;) {
matches = reEscapeSequence.exec(escaped);
if ( matches === null ) {
return key + escaped.slice(beg);
}
key += escaped.slice(beg, matches.index);
beg = reEscapeSequence.lastIndex;
if ( matches[1].length === 1 ) {
key += matches[1];
} else {
key += String.fromCharCode(parseInt(matches[1], 16));
}
}
};
/******************************************************************************/
function addExtendedToDNR(context, parser) {
if ( parser.category !== parser.CATStaticExtFilter ) { return false; }
@ -87,13 +138,35 @@ function addExtendedToDNR(context, parser) {
}
// Cosmetic filtering
if ( context.cosmeticFilters === undefined ) {
context.cosmeticFilters = new Map();
// Generic cosmetic filtering
if ( parser.hasOptions() === false ) {
if ( context.genericCosmeticFilters === undefined ) {
context.genericCosmeticFilters = new Map();
}
const { compiled } = parser.result;
if ( compiled === undefined ) { return; }
if ( compiled.length <= 1 ) { return; }
if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) { return; }
const key = keyFromSelector(compiled);
if ( key === undefined ) { return; }
const type = key.charCodeAt(0);
const hash = hashFromStr(type, key.slice(1));
let bucket = context.genericCosmeticFilters.get(hash);
if ( bucket === undefined ) {
context.genericCosmeticFilters.set(hash, bucket = []);
}
bucket.push(compiled);
return;
}
// Specific cosmetic filtering
// https://github.com/chrisaljoudi/uBlock/issues/151
// Negated hostname means the filter applies to all non-negated hostnames
// of same filter OR globally if there is no non-negated hostnames.
if ( context.specificCosmeticFilters === undefined ) {
context.specificCosmeticFilters = new Map();
}
for ( const { hn, not, bad } of parser.extOptions() ) {
if ( bad ) { continue; }
let { compiled, exception, raw } = parser.result;
@ -107,11 +180,11 @@ function addExtendedToDNR(context, parser) {
if ( rejected ) {
compiled = rejected;
}
let details = context.cosmeticFilters.get(compiled);
let details = context.specificCosmeticFilters.get(compiled);
if ( details === undefined ) {
details = {};
if ( rejected ) { details.rejected = true; }
context.cosmeticFilters.set(compiled, details);
context.specificCosmeticFilters.set(compiled, details);
}
if ( rejected ) { continue; }
if ( not ) {
@ -206,7 +279,8 @@ async function dnrRulesetFromRawLists(lists, options = {}) {
return {
network: staticNetFilteringEngine.dnrFromCompiled('end', context),
cosmetic: context.cosmeticFilters,
genericCosmetic: context.genericCosmeticFilters,
specificCosmetic: context.specificCosmeticFilters,
scriptlet: context.scriptletFilters,
};
}

View File

@ -4033,6 +4033,24 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) {
}
}
// Collect generichide filters
const generichideExclusions = [];
{
const bucket = buckets.get(AllowAction | typeNameToTypeValue['generichide']);
if ( bucket ) {
for ( const rules of bucket.values() ) {
for ( const rule of rules ) {
if ( rule.condition === undefined ) { continue; }
if ( rule.condition.initiatorDomains ) {
generichideExclusions.push(...rule.condition.initiatorDomains);
} else if ( rule.condition.requestDomains ) {
generichideExclusions.push(...rule.condition.requestDomains);
}
}
}
}
}
// Patch modifier filters
for ( const rule of ruleset ) {
if ( rule.__modifierType === undefined ) { continue; }
@ -4247,6 +4265,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) {
filterCount: context.filterCount,
acceptedFilterCount: context.acceptedFilterCount,
rejectedFilterCount: context.rejectedFilterCount,
generichideExclusions,
};
};