[mv3] Add ability to enable/disable filter lists

This commit is contained in:
Raymond Hill 2022-09-13 17:44:24 -04:00
parent d11a3f2fa3
commit e31637af78
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
63 changed files with 2344 additions and 742 deletions

View file

@ -4,7 +4,6 @@
"eqeqeq": true, "eqeqeq": true,
"esversion": 8, "esversion": 8,
"globals": { "globals": {
"browser": false, // global variable in Firefox, Edge
"chrome": false, // global variable in Chromium, Chrome, Opera "chrome": false, // global variable in Chromium, Chrome, Opera
"self": false, "self": false,
"vAPI": false, "vAPI": false,

View file

@ -1,11 +1,11 @@
# https://stackoverflow.com/a/6273809 # https://stackoverflow.com/a/6273809
run_options := $(filter-out $@,$(MAKECMDGOALS)) run_options := $(filter-out $@,$(MAKECMDGOALS))
.PHONY: all clean test lint chromium firefox npm dig mv3 \ .PHONY: all clean test lint chromium firefox npm dig mv3 mv3-quick \
compare maxcost medcost mincost modifiers record wasm compare maxcost medcost mincost modifiers record wasm
sources := $(wildcard assets/resources/* dist/version src/* src/*/* src/*/*/* src/*/*/*/*) sources := $(wildcard assets/resources/* dist/version src/* src/*/* src/*/*/* src/*/*/*/*)
platform := $(wildcard platform/* platform/*/* platform/*/*/*) platform := $(wildcard platform/* platform/*/* platform/*/*/* platform/*/*/*/*)
assets := $(wildcard submodules/uAssets/* \ assets := $(wildcard submodules/uAssets/* \
submodules/uAssets/*/* \ submodules/uAssets/*/* \
submodules/uAssets/*/*/* \ submodules/uAssets/*/*/* \
@ -57,7 +57,7 @@ mv3: tools/make-mv3.sh $(sources) $(platform)
mv3-quick: tools/make-mv3.sh $(sources) $(platform) mv3-quick: tools/make-mv3.sh $(sources) $(platform)
tools/make-mv3.sh quick tools/make-mv3.sh quick
mv3-full: tools/make-mv3.sh $(sources) $(platform) mv3-full: tools/make-mv3.sh $(sources) $(platform)
tools/make-mv3.sh full tools/make-mv3.sh full

View file

@ -291,6 +291,10 @@ if response.status_code != 204:
# package is higher version than current one. # package is higher version than current one.
# #
# Be sure in sync with potentially modified files on remote
r = subprocess.run(['git', 'checkout', 'origin/master', '--', 'dist/chromium-mv3/log.txt'], stdout=subprocess.PIPE)
rout = bytes.decode(r.stdout).strip()
print('Update GitHub to point to newly signed self-hosted xpi package...') print('Update GitHub to point to newly signed self-hosted xpi package...')
updates_json_filepath = os.path.join(projdir, 'dist', 'firefox', 'updates.json') updates_json_filepath = os.path.join(projdir, 'dist', 'firefox', 'updates.json')
with open(updates_json_filepath) as f: with open(updates_json_filepath) as f:
@ -305,7 +309,6 @@ with open(updates_json_filepath) as f:
with open(updates_json_filepath, 'w') as f: with open(updates_json_filepath, 'w') as f:
f.write(updates_json) f.write(updates_json)
f.close() f.close()
# Automatically git add/commit if needed.
# - Stage the changed file # - Stage the changed file
r = subprocess.run(['git', 'status', '-s', updates_json_filepath], stdout=subprocess.PIPE) r = subprocess.run(['git', 'status', '-s', updates_json_filepath], stdout=subprocess.PIPE)
rout = bytes.decode(r.stdout).strip() rout = bytes.decode(r.stdout).strip()

View file

@ -112,18 +112,6 @@ vAPI.getURL = browser.runtime.getURL;
/******************************************************************************/ /******************************************************************************/
vAPI.i18n = browser.i18n.getMessage;
// http://www.w3.org/International/questions/qa-scripts#directions
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(vAPI.i18n('@@ui_locale')) !== -1
? 'rtl'
: 'ltr'
);
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/3057 // https://github.com/gorhill/uBlock/issues/3057
// - webNavigation.onCreatedNavigationTarget become broken on Firefox when we // - webNavigation.onCreatedNavigationTarget become broken on Firefox when we
// try to make the popup panel close itself using the original // try to make the popup panel close itself using the original

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>uBlock Origin Lite — Filter lists</title>
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/fa-icons.css">
<link rel="stylesheet" type="text/css" href="css/3p-filters.css">
</head>
<body>
<div class="body">
<div id="actions">
<p id="listsOfBlockedHostsPrompt">
<p><button id="buttonApply" class="preferred disabled iconified" type="button" data-i18n-title="genericApplyChanges"><span class="fa-icon">check</span><span data-i18n="genericApplyChanges">_</span><span class="hover"></span></button>
</div>
<div>
<div id="lists"></div>
</div>
</div>
<div id="templates" style="display: none;">
<div class="groupEntry">
<div class="geDetails"><span class="geName"></span> <span class="geCount"></span></div>
<div class="listEntries"></div>
</div>
<div class="li listEntry">
<label><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span><span class="listname forinput"></span> <span class="iconbar"><!--
--><a class="fa-icon support" href="#" target="_blank">home</a><!--
--><a class="fa-icon mustread" href="#" target="_blank">info-circle</a><!--
--></span></span></label>
</div>
</div>
<script src="js/fa-icons.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>
<script src="js/3p-filters.js" type="module"></script>
</body>
</html>

View file

@ -0,0 +1,202 @@
{
"extName": {
"message": "uBlock Origin Lite",
"description": "extension name."
},
"extShortDesc": {
"message": "An experimental, permission-less lite content blocker -- block ads, trackers, miners and more by default.",
"description": "this will be in the Chrome web store: must be 132 characters or less"
},
"perRulesetStats": {
"message": "{{ruleCount}} rules, converted from {{filterCount}} network filters",
"description": "Appears aside each filter list in the _3rd-party filters_ pane"
},
"dashboardName": {
"message": "uBO Lite — Dashboard",
"description": "English: uBO Lite — Dashboard"
},
"dashboardUnsavedWarning": {
"message": "Warning! You have unsaved changes",
"description": "A warning in the dashboard when navigating away from unsaved changes"
},
"dashboardUnsavedWarningStay": {
"message": "Stay",
"description": "Label for button to prevent navigating away from unsaved changes"
},
"dashboardUnsavedWarningIgnore": {
"message": "Ignore",
"description": "Label for button to ignore unsaved changes"
},
"settingsPageName": {
"message": "Settings",
"description": "appears as tab name in dashboard"
},
"3pPageName": {
"message": "Filter lists",
"description": "appears as tab name in dashboard"
},
"1pPageName": {
"message": "My filters",
"description": "appears as tab name in dashboard"
},
"whitelistPageName": {
"message": "Trusted sites",
"description": "appears as tab name in dashboard"
},
"aboutPageName": {
"message": "About",
"description": "appears as tab name in dashboard"
},
"aboutPrivacyPolicy": {
"message": "Privacy policy",
"description": "Link to privacy policy on GitHub (English)"
},
"popupPowerSwitchInfo": {
"message": "Disable/enable uBO Lite for this site",
"description": "Tooltip for the main power button in the popup panel"
},
"popupTipDashboard": {
"message": "Open the dashboard",
"description": "English: Click to open the dashboard"
},
"popupTipZapper": {
"message": "Enter element zapper mode",
"description": "Tooltip for the element-zapper icon in the popup panel"
},
"popupTipPicker": {
"message": "Enter element picker mode",
"description": "English: Enter element picker mode"
},
"popupTipReport": {
"message": "Report an issue on this website",
"description": "Tooltip used for the 'chat' icon in the panel"
},
"popupTipSaveRules": {
"message": "Click to make your changes permanent.",
"description": "Tooltip when hovering over the padlock in the dynamic filtering pane."
},
"popupTipRevertRules": {
"message": "Click to revert your changes.",
"description": "Tooltip when hovering over the eraser in the dynamic filtering pane."
},
"settingsIconBadgePrompt": {
"message": "Show the number of blocked requests on the icon",
"description": "English: Show the number of blocked requests on the icon"
},
"settingsAppearance": {
"message": "Appearance",
"description": "Section for controlling user interface appearance"
},
"settingsThemeLabel": {
"message": "Theme",
"description": "Label for checkbox to enable a custom dark theme"
},
"settingsThemeAccent0Label": {
"message": "Custom accent color",
"description": "Label for checkbox to pick an accent color"
},
"settingsNoCSPReportsPrompt": {
"message": "Block CSP reports",
"description": "background information: https://github.com/gorhill/uBlock/issues/3150"
},
"3pGroupDefault": {
"message": "Default",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupAds": {
"message": "Ads",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupPrivacy": {
"message": "Privacy",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupMalware": {
"message": "Malware domains",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupAnnoyances": {
"message": "Annoyances",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupMisc": {
"message": "Miscellaneous",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupRegions": {
"message": "Regions, languages",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"1pFormatHint": {
"message": "One filter per line. A filter can be a plain hostname, or an EasyList-compatible filter. Lines prefixed with <code>!</code> will be ignored.",
"description": "Short information about how to create custom filters"
},
"1pImport": {
"message": "Import and append",
"description": "English: Import and append"
},
"1pExport": {
"message": "Export",
"description": "English: Export"
},
"1pExportFilename": {
"message": "my-ublock-static-filters_{{datetime}}.txt",
"description": "English: my-ublock-static-filters_{{datetime}}.txt"
},
"whitelistPrompt": {
"message": "The trusted site directives dictate on which web pages uBO Lite should be disabled. One entry per line.",
"description": "A concise description of the 'Trusted sites' pane."
},
"whitelistImport": {
"message": "Import and append",
"description": "English: Import and append"
},
"whitelistExport": {
"message": "Export",
"description": "English: Export"
},
"whitelistExportFilename": {
"message": "my-ublock-trusted-sites_{{datetime}}.txt",
"description": "The default filename to use for import/export purpose"
},
"aboutChangelog": {
"message": "Changelog",
"description": ""
},
"aboutCode": {
"message": "Source code (GPLv3)",
"description": "English: Source code (GPLv3)"
},
"aboutContributors": {
"message": "Contributors",
"description": "English: Contributors"
},
"aboutSourceCode": {
"message": "Source code",
"description": "Link text to source code repo"
},
"aboutTranslations": {
"message": "Translations",
"description": "Link text to translations repo"
},
"aboutFilterLists": {
"message": "Filter lists",
"description": "Link text to uBO's own filter lists repo"
},
"aboutDependencies": {
"message": "External dependencies (GPLv3-compatible):",
"description": "Shown in the About pane"
},
"genericSubmit": {
"message": "Submit",
"description": "for generic 'Submit' buttons"
},
"genericApplyChanges": {
"message": "Apply changes",
"description": "for generic 'Apply changes' buttons"
},
"genericRevert": {
"message": "Revert",
"description": "for generic 'Revert' buttons"
}
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>uBlock Origin Lite — About</title>
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/about.css">
</head>
<body>
<div class="body">
<div id="aboutNameVer" class="li"></div>
<div class="liul">
<div class="li">Copyright (c) Raymond Hill 2014-present</div>
</div>
<div class="li"><a href="https://github.com/gorhill/uBlock/wiki/Privacy-policy" data-i18n="aboutPrivacyPolicy"></a></div>
<div class="li"><a href="https://github.com/gorhill/uBlock/releases" data-i18n="aboutChangelog"></a></div>
<div class="li"><a href="https://github.com/gorhill/uBlock" data-i18n="aboutCode"></a></div>
<div class="li"><span data-i18n="aboutContributors"></span></div>
<div class="liul">
<div class="li"><a href="https://github.com/gorhill/uBlock/graphs/contributors" data-i18n="aboutSourceCode"></a></div>
<div class="li"><a href="https://crowdin.com/project/ublock" data-i18n="aboutTranslations"></a></div>
<div class="li"><a href="https://github.com/uBlockOrigin/uAssets/graphs/contributors" data-i18n="aboutFilterLists"></a></div>
</div>
<div class="li"><span data-i18n="aboutDependencies"></span></div>
<div class="liul">
<div class="li"><span><a href="https://github.com/chrismsimpson/Metropolis" target="_blank">Metropolis font family</a> by <a href="https://github.com/chrismsimpson">Chris Simpson</a></span></div>
<div class="li"><span><a href="https://github.com/rsms/inter" target="_blank">Inter font family</a> by <a href="https://github.com/rsms">Rasmus Andersson</a></span></div>
<div class="li"><span><a href="https://fontawesome.com/" target="_blank">FontAwesome font family</a> by <a href="https://github.com/davegandy">Dave Gandy</a></span></div>
</div>
</div>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>
<script src="js/about.js" type="module"></script>
</body>
</html>

View file

@ -0,0 +1,186 @@
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
body {
margin-bottom: 6rem;
}
#actions {
background-color: var(--surface-1);
position: sticky;
top: 0;
z-index: 10;
}
#buttonUpdate.active {
pointer-events: none;
}
#buttonUpdate.active .fa-icon svg {
animation: spin 1s linear infinite;
transform-origin: 50%;
}
#lists {
margin: 0.5em 0 0 0;
padding: 0;
}
.groupEntry:not([data-groupkey="user"]) .geDetails::before {
color: var(--ink-3);
content: '\2212';
font-family: monospace;
font-size: large;
margin-inline-end: 0.25em;
-webkit-margin-end: 0.25em;
}
.groupEntry.hideUnused:not([data-groupkey="user"]) .geDetails::before {
content: '+';
}
.groupEntry {
margin: 0.5em 0;
}
.groupEntry .geDetails {
cursor: pointer;
}
.groupEntry .geName {
pointer-events: none;
}
.groupEntry .geCount {
color: var(--ink-3);
font-size: 90%;
pointer-events: none;
}
.listEntries {
margin-inline-start: 0.6em;
-webkit-margin-start: 0.6em;
}
.groupEntry:not([data-groupkey="user"]) .listEntry:not(.isDefault).unused {
display: none;
}
.listEntry > * {
margin-left: 0;
margin-right: 0;
unicode-bidi: embed;
}
.listEntry .listname {
white-space: nowrap;
}
.listEntry.toRemove .checkbox {
visibility: hidden;
}
.listEntry.toRemove .listname {
text-decoration: line-through;
}
.listEntry a,
.listEntry .fa-icon,
.listEntry .counts {
color: var(--info0-ink);
fill: var(--info0-ink);
display: none;
font-size: 120%;
margin: 0 0.2em 0 0;
}
.listEntry .fa-icon:hover {
transform: scale(1.25);
}
.listEntry .content {
display: inline-flex;
}
.listEntry a.towiki {
display: inline-flex;
}
.listEntry.support a.support {
display: inline-flex;
}
.listEntry .remove,
.listEntry .unsecure,
.listEntry .failed {
color: var(--info3-ink);
fill: var(--info3-ink);
cursor: pointer;
}
.listEntry.external .remove {
display: inline-flex;
}
.listEntry.mustread a.mustread {
color: var(--info1-ink);
fill: var(--info1-ink);
display: inline-flex;
}
.listEntry .counts {
font-size: smaller;
}
.listEntry.checked .counts {
display: inline-block;
}
.listEntry .status {
cursor: default;
display: none;
}
.listEntry.checked.unsecure .unsecure {
display: inline-flex;
}
.listEntry.failed .failed {
display: inline-flex;
}
.listEntry .cache {
cursor: pointer;
}
.listEntry.checked.cached:not(.obsolete) .cache {
display: inline-flex;
}
.listEntry .obsolete {
color: var(--info2-ink);
fill: var(--info2-ink);
}
body:not(.updating) .listEntry.checked.obsolete .obsolete {
display: inline-flex;
}
.listEntry .updating {
transform-origin: 50%;
}
body.updating .listEntry.checked.obsolete .updating {
animation: spin 1s steps(8) infinite;
display: inline-flex;
}
.listEntry.toImport {
margin: 0.5em 0;
}
.listEntry.toImport textarea {
border: 1px solid #ccc;
box-sizing: border-box;
display: block;
font-size: smaller;
height: 6em;
margin: 0;
resize: vertical;
visibility: hidden;
white-space: pre;
width: 100%;
}
.listEntry.toImport.checked textarea {
visibility: visible;
}
/* touch-screen devices */
:root.mobile .listEntry .fa-icon {
font-size: 120%;
margin: 0 0.5em 0 0;
}
:root.mobile .listEntries {
margin-inline-start: 0;
-webkit-margin-start: 0;
}
:root.mobile .li.listEntry {
/* background-color: var(--bg-1); */
overflow-x: auto;
}
:root.mobile .li.listEntry label > span:not([class]) {
flex-grow: 1;
}
:root.mobile .li.listEntry .listname,
:root.mobile .li.listEntry .iconbar {
align-items: flex-start;
display: flex;
white-space: nowrap;
}
:root.mobile .li.listEntry .iconbar {
margin-top: 0.2em;
}

View file

@ -0,0 +1,55 @@
body > div.body {
margin: 0 1em;
}
h2, h3 {
margin: 1em 0;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
a {
text-decoration: none;
}
.fa-icon.info {
color: var(--info0-ink);
fill: var(--info0-ink);
font-size: 115%;
}
.fa-icon.info:hover {
transform: scale(1.25);
}
.fa-icon.info.important {
color: var(--info2-ink);
fill: var(--info2-ink);
}
.fa-icon.info.very-important {
color: var(--info3-ink);
fill: var(--info3-ink);
}
input[type="number"] {
width: 5em;
}
@media (max-height: 640px), (max-height: 800px) and (max-width: 480px) {
.body > p,
.body > ul {
margin: 0.5em 0;
}
.vverbose {
display: none !important;
}
}
/**
On mobile device, the on-screen keyboard may take up
so much space that it overlaps the content being edited.
The rule below makes it possible to scroll the edited
content within view.
*/
:root.mobile {
overflow: auto;
}
:root.mobile body {
min-height: 600px;
}

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title data-i18n="dashboardName"></title>
<link rel="stylesheet" href="css/default.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/fa-icons.css">
<link rel="stylesheet" href="css/dashboard.css">
<link rel="shortcut icon" type="image/png" href="img/icon_16.png"/>
</head>
<body>
<div id="dashboard-nav">
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg"></span><!--
--><button class="tabButton" type="button" data-pane="3p-filters.html" data-i18n="3pPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="about.html" data-i18n="aboutPageName" tabindex="0"></button>
</div>
<section id="unsavedWarning" class="notice">
<div>
<span data-i18n="dashboardUnsavedWarning"></span>&emsp;
<button type="button" data-i18n="dashboardUnsavedWarningStay">_<span class="hover"></span></button>&ensp;
<button type="button" data-i18n="dashboardUnsavedWarningIgnore">_<span class="hover"></span></button>
</div>
<div></div>
</section>
<iframe id="iframe" src=""></iframe>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard.js" type="module"></script>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,379 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
import { sendMessage } from './ext.js';
import { i18n$ } from './i18n.js';
import { dom, qs$, qsa$ } from './dom.js';
import { simpleStorage } from './storage.js';
/******************************************************************************/
let cachedRulesetData = {};
let filteringSettingsHash = '';
let hideUnusedSet = new Set([ 'regions' ]);
/******************************************************************************/
const renderNumber = function(value) {
return value.toLocaleString();
};
/******************************************************************************/
const renderFilterLists = function(soft) {
const { enabledRulesets, rulesetDetails } = cachedRulesetData;
const listGroupTemplate = qs$('#templates .groupEntry');
const listEntryTemplate = qs$('#templates .listEntry');
const listStatsTemplate = i18n$('perRulesetStats');
const groupNames = new Map([ [ 'user', '' ] ]);
const liFromListEntry = function(ruleset, li, hideUnused) {
if ( !li ) {
li = listEntryTemplate.cloneNode(true);
}
const on = enabledRulesets.includes(ruleset.id);
li.classList.toggle('checked', on);
let elem;
if ( dom.attr(li, 'data-listkey') !== ruleset.id ) {
dom.attr(li, 'data-listkey', ruleset.id);
qs$('input[type="checkbox"]', li).checked = on;
qs$('.listname', li).textContent = ruleset.name || ruleset.id;
dom.removeClass(li, 'toRemove');
if ( ruleset.supportName ) {
dom.addClass(li, 'support');
elem = qs$('a.support', li);
dom.attr(elem, 'href', ruleset.supportURL);
dom.attr(elem, 'title', ruleset.supportName);
} else {
dom.removeClass(li, 'support');
}
if ( ruleset.instructionURL ) {
dom.addClass(li, 'mustread');
dom.attr(qs$('a.mustread', li), 'href', ruleset.instructionURL);
} else {
dom.removeClass(li, 'mustread');
}
dom.toggleClass(li, 'isDefault', ruleset.isDefault === true);
dom.toggleClass(li, 'unused', hideUnused && !on);
}
// https://github.com/gorhill/uBlock/issues/1429
if ( !soft ) {
qs$('input[type="checkbox"]', li).checked = on;
}
li.title = listStatsTemplate
.replace('{{ruleCount}}', renderNumber(ruleset.rules.accepted))
.replace('{{filterCount}}', renderNumber(ruleset.filters.accepted));
return li;
};
const listEntryCountFromGroup = function(groupRulesets) {
if ( Array.isArray(groupRulesets) === false ) { return ''; }
let count = 0,
total = 0;
for ( const ruleset of groupRulesets ) {
if ( enabledRulesets.includes(ruleset.id) ) {
count += 1;
}
total += 1;
}
return total !== 0 ?
`(${count.toLocaleString()}/${total.toLocaleString()})` :
'';
};
const liFromListGroup = function(groupKey, groupRulesets) {
let liGroup = qs$(`#lists > .groupEntry[data-groupkey="${groupKey}"]`);
if ( liGroup === null ) {
liGroup = listGroupTemplate.cloneNode(true);
let groupName = groupNames.get(groupKey);
if ( groupName === undefined ) {
groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
groupNames.set(groupKey, groupName);
}
if ( groupName !== '' ) {
qs$('.geName', liGroup).textContent = groupName;
}
}
if ( qs$('.geName:empty', liGroup) === null ) {
qs$('.geCount', liGroup).textContent = listEntryCountFromGroup(groupRulesets);
}
const hideUnused = mustHideUnusedLists(groupKey);
liGroup.classList.toggle('hideUnused', hideUnused);
const ulGroup = qs$('.listEntries', liGroup);
if ( !groupRulesets ) { return liGroup; }
groupRulesets.sort(function(a, b) {
return (a.name || '').localeCompare(b.name || '');
});
for ( let i = 0; i < groupRulesets.length; i++ ) {
const liEntry = liFromListEntry(
groupRulesets[i],
ulGroup.children[i],
hideUnused
);
if ( liEntry.parentElement === null ) {
ulGroup.appendChild(liEntry);
}
}
return liGroup;
};
// Incremental rendering: this will allow us to easily discard unused
// DOM list entries.
dom.addClass(
qsa$('#lists .listEntries .listEntry[data-listkey]'),
'discard'
);
// Visually split the filter lists in three groups
const ulLists = qs$('#lists');
const groups = new Map([
[
'default',
rulesetDetails.filter(ruleset =>
ruleset.id === 'default'
),
],
[
'misc',
rulesetDetails.filter(ruleset =>
ruleset.id !== 'default' && typeof ruleset.lang !== 'string'
),
],
[
'regions',
rulesetDetails.filter(ruleset =>
typeof ruleset.lang === 'string'
),
],
]);
dom.toggleClass(dom.body, 'hideUnused', mustHideUnusedLists('*'));
for ( const [ groupKey, groupRulesets ] of groups ) {
let liGroup = liFromListGroup(groupKey, groupRulesets);
liGroup.setAttribute('data-groupkey', groupKey);
if ( liGroup.parentElement === null ) {
ulLists.appendChild(liGroup);
}
}
dom.remove(qsa$('#lists .listEntries .listEntry.discard'));
// Compute a hash of the settings so that we can keep track of changes
// affecting the loading of filter lists.
if ( !soft ) {
filteringSettingsHash = hashFromCurrentFromSettings();
}
renderWidgets();
};
/******************************************************************************/
const renderWidgets = function() {
dom.toggleClass(
qs$('#buttonApply'),
'disabled',
filteringSettingsHash === hashFromCurrentFromSettings()
);
// Compute total counts
const rulesetMap = new Map(
cachedRulesetData.rulesetDetails.map(rule => [ rule.id, rule ])
);
let filterCount = 0;
let ruleCount = 0;
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) === null ) { continue; }
const ruleset = rulesetMap.get(liEntry.dataset.listkey);
if ( ruleset === undefined ) { continue; }
filterCount += ruleset.filters.accepted;
ruleCount += ruleset.rules.accepted;
}
qs$('#listsOfBlockedHostsPrompt').textContent = i18n$('perRulesetStats')
.replace('{{ruleCount}}', ruleCount.toLocaleString())
.replace('{{filterCount}}', filterCount.toLocaleString());
};
/******************************************************************************/
const hashFromCurrentFromSettings = function() {
const hash = [];
const listHash = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) ) {
listHash.push(dom.attr(liEntry, 'data-listkey'));
}
}
hash.push(listHash.sort().join());
return hash.join();
};
/******************************************************************************/
function onListsetChanged(ev) {
const input = ev.target;
const li = input.closest('.listEntry');
dom.toggleClass(li, 'checked', input.checked);
renderWidgets();
}
dom.on(
qs$('#lists'),
'change',
'.listEntry input',
onListsetChanged
);
/******************************************************************************/
const applyEnabledRulesets = async function() {
const enabledRulesets = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) === null ) { continue; }
enabledRulesets.push(liEntry.dataset.listkey);
}
await sendMessage({
what: 'applyRulesets',
enabledRulesets,
});
filteringSettingsHash = hashFromCurrentFromSettings();
};
const buttonApplyHandler = async function() {
dom.removeClass(qs$('#buttonApply'), 'enabled');
await applyEnabledRulesets();
renderWidgets();
};
dom.on(
qs$('#buttonApply'),
'click',
( ) => { buttonApplyHandler(); }
);
/******************************************************************************/
// Collapsing of unused lists.
const mustHideUnusedLists = function(which) {
const hideAll = hideUnusedSet.has('*');
if ( which === '*' ) { return hideAll; }
return hideUnusedSet.has(which) !== hideAll;
};
const toggleHideUnusedLists = function(which) {
const doesHideAll = hideUnusedSet.has('*');
let groupSelector;
let mustHide;
if ( which === '*' ) {
mustHide = doesHideAll === false;
groupSelector = '';
hideUnusedSet.clear();
if ( mustHide ) {
hideUnusedSet.add(which);
}
document.body.classList.toggle('hideUnused', mustHide);
dom.toggleClass(qsa$('.groupEntry[data-groupkey]'), 'hideUnused', mustHide);
} else {
const doesHide = hideUnusedSet.has(which);
if ( doesHide ) {
hideUnusedSet.delete(which);
} else {
hideUnusedSet.add(which);
}
mustHide = doesHide === doesHideAll;
groupSelector = `.groupEntry[data-groupkey="${which}"]`;
dom.toggleClass(qsa$(groupSelector), 'hideUnused', mustHide);
}
for ( const elem of qsa$(`#lists ${groupSelector} .listEntry[data-listkey] input[type="checkbox"]:not(:checked)`) ) {
dom.toggleClass(
elem.closest('.listEntry[data-listkey]'),
'unused',
mustHide
);
}
simpleStorage.setItem(
'hideUnusedFilterLists',
Array.from(hideUnusedSet)
);
};
dom.on(
qs$('#lists'),
'click',
'.groupEntry[data-groupkey] > .geDetails',
ev => {
toggleHideUnusedLists(
dom.attr(ev.target.closest('[data-groupkey]'), 'data-groupkey')
);
}
);
// Initialize from saved state.
simpleStorage.getItem('hideUnusedFilterLists').then(value => {
if ( Array.isArray(value) ) {
hideUnusedSet = new Set(value);
}
});
/******************************************************************************/
self.hasUnsavedData = function() {
return hashFromCurrentFromSettings() !== filteringSettingsHash;
};
/******************************************************************************/
dom.on(
qs$('#lists'),
'click',
'.listEntry label *',
ev => {
if ( ev.target.matches('input,.forinput') ) { return; }
ev.preventDefault();
}
);
/******************************************************************************/
sendMessage({
what: 'getRulesetData',
}).then(data => {
if ( !data ) { return; }
cachedRulesetData = data;
try {
renderFilterLists();
} catch(ex) {
}
}).catch(reason => {
console.trace(reason);
});
/******************************************************************************/

View file

@ -0,0 +1,35 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
import { runtime } from './ext.js';
import { qs$ } from './dom.js';
/******************************************************************************/
(async ( ) => {
const manifest = runtime.getManifest();
qs$('#aboutNameVer').textContent = `${manifest.name} ${manifest.version}`;
})();

View file

@ -1,27 +1,151 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict'; 'use strict';
import rulesetDetails from '/rulesets/ruleset-details.js'; /******************************************************************************/
import { dnr, i18n, runtime } from './ext.js';
/******************************************************************************/ /******************************************************************************/
const dnr = chrome.declarativeNetRequest; const RULE_REALM_SIZE = 1000000;
const TRUSTED_DIRECTIVE_BASE_RULE_ID = 1000000; const REGEXES_REALM_START = 1000000;
const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE;
const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000;
const CURRENT_CONFIG_BASE_RULE_ID = 9000000;
const dynamicRuleMap = new Map(); const dynamicRuleMap = new Map();
const rulesetDetails = new Map();
const rulesetConfig = {
version: '',
enabledRulesets: [],
};
/******************************************************************************/ /******************************************************************************/
async function updateRegexRules() { function getCurrentVersion() {
return runtime.getManifest().version;
}
async function loadRulesetConfig() {
const configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID);
if ( configRule === undefined ) {
rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage();
return;
}
const match = /^\|\|example.invalid\/([^\/]+)\/(?:([^\/]+)\/)?/.exec(
configRule.condition.urlFilter
);
if ( match === null ) { return; }
rulesetConfig.version = match[1];
if ( match[2] ) {
rulesetConfig.enabledRulesets =
decodeURIComponent(match[2] || '').split(' ');
}
}
async function saveRulesetConfig() {
let configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID);
if ( configRule === undefined ) {
configRule = {
id: CURRENT_CONFIG_BASE_RULE_ID,
action: {
type: 'allow',
},
condition: {
urlFilter: '',
},
};
}
const version = rulesetConfig.version;
const enabledRulesets = encodeURIComponent(rulesetConfig.enabledRulesets.join(' '));
const urlFilter = `||example.invalid/${version}/${enabledRulesets}/`;
if ( urlFilter === configRule.condition.urlFilter ) { return; }
configRule.condition.urlFilter = urlFilter;
return dnr.updateDynamicRules({
addRules: [ configRule ],
removeRuleIds: [ CURRENT_CONFIG_BASE_RULE_ID ],
});
}
/******************************************************************************/
function fetchJSON(filename) {
return fetch(`/rulesets/${filename}.json`).then(response =>
response.json()
).catch(reason => {
console.info(reason);
});
}
/******************************************************************************/
async function updateRegexRules(dynamicRules) {
// Avoid testing already tested regexes
const validRegexSet = new Set(
dynamicRules.filter(rule =>
rule.condition?.regexFilter && true || false
).map(rule =>
rule.condition.regexFilter
)
);
const allRules = []; const allRules = [];
const toCheck = []; const toCheck = [];
for ( const details of rulesetDetails ) {
// Fetch regexes for all enabled rulesets
const toFetch = [];
for ( const details of rulesetDetails.values() ) {
if ( details.enabled !== true ) { continue; } if ( details.enabled !== true ) { continue; }
for ( const rule of details.rules.regexes ) { toFetch.push(fetchJSON(`${details.id}.regexes`));
const regex = rule.condition.regexFilter; }
const isCaseSensitive = rule.condition.isUrlFilterCaseSensitive === true; const regexRulesets = await Promise.all(toFetch);
// Validate fetched regexes
let regexRuleId = REGEXES_REALM_START;
for ( const rules of regexRulesets ) {
if ( Array.isArray(rules) === false ) { continue; }
for ( const rule of rules ) {
rule.id = regexRuleId++;
const {
regexFilter: regex,
isUrlFilterCaseSensitive: isCaseSensitive
} = rule.condition;
allRules.push(rule); allRules.push(rule);
toCheck.push(dnr.isRegexSupported({ regex, isCaseSensitive })); toCheck.push(
validRegexSet.has(regex)
? { isSupported: true }
: dnr.isRegexSupported({ regex, isCaseSensitive })
);
} }
} }
// Collate results
const results = await Promise.all(toCheck); const results = await Promise.all(toCheck);
const newRules = []; const newRules = [];
for ( let i = 0; i < allRules.length; i++ ) { for ( let i = 0; i < allRules.length; i++ ) {
@ -33,11 +157,18 @@ async function updateRegexRules() {
console.info(`${result.reason}: ${rule.condition.regexFilter}`); console.info(`${result.reason}: ${rule.condition.regexFilter}`);
} }
} }
console.info(
`Rejected regex filters: ${allRules.length-newRules.length} out of ${allRules.length}`
);
// Add validated regex rules to dynamic ruleset without affecting rules
// outside regex rule realm.
const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ])); const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ]));
const addRules = []; const addRules = [];
const removeRuleIds = []; const removeRuleIds = [];
for ( const oldRule of dynamicRuleMap.values() ) { for ( const oldRule of dynamicRuleMap.values() ) {
if ( oldRule.id >= TRUSTED_DIRECTIVE_BASE_RULE_ID ) { continue; } if ( oldRule.id < REGEXES_REALM_START ) { continue; }
if ( oldRule.id >= REGEXES_REALM_END ) { continue; }
const newRule = newRuleMap.get(oldRule.id); const newRule = newRuleMap.get(oldRule.id);
if ( newRule === undefined ) { if ( newRule === undefined ) {
removeRuleIds.push(oldRule.id); removeRuleIds.push(oldRule.id);
@ -148,50 +279,168 @@ async function toggleTrustedSiteDirective(details) {
/******************************************************************************/ /******************************************************************************/
(async ( ) => { async function enableRulesets(ids) {
const afterIds = new Set(ids);
const beforeIds = new Set(await dnr.getEnabledRulesets());
const enableRulesetIds = [];
const disableRulesetIds = [];
for ( const id of afterIds ) {
if ( beforeIds.has(id) ) { continue; }
enableRulesetIds.push(id);
}
for ( const id of beforeIds ) {
if ( afterIds.has(id) ) { continue; }
disableRulesetIds.push(id);
}
if ( enableRulesetIds.length !== 0 || disableRulesetIds.length !== 0 ) {
return dnr.updateEnabledRulesets({ enableRulesetIds,disableRulesetIds });
}
}
async function getEnabledRulesetsStats() {
const ids = await dnr.getEnabledRulesets();
const out = [];
for ( const id of ids ) {
const ruleset = rulesetDetails.get(id);
if ( ruleset === undefined ) { continue; }
out.push({
name: ruleset.name,
filterCount: ruleset.filters.accepted,
ruleCount: ruleset.rules.accepted,
});
}
return out;
}
async function defaultRulesetsFromLanguage() {
const out = [ 'default' ];
const dropCountry = lang => {
const pos = lang.indexOf('-');
if ( pos === -1 ) { return lang; }
return lang.slice(0, pos);
};
const langSet = new Set();
await i18n.getAcceptLanguages().then(langs => {
for ( const lang of langs.map(dropCountry) ) {
langSet.add(lang);
}
});
langSet.add(dropCountry(i18n.getUILanguage()));
const reTargetLang = new RegExp(
`\\b(${Array.from(langSet).join('|')})\\b`
);
for ( const [ id, details ] of rulesetDetails ) {
if ( typeof details.lang !== 'string' ) { continue; }
if ( reTargetLang.test(details.lang) === false ) { continue; }
out.push(id);
}
return out;
}
/******************************************************************************/
async function start() {
// Fetch enabled rulesets and dynamic rules
const dynamicRules = await dnr.getDynamicRules(); const dynamicRules = await dnr.getDynamicRules();
for ( const rule of dynamicRules ) { for ( const rule of dynamicRules ) {
dynamicRuleMap.set(rule.id, rule); dynamicRuleMap.set(rule.id, rule);
} }
await updateRegexRules(); // Fetch ruleset details
await fetchJSON('ruleset-details').then(entries => {
if ( entries === undefined ) { return; }
for ( const entry of entries ) {
rulesetDetails.set(entry.id, entry);
}
});
await loadRulesetConfig();
console.log(`Dynamic rule count: ${dynamicRuleMap.size}`); console.log(`Dynamic rule count: ${dynamicRuleMap.size}`);
console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - dynamicRuleMap.size}`);
await enableRulesets(rulesetConfig.enabledRulesets);
// We need to update the regex rules only when ruleset version changes.
const currentVersion = getCurrentVersion();
if ( currentVersion !== rulesetConfig.version ) {
await updateRegexRules(dynamicRules);
console.log(`Version change: ${rulesetConfig.version} => ${currentVersion}`);
rulesetConfig.version = currentVersion;
}
saveRulesetConfig();
const enabledRulesets = await dnr.getEnabledRulesets(); const enabledRulesets = await dnr.getEnabledRulesets();
console.log(`Enabled rulesets: ${enabledRulesets}`); console.log(`Enabled rulesets: ${enabledRulesets}`);
console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - dynamicRuleMap.size}`);
dnr.getAvailableStaticRuleCount().then(count => { dnr.getAvailableStaticRuleCount().then(count => {
console.log(`Available static rule count: ${count}`); console.log(`Available static rule count: ${count}`);
}); });
dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: true }); dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: true });
}
chrome.runtime.onMessage.addListener((request, sender, callback) => { /******************************************************************************/
switch ( request.what ) {
case 'popupPanelData': function messageListener(request, sender, callback) {
matchesTrustedSiteDirective(request).then(response => { switch ( request.what ) {
callback({
isTrusted: response, case 'getRulesetData': {
rulesetDetails: rulesetDetails.filter(details => dnr.getEnabledRulesets().then(enabledRulesets => {
details.enabled callback({
).map(details => ({ enabledRulesets,
name: details.name, rulesetDetails: Array.from(rulesetDetails.values()),
filterCount: details.filters.accepted,
ruleCount: details.rules.accepted,
})),
});
}); });
return true; });
case 'toggleTrustedSiteDirective': return true;
toggleTrustedSiteDirective(request).then(response => { }
callback(response);
case 'applyRulesets': {
enableRulesets(request.enabledRulesets).then(( ) => {
rulesetConfig.enabledRulesets = request.enabledRulesets;
return saveRulesetConfig();
}).then(( ) => {
callback();
});
return true;
}
case 'popupPanelData': {
Promise.all([
matchesTrustedSiteDirective(request),
getEnabledRulesetsStats(),
]).then(results => {
callback({
isTrusted: results[0],
rulesetDetails: results[1],
}); });
return true; });
default: return true;
break; }
}
}); case 'toggleTrustedSiteDirective': {
toggleTrustedSiteDirective(request).then(response => {
callback(response);
});
return true;
}
default:
break;
}
}
/******************************************************************************/
(async ( ) => {
await start();
runtime.onMessage.addListener(messageListener);
})(); })();

View file

@ -0,0 +1,32 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
import { dom, qsa$ } from './dom.js';
/******************************************************************************/
// Open links in the proper window
dom.attr(qsa$('a'), 'target', '_blank');
dom.attr(qsa$('a[href*="dashboard.html"]'), 'target', '_parent');

View file

@ -0,0 +1,135 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
import { simpleStorage } from './storage.js';
import { dom, qs$ } from './dom.js';
/******************************************************************************/
const discardUnsavedData = function(synchronous = false) {
const paneFrame = document.getElementById('iframe');
const paneWindow = paneFrame.contentWindow;
if (
typeof paneWindow.hasUnsavedData !== 'function' ||
paneWindow.hasUnsavedData() === false
) {
return true;
}
if ( synchronous ) {
return false;
}
return new Promise(resolve => {
const modal = document.querySelector('#unsavedWarning');
modal.classList.add('on');
modal.focus();
const onDone = status => {
modal.classList.remove('on');
document.removeEventListener('click', onClick, true);
resolve(status);
};
const onClick = ev => {
const target = ev.target;
if ( target.matches('[data-i18n="dashboardUnsavedWarningStay"]') ) {
return onDone(false);
}
if ( target.matches('[data-i18n="dashboardUnsavedWarningIgnore"]') ) {
return onDone(true);
}
if ( modal.querySelector('[data-i18n="dashboardUnsavedWarning"]').contains(target) ) {
return;
}
onDone(false);
};
document.addEventListener('click', onClick, true);
});
};
const loadDashboardPanel = function(pane, first) {
const tabButton = document.querySelector(`[data-pane="${pane}"]`);
if ( tabButton === null || tabButton.classList.contains('selected') ) {
return;
}
const loadPane = ( ) => {
self.location.replace(`#${pane}`);
for ( const node of document.querySelectorAll('.tabButton.selected') ) {
node.classList.remove('selected');
}
tabButton.classList.add('selected');
tabButton.scrollIntoView();
document.querySelector('#iframe').contentWindow.location.replace(pane);
if ( pane !== 'no-dashboard.html' ) {
simpleStorage.setItem('dashboardLastVisitedPane', pane);
}
};
if ( first ) {
return loadPane();
}
const r = discardUnsavedData();
if ( r === false ) { return; }
if ( r === true ) {
return loadPane();
}
r.then(status => {
if ( status === false ) { return; }
loadPane();
});
};
const onTabClickHandler = function(ev) {
loadDashboardPanel(ev.target.getAttribute('data-pane'));
};
if ( self.location.hash.slice(1) === 'no-dashboard.html' ) {
document.body.classList.add('noDashboard');
}
(async ( ) => {
let pane = null;
if ( self.location.hash !== '' ) {
pane = self.location.hash.slice(1) || null;
}
loadDashboardPanel(pane !== null ? pane : '3p-filters.html', true);
dom.on(
qs$('#dashboard-nav'),
'click',
'.tabButton',
onTabClickHandler
);
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
window.addEventListener('beforeunload', ( ) => {
if ( discardUnsavedData(true) ) { return; }
event.preventDefault();
event.returnValue = '';
});
})();
/******************************************************************************/

View file

@ -0,0 +1,121 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
function normalizeTarget(target) {
if ( target === null ) { return []; }
if ( Array.isArray(target) ) { return target; }
return target instanceof Element
? [ target ]
: Array.from(target);
}
function makeEventHandler(selector, callback) {
return function(event) {
const dispatcher = event.currentTarget;
if (
dispatcher instanceof HTMLElement === false ||
typeof dispatcher.querySelectorAll !== 'function'
) {
return;
}
const receiver = event.target;
const ancestor = receiver.closest(selector);
if (
ancestor === receiver &&
ancestor !== dispatcher &&
dispatcher.contains(ancestor)
) {
callback.call(receiver, event);
}
};
}
/******************************************************************************/
class dom {
static addClass(target, cl) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.add(cl);
}
}
static toggleClass(target, cl, state = undefined) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.toggle(cl, state);
}
}
static removeClass(target, cl) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.remove(cl);
}
}
static attr(target, attr, value = undefined) {
for ( const elem of normalizeTarget(target) ) {
if ( value === undefined ) {
return elem.getAttribute(attr);
}
elem.setAttribute(attr, value);
}
}
static remove(target) {
for ( const elem of normalizeTarget(target) ) {
elem.remove();
}
}
static on(target, type, selector, callback) {
if ( typeof selector === 'function' ) {
callback = selector;
selector = undefined;
} else {
callback = makeEventHandler(selector, callback);
}
for ( const elem of normalizeTarget(target) ) {
elem.addEventListener(type, callback, selector !== undefined);
}
}
}
dom.body = document.body;
/******************************************************************************/
function qs$(s, elem = undefined) {
return (elem || document).querySelector(s);
}
function qsa$(s, elem = undefined) {
return (elem || document).querySelectorAll(s);
}
/******************************************************************************/
export { dom, qs$, qsa$ };

View file

@ -0,0 +1,64 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
const browser =
self.browser instanceof Object &&
self.browser instanceof Element === false
? self.browser
: self.chrome;
const dnr = browser.declarativeNetRequest;
const i18n = browser.i18n;
const runtime = browser.runtime;
/******************************************************************************/
// The extension's service worker can be evicted at any time, so when we
// send a message, we try a few more times when the message fails to be sent.
function sendMessage(msg) {
return new Promise((resolve, reject) => {
let i = 5;
const send = ( ) => {
runtime.sendMessage(msg).then(response => {
resolve(response);
}).catch(reason => {
i -= 1;
if ( i <= 0 ) {
reject(reason);
} else {
setTimeout(send, 200);
}
});
};
send();
});
}
/******************************************************************************/
export { browser, dnr, i18n, runtime, sendMessage };

