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:
parent
5777b672a4
commit
8eb28a446c
@ -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>
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
body > div.body {
|
||||
margin: 0 1em;
|
||||
body {
|
||||
}
|
||||
h2, h3 {
|
||||
margin: 1em 0;
|
||||
|
@ -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;
|
||||
|
92
platform/mv3/extension/css/filtering-mode.css
Normal file
92
platform/mv3/extension/css/filtering-mode.css
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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]) {
|
@ -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>
|
||||
<!-- -------- -->
|
||||
|
@ -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 ) {
|
||||
|
@ -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'),
|
||||
|
366
platform/mv3/extension/js/mode-manager.js
Normal file
366
platform/mv3/extension/js/mode-manager.js
Normal 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,
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
|
@ -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>­<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>­<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>
|
||||
|
109
platform/mv3/extension/settings.html
Normal file
109
platform/mv3/extension/settings.html
Normal 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>
|
@ -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',
|
||||
|
291
platform/mv3/scriptlets/css-generic.js
Normal file
291
platform/mv3/scriptlets/css-generic.js
Normal 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}`);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)}`);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user