[mv3] Expand "Develop" pane

Move "No filtering" section in "Settings" to "Develop" pane. It is
now possible to view/edit the list of hostnames for any of the
filtering mode. This takes care of these issues:
- https://github.com/uBlockOrigin/uBOL-home/issues/270
- https://github.com/uBlockOrigin/uBOL-home/issues/297

Add ability to see all rulesets (read-only), to assist in investigating
filtering issues.
This commit is contained in:
Raymond Hill 2025-06-13 12:46:05 -04:00
parent a12ed895dd
commit b50341089d
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
19 changed files with 1765 additions and 1048 deletions

View file

@ -295,6 +295,30 @@
"message": "Exit element zapper mode",
"description": "Tooltip for the button used to exit zapper mode"
},
"developDropdownLabel": {
"message": "View:",
"description": "A label of a dropdown list"
},
"developOptionFilteringModeDetails": {
"message": "Filtering mode details",
"description": "An option in a dropdown list"
},
"developOptionCustomDnrRules": {
"message": "Custom DNR rules",
"description": "An option in a dropdown list"
},
"developOptionDnrRulesOf": {
"message": "DNR rules of …",
"description": "A section header in a dropdown list"
},
"developOptionDynamicRuleset": {
"message": "Dynamic ruleset",
"description": "An option in a dropdown list"
},
"developOptionSessionRuleset": {
"message": "Session ruleset",
"description": "An option in a dropdown list"
},
"saveButton": {
"message": "Save",
"description": "Text for buttons used to save changes"
@ -311,8 +335,8 @@
"message": "Export…",
"description": "Text for buttons used to export content"
},
"dnrRulesSummary": {
"message": "Enter your own DNR rules below. <b>Do not add content from untrusted sources.</b>",
"dnrRulesWarning": {
"message": "Do not add content from untrusted sources",
"description": "Short description of the DNR rules editor pane"
},
"dnrRulesCountInfo": {

View file

@ -19,38 +19,35 @@ section[data-pane="develop"] > div > * {
margin-top: 1em;
}
#cm-dnrRules {
#cm-container {
flex-grow: 1;
font-size: var(--monospace-size);
overflow: hidden;
}
/* https://discuss.codemirror.net/t/how-to-set-max-height-of-the-editor/2882/2 */
#cm-dnrRules .cm-editor {
#cm-container .cm-editor {
background-color: var(--surface-0);
height: 100%;
}
#cm-dnrRules .cm-editor .cm-line {
border-bottom: 1px dotted transparent;
border-top: 1px dotted transparent;
#cm-container .cm-editor .cm-line:has(.ͼ5),
#cm-container .cm-editor .cm-line:has(.ͼw) {
background-image: url('line-hor-dashed.png'), url('line-hor-dashed.png');
background-position: left 3px, left calc(100% - 3px);
background-repeat: repeat-x;
}
#cm-dnrRules .cm-editor .cm-line:has(.ͼc) {
border-bottom: 1px dotted var(--border-1);
border-top: 1px dotted var(--border-1);
}
#cm-dnrRules .cm-editor .cm-line.badline:not(.cm-activeLine) {
#cm-container .cm-editor .cm-line.badline:not(.cm-activeLine) {
background-color: color-mix(in srgb, var(--info3-ink) 15%, transparent 85%);
}
#cm-dnrRules .cm-editor .cm-line .badmark {
#cm-container .cm-editor .cm-line .badmark {
text-decoration: underline var(--cm-negative) wavy;
text-decoration-skip-ink: none;
}
#cm-dnrRules .cm-editor .cm-panel.cm-search {
#cm-container .cm-editor .cm-panel.cm-search {
display: flex;
flex-wrap: wrap;
font-family: sans-serif;
@ -59,13 +56,13 @@ section[data-pane="develop"] > div > * {
padding: 0.5em 1.5em 0.5em 0.5em;
}
#cm-dnrRules .cm-editor .cm-panel.cm-search > * {
#cm-container .cm-editor .cm-panel.cm-search > * {
margin: 0;
}
#cm-dnrRules .cm-editor .cm-panel.cm-search .cm-textfield,
#cm-dnrRules .cm-editor .cm-panel.cm-search .cm-button,
#cm-dnrRules .cm-editor .cm-panel.cm-search label {
#cm-container .cm-editor .cm-panel.cm-search .cm-textfield,
#cm-container .cm-editor .cm-panel.cm-search .cm-button,
#cm-container .cm-editor .cm-panel.cm-search label {
background-image: inherit;
border: inherit;
flex-grow: 0;
@ -73,40 +70,77 @@ section[data-pane="develop"] > div > * {
min-height: calc(var(--button-font-size) * 1.8);
}
#cm-dnrRules .cm-editor .cm-panel.info-panel {
#cm-container .cm-editor .cm-panel .warning {
color: var(--info3-ink);
}
#cm-container .cm-editor .cm-panel.io-panel {
background-color: var(--surface-1);
box-sizing: border-box;
display: inline-flex;
gap: 0.25em;
padding: 0.25em;
padding-inline-start: 0;
width: 100%;
}
#cm-container .cm-editor .cm-panel.io-panel button {
min-height: 30px;
}
#cm-container .cm-editor .cm-panel.io-panel button#revert {
margin-inline-end: 1em;
}
#cm-container .cm-editor .cm-panel.io-panel:not([data-io~="apply"]) button#apply {
display: none;
}
#cm-container .cm-editor .cm-panel.io-panel:not([data-io~="revert"]) button#revert {
display: none;
}
#cm-container .cm-editor .cm-panel.io-panel:not([data-io~="import"]) button#import {
display: none;
}
#cm-container .cm-editor .cm-panel.io-panel:not([data-io~="export"]) button#export {
display: none;
}
#cm-container .cm-editor .cm-panel.info-panel {
display: flex;
flex-wrap: nowrap;
font-size: var(--font-size);
padding: var(--default-gap-xxsmall) var(--default-gap-xsmall);
}
#cm-dnrRules .cm-editor .cm-panel.info-panel .info {
#cm-container .cm-editor .cm-panel.info-panel .info {
flex-grow: 1;
overflow: auto;
}
#cm-dnrRules .cm-editor .cm-panel.info-panel .close {
#cm-container .cm-editor .cm-panel.info-panel .close {
cursor: default;
flex-shrink: 0;
padding-inline-start: 1em;
}
#cm-dnrRules .cm-editor .cm-panel.info-panel .close::after {
#cm-container .cm-editor .cm-panel.info-panel .close::after {
content: '\2715';
}
#cm-dnrRules .cm-editor .cm-panel.summary-panel {
#cm-container .cm-editor .cm-panel.summary-panel {
background-color: color-mix(in srgb, var(--info1-ink) 15%, transparent 85%);
gap: 1em;
}
#cm-container .cm-editor .cm-panel.summary-panel .info {
flex-shrink: 0;
}
#cm-dnrRules .cm-editor .cm-panel.feedback-panel {
#cm-container .cm-editor .cm-panel.feedback-panel {
background-color: color-mix(in srgb, var(--info3-ink) 15%, transparent 85%);
white-space: pre;
max-height: 10cqh;
}
#cm-dnrRules .cm-editor .cm-gutterElement {
#cm-container .cm-editor .cm-gutterElement {
cursor: default;
user-select: none;
}
#cm-dnrRules .cm-editor .cm-tooltip .badmark-tooltip {
#cm-container .cm-editor .cm-tooltip .badmark-tooltip {
background-color: color-mix(in srgb, var(--info3-ink) 15%, transparent 85%);
padding: var(--default-gap-xxsmall) var(--default-gap-xsmall);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View file

@ -89,12 +89,6 @@
<p><label id="strictBlockMode" data-i18n="enableStrictBlockLabel"><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><legend data-i18n="enableStrictBlockLegend"></legend>
<p id="developerMode"><label data-i18n="developerModeLabel"><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><legend data-i18n="developerModeLegend"></legend>
</div>
<div>
<h3 data-i18n="filteringMode0Name"></h3>
<p data-i18n="noFilteringModeDescription">_</p>
<div id="trustedSites"></div>
</div>
</section>
<!-- -------- -->
<section data-pane="rulesets">
@ -108,14 +102,17 @@
<section data-pane="develop">
<div>
<p>
<button id="dnrRulesApply" class="preferred iconified" type="button" disabled><span class="fa-icon">save</span><span data-i18n="saveButton">_</span><span class="hover"></span></button>
<button id="dnrRulesRevert" class="iconified" type="button" disabled><span class="fa-icon">undo</span><span data-i18n="revertButton">_</span><span class="hover"></span></button>
&emsp;
<button id="dnrRulesImport" class="iconified" type="button"><span class="fa-icon">download-alt</span><span data-i18n="importAndAppendButton">_</span><span class="hover"></span></button>
<button id="dnrRulesExport" class="iconified" type="button"><span class="fa-icon">upload-alt</span><span data-i18n="exportButton">_</span><span class="hover"></span></button><input type="file" accept="json/application">
<label for="editors" data-i18n="developDropdownLabel"></label> <select id="editors">
<option value="modes" selected data-i18n="developOptionFilteringModeDetails"></option>
<option value="dnr.rw.user" data-i18n="developOptionCustomDnrRules"></option>
<hr>
<optgroup label="" data-i18n-label="developOptionDnrRulesOf">
<option value="dnr.ro.dynamic" data-i18n="developOptionDynamicRuleset"></option>
<option value="dnr.ro.session" data-i18n="developOptionSessionRuleset"></option>
</optgroup>
</select>&nbsp;
</p>
<p data-i18n="dnrRulesSummary"></p>
<div id="cm-dnrRules"></div>
<div id="cm-container"></div>
</div>
</section>
<!-- -------- -->
@ -165,16 +162,24 @@
</span>
</div>
<div class="listEntry expandable" data-role="rootnode">
<span class="detailbar">
<div class="detailbar">
<h3 class="listname"></h3>
<span class="count"></span>
<span class="fa-icon listExpander">angle-up</span>
</span>
</div>
</div>
</div>
<template class="io-panel">
<span class="io-panel">
<button id="apply" class="preferred iconified" type="button" disabled><span class="fa-icon">save</span><span data-i18n="saveButton">_</span><span class="hover"></span></button>
<button id="revert" class="iconified" type="button" disabled><span class="fa-icon">undo</span><span data-i18n="revertButton">_</span><span class="hover"></span></button>
<button id="import" class="iconified" type="button"><span class="fa-icon">download-alt</span><span data-i18n="importAndAppendButton">_</span><span class="hover"></span></button>
<button id="export" class="iconified" type="button"><span class="fa-icon">upload-alt</span><span data-i18n="exportButton">_</span><span class="hover"></span></button><input type="file" accept="json/application">
</span>
</template>
<template class="summary-panel">
<div class="info-panel summary-panel">
<div class="info"></div>
<span class="info"></span><span class="warning" data-i18n="dnrRulesWarning"></span>
</div>
</template>
<template class="feedback-panel">
@ -183,6 +188,10 @@
<div class="close"></div>
</div>
</template>
<template class="ro-summary-panel">
<div class="info-panel summary-panel">
</div>
</template>
<template class="badmark-tooltip">
<div class="badmark-tooltip">
</div>

View file

@ -21,7 +21,7 @@
import {
adminRead,
localRead, localWrite,
localRead, localRemove, localWrite,
sessionRead, sessionWrite,
} from './ext.js';
@ -33,7 +33,6 @@ import {
import {
getDefaultFilteringMode,
getTrustedSites,
readFilteringModeDetails,
} from './mode-manager.js';
@ -132,9 +131,8 @@ const adminSettings = {
}
if ( this.keys.has('noFiltering') ) {
ubolLog('admin setting "noFiltering" changed');
await readFilteringModeDetails(true);
const trustedSites = await getTrustedSites();
broadcastMessage({ trustedSites: Array.from(trustedSites) });
const filteringModeDetails = await readFilteringModeDetails(true);
broadcastMessage({ filteringModeDetails });
}
if ( this.keys.has('showBlockedCount') ) {
ubolLog('admin setting "showBlockedCount" changed');
@ -192,17 +190,18 @@ export async function getAdminRulesets() {
export async function adminReadEx(key) {
let cacheValue;
const session = await sessionRead(`admin_${key}`);
const session = await sessionRead(`admin.${key}`);
if ( session ) {
cacheValue = session.data;
} else {
const local = await localRead(`admin_${key}`);
const local = await localRead(`admin.${key}`);
if ( local ) {
cacheValue = local.data;
}
localRemove(`admin_${key}`); // TODO: remove eventually
}
adminRead(key).then(async value => {
const adminKey = `admin_${key}`;
const adminKey = `admin.${key}`;
await Promise.all([
sessionWrite(adminKey, { data: value }),
localWrite(adminKey, { data: value }),

View file

@ -24,10 +24,10 @@ import {
MODE_OPTIMAL,
getDefaultFilteringMode,
getFilteringMode,
getTrustedSites,
getFilteringModeDetails,
setDefaultFilteringMode,
setFilteringMode,
setTrustedSites,
setFilteringModeDetails,
syncWithBrowserPermissions,
} from './mode-manager.js';
@ -53,6 +53,8 @@ import {
import {
enableRulesets,
excludeFromStrictBlock,
getEffectiveDynamicRules,
getEffectiveSessionRules,
getEffectiveUserRules,
getEnabledRulesetsDetails,
getRulesetDetails,
@ -219,11 +221,10 @@ function onMessage(request, sender, callback) {
return true;
}
case 'getOptionsPageData': {
case 'getOptionsPageData':
Promise.all([
hasBroadHostPermissions(),
getDefaultFilteringMode(),
getTrustedSites(),
getRulesetDetails(),
dnr.getEnabledRulesets(),
getAdminRulesets(),
@ -232,7 +233,6 @@ function onMessage(request, sender, callback) {
const [
hasOmnipotence,
defaultFilteringMode,
trustedSites,
rulesetDetails,
enabledRulesets,
adminRulesets,
@ -241,7 +241,6 @@ function onMessage(request, sender, callback) {
callback({
hasOmnipotence,
defaultFilteringMode,
trustedSites: Array.from(trustedSites),
enabledRulesets,
adminRulesets,
maxNumberOfEnabledRulesets: dnr.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
@ -258,7 +257,12 @@ function onMessage(request, sender, callback) {
process.firstRun = false;
});
return true;
}
case 'getRulesetDetails':
getRulesetDetails().then(rulesetDetails => {
callback(Array.from(rulesetDetails.values()));
});
return true;
case 'setAutoReload':
rulesetConfig.autoReload = request.state && true || false;
@ -353,7 +357,7 @@ function onMessage(request, sender, callback) {
return true;
}
case 'setDefaultFilteringMode': {
case 'setDefaultFilteringMode':
getDefaultFilteringMode().then(beforeLevel =>
setDefaultFilteringMode(request.level).then(afterLevel =>
({ beforeLevel, afterLevel })
@ -365,19 +369,21 @@ function onMessage(request, sender, callback) {
callback(afterLevel);
});
return true;
}
case 'setTrustedSites':
setTrustedSites(request.hostnames).then(( ) => {
case 'getFilteringModeDetails':
getFilteringModeDetails(true).then(details => {
callback(details);
});
return true;
case 'setFilteringModeDetails':
setFilteringModeDetails(request.modes).then(( ) => {
registerInjectables();
return Promise.all([
getDefaultFilteringMode(),
getTrustedSites(),
]);
}).then(results => {
callback({
defaultFilteringMode: results[0],
trustedSites: Array.from(results[1]),
getDefaultFilteringMode().then(defaultFilteringMode => {
broadcastMessage({ defaultFilteringMode });
});
getFilteringModeDetails(true).then(details => {
callback(details);
});
});
return true;
@ -402,6 +408,18 @@ function onMessage(request, sender, callback) {
});
break;
case 'getEffectiveDynamicRules':
getEffectiveDynamicRules().then(result => {
callback(result);
});
return true;
case 'getEffectiveSessionRules':
getEffectiveSessionRules().then(result => {
callback(result);
});
return true;
case 'getEffectiveUserRules':
getEffectiveUserRules().then(result => {
callback(result);

View file

@ -41,13 +41,13 @@ dom.on('#dashboard-nav', 'click', '.tabButton', ev => {
const { pane } = ev.target.dataset;
dom.body.dataset.pane = pane;
if ( pane === 'settings' ) {
localRemove('activeDashboardPane');
localRemove('dashboard.activePane');
} else {
localWrite('activeDashboardPane', pane);
localWrite('dashboard.activePane', pane);
}
});
localRead('activeDashboardPane').then(pane => {
localRead('dashboard.activePane').then(pane => {
if ( typeof pane !== 'string' ) { return; }
dom.body.dataset.pane = pane;
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
import { dnr } from './ext-compat.js';
import { rulesFromText } from './dnr-parser.js';
/******************************************************************************/
export class DNREditor {
constructor() {
this.validatedRegexes = [];
this.validatedRegexResults = [];
}
updateView(editor, firstLine, lastLine) {
const { doc } = editor.view.state;
const text = doc.sliceString(firstLine.from, lastLine.to);
const { bad } = rulesFromText(text);
if ( Array.isArray(bad) && bad.length !== 0 ) {
self.cm6.lineErrorAdd(editor.view, bad.map(i => i + firstLine.number));
}
const entries = self.cm6.findAll(
editor.view,
'\\bregexFilter: (\\S+)',
firstLine.from,
lastLine.to
);
const regexes = [];
for ( const entry of entries ) {
const regex = entry.match[1];
const i = this.validatedRegexes.indexOf(regex);
if ( i !== -1 ) {
const reason = this.validatedRegexResults[i];
if ( reason === true ) { continue; }
self.cm6.spanErrorAdd(editor.view, entry.from+13, entry.to, reason);
} else {
regexes.push(regex);
}
}
this.validateRegexes(editor, regexes);
}
exportToFile(text, fname) {
const { rules } = rulesFromText(text);
if ( Array.isArray(rules) === false ) { return; }
let ruleId = 1;
for ( const rule of rules ) {
rule.id = ruleId++;
}
return {
fname,
data: JSON.stringify(rules, null, 2),
mime: 'application/json',
};
}
async validateRegexes(editor, regexes) {
if ( regexes.length === 0 ) { return; }
const promises = regexes.map(regex => this.validateRegex(regex));
await Promise.all(promises);
for ( const regex of regexes ) {
const i = this.validatedRegexes.indexOf(regex);
if ( i === -1 ) { continue; }
const reason = this.validatedRegexResults[i];
if ( reason === true ) { continue; }
const entries = self.cm6.findAll(editor.view,
`(?<=\\bregexFilter: )${RegExp.escape(regex)}`
);
for ( const entry of entries ) {
self.cm6.spanErrorAdd(editor.view, entry.from, entry.to, reason);
}
}
}
async validateRegex(regex) {
const details = await dnr.isRegexSupported({ regex });
const result = details.isSupported || details.reason;
if ( this.validatedRegexes.length > 32 ) {
this.validatedRegexes.pop();
this.validatedRegexResults.pop();
}
this.validatedRegexes.unshift(regex);
this.validatedRegexResults.unshift(result);
}
createTooltipWidget(text) {
const template = document.querySelector('.badmark-tooltip');
const fragment = template.content.cloneNode(true);
const dom = fragment.querySelector('.badmark-tooltip');
dom.textContent = text;
return dom;
}
foldService(state, from) {
const { doc } = state;
const lineFrom = doc.lineAt(from);
if ( this.reFoldable.test(lineFrom.text) === false ) { return null; }
if ( lineFrom.number <= 5 ) { return null ; }
const lineBlockStart = doc.line(lineFrom.number - 5);
if ( this.reFoldCandidates.test(lineBlockStart.text) === false ) { return null; }
for ( let i = lineFrom.number-4; i < lineFrom.number; i++ ) {
const line = doc.line(i);
if ( this.reFoldable.test(line.text) === false ) { return null; }
}
let i = lineFrom.number + 1;
for ( ; i <= doc.lines; i++ ) {
const lineNext = doc.line(i);
if ( this.reFoldable.test(lineNext.text) === false ) { break; }
}
i -= 1;
if ( i === lineFrom.number ) { return null; }
const lineFoldEnd = doc.line(i);
return { from: lineFrom.from+6, to: lineFoldEnd.to };
}
reFoldable = /^ {4}- \S/;
reFoldCandidates = new RegExp(`^(?: {2})+${[
'initiatorDomains',
'excludedInitiatorDomains',
'requestDomains',
'excludedRequestDomains',
].join('|')}:$`);
streamParserKeywords = new RegExp(`\\b(${[
'block',
'redirect',
'allow',
'modifyHeaders',
'upgradeScheme',
'allowAllRequest',
'append',
'set',
'remove',
'firstParty',
'thirdParty',
'true',
'false',
'connect',
'delete',
'get',
'head',
'options',
'patch',
'post',
'put',
'other',
'main_frame',
'sub_frame',
'stylesheet',
'script',
'image',
'font',
'object',
'xmlhttprequest',
'ping',
'csp_report',
'media',
'websocket',
'webtransport',
'webbundle',
'other',
].join('|')})\\b`);
};

View file

@ -94,9 +94,15 @@ const perScopeParsers = {
rule[key] = {};
scope.push(key);
break;
case 'id': {
const n = parseInt(val, 10);
if ( isNaN(n) || n < 1) { return false; }
rule.id = n;
break;
}
case 'priority': {
const n = parseInt(val, 10);
if ( isNaN(n) || n <= 1 ) { return false; }
if ( isNaN(n) || n < 1 ) { return false; }
rule.priority = n;
break;
}
@ -283,6 +289,10 @@ const perScopeParsers = {
rule.condition[key] = [];
scope.push(key);
break;
case 'tabIds':
rule.condition.tabIds = [];
scope.push('tabIds');
break;
default:
return false;
}
@ -412,6 +422,12 @@ const perScopeParsers = {
item.excludedValues.push(node.val);
return true;
},
'condition.tabIds': function(scope, rule, node) {
if ( node.list !== true ) { return false; }
const n = parseInt(node.val, 10);
if ( isNaN(n) || n === 0 ) { return false; }
rule.condition.tabIds.push(n);
},
};
/******************************************************************************/
@ -479,16 +495,17 @@ export function rulesFromText(text) {
const indices = [];
for ( let i = 0; i < lines.length; i++ ) {
const line = lines[i].trimEnd();
const trimmed = line.trimStart();
if ( trimmed.startsWith('#') ) { continue; }
// Discard leading empty lines
if ( trimmed === '' ) {
if ( indices.length === 0 ) { continue; }
}
if ( line.trim().startsWith('#') ) { continue; }
if ( line !== '---' && line !== '...' ) {
indices.push(i);
continue;
}
// Discard leading empty lines
while ( indices.length !== 0 ) {
const s = lines[indices[0]].trim();
if ( s.length !== 0 ) { break; }
indices.shift();
}
// Discard trailing empty lines
while ( indices.length !== 0 ) {
const s = lines[indices.at(-1)].trim();
@ -544,18 +561,21 @@ function textFromValue(val, depth) {
/******************************************************************************/
export function textFromRules(rules) {
export function textFromRules(rules, option = {}) {
if ( Array.isArray(rules) === false ) {
if ( rules instanceof Object === false ) { return; }
rules = [ rules ];
}
const out = [];
for ( const rule of rules ) {
if ( rule.id ) { rule.id = undefined };
if ( option.keepId !== true && rule.id ) { rule.id = undefined };
const text = textFromValue(rule, 0);
if ( text === undefined ) { continue; }
out.push(text, '---' );
}
out.push('');
if ( out.length !== 0 ) {
out.unshift('---');
out.push('');
}
return out.join('\n');
}

View file

@ -0,0 +1,76 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
import {
modesFromText,
textFromModes,
} from './mode-parser.js';
import { i18n$ } from './i18n.js';
import { sendMessage } from './ext.js';
/******************************************************************************/
export class ModeEditor {
constructor(editor) {
this.editor = editor;
this.bc = new self.BroadcastChannel('uBOL');
this.bc.onmessage = ev => {
const message = ev.data;
if ( message instanceof Object === false ) { return; }
if ( message.filteringModeDetails === undefined ) { return; }
// TODO: merge with ongoing edits?
const text = textFromModes(message.filteringModeDetails);
editor.setEditorText(text, true);
};
}
async getText() {
const modes = await sendMessage({ what: 'getFilteringModeDetails' });
return textFromModes(modes);
}
async saveEditorText(editor) {
const { modes } = modesFromText(editor.getEditorText());
if ( modes instanceof Object === false ) { return; }
const modesAfter = await sendMessage({ what: 'setFilteringModeDetails', modes });
const text = textFromModes(modesAfter);
editor.setEditorText(text);
return true;
}
updateView(editor, firstLine, lastLine) {
const { doc } = editor.view.state;
const text = doc.sliceString(firstLine.from, lastLine.to);
const { bad } = modesFromText(text, true);
if ( Array.isArray(bad) && bad.length !== 0 ) {
self.cm6.lineErrorAdd(editor.view, bad.map(i => i + firstLine.number));
}
}
sequenceScopes = [
`${i18n$('filteringMode0Name')}:`, 'none:',
`${i18n$('filteringMode1Name')}:`, 'basic:',
`${i18n$('filteringMode2Name')}:`, 'optimal:',
`${i18n$('filteringMode3Name')}:`, 'complete:',
];
ioAccept = '.json,application/json';
};

View file

@ -225,7 +225,7 @@ export async function readFilteringModeDetails(bypassCache = false) {
}
}
let [
userModes,
userModes = { optimal: [ 'all-urls' ] },
adminDefaultFiltering,
adminNoFiltering,
] = await Promise.all([
@ -233,9 +233,6 @@ export async function readFilteringModeDetails(bypassCache = false) {
adminReadEx('defaultFiltering'),
adminReadEx('noFiltering'),
]);
if ( userModes === undefined ) {
userModes = { optimal: [ 'all-urls' ] };
}
userModes = unserializeModeDetails(userModes);
if ( adminDefaultFiltering !== undefined ) {
const modefromName = {
@ -277,29 +274,34 @@ async function writeFilteringModeDetails(afterDetails) {
readFilteringModeDetails.cache = unserializeModeDetails(data);
return Promise.all([
getDefaultFilteringMode(),
getTrustedSites(),
hasBroadHostPermissions(),
localWrite('filteringModeDetails', data),
sessionWrite('filteringModeDetails', data),
]).then(results => {
broadcastMessage({
defaultFilteringMode: results[0],
trustedSites: Array.from(results[1]),
hasOmnipotence: results[2],
hasOmnipotence: results[1],
filteringModeDetails: readFilteringModeDetails.cache,
});
});
}
/******************************************************************************/
export async function getFilteringModeDetails() {
export async function getFilteringModeDetails(serializable = false) {
const actualDetails = await readFilteringModeDetails();
return {
const out = {
none: new Set(actualDetails.none),
basic: new Set(actualDetails.basic),
optimal: new Set(actualDetails.optimal),
complete: new Set(actualDetails.complete),
};
return serializable ? serializeModeDetails(out) : out;
}
export async function setFilteringModeDetails(details) {
await localWrite('filteringModeDetails', serializeModeDetails(details));
await readFilteringModeDetails(true);
}
/******************************************************************************/
@ -328,45 +330,6 @@ export function setDefaultFilteringMode(afterLevel) {
/******************************************************************************/
export async function getTrustedSites() {
const filteringModes = await getFilteringModeDetails();
return filteringModes.none;
}
export async function setTrustedSites(hostnames) {
const [
filteringModes,
hasOmnipotence,
] = await Promise.all([
getFilteringModeDetails(),
hasBroadHostPermissions(),
]);
const { none } = filteringModes;
const hnSet = new Set(hostnames);
let modified = false;
if ( none.has('all-urls') && hnSet.has('all-urls') === false ) {
applyFilteringMode(filteringModes, 'all-urls', hasOmnipotence ? MODE_OPTIMAL : MODE_BASIC);
modified = true;
}
for ( const hn of none ) {
if ( hnSet.has(hn) ) {
hnSet.delete(hn);
} else {
none.delete(hn);
modified = true;
}
}
for ( const hn of hnSet ) {
const level = applyFilteringMode(filteringModes, hn, MODE_NONE);
if ( level !== MODE_NONE ) { continue; }
modified = true;
}
if ( modified === false ) { return; }
return writeFilteringModeDetails(filteringModes);
}
/******************************************************************************/
export async function syncWithBrowserPermissions() {
const [
permissions,

View file

@ -0,0 +1,211 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
import { i18n$ } from './i18n.js';
import punycode from './punycode.js';
/******************************************************************************/
function selectParser(scope, modes, node) {
const parser = perScopeParsers[scope.join('.')];
if ( parser === undefined ) { return false; }
return parser(scope, modes, node);
}
const validModes = [
'none',
'basic',
'optimal',
'complete',
];
const uglyModeNames = {
[i18n$('filteringMode0Name')]: 'none',
[i18n$('filteringMode1Name')]: 'basic',
[i18n$('filteringMode2Name')]: 'optimal',
[i18n$('filteringMode3Name')]: 'complete',
};
const prettyModeNames = {
none: i18n$('filteringMode0Name'),
basic: i18n$('filteringMode1Name'),
optimal: i18n$('filteringMode2Name'),
complete: i18n$('filteringMode3Name'),
};
const perScopeParsers = {
'': function(scope, modes, node) {
const { key, val } = node;
switch ( key ) {
case 'none':
case 'basic':
case 'optimal':
case 'complete':
case prettyModeNames.none:
case prettyModeNames.basic:
case prettyModeNames.optimal:
case prettyModeNames.complete: {
const mode = uglyModeNames[key] || key;
if ( val !== undefined ) { return false; }
modes[mode] ||= [];
scope.push(mode);
break;
}
default:
return false;
}
return true;
},
none: function(scope, modes, node) {
return addHostnameToMode(modes, 'none', node)
},
basic: function(scope, modes, node) {
return addHostnameToMode(modes, 'basic', node)
},
optimal: function(scope, modes, node) {
return addHostnameToMode(modes, 'optimal', node)
},
complete: function(scope, modes, node) {
return addHostnameToMode(modes, 'complete', node)
},
};
const addHostnameToMode = (modes, mode, node) => {
if ( node.list !== true ) { return false; }
modes[mode].push(punycode.toASCII(node.val));
};
/******************************************************************************/
function depthFromIndent(line) {
const match = /^\s*/.exec(line);
const count = match[0].length;
if ( (count & 1) !== 0 ) { return -1; }
return count / 2;
}
/******************************************************************************/
function nodeFromLine(line) {
const match = reNodeParser.exec(line);
const out = {};
if ( match === null ) { return out; }
if ( match[1] ) {
out.list = true;
}
if ( match[4] ) {
out.val = match[4].trim();
} else if ( match[3] ) {
out.key = match[2];
out.val = match[3].trim();
if ( out.val === "''" ) { out.val = '' };
} else {
out.key = match[2];
}
return out;
}
const reNodeParser = /^\s*(- )?(?:([^:]+):( \S.*)?|(\S.*))$/;
/******************************************************************************/
export function modesFromText(text, justbad = false) {
const lines = [ ...text.split(/\n\r|\r\n|\n|\r/) ];
const indices = [];
for ( let i = 0; i < lines.length; i++ ) {
const line = lines[i].trimEnd();
if ( line.trim().startsWith('#') ) { continue; }
indices.push(i);
}
// Discard leading empty lines
while ( indices.length !== 0 ) {
const s = lines[indices[0]].trim();
if ( s.length !== 0 ) { break; }
indices.shift();
}
// Discard trailing empty lines
while ( indices.length !== 0 ) {
const s = lines[indices.at(-1)].trim();
if ( s.length !== 0 ) { break; }
indices.pop();
}
// Parse
const modes = {};
const bad = [];
const scope = [];
for ( const i of indices ) {
const line = lines[i];
const depth = depthFromIndent(line);
if ( depth < 0 ) {
bad.push(i);
continue;
}
scope.length = depth;
const node = nodeFromLine(line);
const result = selectParser(scope, modes, node);
if ( result === false ) {
bad.push(i);
}
}
if ( justbad ) {
return bad.length !== 0 ? { bad } : { };
}
// Ensure all modes are present, and that one mode is the default one
const seen = new Map();
let defaultMode = '';
for ( const mode of validModes ) {
modes[mode] = new Set(modes[mode]);
if ( modes[mode].has('all-urls') ) {
defaultMode = mode;
}
for ( const hn of modes[mode] ) {
if ( seen.has(hn) ) {
modes[seen.get(hn)].delete(hn);
}
seen.set(hn, mode);
}
}
if ( defaultMode === '' ) {
defaultMode = 'optimal';
}
modes[defaultMode].clear();
modes[defaultMode].add('all-urls');
for ( const mode of validModes ) {
modes[mode] = Array.from(modes[mode]);
}
return { modes };
}
/******************************************************************************/
export function textFromModes(modes) {
const out = [];
for ( const mode of validModes ) {
const hostnames = modes[mode];
if ( hostnames === undefined ) { continue; }
out.push(`${prettyModeNames[mode]}:`);
for ( const hn of hostnames ) {
out.push(` - ${punycode.toUnicode(hn)}`);
}
}
out.push('');
return out.join('\n');
}

View file

@ -0,0 +1,95 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
import { DNREditor } from './dnr-editor.js';
import { i18n$ } from './i18n.js';
import { sendMessage } from './ext.js';
import { textFromRules } from './dnr-parser.js';
/******************************************************************************/
export class ReadOnlyDNREditor extends DNREditor {
async getText(hint) {
if ( hint === 'dnr.ro.dynamic' ) {
const rules = await sendMessage({ what: 'getEffectiveDynamicRules' });
if ( Array.isArray(rules) === false ) { return; }
this.id = 'dynamic';
this.count = rules.length;
return textFromRules(rules, { keepId: true });
}
if ( hint === 'dnr.ro.session' ) {
const rules = await sendMessage({ what: 'getEffectiveSessionRules' });
if ( Array.isArray(rules) === false ) { return; }
this.id = 'session';
this.count = rules.length;
return textFromRules(rules, { keepId: true });
}
const match = /^dnr\.ro\.(.+)$/.exec(hint);
if ( match === null ) { return; }
this.id = match[1];
const allRulesetDetails = await sendMessage({ what: 'getRulesetDetails' });
const rulesetDetails = allRulesetDetails.find(a => a.id === this.id);
if ( rulesetDetails === undefined ) { return; }
const realms = {
plain: 'main',
regex: 'regex',
redirect: 'redirect',
modifyHeaders: 'modify-headers',
removeparam: 'removeparam',
};
const promises = [];
for ( const [ realm, dir ] of Object.entries(realms) ) {
if ( Boolean(rulesetDetails.rules?.[realm]) === false ) { continue; }
promises.push(
fetch(`./rulesets/${dir}/${this.id}.json`).then(response => response.json())
);
}
const parts = await Promise.all(promises);
const allRules = [];
for ( const rules of parts ) {
for ( const rule of rules ) {
allRules.push(rule);
}
}
this.count = allRules.length;
return textFromRules(allRules, { keepId: true });
}
on(editor) {
if ( typeof this.count !== 'number' ) {
return editor.updateSummaryPanel(null);
}
const template = document.querySelector('template.ro-summary-panel');
const fragment = template.content.cloneNode(true);
const root = fragment.querySelector('.summary-panel');
root.textContent = i18n$('dnrRulesCountInfo')
.replace('{count}', (this.count || 0).toLocaleString())
editor.updateSummaryPanel(root);
}
off(editor) {
editor.updateSummaryPanel(null);
}
exportToFile(text) {
return super.exportToFile(text, `${this.id}-dnr-ruleset.json`);
}
};

View file

@ -306,19 +306,30 @@ async function updateDynamicRules() {
if ( dynamicRegexCount !== 0 ) {
ubolLog(`Using ${dynamicRegexCount}/${dnr.MAX_NUMBER_OF_REGEX_RULES} dynamic regex-based DNR rules`);
}
return Promise.all([
dnr.updateDynamicRules({ addRules, removeRuleIds }).then(( ) => {
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} dynamic DNR rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} dynamic DNR rules`);
}
}).catch(reason => {
console.error(`updateDynamicRules() / ${reason}`);
}),
updateSessionRules(),
]);
try {
await dnr.updateDynamicRules({ addRules, removeRuleIds });
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} dynamic DNR rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} dynamic DNR rules`);
}
} catch(reason) {
console.error(`updateDynamicRules() / ${reason}`);
}
await updateSessionRules();
}
/******************************************************************************/
async function getEffectiveDynamicRules() {
const allRules = await dnr.getDynamicRules();
const dynamicRules = [];
for ( const rule of allRules ) {
if ( rule.id >= USER_RULES_BASE_RULE_ID ) { continue; }
dynamicRules.push(rule);
}
return dynamicRules;
}
/******************************************************************************/
@ -441,16 +452,29 @@ async function updateSessionRules() {
if ( sessionRegexCount !== 0 ) {
ubolLog(`Using ${sessionRegexCount}/${dnr.MAX_NUMBER_OF_REGEX_RULES} session regex-based DNR rules`);
}
return dnr.updateSessionRules({ addRules, removeRuleIds }).then(( ) => {
try {
await dnr.updateSessionRules({ addRules, removeRuleIds });
if ( removeRuleIds.length !== 0 ) {
ubolLog(`Remove ${removeRuleIds.length} session DNR rules`);
}
if ( addRules.length !== 0 ) {
ubolLog(`Add ${addRules.length} session DNR rules`);
}
}).catch(reason => {
} catch(reason) {
console.error(`updateSessionRules() / ${reason}`);
});
}
}
/******************************************************************************/
async function getEffectiveSessionRules() {
const allRules = await dnr.getSessionRules();
const sessionRules = [];
for ( const rule of allRules ) {
if ( rule.id >= USER_RULES_BASE_RULE_ID ) { continue; }
sessionRules.push(rule);
}
return sessionRules;
}
/******************************************************************************/
@ -751,6 +775,8 @@ export {
enableRulesets,
excludeFromStrictBlock,
filteringModesToDNR,
getEffectiveDynamicRules,
getEffectiveSessionRules,
getEffectiveUserRules,
getEnabledRulesetsDetails,
getRulesetDetails,

View file

@ -0,0 +1,401 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
import {
browser,
localRead,
localRemove,
localWrite,
sendMessage,
} from './ext.js';
import { dom, qs$ } from './dom.js';
import { i18n, i18n$ } from './i18n.js';
import { DNREditor } from './dnr-editor.js';
import { textFromRules } from './dnr-parser.js';
/******************************************************************************/
export class ReadWriteDNREditor extends DNREditor {
constructor(editor) {
super();
this.feedbackPanel = self.cm6.createViewPanel();
editor.panels.push(this.feedbackPanel);
}
async getText() {
return localRead('userDnrRules');
}
on(editor) {
localRead('userDnrRuleCount').then(userDnrRuleCount => {
this.updateSummaryPanel(editor, { userDnrRuleCount })
});
browser.storage.onChanged.addListener((changes, area) => {
if ( area !== 'local' ) { return; }
const { userDnrRuleCount } = changes;
if ( userDnrRuleCount instanceof Object === false ) { return; }
const { newValue } = changes.userDnrRuleCount;
this.updateSummaryPanel(editor, { userDnrRuleCount: newValue });
});
}
off(editor) {
this.updateSummaryPanel(editor, null);
this.updateFeedbackPanel(editor, null);
}
rulesFromJSON(json) {
let content = json.trim();
if ( /^[[{]/.test(content) === false ) {
const match = /^[^[{]+/.exec(content);
if ( match === null ) { return; }
content = content.slice(match[0].length);
}
const firstChar = content.charAt(0);
const expectedLastChar = firstChar === '[' ? ']' : '}';
if ( content.at(-1) !== expectedLastChar ) {
const re = new RegExp(`\\${expectedLastChar}[^\\${expectedLastChar}]+$`);
const match = re.exec(content);
if ( match === null ) { return; }
content = content.slice(0, match.index+1);
}
if ( content.startsWith('{') && content.endsWith('}') ) {
content = `[${content}]`;
}
try {
const rules = JSON.parse(content);
if ( Array.isArray(rules) ) { return rules; }
}
catch {
}
}
getAutocompleteCandidates(editor, from) {
const { scope } = editor.getScopeAt(from);
switch ( scope ) {
case '':
return {
before: /^$/,
candidates: [
{ token: 'action:', after: '\n ' },
{ token: 'condition:', after: '\n ' },
{ token: 'priority:', after: ' ' },
{ token: '---', after: '\n' },
]
};
case 'action:':
return {
before: /^ {2}$/,
candidates: [
{ token: 'type:', after: ' ' },
{ token: 'redirect:', after: '\n ' },
{ token: 'requestHeaders:', after: '\n - header: ' },
{ token: 'responseHeaders:', after: '\n - header: ' },
],
};
case 'action:type:':
return {
before: /: $/,
candidates: [
{ token: 'block', after: '\n ' },
{ token: 'redirect', after: '\n ' },
{ token: 'allow', after: '\n ' },
{ token: 'modifyHeaders', after: '\n ' },
{ token: 'upgradeScheme', after: '\n ' },
{ token: 'allowAllRequest', after: '\n ' },
],
};
case 'action:redirect:':
return {
before: /^ {4}$/,
candidates: [
{ token: 'extensionPath:', after: ' ' },
{ token: 'regexSubstitution:', after: ' ' },
{ token: 'transform:', after: '\n ' },
{ token: 'url:', after: ' ' },
],
};
case 'action:redirect:transform:':
return {
before: /^ {6}$/,
candidates: [
{ token: 'fragment:', after: ' ' },
{ token: 'host:', after: ' ' },
{ token: 'path:', after: ' ' },
{ token: 'port:', after: ' ' },
{ token: 'query:', after: ' ' },
{ token: 'scheme:', after: ' ' },
{ token: 'queryTransform:', after: '\n ' },
],
};
case 'action:redirect:transform:queryTransform:':
return {
before: /^ {8}$/,
candidates: [
{ token: 'addOrReplaceParams:', after: '\n - ' },
{ token: 'removeParams:', after: '\n - ' },
],
};
case 'action:responseHeaders:':
case 'action:requestHeaders:':
return {
before: /^ {4}- $/,
candidates: [
{ token: 'header:', after: ' ' },
],
};
case 'action:responseHeaders:header:':
case 'action:requestHeaders:header:':
return {
before: /^ {6}$/,
candidates: [
{ token: 'operation:', after: ' ' },
{ token: 'value:', after: ' ' },
],
};
case 'action:responseHeaders:header:operation:':
case 'action:requestHeaders:header:operation:':
return {
before: /: $/,
candidates: [
{ token: 'append', after: '\n value: ' },
{ token: 'set', after: '\n value: ' },
{ token: 'remove', after: '\n ' },
],
};
case 'condition:':
return {
before: /^ {2}$/,
candidates: [
{ token: 'domainType:', after: ' ' },
{ token: 'isUrlFilterCaseSensitive:', after: ' ' },
{ token: 'regexFilter:', after: ' ' },
{ token: 'urlFilter:', after: ' ' },
{ token: 'initiatorDomains:', after: '\n - ' },
{ token: 'excludedInitiatorDomains:', after: '\n - ' },
{ token: 'requestDomains:', after: '\n - ' },
{ token: 'excludedRequestDomains:', after: '\n - ' },
{ token: 'resourceTypes:', after: '\n - ' },
{ token: 'excludedResourceTypes:', after: '\n - ' },
{ token: 'requestMethods:', after: '\n - ' },
{ token: 'excludedRequestMethods:', after: '\n - ' },
{ token: 'responseHeaders:', after: '\n - ' },
{ token: 'excludedResponseHeaders:', after: '\n - ' },
],
};
case 'condition:domainType:':
return {
before: /: $/,
candidates: [
{ token: 'firstParty', after: '\n ' },
{ token: 'thirdParty', after: '\n ' },
],
};
case 'condition:isUrlFilterCaseSensitive:':
return {
before: /: $/,
candidates: [
{ token: 'true', after: '\n ' },
{ token: 'false', after: '\n ' },
],
};
case 'condition:requestMethods:':
case 'condition:excludedRequestMethods:':
return {
before: /^ {4}- $/,
candidates: [
{ token: 'connect', after: '\n - ' },
{ token: 'delete', after: '\n - ' },
{ token: 'get', after: '\n - ' },
{ token: 'head', after: '\n - ' },
{ token: 'options', after: '\n - ' },
{ token: 'patch', after: '\n - ' },
{ token: 'post', after: '\n - ' },
{ token: 'put', after: '\n - ' },
{ token: 'other', after: '\n ' },
],
};
case 'condition:resourceTypes:':
case 'condition:excludedResourceTypes:':
return {
before: /^ {4}- $/,
candidates: [
{ token: 'main_frame', after: '\n - ' },
{ token: 'sub_frame', after: '\n - ' },
{ token: 'stylesheet', after: '\n - ' },
{ token: 'script', after: '\n - ' },
{ token: 'image', after: '\n - ' },
{ token: 'font', after: '\n - ' },
{ token: 'object', after: '\n - ' },
{ token: 'xmlhttprequest', after: '\n - ' },
{ token: 'ping', after: '\n - ' },
{ token: 'csp_report', after: '\n - ' },
{ token: 'media', after: '\n - ' },
{ token: 'websocket', after: '\n - ' },
{ token: 'webtransport', after: '\n - ' },
{ token: 'webbundle', after: '\n - ' },
{ token: 'other', after: '\n ' },
],
};
}
}
autoComplete(editor, context) {
const match = context.matchBefore(/[\w-]*/);
if ( match === undefined ) { return null; }
const result = this.getAutocompleteCandidates(editor, match.from);
if ( result === undefined ) { return null; }
if ( result.before !== undefined ) {
const { doc } = context.state;
const line = doc.lineAt(context.pos);
const before = doc.sliceString(line.from, match.from);
if ( result.before.test(before) === false ) { return null; }
}
const filtered = result.candidates.filter(e =>
e.token !== match.text || e.after !== '\n'
);
return {
from: match.from,
options: filtered.map(e => ({ label: e.token, apply: `${e.token}${e.after}` })),
validFor: /\w*/,
};
}
async saveEditorText(editor) {
const text = editor.getEditorText().trim();
await (text.length !== 0
? localWrite('userDnrRules', text)
: localRemove('userDnrRules')
);
const response = await sendMessage({ what: 'updateUserDnrRules' })
if ( response instanceof Object ) {
this.updateFeedbackPanel(editor, response);
}
return true;
}
updateSummaryPanel(editor, details) {
if ( details instanceof Object === false ) {
return editor.updateSummaryPanel(null);
}
const template = document.querySelector('template.summary-panel');
const fragment = template.content.cloneNode(true);
const root = fragment.querySelector('.summary-panel');
i18n.render(root);
const info = root.querySelector('.info');
info.textContent = i18n$('dnrRulesCountInfo')
.replace('{count}', (details.userDnrRuleCount || 0).toLocaleString())
editor.updateSummaryPanel(root);
}
updateFeedbackPanel(editor, details) {
if ( details instanceof Object === false ) {
return this.feedbackPanel.render(editor.view, null);
}
const errors = [];
if ( Array.isArray(details.errors) ) {
details.errors.forEach(e => errors.push(e));
}
const text = errors.join('\n');
const config = (( ) => {
if ( text === '' ) { return null; }
const template = document.querySelector('template.feedback-panel');
const fragment = template.content.cloneNode(true);
const root = fragment.querySelector('.feedback-panel');
const info = root.querySelector('.info');
info.textContent = text;
const closeFn = this.updateFeedbackPanel.bind(this, editor, null);
return {
dom: root,
mount() {
dom.on(qs$('.feedback-panel .close'), 'click', closeFn);
}
};
})();
this.feedbackPanel.render(editor.view, config);
}
importFromFile(editor, json) {
const rules = this.rulesFromJSON(json);
if ( rules === undefined ) { return; }
const text = textFromRules(rules);
if ( text === undefined ) { return; }
const { doc } = editor.view.state;
const lastChars = doc.toString().trimEnd().slice(-4);
const lastLine = doc.line(doc.lines);
let from = lastLine.to;
let prepend = '';
if ( lastLine.text !== '' ) {
prepend = '\n';
} else {
from = lastLine.from;
}
if ( /(?:^|\n)---$/.test(lastChars) === false ) {
prepend = `${prepend}---\n`;
}
editor.view.dispatch({ changes: { from, insert: `${prepend}${text}` } });
self.cm6.foldAll(editor.view);
editor.view.focus();
}
exportToFile(text) {
return super.exportToFile(text, 'my-ubol-dnr-rules.json');
}
importFromPaste(editor, transaction) {
const { from, to } = editor.rangeFromTransaction(transaction);
if ( from === undefined || to === undefined ) { return; }
// Paste position must match start of a line
const { newDoc } = transaction;
const lineFrom = newDoc.lineAt(from);
if ( lineFrom.from !== from ) { return; }
// Paste position must match a rule boundary
if ( lineFrom.number !== 1 ) {
const lineBefore = newDoc.line(lineFrom.number-1);
if ( /^---\s*$/.test(lineBefore.text) === false ) { return; }
}
const pastedText = newDoc.sliceString(from, to);
const rules = this.rulesFromJSON(pastedText);
if ( rules === undefined ) { return; }
const yamlText = textFromRules(rules);
if ( yamlText === undefined ) { return; }
editor.view.dispatch({ changes: { from, to, insert: yamlText } });
self.cm6.foldAll(editor.view);
return true;
}
sequenceScopes = [
'action:redirect:transform:queryTransform:addOrReplaceParams:',
'action:redirect:transform:queryTransform:removeParams:',
'condition:resourceTypes:',
'condition:excludedResourceTypes:',
'condition:initiatorDomains:',
'condition:excludedInitiatorDomains:',
'condition:requestDomains:',
'condition:excludedRequestDomains:',
'condition:requestMethods:',
'condition:excludedRequestMethods:',
'condition:responseHeaders:',
'condition:excludedResponseHeaders:',
];
ioAccept = '.json,application/json';
};

View file

@ -22,23 +22,10 @@
import { browser, sendMessage } from './ext.js';
import { dom, qs$ } from './dom.js';
import { hashFromIterable } from './dashboard.js';
import { i18n$ } from './i18n.js';
import punycode from './punycode.js';
import { renderFilterLists } from './filter-lists.js';
/******************************************************************************/
const cm6 = self.cm6;
const cmTrustedSites = (( ) => {
const options = {};
if ( dom.cl.has(':root', 'dark') ) {
options.oneDark = true;
}
options.placeholder = i18n$('noFilteringModePlaceholder');
return cm6.createEditorView(options, qs$('#trustedSites'));
})();
let cachedRulesetData = {};
/******************************************************************************/
@ -60,7 +47,6 @@ function renderWidgets() {
}
renderDefaultMode();
renderTrustedSites();
qs$('#autoReload input[type="checkbox"]').checked = cachedRulesetData.autoReload;
@ -175,48 +161,6 @@ dom.on('#developerMode input[type="checkbox"]', 'change', ev => {
/******************************************************************************/
function renderTrustedSites() {
const hostnames = cachedRulesetData.trustedSites || [];
let text = hostnames.map(hn => punycode.toUnicode(hn)).join('\n');
if ( text !== '' ) { text += '\n'; }
cmTrustedSites.dispatch({
changes: {
from: 0, to: cmTrustedSites.state.doc.length,
insert: text
},
});
}
function changeTrustedSites() {
const hostnames = getStagedTrustedSites();
const hash = hashFromIterable(cachedRulesetData.trustedSites || []);
if ( hashFromIterable(hostnames) === hash ) { return; }
sendMessage({
what: 'setTrustedSites',
hostnames,
});
}
function getStagedTrustedSites() {
const text = cmTrustedSites.state.doc.toString();
return text.split(/\s/).map(hn => {
if ( hn === '' ) { return ''; }
try {
return punycode.toASCII(
(new URL(`https://${hn}/`)).hostname
);
} catch {
}
return '';
}).filter(hn => hn !== '');
}
dom.on(cmTrustedSites.contentDOM, 'blur', changeTrustedSites);
self.addEventListener('beforeunload', changeTrustedSites);
/******************************************************************************/
function listen() {
const bc = new self.BroadcastChannel('uBOL');
bc.onmessage = listen.onmessage;
@ -228,21 +172,6 @@ listen.onmessage = ev => {
const local = cachedRulesetData;
let render = false;
// Keep added sites which have not yet been committed
if ( message.trustedSites !== undefined ) {
if ( hashFromIterable(message.trustedSites) !== hashFromIterable(local.trustedSites) ) {
const current = new Set(local.trustedSites);
const staged = new Set(getStagedTrustedSites());
for ( const hn of staged ) {
if ( current.has(hn) === false ) { continue; }
staged.delete(hn);
}
const combined = Array.from(new Set([ ...message.trustedSites, ...staged ]));
local.trustedSites = combined;
render = true;
}
}
if ( message.hasOmnipotence !== undefined ) {
if ( message.hasOmnipotence !== local.hasOmnipotence ) {
local.hasOmnipotence = message.hasOmnipotence;

@ -1 +1 @@
Subproject commit 4b9666da6d3cf0321493c6a267b11cee88677411
Subproject commit 4352f572758bc43ebabddb67b98875e8fde7c297

View file

@ -263,6 +263,11 @@ if ( isBackgroundProcess !== true ) {
elem.setAttribute('aria-label', text);
}
}
for ( const elem of root.querySelectorAll('[data-i18n-label]') ) {
const text = i18n$(elem.getAttribute('data-i18n-label'));
elem.setAttribute('label', text);
}
};
i18n.renderElapsedTimeToString = function(tstamp) {