View file

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to block requests. uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@ -19,35 +19,23 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* jshint esversion:11 */
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
import { browser, sendMessage } from './ext.js';
import { i18n$ } from './i18n.js';
import { simpleStorage } from './storage.js';
/******************************************************************************/
let currentTab = {}; let currentTab = {};
let originalTrustedState = false; let originalTrustedState = false;
/******************************************************************************/ /******************************************************************************/
class safeLocalStorage {
static getItem(k) {
try {
return self.localStorage.getItem(k);
}
catch(ex) {
}
return null;
}
static setItem(k, v) {
try {
self.localStorage.setItem(k, v);
}
catch(ex) {
}
}
}
/******************************************************************************/
async function toggleTrustedSiteDirective() { async function toggleTrustedSiteDirective() {
let url; let url;
try { try {
@ -57,7 +45,7 @@ async function toggleTrustedSiteDirective() {
} }
if ( url instanceof URL === false ) { return; } if ( url instanceof URL === false ) { return; }
const targetTrustedState = document.body.classList.contains('off'); const targetTrustedState = document.body.classList.contains('off');
const newTrustedState = await chrome.runtime.sendMessage({ const newTrustedState = await sendMessage({
what: 'toggleTrustedSiteDirective', what: 'toggleTrustedSiteDirective',
origin: url.origin, origin: url.origin,
state: targetTrustedState, state: targetTrustedState,
@ -75,7 +63,7 @@ async function toggleTrustedSiteDirective() {
/******************************************************************************/ /******************************************************************************/
function reloadTab(ev) { function reloadTab(ev) {
chrome.tabs.reload(currentTab.id, { browser.tabs.reload(currentTab.id, {
bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey, bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey,
}); });
document.body.classList.remove('needReload'); document.body.classList.remove('needReload');
@ -85,7 +73,7 @@ function reloadTab(ev) {
/******************************************************************************/ /******************************************************************************/
async function init() { async function init() {
const [ tab ] = await chrome.tabs.query({ active: true }); const [ tab ] = await browser.tabs.query({ active: true });
if ( tab instanceof Object === false ) { return true; } if ( tab instanceof Object === false ) { return true; }
currentTab = tab; currentTab = tab;
@ -97,7 +85,7 @@ async function init() {
let popupPanelData; let popupPanelData;
if ( url !== undefined ) { if ( url !== undefined ) {
popupPanelData = await chrome.runtime.sendMessage({ popupPanelData = await sendMessage({
what: 'popupPanelData', what: 'popupPanelData',
origin: url.origin, origin: url.origin,
}); });
@ -126,7 +114,9 @@ async function init() {
h1.textContent = details.name; h1.textContent = details.name;
parent.append(h1); parent.append(h1);
const p = document.createElement('p'); const p = document.createElement('p');
p.textContent = `${details.ruleCount.toLocaleString()} rules, converted from ${details.filterCount.toLocaleString()} network filters`; p.textContent = i18n$('perRulesetStats')
.replace('{{ruleCount}}', details.ruleCount.toLocaleString())
.replace('{{filterCount}}', details.filterCount.toLocaleString());
parent.append(p); parent.append(p);
} }
} }
@ -189,12 +179,12 @@ async function toggleSections(more) {
} }
if ( newBits === currentBits ) { return; } if ( newBits === currentBits ) { return; }
sectionBitsToAttribute(newBits); sectionBitsToAttribute(newBits);
safeLocalStorage.setItem('popupPanelSections', newBits); simpleStorage.setItem('popupPanelSections', newBits);
} }
sectionBitsToAttribute( simpleStorage.getItem('popupPanelSections').then(s => {
parseInt(safeLocalStorage.getItem('popupPanelSections'), 10) sectionBitsToAttribute(parseInt(s, 10) || 0);
); });
document.querySelector('#moreButton').addEventListener('click', ( ) => { document.querySelector('#moreButton').addEventListener('click', ( ) => {
toggleSections(true); toggleSections(true);

View file

@ -0,0 +1,44 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
export class simpleStorage {
static getItem(k) {
try {
return Promise.resolve(JSON.parse(self.localStorage.getItem(k)));
}
catch(ex) {
}
return Promise.resolve(null);
}
static setItem(k, v) {
try {
self.localStorage.setItem(k, JSON.stringify(v));
}
catch(ex) {
}
}
}

View file

@ -16,6 +16,7 @@
"rule_resources": [ "rule_resources": [
] ]
}, },
"default_locale": "en",
"description": "uBO Minus is permission-less experimental MV3-based network request blocker", "description": "uBO Minus is permission-less experimental MV3-based network request blocker",
"icons": { "icons": {
"16": "img/icon_16.png", "16": "img/icon_16.png",
@ -26,6 +27,7 @@
"manifest_version": 3, "manifest_version": 3,
"minimum_chrome_version": "101.0", "minimum_chrome_version": "101.0",
"name": "uBlock Origin Lite", "name": "uBlock Origin Lite",
"options_page": "dashboard.html",
"permissions": [ "permissions": [
"activeTab", "activeTab",
"declarativeNetRequest" "declarativeNetRequest"

View file

@ -19,7 +19,7 @@
<span id="saveRules" class="fa-icon" data-i18n-title="popupTipSaveRules">lock</span> <span id="saveRules" class="fa-icon" data-i18n-title="popupTipSaveRules">lock</span>
<span id="revertRules" class="fa-icon" data-i18n-title="popupTipRevertRules">eraser</span> <span id="revertRules" class="fa-icon" data-i18n-title="popupTipRevertRules">eraser</span>
</div> </div>
<div id="switch" role="button" aria-label tabindex="0" title> <div id="switch" role="button" aria-label tabindex="0" data-i18n-title="popupPowerSwitchInfo">
<span class="fa-icon"><!-- <span class="fa-icon"><!--
Power button taken from Font Awesome v4.7.0 by Dave Gandy. Power button taken from Font Awesome v4.7.0 by Dave Gandy.
Unlike other FA icons, the power button is inlined here so Unlike other FA icons, the power button is inlined here so
@ -40,6 +40,13 @@
</div> </div>
<div id="hostname"><span></span>&shy;<span></span></div> <div id="hostname"><span></span>&shy;<span></span></div>
</div> </div>
<div id="basicTools" class="toolRibbon" data-more="c">
<span class="fa-icon tool needPick" data-i18n-title="popupTipZapper">bolt<span class="caption"></span></span>
<span class="fa-icon tool needPick" data-i18n-title="popupTipPicker">eye-dropper<span class="caption"></span></span>
<span class="fa-icon tool needPick" data-i18n-title="popupTipReport">comment-alt<span class="caption"></span></span>
<a href="logger-ui.html#_" class="fa-icon tool" target="uBOLogger" tabindex="0" data-i18n-title="popupTipLog">list-alt<span class="caption"></span></a>
<a href="dashboard.html" class="fa-icon tool enabled" target="uBODashboard" tabindex="0" data-i18n-title="popupTipDashboard">cogs<span class="caption" data-i18n="popupTipDashboard"></span></a>
</div>
<hr data-section="a"> <hr data-section="a">
<div id="rulesetStats" data-section="a"> <div id="rulesetStats" data-section="a">
</div> </div>
@ -55,6 +62,7 @@
</div> </div>
<script src="js/fa-icons.js"></script> <script src="js/fa-icons.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/popup.js" type="module"></script> <script src="js/popup.js" type="module"></script>
</body> </body>

View file

@ -27,8 +27,8 @@ import fs from 'fs/promises';
import https from 'https'; import https from 'https';
import process from 'process'; import process from 'process';
import rulesetConfigs from './ruleset-config.js';
import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js'; import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js';
import { StaticFilteringParser } from './js/static-filtering-parser.js';
/******************************************************************************/ /******************************************************************************/
@ -51,21 +51,75 @@ const commandLineArgs = (( ) => {
/******************************************************************************/ /******************************************************************************/
const isUnsupported = rule =>
rule._error !== undefined;
const isRegex = rule =>
rule.condition !== undefined &&
rule.condition.regexFilter !== undefined;
const isRedirect = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.extensionPath !== undefined;
const isCsp = rule =>
rule.action !== undefined &&
rule.action.type === 'modifyHeaders';
const isRemoveparam = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.transform !== undefined;
const isGood = rule =>
isUnsupported(rule) === false &&
isRedirect(rule) === false &&
isCsp(rule) === false &&
isRemoveparam(rule) === false;
/******************************************************************************/
const stdOutput = [];
const log = (text, silent = false) => {
stdOutput.push(text);
if ( silent === false ) {
console.log(text);
}
};
/******************************************************************************/
const fetchList = url => {
return new Promise((resolve, reject) => {
log(`\tFetching ${url}`);
https.get(url, response => {
const data = [];
response.on('data', chunk => {
data.push(chunk.toString());
});
response.on('end', ( ) => {
resolve({ url, content: data.join('') });
});
}).on('error', error => {
reject(error);
});
});
};
/******************************************************************************/
async function main() { async function main() {
const env = [ 'chromium' ];
const writeOps = []; const writeOps = [];
const ruleResources = []; const ruleResources = [];
const rulesetDetails = []; const rulesetDetails = [];
const regexRulesetDetails = new Map();
const outputDir = commandLineArgs.get('output') || '.'; const outputDir = commandLineArgs.get('output') || '.';
const output = [];
const log = (text, silent = false) => {
output.push(text);
if ( silent === false ) {
console.log(text);
}
};
// Get manifest content // Get manifest content
const manifest = await fs.readFile( const manifest = await fs.readFile(
`${outputDir}/manifest.json`, `${outputDir}/manifest.json`,
@ -104,78 +158,62 @@ async function main() {
return v; return v;
}; };
const isUnsupported = rule =>
rule._error !== undefined;
const isRegex = rule =>
rule.condition !== undefined &&
rule.condition.regexFilter !== undefined;
const isRedirect = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.extensionPath !== undefined;
const isCsp = rule =>
rule.action !== undefined &&
rule.action.type === 'modifyHeaders';
const isRemoveparam = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.transform !== undefined;
const isGood = rule =>
isUnsupported(rule) === false &&
isRedirect(rule) === false &&
isCsp(rule) === false &&
isRemoveparam(rule) === false
;
const rulesetDir = `${outputDir}/rulesets`; const rulesetDir = `${outputDir}/rulesets`;
const rulesetDirPromise = fs.mkdir(`${rulesetDir}`, { recursive: true }); const rulesetDirPromise = fs.mkdir(`${rulesetDir}`, { recursive: true });
const fetchList = url => {
return new Promise((resolve, reject) => {
https.get(url, response => {
const data = [];
response.on('data', chunk => {
data.push(chunk.toString());
});
response.on('end', ( ) => {
resolve({ name: url, text: data.join('') });
});
}).on('error', error => {
reject(error);
});
});
};
const readList = path =>
fs.readFile(path, { encoding: 'utf8' })
.then(text => ({ name: path, text }));
const writeFile = (path, data) => const writeFile = (path, data) =>
rulesetDirPromise.then(( ) => rulesetDirPromise.then(( ) =>
fs.writeFile(path, data)); fs.writeFile(path, data));
for ( const ruleset of rulesetConfigs ) { const rulesetFromURLS = async function(assetDetails) {
const lists = [];
log('============================'); log('============================');
log(`Listset for '${ruleset.id}':`); log(`Listset for '${assetDetails.id}':`);
if ( Array.isArray(ruleset.paths) ) { // Remember fetched URLs
for ( const path of ruleset.paths ) { const fetchedURLs = new Set();
log(`\t${path}`);
lists.push(readList(`assets/${path}`)); // Fetch list and expand `!#include` directives
let parts = assetDetails.urls.map(url => ({ url }));
while ( parts.every(v => typeof v === 'string') === false ) {
const newParts = [];
for ( const part of parts ) {
if ( typeof part === 'string' ) {
newParts.push(part);
continue;
}
if ( fetchedURLs.has(part.url) ) {
newParts.push('');
continue;
}
fetchedURLs.add(part.url);
newParts.push(
fetchList(part.url).then(details => {
const { url } = details;
const content = details.content.trim();
if ( typeof content === 'string' && content !== '' ) {
if (
content.startsWith('<') === false ||
content.endsWith('>') === false
) {
return { url, content };
}
}
log(`No valid content for ${details.name}`);
return { url, content: '' };
})
);
} }
parts = await Promise.all(newParts);
parts = StaticFilteringParser.utils.preparser.expandIncludes(parts, env);
} }
if ( Array.isArray(ruleset.urls) ) { const text = parts.join('\n');
for ( const url of ruleset.urls ) {
log(`\t${url}`); if ( text === '' ) {
lists.push(fetchList(url)); log('No filterset found');
} return;
} }
const details = await dnrRulesetFromRawLists(lists, { const details = await dnrRulesetFromRawLists([ { name: assetDetails.id, text } ], { env });
env: [ 'chromium' ],
});
const { ruleset: rules } = details; const { ruleset: rules } = details;
log(`Input filter count: ${details.filterCount}`); log(`Input filter count: ${details.filterCount}`);
log(`\tAccepted filter count: ${details.acceptedFilterCount}`); log(`\tAccepted filter count: ${details.acceptedFilterCount}`);
@ -215,17 +253,11 @@ async function main() {
true true
); );
writeOps.push(
writeFile(
`${rulesetDir}/${ruleset.id}.json`,
`${JSON.stringify(good, replacer, 2)}\n`
)
);
rulesetDetails.push({ rulesetDetails.push({
id: ruleset.id, id: assetDetails.id,
name: ruleset.name, name: assetDetails.name,
enabled: ruleset.enabled, enabled: assetDetails.enabled,
lang: assetDetails.lang,
filters: { filters: {
total: details.filterCount, total: details.filterCount,
accepted: details.acceptedFilterCount, accepted: details.acceptedFilterCount,
@ -236,24 +268,100 @@ async function main() {
accepted: good.length, accepted: good.length,
discarded: redirects.length + headers.length + removeparams.length, discarded: redirects.length + headers.length + removeparams.length,
rejected: bad.length, rejected: bad.length,
regexes, regexes: regexes.length,
}, },
}); });
writeOps.push(
writeFile(
`${rulesetDir}/${assetDetails.id}.json`,
`${JSON.stringify(good, replacer, 2)}\n`
)
);
regexRulesetDetails.set(assetDetails.id, regexes);
writeOps.push(
writeFile(
`${rulesetDir}/${assetDetails.id}.regexes.json`,
`${JSON.stringify(regexes, replacer, 2)}\n`
)
);
ruleResources.push({ ruleResources.push({
id: ruleset.id, id: assetDetails.id,
enabled: ruleset.enabled, enabled: assetDetails.enabled,
path: `/rulesets/${ruleset.id}.json` path: `/rulesets/${assetDetails.id}.json`
}); });
goodTotalCount += good.length; goodTotalCount += good.length;
maybeGoodTotalCount += regexes.length; maybeGoodTotalCount += regexes.length;
};
// Get assets.json content
const assets = await fs.readFile(
`./assets.json`,
{ encoding: 'utf8' }
).then(text =>
JSON.parse(text)
);
// Assemble all default lists as the default ruleset
const contentURLs = [];
for ( const asset of Object.values(assets) ) {
if ( asset.content !== 'filters' ) { continue; }
if ( asset.off === true ) { continue; }
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
contentURLs.push(contentURL);
}
await rulesetFromURLS({
id: 'default',
name: 'Ads, trackers, miners, and more' ,
enabled: true,
urls: contentURLs,
});
// Regional rulesets
for ( const [ id, asset ] of Object.entries(assets) ) {
if ( asset.content !== 'filters' ) { continue; }
if ( asset.off !== true ) { continue; }
if ( typeof asset.lang !== 'string' ) { continue; }
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
await rulesetFromURLS({
id: id.toLowerCase(),
lang: asset.lang,
name: asset.title,
enabled: false,
urls: [ contentURL ],
});
}
// Handpicked rulesets
const handpicked = [ 'block-lan', 'dpollock-0' ];
for ( const id of handpicked ) {
const asset = assets[id];
if ( asset.content !== 'filters' ) { continue; }
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
await rulesetFromURLS({
id: id.toLowerCase(),
name: asset.title,
enabled: false,
urls: [ contentURL ],
});
} }
writeOps.push( writeOps.push(
writeFile( writeFile(
`${rulesetDir}/ruleset-details.js`, `${rulesetDir}/ruleset-details.json`,
`export default ${JSON.stringify(rulesetDetails, replacer, 2)};\n` `${JSON.stringify(rulesetDetails, replacer, 2)}\n`
) )
); );
@ -276,7 +384,7 @@ async function main() {
); );
// Log results // Log results
await fs.writeFile(`${outputDir}/log.txt`, output.join('\n') + '\n'); await fs.writeFile(`${outputDir}/log.txt`, stdOutput.join('\n') + '\n');
} }
main(); main();

View file

@ -1,74 +0,0 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2022-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
export default [
{
id: 'default',
name: 'Default: Ads and trackers',
enabled: true,
paths: [
],
urls: [
'https://ublockorigin.github.io/uAssets/filters/badware.txt',
'https://ublockorigin.github.io/uAssets/filters/filters.txt',
'https://ublockorigin.github.io/uAssets/filters/filters-2020.txt',
'https://ublockorigin.github.io/uAssets/filters/filters-2021.txt',
'https://ublockorigin.github.io/uAssets/filters/filters-2022.txt',
'https://ublockorigin.github.io/uAssets/filters/privacy.txt',
'https://ublockorigin.github.io/uAssets/filters/quick-fixes.txt',
'https://ublockorigin.github.io/uAssets/filters/resource-abuse.txt',
'https://ublockorigin.github.io/uAssets/filters/unbreak.txt',
'https://easylist.to/easylist/easylist.txt',
'https://easylist.to/easylist/easyprivacy.txt',
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
]
},
{
id: 'DEU-0',
name: 'DEU: EasyList Germany',
enabled: false,
paths: [
],
urls: [
'https://easylist.to/easylistgermany/easylistgermany.txt',
]
},
{
id: 'RUS-0',
name: 'RUS: RU AdList',
enabled: false,
paths: [
],
urls: [
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/adservers.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/first_level.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/general_block.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/specific_antisocial.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/specific_block.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/specific_special.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/thirdparty.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/whitelist.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/AWRL-non-sync.txt',
]
},
];

View file

@ -61,9 +61,9 @@
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script> <script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script> <script src="js/cloud-ui.js" type="module"></script>
<script src="js/1p-filters.js" type="module"></script> <script src="js/1p-filters.js" type="module"></script>
</body> </body>

View file

@ -78,10 +78,10 @@
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script> <script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script> <script src="js/cloud-ui.js" type="module"></script>
<script src="js/3p-filters.js"></script> <script src="js/3p-filters.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -52,7 +52,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/about.js"></script> <script src="js/about.js"></script>

View file

@ -34,7 +34,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/advanced-settings.js"></script> <script src="js/advanced-settings.js"></script>

View file

@ -43,7 +43,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/asset-viewer.js" type="module"></script> <script src="js/asset-viewer.js" type="module"></script>

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>uBlock Origin</title> <title>uBlock Origin Background Page</title>
</head> </head>
<body> <body>
<script src="lib/lz4/lz4-block-codec-any.js"></script> <script src="lib/lz4/lz4-block-codec-any.js"></script>

View file

@ -40,7 +40,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard.js"></script> <script src="js/dashboard.js"></script>
</body> </body>

View file

@ -49,7 +49,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/devtools.js" type="module"></script> <script src="js/devtools.js" type="module"></script>

View file

@ -57,7 +57,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/document-blocked.js"></script> <script src="js/document-blocked.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -58,9 +58,9 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script> <script src="js/cloud-ui.js" type="module"></script>
<script src="js/dyna-rules.js" type="module"></script> <script src="js/dyna-rules.js" type="module"></script>
</body> </body>

View file

@ -25,6 +25,7 @@
/******************************************************************************/ /******************************************************************************/
import { i18n$ } from './i18n.js';
import './codemirror/ubo-static-filtering.js'; import './codemirror/ubo-static-filtering.js';
/******************************************************************************/ /******************************************************************************/
@ -234,7 +235,7 @@ const startImportFilePicker = function() {
const exportUserFiltersToFile = function() { const exportUserFiltersToFile = function() {
const val = getEditorText(); const val = getEditorText();
if ( val === '' ) { return; } if ( val === '' ) { return; }
const filename = vAPI.i18n('1pExportFilename') const filename = i18n$('1pExportFilename')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
vAPI.download({ vAPI.download({

View file

@ -25,13 +25,12 @@
/******************************************************************************/ /******************************************************************************/
{ import { i18n, i18n$ } from './i18n.js';
// >>>>> start of local scope
/******************************************************************************/ /******************************************************************************/
const lastUpdateTemplateString = vAPI.i18n('3pLastUpdate'); const lastUpdateTemplateString = i18n$('3pLastUpdate');
const obsoleteTemplateString = vAPI.i18n('3pExternalListObsolete'); const obsoleteTemplateString = i18n$('3pExternalListObsolete');
const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m; const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m;
let listDetails = {}; let listDetails = {};
@ -70,8 +69,8 @@ const renderNumber = function(value) {
const renderFilterLists = function(soft) { const renderFilterLists = function(soft) {
const listGroupTemplate = uDom('#templates .groupEntry'); const listGroupTemplate = uDom('#templates .groupEntry');
const listEntryTemplate = uDom('#templates .listEntry'); const listEntryTemplate = uDom('#templates .listEntry');
const listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'); const listStatsTemplate = i18n$('3pListsOfBlockedHostsPerListStats');
const renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString; const renderElapsedTimeToString = i18n.renderElapsedTimeToString;
const groupNames = new Map([ [ 'user', '' ] ]); const groupNames = new Map([ [ 'user', '' ] ]);
// Assemble a pretty list name if possible // Assemble a pretty list name if possible
@ -189,7 +188,7 @@ const renderFilterLists = function(soft) {
liGroup = listGroupTemplate.clone().nodeAt(0); liGroup = listGroupTemplate.clone().nodeAt(0);
let groupName = groupNames.get(groupKey); let groupName = groupNames.get(groupKey);
if ( groupName === undefined ) { if ( groupName === undefined ) {
groupName = vAPI.i18n('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1)); groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
groupNames.set(groupKey, groupName); groupNames.set(groupKey, groupName);
} }
if ( groupName !== '' ) { if ( groupName !== '' ) {
@ -289,7 +288,7 @@ const renderFilterLists = function(soft) {
uDom.nodeFromId('autoUpdate').checked = uDom.nodeFromId('autoUpdate').checked =
listDetails.autoUpdate === true; listDetails.autoUpdate === true;
uDom.nodeFromId('listsOfBlockedHostsPrompt').textContent = uDom.nodeFromId('listsOfBlockedHostsPrompt').textContent =
vAPI.i18n('3pListsOfBlockedHostsPrompt') i18n$('3pListsOfBlockedHostsPrompt')
.replace( .replace(
'{{netFilterCount}}', '{{netFilterCount}}',
renderNumber(details.netFilterCount) renderNumber(details.netFilterCount)
@ -362,7 +361,7 @@ const updateAssetStatus = function(details) {
'title', 'title',
lastUpdateTemplateString.replace( lastUpdateTemplateString.replace(
'{{ago}}', '{{ago}}',
vAPI.i18n.renderElapsedTimeToString(Date.now()) i18n$.renderElapsedTimeToString(Date.now())
) )
); );
} }
@ -711,7 +710,3 @@ uDom('#lists').on('click', '.listEntry label *', ev => {
renderFilterLists(); renderFilterLists();
/******************************************************************************/ /******************************************************************************/
// <<<<< end of local scope
}

View file

@ -27,12 +27,13 @@ import cacheStorage from './cachestorage.js';
import logger from './logger.js'; import logger from './logger.js';
import µb from './background.js'; import µb from './background.js';
import { StaticFilteringParser } from './static-filtering-parser.js'; import { StaticFilteringParser } from './static-filtering-parser.js';
import { i18n$ } from './i18n.js';
/******************************************************************************/ /******************************************************************************/
const reIsExternalPath = /^(?:[a-z-]+):\/\//; const reIsExternalPath = /^(?:[a-z-]+):\/\//;
const reIsUserAsset = /^user-/; const reIsUserAsset = /^user-/;
const errorCantConnectTo = vAPI.i18n('errorCantConnectTo'); const errorCantConnectTo = i18n$('errorCantConnectTo');
const assets = {}; const assets = {};

View file

@ -23,6 +23,8 @@
'use strict'; 'use strict';
import { i18n, i18n$ } from './i18n.js';
/******************************************************************************/ /******************************************************************************/
(( ) => { (( ) => {
@ -58,7 +60,7 @@ const fetchStorageUsed = async function() {
elem.classList.add('hide'); elem.classList.add('hide');
return; return;
} }
const units = ' ' + vAPI.i18n('genericBytes'); const units = ' ' + i18n$('genericBytes');
elem.title = result.max.toLocaleString() + units; elem.title = result.max.toLocaleString() + units;
const total = (result.total / result.max * 100).toFixed(1); const total = (result.total / result.max * 100).toFixed(1);
elem = elem.firstElementChild; elem = elem.firstElementChild;
@ -206,7 +208,7 @@ const onInitialize = function(options) {
faIconsInit(widget); faIconsInit(widget);
vAPI.i18n.render(widget); i18n.render(widget);
widget.classList.remove('hide'); widget.classList.remove('hide');
uDom('#cloudPush').on('click', ( ) => { pushData(); }); uDom('#cloudPush').on('click', ( ) => { pushData(); });

View file

@ -24,6 +24,7 @@
/******************************************************************************/ /******************************************************************************/
import µb from './background.js'; import µb from './background.js';
import { i18n$ } from './i18n.js';
/******************************************************************************/ /******************************************************************************/
@ -134,28 +135,28 @@ const onEntryClicked = function(details, tab) {
const menuEntries = { const menuEntries = {
blockElement: { blockElement: {
id: 'uBlock0-blockElement', id: 'uBlock0-blockElement',
title: vAPI.i18n('pickerContextMenuEntry'), title: i18n$('pickerContextMenuEntry'),
contexts: [ 'all' ], contexts: [ 'all' ],
}, },
blockElementInFrame: { blockElementInFrame: {
id: 'uBlock0-blockElementInFrame', id: 'uBlock0-blockElementInFrame',
title: vAPI.i18n('contextMenuBlockElementInFrame'), title: i18n$('contextMenuBlockElementInFrame'),
contexts: [ 'frame' ], contexts: [ 'frame' ],
}, },
blockResource: { blockResource: {
id: 'uBlock0-blockResource', id: 'uBlock0-blockResource',
title: vAPI.i18n('pickerContextMenuEntry'), title: i18n$('pickerContextMenuEntry'),
contexts: [ 'audio', 'frame', 'image', 'video' ], contexts: [ 'audio', 'frame', 'image', 'video' ],
}, },
subscribeToList: { subscribeToList: {
id: 'uBlock0-subscribeToList', id: 'uBlock0-subscribeToList',
title: vAPI.i18n('contextMenuSubscribeToList'), title: i18n$('contextMenuSubscribeToList'),
contexts: [ 'link' ], contexts: [ 'link' ],
targetUrlPatterns: [ 'abp:*', 'https://subscribe.adblockplus.org/*' ], targetUrlPatterns: [ 'abp:*', 'https://subscribe.adblockplus.org/*' ],
}, },
temporarilyAllowLargeMediaElements: { temporarilyAllowLargeMediaElements: {
id: 'uBlock0-temporarilyAllowLargeMediaElements', id: 'uBlock0-temporarilyAllowLargeMediaElements',
title: vAPI.i18n('contextMenuTemporarilyAllowLargeMediaElements'), title: i18n$('contextMenuTemporarilyAllowLargeMediaElements'),
contexts: [ 'all' ], contexts: [ 'all' ],
} }
}; };

View file

@ -25,11 +25,6 @@
/******************************************************************************/ /******************************************************************************/
{
// >>>>> start of local scope
/******************************************************************************/
const discardUnsavedData = function(synchronous = false) { const discardUnsavedData = function(synchronous = false) {
const paneFrame = document.getElementById('iframe'); const paneFrame = document.getElementById('iframe');
const paneWindow = paneFrame.contentWindow; const paneWindow = paneFrame.contentWindow;
@ -148,8 +143,3 @@ if ( self.location.hash.slice(1) === 'no-dashboard.html' ) {
}); });
} }
})(); })();
/******************************************************************************/
// <<<<< end of local scope
}

View file

@ -25,7 +25,7 @@
/******************************************************************************/ /******************************************************************************/
(( ) => { import { i18n$ } from './i18n.js';
/******************************************************************************/ /******************************************************************************/
@ -128,7 +128,7 @@ uDom.nodeFromId('why').textContent = details.fs;
if ( search === '' ) { return false; } if ( search === '' ) { return false; }
url.search = ''; url.search = '';
const li = liFromParam(vAPI.i18n('docblockedNoParamsPrompt'), url.href); const li = liFromParam(i18n$('docblockedNoParamsPrompt'), url.href);
parentNode.appendChild(li); parentNode.appendChild(li);
const params = new self.URLSearchParams(search); const params = new self.URLSearchParams(search);
@ -239,7 +239,3 @@ uDom('#proceed').on('click', ( ) => {
}); });
/******************************************************************************/ /******************************************************************************/
})();
/******************************************************************************/

View file

@ -28,6 +28,7 @@
import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js'; import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js';
import { hostnameFromURI } from './uri-utils.js'; import { hostnameFromURI } from './uri-utils.js';
import { i18n$ } from './i18n.js';
import './codemirror/ubo-dynamic-filtering.js'; import './codemirror/ubo-dynamic-filtering.js';
@ -80,13 +81,13 @@ let isCollapsed = false;
// reliably the default title attribute assigned by CodeMirror. // reliably the default title attribute assigned by CodeMirror.
{ {
const i18nCommitStr = vAPI.i18n('rulesCommit'); const i18nCommitStr = i18n$('rulesCommit');
const i18nRevertStr = vAPI.i18n('rulesRevert'); const i18nRevertStr = i18n$('rulesRevert');
const commitArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy-reverse:not([title="' + i18nCommitStr + '"])'; const commitArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy-reverse:not([title="' + i18nCommitStr + '"])';
const revertArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy:not([title="' + i18nRevertStr + '"])'; const revertArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy:not([title="' + i18nRevertStr + '"])';
uDom.nodeFromSelector('.CodeMirror-merge-scrolllock') uDom.nodeFromSelector('.CodeMirror-merge-scrolllock')
.setAttribute('title', vAPI.i18n('genericMergeViewScrollLock')); .setAttribute('title', i18n$('genericMergeViewScrollLock'));
const translate = function() { const translate = function() {
let elems = document.querySelectorAll(commitArrowSelector); let elems = document.querySelectorAll(commitArrowSelector);
@ -340,7 +341,7 @@ const startImportFilePicker = function() {
/******************************************************************************/ /******************************************************************************/
function exportUserRulesToFile() { function exportUserRulesToFile() {
const filename = vAPI.i18n('rulesDefaultFileName') const filename = i18n$('rulesDefaultFileName')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
vAPI.download({ vAPI.download({

View file

@ -23,270 +23,280 @@
/******************************************************************************/ /******************************************************************************/
// This file should always be included at the end of the `body` tag, so as const i18n =
// to ensure all i18n targets are already loaded. self.browser instanceof Object &&
self.browser instanceof Element === false
{ ? self.browser.i18n
// >>>>> start of local scope : self.chrome.i18n;
/******************************************************************************/ /******************************************************************************/
// https://github.com/gorhill/uBlock/issues/2084 function i18n$(...args) {
// Anything else than <a>, <b>, <code>, <em>, <i>, and <span> will return i18n.getMessage(...args);
// be rendered as plain text.
// For <a>, only href attribute must be present, and it MUST starts with
// `https://`, and includes no single- or double-quotes.
// No HTML entities are allowed, there is code to handle existing HTML
// entities already present in translation files until they are all gone.
const allowedTags = new Set([
'a',
'b',
'code',
'em',
'i',
'span',
'u',
]);
const expandHtmlEntities = (( ) => {
const entities = new Map([
// TODO: Remove quote entities once no longer present in translation
// files. Other entities must stay.
[ '&shy;', '\u00AD' ],
[ '&ldquo;', '“' ],
[ '&rdquo;', '”' ],
[ '&lsquo;', '' ],
[ '&rsquo;', '' ],
[ '&lt;', '<' ],
[ '&gt;', '>' ],
]);
const decodeEntities = match => {
return entities.get(match) || match;
};
return function(text) {
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&[a-z]+;/g, decodeEntities);
}
return text;
};
})();
const safeTextToTextNode = function(text) {
return document.createTextNode(expandHtmlEntities(text));
};
const sanitizeElement = function(node) {
if ( allowedTags.has(node.localName) === false ) { return null; }
node.removeAttribute('style');
let child = node.firstElementChild;
while ( child !== null ) {
const next = child.nextElementSibling;
if ( sanitizeElement(child) === null ) {
child.remove();
}
child = next;
}
return node;
};
const safeTextToDOM = function(text, parent) {
if ( text === '' ) { return; }
// Fast path (most common).
if ( text.indexOf('<') === -1 ) {
const toInsert = safeTextToTextNode(text);
let toReplace = parent.childCount !== 0
? parent.firstChild
: null;
while ( toReplace !== null ) {
if ( toReplace.nodeType === 3 && toReplace.nodeValue === '_' ) {
break;
}
toReplace = toReplace.nextSibling;
}
if ( toReplace !== null ) {
parent.replaceChild(toInsert, toReplace);
} else {
parent.appendChild(toInsert);
}
return;
}
// Slow path.
// `<p>` no longer allowed. Code below can be removed once all <p>'s are
// gone from translation files.
text = text.replace(/^<p>|<\/p>/g, '')
.replace(/<p>/g, '\n\n');
// Parse allowed HTML tags.
const domParser = new DOMParser();
const parsedDoc = domParser.parseFromString(text, 'text/html');
let node = parsedDoc.body.firstChild;
while ( node !== null ) {
const next = node.nextSibling;
switch ( node.nodeType ) {
case 1: // element
if ( sanitizeElement(node) === null ) { break; }
parent.appendChild(node);
break;
case 3: // text
parent.appendChild(node);
break;
default:
break;
}
node = next;
}
};
/******************************************************************************/
vAPI.i18n.safeTemplateToDOM = function(id, dict, parent) {
if ( parent === undefined ) {
parent = document.createDocumentFragment();
}
let textin = vAPI.i18n(id);
if ( textin === '' ) {
return parent;
}
if ( textin.indexOf('{{') === -1 ) {
safeTextToDOM(textin, parent);
return parent;
}
const re = /\{\{\w+\}\}/g;
let textout = '';
for (;;) {
let match = re.exec(textin);
if ( match === null ) {
textout += textin;
break;
}
textout += textin.slice(0, match.index);
let prop = match[0].slice(2, -2);
if ( dict.hasOwnProperty(prop) ) {
textout += dict[prop].replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
textout += prop;
}
textin = textin.slice(re.lastIndex);
}
safeTextToDOM(textout, parent);
return parent;
};
/******************************************************************************/
// Helper to deal with the i18n'ing of HTML files.
vAPI.i18n.render = function(context) {
const docu = document;
const root = context || docu;
for ( const elem of root.querySelectorAll('[data-i18n]') ) {
let text = vAPI.i18n(elem.getAttribute('data-i18n'));
if ( !text ) { continue; }
if ( text.indexOf('{{') === -1 ) {
safeTextToDOM(text, elem);
continue;
}
// Handle selector-based placeholders: these placeholders tell where
// existing child DOM element are to be positioned relative to the
// localized text nodes.
const parts = text.split(/(\{\{[^}]+\}\})/);
const fragment = document.createDocumentFragment();
let textBefore = '';
for ( let part of parts ) {
if ( part === '' ) { continue; }
if ( part.startsWith('{{') && part.endsWith('}}') ) {
// TODO: remove detection of ':' once it no longer appears
// in translation files.
const pos = part.indexOf(':');
if ( pos !== -1 ) {
part = part.slice(0, pos) + part.slice(-2);
}
const selector = part.slice(2, -2);
let node;
// Ideally, the i18n strings explicitly refer to the
// class of the element to insert. However for now we
// will create a class from what is currently found in
// the placeholder and first try to lookup the resulting
// selector. This way we don't have to revisit all
// translations just for the sake of declaring the proper
// selector in the placeholder field.
if ( selector.charCodeAt(0) !== 0x2E /* '.' */ ) {
node = elem.querySelector(`.${selector}`);
}
if ( node instanceof Element === false ) {
node = elem.querySelector(selector);
}
if ( node instanceof Element ) {
safeTextToDOM(textBefore, fragment);
fragment.appendChild(node);
textBefore = '';
continue;
}
}
textBefore += part;
}
if ( textBefore !== '' ) {
safeTextToDOM(textBefore, fragment);
}
elem.appendChild(fragment);
}
for ( const elem of root.querySelectorAll('[data-i18n-title]') ) {
const text = vAPI.i18n(elem.getAttribute('data-i18n-title'));
if ( !text ) { continue; }
elem.setAttribute('title', expandHtmlEntities(text));
}
for ( const elem of root.querySelectorAll('[placeholder]') ) {
elem.setAttribute(
'placeholder',
vAPI.i18n(elem.getAttribute('placeholder'))
);
}
for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) {
const text = vAPI.i18n(elem.getAttribute('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n');
elem.setAttribute('data-tip', text);
if ( elem.getAttribute('aria-label') === 'data-tip' ) {
elem.setAttribute('aria-label', text);
}
}
};
vAPI.i18n.render();
/******************************************************************************/
vAPI.i18n.renderElapsedTimeToString = function(tstamp) {
let value = (Date.now() - tstamp) / 60000;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneMinuteAgo');
}
if ( value < 60 ) {
return vAPI.i18n('elapsedManyMinutesAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 60;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneHourAgo');
}
if ( value < 24 ) {
return vAPI.i18n('elapsedManyHoursAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 24;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneDayAgo');
}
return vAPI.i18n('elapsedManyDaysAgo').replace('{{value}}', Math.floor(value).toLocaleString());
};
/******************************************************************************/
// <<<<< end of local scope
} }
/******************************************************************************/ /******************************************************************************/
const isBackgroundProcess = document.title === 'uBlock Origin Background Page';
if ( isBackgroundProcess !== true ) {
// http://www.w3.org/International/questions/qa-scripts#directions
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(i18n$('@@ui_locale')) !== -1
? 'rtl'
: 'ltr'
);
// https://github.com/gorhill/uBlock/issues/2084
// Anything else than <a>, <b>, <code>, <em>, <i>, and <span> will
// be rendered as plain text.
// For <a>, only href attribute must be present, and it MUST starts with
// `https://`, and includes no single- or double-quotes.
// No HTML entities are allowed, there is code to handle existing HTML
// entities already present in translation files until they are all gone.
const allowedTags = new Set([
'a',
'b',
'code',
'em',
'i',
'span',
'u',
]);
const expandHtmlEntities = (( ) => {
const entities = new Map([
// TODO: Remove quote entities once no longer present in translation
// files. Other entities must stay.
[ '&shy;', '\u00AD' ],
[ '&ldquo;', '“' ],
[ '&rdquo;', '”' ],
[ '&lsquo;', '' ],
[ '&rsquo;', '' ],
[ '&lt;', '<' ],
[ '&gt;', '>' ],
]);
const decodeEntities = match => {
return entities.get(match) || match;
};
return function(text) {
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&[a-z]+;/g, decodeEntities);
}
return text;
};
})();
const safeTextToTextNode = function(text) {
return document.createTextNode(expandHtmlEntities(text));
};
const sanitizeElement = function(node) {
if ( allowedTags.has(node.localName) === false ) { return null; }
node.removeAttribute('style');
let child = node.firstElementChild;
while ( child !== null ) {
const next = child.nextElementSibling;
if ( sanitizeElement(child) === null ) {
child.remove();
}
child = next;
}
return node;
};
const safeTextToDOM = function(text, parent) {
if ( text === '' ) { return; }
// Fast path (most common).
if ( text.indexOf('<') === -1 ) {
const toInsert = safeTextToTextNode(text);
let toReplace = parent.childCount !== 0
? parent.firstChild
: null;
while ( toReplace !== null ) {
if ( toReplace.nodeType === 3 && toReplace.nodeValue === '_' ) {
break;
}
toReplace = toReplace.nextSibling;
}
if ( toReplace !== null ) {
parent.replaceChild(toInsert, toReplace);
} else {
parent.appendChild(toInsert);
}
return;
}
// Slow path.
// `<p>` no longer allowed. Code below can be removed once all <p>'s are
// gone from translation files.
text = text.replace(/^<p>|<\/p>/g, '')
.replace(/<p>/g, '\n\n');
// Parse allowed HTML tags.
const domParser = new DOMParser();
const parsedDoc = domParser.parseFromString(text, 'text/html');
let node = parsedDoc.body.firstChild;
while ( node !== null ) {
const next = node.nextSibling;
switch ( node.nodeType ) {
case 1: // element
if ( sanitizeElement(node) === null ) { break; }
parent.appendChild(node);
break;
case 3: // text
parent.appendChild(node);
break;
default:
break;
}
node = next;
}
};
i18n.safeTemplateToDOM = function(id, dict, parent) {
if ( parent === undefined ) {
parent = document.createDocumentFragment();
}
let textin = i18n$(id);
if ( textin === '' ) {
return parent;
}
if ( textin.indexOf('{{') === -1 ) {
safeTextToDOM(textin, parent);
return parent;
}
const re = /\{\{\w+\}\}/g;
let textout = '';
for (;;) {
let match = re.exec(textin);
if ( match === null ) {
textout += textin;
break;
}
textout += textin.slice(0, match.index);
let prop = match[0].slice(2, -2);
if ( dict.hasOwnProperty(prop) ) {
textout += dict[prop].replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
textout += prop;
}
textin = textin.slice(re.lastIndex);
}
safeTextToDOM(textout, parent);
return parent;
};
// Helper to deal with the i18n'ing of HTML files.
i18n.render = function(context) {
const docu = document;
const root = context || docu;
for ( const elem of root.querySelectorAll('[data-i18n]') ) {
let text = i18n$(elem.getAttribute('data-i18n'));
if ( !text ) { continue; }
if ( text.indexOf('{{') === -1 ) {
safeTextToDOM(text, elem);
continue;
}
// Handle selector-based placeholders: these placeholders tell where
// existing child DOM element are to be positioned relative to the
// localized text nodes.
const parts = text.split(/(\{\{[^}]+\}\})/);
const fragment = document.createDocumentFragment();
let textBefore = '';
for ( let part of parts ) {
if ( part === '' ) { continue; }
if ( part.startsWith('{{') && part.endsWith('}}') ) {
// TODO: remove detection of ':' once it no longer appears
// in translation files.
const pos = part.indexOf(':');
if ( pos !== -1 ) {
part = part.slice(0, pos) + part.slice(-2);
}
const selector = part.slice(2, -2);
let node;
// Ideally, the i18n strings explicitly refer to the
// class of the element to insert. However for now we
// will create a class from what is currently found in
// the placeholder and first try to lookup the resulting
// selector. This way we don't have to revisit all
// translations just for the sake of declaring the proper
// selector in the placeholder field.
if ( selector.charCodeAt(0) !== 0x2E /* '.' */ ) {
node = elem.querySelector(`.${selector}`);
}
if ( node instanceof Element === false ) {
node = elem.querySelector(selector);
}
if ( node instanceof Element ) {
safeTextToDOM(textBefore, fragment);
fragment.appendChild(node);
textBefore = '';
continue;
}
}
textBefore += part;
}
if ( textBefore !== '' ) {
safeTextToDOM(textBefore, fragment);
}
elem.appendChild(fragment);
}
for ( const elem of root.querySelectorAll('[data-i18n-title]') ) {
const text = i18n$(elem.getAttribute('data-i18n-title'));
if ( !text ) { continue; }
elem.setAttribute('title', expandHtmlEntities(text));
}
for ( const elem of root.querySelectorAll('[placeholder]') ) {
elem.setAttribute(
'placeholder',
i18n$(elem.getAttribute('placeholder'))
);
}
for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) {
const text = i18n$(elem.getAttribute('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n');
elem.setAttribute('data-tip', text);
if ( elem.getAttribute('aria-label') === 'data-tip' ) {
elem.setAttribute('aria-label', text);
}
}
};
i18n.renderElapsedTimeToString = function(tstamp) {
let value = (Date.now() - tstamp) / 60000;
if ( value < 2 ) {
return i18n$('elapsedOneMinuteAgo');
}
if ( value < 60 ) {
return i18n$('elapsedManyMinutesAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 60;
if ( value < 2 ) {
return i18n$('elapsedOneHourAgo');
}
if ( value < 24 ) {
return i18n$('elapsedManyHoursAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 24;
if ( value < 2 ) {
return i18n$('elapsedOneDayAgo');
}
return i18n$('elapsedManyDaysAgo').replace('{{value}}', Math.floor(value).toLocaleString());
};
i18n.render();
}
/******************************************************************************/
export { i18n, i18n$ };

View file

@ -26,6 +26,7 @@
/******************************************************************************/ /******************************************************************************/
import { hostnameFromURI } from './uri-utils.js'; import { hostnameFromURI } from './uri-utils.js';
import { i18n, i18n$ } from './i18n.js';
/******************************************************************************/ /******************************************************************************/
@ -869,7 +870,7 @@ const viewPort = (( ) => {
/******************************************************************************/ /******************************************************************************/
const updateCurrentTabTitle = (( ) => { const updateCurrentTabTitle = (( ) => {
const i18nCurrentTab = vAPI.i18n('loggerCurrentTab'); const i18nCurrentTab = i18n$('loggerCurrentTab');
return function() { return function() {
const select = uDom.nodeFromId('pageSelector'); const select = uDom.nodeFromId('pageSelector');
@ -1572,7 +1573,7 @@ const reloadTab = function(ev) {
} }
// https://github.com/gorhill/uBlock/issues/2179 // https://github.com/gorhill/uBlock/issues/2179
if ( rows[1].children[1].childElementCount === 0 ) { if ( rows[1].children[1].childElementCount === 0 ) {
vAPI.i18n.safeTemplateToDOM( i18n.safeTemplateToDOM(
'loggerStaticFilteringFinderSentence2', 'loggerStaticFilteringFinderSentence2',
{ filter: rawFilter }, { filter: rawFilter },
rows[1].children[1] rows[1].children[1]
@ -1728,7 +1729,7 @@ const reloadTab = function(ev) {
}; };
const fillOriginSelect = function(select, hostname, domain) { const fillOriginSelect = function(select, hostname, domain) {
const template = vAPI.i18n('loggerStaticFilteringSentencePartOrigin'); const template = i18n$('loggerStaticFilteringSentencePartOrigin');
let value = hostname; let value = hostname;
for (;;) { for (;;) {
const option = document.createElement('option'); const option = document.createElement('option');
@ -1748,7 +1749,7 @@ const reloadTab = function(ev) {
return; return;
} }
const template = vAPI.i18n('loggerStaticFilteringSentence'); const template = i18n$('loggerStaticFilteringSentence');
const rePlaceholder = /\{\{[^}]+?\}\}/g; const rePlaceholder = /\{\{[^}]+?\}\}/g;
const nodes = []; const nodes = [];
let pos = 0; let pos = 0;
@ -1770,11 +1771,11 @@ const reloadTab = function(ev) {
select.className = 'static action'; select.className = 'static action';
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', ''); option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartBlock'); option.textContent = i18n$('loggerStaticFilteringSentencePartBlock');
select.appendChild(option); select.appendChild(option);
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', '@@'); option.setAttribute('value', '@@');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartAllow'); option.textContent = i18n$('loggerStaticFilteringSentencePartAllow');
select.appendChild(option); select.appendChild(option);
nodes.push(select); nodes.push(select);
break; break;
@ -1785,11 +1786,11 @@ const reloadTab = function(ev) {
select.className = 'static type'; select.className = 'static type';
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', filterType); option.setAttribute('value', filterType);
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartType').replace('{{type}}', filterType); option.textContent = i18n$('loggerStaticFilteringSentencePartType').replace('{{type}}', filterType);
select.appendChild(option); select.appendChild(option);
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', ''); option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartAnyType'); option.textContent = i18n$('loggerStaticFilteringSentencePartAnyType');
select.appendChild(option); select.appendChild(option);
nodes.push(select); nodes.push(select);
break; break;
@ -1813,7 +1814,7 @@ const reloadTab = function(ev) {
fillOriginSelect(select, targetFrameHostname, targetFrameDomain); fillOriginSelect(select, targetFrameHostname, targetFrameDomain);
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', ''); option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartAnyOrigin'); option.textContent = i18n$('loggerStaticFilteringSentencePartAnyOrigin');
select.appendChild(option); select.appendChild(option);
nodes.push(select); nodes.push(select);
break; break;
@ -1823,11 +1824,11 @@ const reloadTab = function(ev) {
select.className = 'static importance'; select.className = 'static importance';
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', ''); option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartNotImportant'); option.textContent = i18n$('loggerStaticFilteringSentencePartNotImportant');
select.appendChild(option); select.appendChild(option);
option = document.createElement('option'); option = document.createElement('option');
option.setAttribute('value', 'important'); option.setAttribute('value', 'important');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartImportant'); option.textContent = i18n$('loggerStaticFilteringSentencePartImportant');
select.appendChild(option); select.appendChild(option);
nodes.push(select); nodes.push(select);
break; break;

View file

@ -40,6 +40,7 @@ import µb from './background.js';
import webRequest from './traffic.js'; import webRequest from './traffic.js';
import { denseBase64 } from './base64-custom.js'; import { denseBase64 } from './base64-custom.js';
import { dnrRulesetFromRawLists } from './static-dnr-filtering.js'; import { dnrRulesetFromRawLists } from './static-dnr-filtering.js';
import { i18n$ } from './i18n.js';
import { redirectEngine } from './redirect-engine.js'; import { redirectEngine } from './redirect-engine.js';
import { StaticFilteringParser } from './static-filtering-parser.js'; import { StaticFilteringParser } from './static-filtering-parser.js';
@ -1075,7 +1076,7 @@ const backupUserData = async function() {
userFilters: userFilters.content, userFilters: userFilters.content,
}; };
const filename = vAPI.i18n('aboutBackupFilename') const filename = i18n$('aboutBackupFilename')
.replace('{{datetime}}', µb.dateNowToSensibleString()) .replace('{{datetime}}', µb.dateNowToSensibleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
µb.restoreBackupSettings.lastBackupFile = filename; µb.restoreBackupSettings.lastBackupFile = filename;
@ -1316,7 +1317,7 @@ const getShortcuts = function(callback) {
let desc = command.description; let desc = command.description;
let match = /^__MSG_(.+?)__$/.exec(desc); let match = /^__MSG_(.+?)__$/.exec(desc);
if ( match !== null ) { if ( match !== null ) {
desc = vAPI.i18n(match[1]); desc = i18n$(match[1]);
} }
if ( desc === '' ) { continue; } if ( desc === '' ) { continue; }
command.description = desc; command.description = desc;

View file

@ -24,6 +24,7 @@
'use strict'; 'use strict';
import punycode from '../lib/punycode.js'; import punycode from '../lib/punycode.js';
import { i18n$ } from './i18n.js';
/******************************************************************************/ /******************************************************************************/
@ -50,8 +51,8 @@ const scopeToSrcHostnameMap = {
'.': '' '.': ''
}; };
const hostnameToSortableTokenMap = new Map(); const hostnameToSortableTokenMap = new Map();
const statsStr = vAPI.i18n('popupBlockedStats'); const statsStr = i18n$('popupBlockedStats');
const domainsHitStr = vAPI.i18n('popupHitDomainCount'); const domainsHitStr = i18n$('popupHitDomainCount');
let popupData = {}; let popupData = {};
let dfPaneBuilt = false; let dfPaneBuilt = false;
@ -643,7 +644,7 @@ const renderTooltips = function(selector) {
if ( selector !== undefined && key !== selector ) { continue; } if ( selector !== undefined && key !== selector ) { continue; }
const elem = uDom.nodeFromSelector(key); const elem = uDom.nodeFromSelector(key);
if ( elem.hasAttribute('title') === false ) { continue; } if ( elem.hasAttribute('title') === false ) { continue; }
const text = vAPI.i18n( const text = i18n$(
details.i18n + details.i18n +
(uDom.nodeFromSelector(details.state) === null ? '1' : '2') (uDom.nodeFromSelector(details.state) === null ? '1' : '2')
); );

View file

@ -27,6 +27,7 @@ import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js'; import µb from './background.js';
import { CompiledListWriter } from './static-filtering-io.js'; import { CompiledListWriter } from './static-filtering-io.js';
import { StaticFilteringParser } from './static-filtering-parser.js'; import { StaticFilteringParser } from './static-filtering-parser.js';
import { i18n$ } from './i18n.js';
import { import {
domainFromHostname, domainFromHostname,
@ -110,7 +111,7 @@ const initWorker = function() {
entries.set(listKey, { entries.set(listKey, {
title: listKey !== µb.userFiltersPath ? title: listKey !== µb.userFiltersPath ?
entry.title : entry.title :
vAPI.i18n('1pPageName'), i18n$('1pPageName'),
supportURL: entry.supportURL || '' supportURL: entry.supportURL || ''
}); });
} }

View file

@ -23,9 +23,7 @@
'use strict'; 'use strict';
/******************************************************************************/ import { i18n$ } from './i18n.js';
(( ) => {
/******************************************************************************/ /******************************************************************************/
@ -63,11 +61,11 @@ const handleImportFilePicker = function() {
userData = undefined; userData = undefined;
} }
if ( userData === undefined ) { if ( userData === undefined ) {
window.alert(vAPI.i18n('aboutRestoreDataError')); window.alert(i18n$('aboutRestoreDataError'));
return; return;
} }
const time = new Date(userData.timeStamp); const time = new Date(userData.timeStamp);
const msg = vAPI.i18n('aboutRestoreDataConfirm') const msg = i18n$('aboutRestoreDataConfirm')
.replace('{{time}}', time.toLocaleString()); .replace('{{time}}', time.toLocaleString());
const proceed = window.confirm(msg); const proceed = window.confirm(msg);
if ( proceed !== true ) { return; } if ( proceed !== true ) { return; }
@ -137,9 +135,9 @@ const onLocalDataReceived = function(details) {
unit = ''; unit = '';
} }
uDom.nodeFromId('storageUsed').textContent = uDom.nodeFromId('storageUsed').textContent =
vAPI.i18n('storageUsed') i18n$('storageUsed')
.replace('{{value}}', v.toLocaleString(undefined, { maximumSignificantDigits: 3 })) .replace('{{value}}', v.toLocaleString(undefined, { maximumSignificantDigits: 3 }))
.replace('{{unit}}', unit && vAPI.i18n(unit) || ''); .replace('{{unit}}', unit && i18n$(unit) || '');
const timeOptions = { const timeOptions = {
weekday: 'long', weekday: 'long',
@ -154,7 +152,7 @@ const onLocalDataReceived = function(details) {
const lastBackupFile = details.lastBackupFile || ''; const lastBackupFile = details.lastBackupFile || '';
if ( lastBackupFile !== '' ) { if ( lastBackupFile !== '' ) {
const dt = new Date(details.lastBackupTime); const dt = new Date(details.lastBackupTime);
const text = vAPI.i18n('settingsLastBackupPrompt'); const text = i18n$('settingsLastBackupPrompt');
const node = uDom.nodeFromId('settingsLastBackupPrompt'); const node = uDom.nodeFromId('settingsLastBackupPrompt');
node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions); node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions);
node.style.display = ''; node.style.display = '';
@ -163,7 +161,7 @@ const onLocalDataReceived = function(details) {
const lastRestoreFile = details.lastRestoreFile || ''; const lastRestoreFile = details.lastRestoreFile || '';
if ( lastRestoreFile !== '' ) { if ( lastRestoreFile !== '' ) {
const dt = new Date(details.lastRestoreTime); const dt = new Date(details.lastRestoreTime);
const text = vAPI.i18n('settingsLastRestorePrompt'); const text = i18n$('settingsLastRestorePrompt');
const node = uDom.nodeFromId('settingsLastRestorePrompt'); const node = uDom.nodeFromId('settingsLastRestorePrompt');
node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions); node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions);
node.style.display = ''; node.style.display = '';
@ -183,7 +181,7 @@ const onLocalDataReceived = function(details) {
/******************************************************************************/ /******************************************************************************/
const resetUserData = function() { const resetUserData = function() {
const msg = vAPI.i18n('aboutResetDataConfirm'); const msg = i18n$('aboutResetDataConfirm');
const proceed = window.confirm(msg); const proceed = window.confirm(msg);
if ( proceed !== true ) { return; } if ( proceed !== true ) { return; }
vAPI.messaging.send('dashboard', { vAPI.messaging.send('dashboard', {
@ -306,5 +304,3 @@ document.querySelector(
); );
/******************************************************************************/ /******************************************************************************/
})();

View file

@ -91,7 +91,11 @@ async function dnrRulesetFromRawLists(lists, options = {}) {
const toLoad = []; const toLoad = [];
const toDNR = (context, list) => addToDNR(context, list); const toDNR = (context, list) => addToDNR(context, list);
for ( const list of lists ) { for ( const list of lists ) {
toLoad.push(list.then(list => toDNR(context, list))); if ( list instanceof Promise ) {
toLoad.push(list.then(list => toDNR(context, list)));
} else {
toLoad.push(toDNR(context, list));
}
} }
await Promise.all(toLoad); await Promise.all(toLoad);
return staticNetFilteringEngine.dnrFromCompiled('end', context); return staticNetFilteringEngine.dnrFromCompiled('end', context);

View file

@ -3176,10 +3176,17 @@ Parser.utils = Parser.prototype.utils = (( ) => {
[ 'adguard_ext_safari', 'false' ], [ 'adguard_ext_safari', 'false' ],
]); ]);
const toURL = url => {
try {
return new URL(url.trim());
} catch (ex) {
}
};
class preparser { class preparser {
// This method returns an array of indices, corresponding to position in // This method returns an array of indices, corresponding to position in
// the content string which should alternatively be parsed and discarded. // the content string which should alternatively be parsed and discarded.
static splitter(content, env) { static splitter(content, env = []) {
const reIf = /^!#(if|endif)\b([^\n]*)(?:[\n\r]+|$)/gm; const reIf = /^!#(if|endif)\b([^\n]*)(?:[\n\r]+|$)/gm;
const stack = []; const stack = [];
const shouldDiscard = ( ) => stack.some(v => v); const shouldDiscard = ( ) => stack.some(v => v);
@ -3205,7 +3212,6 @@ Parser.utils = Parser.prototype.utils = (( ) => {
} }
stack.push(startDiscard); stack.push(startDiscard);
break; break;
case 'endif': case 'endif':
stack.pop(); stack.pop();
const stopDiscard = shouldDiscard() === false; const stopDiscard = shouldDiscard() === false;
@ -3214,7 +3220,6 @@ Parser.utils = Parser.prototype.utils = (( ) => {
discard = false; discard = false;
} }
break; break;
default: default:
break; break;
} }
@ -3224,6 +3229,47 @@ Parser.utils = Parser.prototype.utils = (( ) => {
return parts; return parts;
} }
static expandIncludes(parts, env = []) {
const out = [];
const reInclude = /^!#include +(\S+)[^\n\r]*(?:[\n\r]+|$)/gm;
for ( const part of parts ) {
if ( typeof part === 'string' ) {
out.push(part);
continue;
}
if ( part instanceof Object === false ) { continue; }
const content = part.content;
const slices = this.splitter(content, env);
for ( let i = 0, n = slices.length - 1; i < n; i++ ) {
const slice = content.slice(slices[i+0], slices[i+1]);
if ( (i & 1) !== 0 ) {
out.push(slice);
continue;
}
let lastIndex = 0;
for (;;) {
const match = reInclude.exec(slice);
if ( match === null ) { break; }
if ( toURL(match[1]) !== undefined ) { continue; }
if ( match[1].indexOf('..') !== -1 ) { continue; }
// Compute nested list path relative to parent list path
const pos = part.url.lastIndexOf('/');
if ( pos === -1 ) { continue; }
const subURL = part.url.slice(0, pos + 1) + match[1].trim();
out.push(
slice.slice(lastIndex, match.index + match[0].length),
`! >>>>>>>> ${subURL}\n`,
{ url: subURL },
`! <<<<<<<< ${subURL}\n`
);
lastIndex = reInclude.lastIndex;
}
out.push(lastIndex === 0 ? slice : slice.slice(lastIndex));
}
}
return out;
}
static prune(content, env) { static prune(content, env) {
const parts = this.splitter(content, env); const parts = this.splitter(content, env);
const out = []; const out = [];

View file

@ -37,6 +37,7 @@ import staticFilteringReverseLookup from './reverselookup.js';
import staticNetFilteringEngine from './static-net-filtering.js'; import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js'; import µb from './background.js';
import { hostnameFromURI } from './uri-utils.js'; import { hostnameFromURI } from './uri-utils.js';
import { i18n, i18n$ } from './i18n.js';
import { redirectEngine } from './redirect-engine.js'; import { redirectEngine } from './redirect-engine.js';
import { sparseBase64 } from './base64-custom.js'; import { sparseBase64 } from './base64-custom.js';
import { StaticFilteringParser } from './static-filtering-parser.js'; import { StaticFilteringParser } from './static-filtering-parser.js';
@ -625,7 +626,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
newAvailableLists[this.userFiltersPath] = { newAvailableLists[this.userFiltersPath] = {
content: 'filters', content: 'filters',
group: 'user', group: 'user',
title: vAPI.i18n('1pPageName'), title: i18n$('1pPageName'),
}; };
// Custom filter lists. // Custom filter lists.
@ -1392,7 +1393,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
if ( typeof details.lang === 'string' ) { if ( typeof details.lang === 'string' ) {
let re = this.listMatchesEnvironment.reLang; let re = this.listMatchesEnvironment.reLang;
if ( re === undefined ) { if ( re === undefined ) {
const match = /^[a-z]+/.exec(browser.i18n.getUILanguage()); const match = /^[a-z]+/.exec(i18n.getUILanguage());
if ( match !== null ) { if ( match !== null ) {
re = new RegExp('\\b' + match[0] + '\\b'); re = new RegExp('\\b' + match[0] + '\\b');
this.listMatchesEnvironment.reLang = re; this.listMatchesEnvironment.reLang = re;

View file

@ -30,6 +30,7 @@ import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js'; import µb from './background.js';
import webext from './webext.js'; import webext from './webext.js';
import { PageStore } from './pagestore.js'; import { PageStore } from './pagestore.js';
import { i18n$ } from './i18n.js';
import { import {
sessionFirewall, sessionFirewall,
@ -1052,7 +1053,7 @@ vAPI.tabs = new vAPI.Tabs();
}; };
const pageStore = new NoPageStore(vAPI.noTabId); const pageStore = new NoPageStore(vAPI.noTabId);
µb.pageStores.set(pageStore.tabId, pageStore); µb.pageStores.set(pageStore.tabId, pageStore);
pageStore.title = vAPI.i18n('logBehindTheScene'); pageStore.title = i18n$('logBehindTheScene');
} }
/******************************************************************************/ /******************************************************************************/

View file

@ -23,9 +23,7 @@
'use strict'; 'use strict';
/******************************************************************************/ import { i18n$ } from './i18n.js';
(( ) => {
/******************************************************************************/ /******************************************************************************/
@ -204,7 +202,7 @@ const exportWhitelistToFile = function() {
const val = getEditorText(); const val = getEditorText();
if ( val === '' ) { return; } if ( val === '' ) { return; }
const filename = const filename =
vAPI.i18n('whitelistExportFilename') i18n$('whitelistExportFilename')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
vAPI.download({ vAPI.download({
@ -262,5 +260,3 @@ uDom('#whitelistRevert').on('click', revertChanges);
renderWhitelist(); renderWhitelist();
/******************************************************************************/ /******************************************************************************/
})();

View file

@ -213,7 +213,7 @@
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script> <script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/logger-ui.js" type="module"></script> <script src="js/logger-ui.js" type="module"></script>
<script src="js/logger-ui-inspector.js" type="module"></script> <script src="js/logger-ui-inspector.js" type="module"></script>

View file

@ -21,7 +21,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -101,7 +101,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/popup-fenix.js" type="module"></script> <script src="js/popup-fenix.js" type="module"></script>
</body> </body>

View file

@ -90,9 +90,9 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/settings.js"></script> <script src="js/settings.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -32,7 +32,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/shortcuts.js"></script> <script src="js/shortcuts.js"></script>

View file

@ -112,7 +112,7 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/support.js"></script> <script src="js/support.js"></script>

View file

@ -69,7 +69,7 @@
<script src="../js/vapi-client.js"></script> <script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.js"></script> <script src="../js/vapi-client-extra.js"></script>
<script src="../js/udom.js"></script> <script src="../js/udom.js"></script>
<script src="../js/i18n.js"></script> <script src="../js/i18n.js" type="module"></script>
<script src="../js/epicker-ui.js" type="module"></script> <script src="../js/epicker-ui.js" type="module"></script>
</body> </body>

View file

@ -53,10 +53,10 @@
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script> <script src="js/udom.js"></script>
<script src="js/i18n.js"></script> <script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script> <script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script> <script src="js/cloud-ui.js" type="module"></script>
<script src="js/whitelist.js"></script> <script src="js/whitelist.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -7,72 +7,35 @@ set -e
echo "*** uBlock: Importing from Crowdin archive" echo "*** uBlock: Importing from Crowdin archive"
SRC=~/Downloads/crowdin SRC=~/Downloads/crowdin
rm -r $SRC || true rm -r $SRC || true > /dev/null
unzip -q ~/Downloads/uBlock\ \(translations\).zip -d $SRC unzip -q ~/Downloads/uBlock\ \(translations\).zip -d $SRC
# https://www.assertnotmagic.com/2018/06/20/bash-brackets-quick-reference/
DES=./src/_locales DES=./src/_locales
cp $SRC/ar/messages.json $DES/ar/messages.json DESMV3=./platform/mv3/extension/_locales
cp $SRC/az/messages.json $DES/az/messages.json
cp $SRC/bg/messages.json $DES/bg/messages.json for dir in $SRC/*/; do
cp $SRC/bn/messages.json $DES/bn/messages.json srclang=$(basename $dir)
cp $SRC/bs/messages.json $DES/bs/messages.json deslang=${srclang/-/_}
cp $SRC/ca/messages.json $DES/ca/messages.json deslang=${deslang%_AM}
cp $SRC/cs/messages.json $DES/cs/messages.json deslang=${deslang%_ES}
cp $SRC/cv/messages.json $DES/cv/messages.json deslang=${deslang%_IE}
cp $SRC/da/messages.json $DES/da/messages.json deslang=${deslang%_IN}
cp $SRC/de/messages.json $DES/de/messages.json deslang=${deslang%_LK}
cp $SRC/el/messages.json $DES/el/messages.json deslang=${deslang%_NL}
cp $SRC/en-GB/messages.json $DES/en_GB/messages.json deslang=${deslang%_PK}
cp $SRC/eo/messages.json $DES/eo/messages.json deslang=${deslang%_SE}
cp $SRC/es-ES/messages.json $DES/es/messages.json if [[ $deslang == 'en' ]]; then
cp $SRC/et/messages.json $DES/et/messages.json continue
cp $SRC/eu/messages.json $DES/eu/messages.json fi
cp $SRC/fa/messages.json $DES/fa/messages.json # ubo
cp $SRC/fi/messages.json $DES/fi/messages.json mkdir -p "$DES/$deslang/" && cp "$SRC/$srclang/messages.json" "$DES/$deslang/"
cp $SRC/fil/messages.json $DES/fil/messages.json # ubo lite
cp $SRC/fr/messages.json $DES/fr/messages.json mkdir -p "$DESMV3/$deslang/" && cp "$SRC/$srclang/uBO Lite/messages.json" "$DESMV3/$deslang/"
cp $SRC/fy-NL/messages.json $DES/fy/messages.json # descriptions
cp $SRC/gl/messages.json $DES/gl/messages.json cp "$SRC/$srclang/description.txt" "./dist/description/description-${deslang}.txt"
cp $SRC/he/messages.json $DES/he/messages.json done
cp $SRC/hi/messages.json $DES/hi/messages.json
cp $SRC/hr/messages.json $DES/hr/messages.json
cp $SRC/hu/messages.json $DES/hu/messages.json
cp $SRC/hy-AM/messages.json $DES/hy/messages.json
cp $SRC/id/messages.json $DES/id/messages.json
cp $SRC/it/messages.json $DES/it/messages.json
cp $SRC/ja/messages.json $DES/ja/messages.json
cp $SRC/ka/messages.json $DES/ka/messages.json
cp $SRC/kk/messages.json $DES/kk/messages.json
cp $SRC/kn/messages.json $DES/kn/messages.json
cp $SRC/ko/messages.json $DES/ko/messages.json
cp $SRC/lt/messages.json $DES/lt/messages.json
cp $SRC/lv/messages.json $DES/lv/messages.json
cp $SRC/ml-IN/messages.json $DES/ml/messages.json
cp $SRC/mr/messages.json $DES/mr/messages.json
cp $SRC/ms/messages.json $DES/ms/messages.json
cp $SRC/nb/messages.json $DES/nb/messages.json
cp $SRC/nl/messages.json $DES/nl/messages.json
cp $SRC/oc/messages.json $DES/oc/messages.json
cp $SRC/pl/messages.json $DES/pl/messages.json
cp $SRC/pt-BR/messages.json $DES/pt_BR/messages.json
cp $SRC/pt-PT/messages.json $DES/pt_PT/messages.json
cp $SRC/ro/messages.json $DES/ro/messages.json
cp $SRC/ru/messages.json $DES/ru/messages.json
cp $SRC/sk/messages.json $DES/sk/messages.json
cp $SRC/sl/messages.json $DES/sl/messages.json
cp $SRC/so/messages.json $DES/so/messages.json
cp $SRC/sq/messages.json $DES/sq/messages.json
cp $SRC/sr/messages.json $DES/sr/messages.json
cp $SRC/sv-SE/messages.json $DES/sv/messages.json
cp $SRC/ta/messages.json $DES/ta/messages.json
cp $SRC/te/messages.json $DES/te/messages.json
cp $SRC/th/messages.json $DES/th/messages.json
cp $SRC/tr/messages.json $DES/tr/messages.json
cp $SRC/ur-PK/messages.json $DES/ur/messages.json
cp $SRC/uk/messages.json $DES/uk/messages.json
cp $SRC/vi/messages.json $DES/vi/messages.json
cp $SRC/zh-CN/messages.json $DES/zh_CN/messages.json
cp $SRC/zh-TW/messages.json $DES/zh_TW/messages.json
# Output files with possible misuse of `$`, as this can lead to severe # Output files with possible misuse of `$`, as this can lead to severe
# consequences, such as not being able to run the extension at all. # consequences, such as not being able to run the extension at all.
@ -80,71 +43,8 @@ cp $SRC/zh-TW/messages.json $DES/zh_TW/messages.json
# See https://issues.adblockplus.org/ticket/6666 # See https://issues.adblockplus.org/ticket/6666
echo "*** uBlock: Instances of '\$':" echo "*** uBlock: Instances of '\$':"
grep -FR "$" $DES/ || true grep -FR "$" $DES/ || true
grep -FR "$" $DESMV3/ || true
#
DES=./dist/description
cp $SRC/ar/description.txt $DES/description-ar.txt
cp $SRC/bg/description.txt $DES/description-bg.txt
cp $SRC/bn/description.txt $DES/description-bn.txt
cp $SRC/bs/description.txt $DES/description-bs.txt
cp $SRC/ca/description.txt $DES/description-ca.txt
cp $SRC/cs/description.txt $DES/description-cs.txt
cp $SRC/cv/description.txt $DES/description-cv.txt
cp $SRC/da/description.txt $DES/description-da.txt
cp $SRC/de/description.txt $DES/description-de.txt
cp $SRC/el/description.txt $DES/description-el.txt
cp $SRC/en-GB/description.txt $DES/description-en_GB.txt
cp $SRC/eo/description.txt $DES/description-eo.txt
cp $SRC/es-ES/description.txt $DES/description-es.txt
cp $SRC/et/description.txt $DES/description-et.txt
cp $SRC/eu/description.txt $DES/description-eu.txt
cp $SRC/fa/description.txt $DES/description-fa.txt
cp $SRC/fi/description.txt $DES/description-fi.txt
cp $SRC/fil/description.txt $DES/description-fil.txt
cp $SRC/fr/description.txt $DES/description-fr.txt
cp $SRC/fy-NL/description.txt $DES/description-fy.txt
cp $SRC/gl/description.txt $DES/description-gl.txt
cp $SRC/he/description.txt $DES/description-he.txt
cp $SRC/hi/description.txt $DES/description-hi.txt
cp $SRC/hr/description.txt $DES/description-hr.txt
cp $SRC/hu/description.txt $DES/description-hu.txt
cp $SRC/hy-AM/description.txt $DES/description-hy.txt
cp $SRC/id/description.txt $DES/description-id.txt
cp $SRC/it/description.txt $DES/description-it.txt
cp $SRC/ja/description.txt $DES/description-ja.txt
cp $SRC/ka/description.txt $DES/description-ka.txt
cp $SRC/kk/description.txt $DES/description-kk.txt
cp $SRC/ko/description.txt $DES/description-ko.txt
cp $SRC/kn/description.txt $DES/description-kn.txt
cp $SRC/lt/description.txt $DES/description-lt.txt
cp $SRC/lv/description.txt $DES/description-lv.txt
cp $SRC/ml-IN/description.txt $DES/description-ml.txt
cp $SRC/ms/description.txt $DES/description-ms.txt
cp $SRC/mr/description.txt $DES/description-mr.txt
cp $SRC/nb/description.txt $DES/description-nb.txt
cp $SRC/nl/description.txt $DES/description-nl.txt
cp $SRC/oc/description.txt $DES/description-oc.txt
cp $SRC/pl/description.txt $DES/description-pl.txt
cp $SRC/pt-BR/description.txt $DES/description-pt_BR.txt
cp $SRC/pt-PT/description.txt $DES/description-pt_PT.txt
cp $SRC/ro/description.txt $DES/description-ro.txt
cp $SRC/ru/description.txt $DES/description-ru.txt
cp $SRC/sk/description.txt $DES/description-sk.txt
cp $SRC/sl/description.txt $DES/description-sl.txt
cp $SRC/sq/description.txt $DES/description-sq.txt
cp $SRC/sr/description.txt $DES/description-sr.txt
cp $SRC/sv-SE/description.txt $DES/description-sv.txt
cp $SRC/ta/description.txt $DES/description-ta.txt
cp $SRC/te/description.txt $DES/description-te.txt
cp $SRC/tr/description.txt $DES/description-tr.txt
cp $SRC/ur-PK/description.txt $DES/description-ur.txt
cp $SRC/uk/description.txt $DES/description-uk.txt
cp $SRC/vi/description.txt $DES/description-vi.txt
cp $SRC/zh-CN/description.txt $DES/description-zh_CN.txt
cp $SRC/zh-TW/description.txt $DES/description-zh_TW.txt
#
rm -r $SRC rm -r $SRC
echo "*** uBlock: Import done." echo "*** uBlock: Import done."

View file

@ -25,8 +25,12 @@ echo "*** uBlock0.mv3: Copying common files"
cp -R src/css/fonts/* $DES/css/fonts/ cp -R src/css/fonts/* $DES/css/fonts/
cp src/css/themes/default.css $DES/css/ cp src/css/themes/default.css $DES/css/
cp src/css/common.css $DES/css/ cp src/css/common.css $DES/css/
cp src/css/dashboard.css $DES/css/
cp src/css/dashboard-common.css $DES/css/
cp src/css/fa-icons.css $DES/css/ cp src/css/fa-icons.css $DES/css/
cp src/js/fa-icons.js $DES/js/ cp src/js/fa-icons.js $DES/js/
cp src/js/i18n.js $DES/js/
cp LICENSE.txt $DES/ cp LICENSE.txt $DES/
@ -35,6 +39,7 @@ cp platform/mv3/extension/*.html $DES/
cp platform/mv3/extension/css/* $DES/css/ cp platform/mv3/extension/css/* $DES/css/
cp platform/mv3/extension/js/* $DES/js/ cp platform/mv3/extension/js/* $DES/js/
cp platform/mv3/extension/img/* $DES/img/ cp platform/mv3/extension/img/* $DES/img/
cp -R platform/mv3/extension/_locales $DES/
if [ "$1" != "quick" ]; then if [ "$1" != "quick" ]; then
echo "*** uBlock0.mv3: Generating rulesets" echo "*** uBlock0.mv3: Generating rulesets"
@ -44,6 +49,7 @@ if [ "$1" != "quick" ]; then
./tools/make-nodejs.sh $TMPDIR ./tools/make-nodejs.sh $TMPDIR
cp platform/mv3/package.json $TMPDIR/ cp platform/mv3/package.json $TMPDIR/
cp platform/mv3/*.js $TMPDIR/ cp platform/mv3/*.js $TMPDIR/
cp assets/assets.json $TMPDIR/
cd $TMPDIR cd $TMPDIR
node --no-warnings make-rulesets.js output=$DES quick=$QUICK node --no-warnings make-rulesets.js output=$DES quick=$QUICK
cd - > /dev/null cd - > /dev/null