mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
[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:
parent
a12ed895dd
commit
b50341089d
19 changed files with 1765 additions and 1048 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
BIN
platform/mv3/extension/css/line-hor-dashed.png
Normal file
BIN
platform/mv3/extension/css/line-hor-dashed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 B |
|
|
@ -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>
|
||||
 
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
180
platform/mv3/extension/js/dnr-editor.js
Normal file
180
platform/mv3/extension/js/dnr-editor.js
Normal 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`);
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
76
platform/mv3/extension/js/mode-editor.js
Normal file
76
platform/mv3/extension/js/mode-editor.js
Normal 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';
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
211
platform/mv3/extension/js/mode-parser.js
Normal file
211
platform/mv3/extension/js/mode-parser.js
Normal 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');
|
||||
}
|
||||
95
platform/mv3/extension/js/ro-dnr-editor.js
Normal file
95
platform/mv3/extension/js/ro-dnr-editor.js
Normal 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`);
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
401
platform/mv3/extension/js/rw-dnr-editor.js
Normal file
401
platform/mv3/extension/js/rw-dnr-editor.js
Normal 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';
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue