diff --git a/.jshintrc b/.jshintrc
index d2fe2c38f..92bcdc78b 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -4,7 +4,6 @@
"eqeqeq": true,
"esversion": 8,
"globals": {
- "browser": false, // global variable in Firefox, Edge
"chrome": false, // global variable in Chromium, Chrome, Opera
"self": false,
"vAPI": false,
diff --git a/Makefile b/Makefile
index d0d0228a9..3ddf87d30 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,11 @@
# https://stackoverflow.com/a/6273809
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
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/* \
submodules/uAssets/*/* \
submodules/uAssets/*/*/* \
@@ -57,7 +57,7 @@ mv3: tools/make-mv3.sh $(sources) $(platform)
mv3-quick: tools/make-mv3.sh $(sources) $(platform)
tools/make-mv3.sh quick
-
+
mv3-full: tools/make-mv3.sh $(sources) $(platform)
tools/make-mv3.sh full
diff --git a/dist/firefox/publish-signed-beta.py b/dist/firefox/publish-signed-beta.py
index 5e23cdd2f..627da71e5 100755
--- a/dist/firefox/publish-signed-beta.py
+++ b/dist/firefox/publish-signed-beta.py
@@ -291,6 +291,10 @@ if response.status_code != 204:
# 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...')
updates_json_filepath = os.path.join(projdir, 'dist', 'firefox', 'updates.json')
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:
f.write(updates_json)
f.close()
- # Automatically git add/commit if needed.
# - Stage the changed file
r = subprocess.run(['git', 'status', '-s', updates_json_filepath], stdout=subprocess.PIPE)
rout = bytes.decode(r.stdout).strip()
diff --git a/platform/common/vapi-common.js b/platform/common/vapi-common.js
index 0bea820f1..9dde268df 100644
--- a/platform/common/vapi-common.js
+++ b/platform/common/vapi-common.js
@@ -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
// - webNavigation.onCreatedNavigationTarget become broken on Firefox when we
// try to make the popup panel close itself using the original
diff --git a/platform/mv3/extension/3p-filters.html b/platform/mv3/extension/3p-filters.html
new file mode 100644
index 000000000..fbd0545d1
--- /dev/null
+++ b/platform/mv3/extension/3p-filters.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+uBlock Origin Lite — Filter lists
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/mv3/extension/_locales/en/messages.json b/platform/mv3/extension/_locales/en/messages.json
new file mode 100644
index 000000000..149a184af
--- /dev/null
+++ b/platform/mv3/extension/_locales/en/messages.json
@@ -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 !
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"
+ }
+}
diff --git a/platform/mv3/extension/about.html b/platform/mv3/extension/about.html
new file mode 100644
index 000000000..b4ea3cee0
--- /dev/null
+++ b/platform/mv3/extension/about.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+uBlock Origin Lite — About
+
+
+
+
+
+
+
+
+
+
+
+
Copyright (c) Raymond Hill 2014-present
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/mv3/extension/css/3p-filters.css b/platform/mv3/extension/css/3p-filters.css
new file mode 100644
index 000000000..f19bf09de
--- /dev/null
+++ b/platform/mv3/extension/css/3p-filters.css
@@ -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;
+ }
diff --git a/platform/mv3/extension/css/dashboard-common.css b/platform/mv3/extension/css/dashboard-common.css
new file mode 100644
index 000000000..da2c05226
--- /dev/null
+++ b/platform/mv3/extension/css/dashboard-common.css
@@ -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;
+ }
diff --git a/platform/mv3/extension/dashboard.html b/platform/mv3/extension/dashboard.html
new file mode 100644
index 000000000..95e3234f7
--- /dev/null
+++ b/platform/mv3/extension/dashboard.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/mv3/ublock.svg b/platform/mv3/extension/img/ublock.svg
similarity index 100%
rename from platform/mv3/ublock.svg
rename to platform/mv3/extension/img/ublock.svg
diff --git a/platform/mv3/extension/js/3p-filters.js b/platform/mv3/extension/js/3p-filters.js
new file mode 100644
index 000000000..eeb685754
--- /dev/null
+++ b/platform/mv3/extension/js/3p-filters.js
@@ -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);
+});
+
+/******************************************************************************/
diff --git a/platform/mv3/extension/js/about.js b/platform/mv3/extension/js/about.js
new file mode 100644
index 000000000..5596b777e
--- /dev/null
+++ b/platform/mv3/extension/js/about.js
@@ -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}`;
+})();
diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js
index 2d3c1e904..1710363f6 100644
--- a/platform/mv3/extension/js/background.js
+++ b/platform/mv3/extension/js/background.js
@@ -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';
-import rulesetDetails from '/rulesets/ruleset-details.js';
+/******************************************************************************/
+
+import { dnr, i18n, runtime } from './ext.js';
/******************************************************************************/
-const dnr = chrome.declarativeNetRequest;
-const TRUSTED_DIRECTIVE_BASE_RULE_ID = 1000000;
+const RULE_REALM_SIZE = 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 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 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; }
- for ( const rule of details.rules.regexes ) {
- const regex = rule.condition.regexFilter;
- const isCaseSensitive = rule.condition.isUrlFilterCaseSensitive === true;
+ toFetch.push(fetchJSON(`${details.id}.regexes`));
+ }
+ 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);
- 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 newRules = [];
for ( let i = 0; i < allRules.length; i++ ) {
@@ -33,11 +157,18 @@ async function updateRegexRules() {
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 addRules = [];
const removeRuleIds = [];
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);
if ( newRule === undefined ) {
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();
for ( const rule of dynamicRules ) {
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(`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();
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 => {
console.log(`Available static rule count: ${count}`);
});
dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: true });
+}
- chrome.runtime.onMessage.addListener((request, sender, callback) => {
- switch ( request.what ) {
- case 'popupPanelData':
- matchesTrustedSiteDirective(request).then(response => {
- callback({
- isTrusted: response,
- rulesetDetails: rulesetDetails.filter(details =>
- details.enabled
- ).map(details => ({
- name: details.name,
- filterCount: details.filters.accepted,
- ruleCount: details.rules.accepted,
- })),
- });
+/******************************************************************************/
+
+function messageListener(request, sender, callback) {
+ switch ( request.what ) {
+
+ case 'getRulesetData': {
+ dnr.getEnabledRulesets().then(enabledRulesets => {
+ callback({
+ enabledRulesets,
+ rulesetDetails: Array.from(rulesetDetails.values()),
});
- return true;
- case 'toggleTrustedSiteDirective':
- toggleTrustedSiteDirective(request).then(response => {
- callback(response);
+ });
+ return true;
+ }
+
+ 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:
- break;
- }
- });
+ });
+ return true;
+ }
+
+ case 'toggleTrustedSiteDirective': {
+ toggleTrustedSiteDirective(request).then(response => {
+ callback(response);
+ });
+ return true;
+ }
+
+ default:
+ break;
+
+ }
+}
+
+/******************************************************************************/
+
+(async ( ) => {
+ await start();
+
+ runtime.onMessage.addListener(messageListener);
})();
diff --git a/platform/mv3/extension/js/dashboard-common.js b/platform/mv3/extension/js/dashboard-common.js
new file mode 100644
index 000000000..7aa3b3261
--- /dev/null
+++ b/platform/mv3/extension/js/dashboard-common.js
@@ -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');
diff --git a/platform/mv3/extension/js/dashboard.js b/platform/mv3/extension/js/dashboard.js
new file mode 100644
index 000000000..8291aa096
--- /dev/null
+++ b/platform/mv3/extension/js/dashboard.js
@@ -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 = '';
+ });
+})();
+
+/******************************************************************************/
diff --git a/platform/mv3/extension/js/dom.js b/platform/mv3/extension/js/dom.js
new file mode 100644
index 000000000..d8119fcc9
--- /dev/null
+++ b/platform/mv3/extension/js/dom.js
@@ -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$ };
diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js
new file mode 100644
index 000000000..9a5aa3750
--- /dev/null
+++ b/platform/mv3/extension/js/ext.js
@@ -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 };
diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js
index 167b7fe5f..898a5b9e1 100644
--- a/platform/mv3/extension/js/popup.js
+++ b/platform/mv3/extension/js/popup.js
@@ -1,7 +1,7 @@
/*******************************************************************************
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
it under the terms of the GNU General Public License as published by
@@ -19,35 +19,23 @@
Home: https://github.com/gorhill/uBlock
*/
+/* jshint esversion:11 */
+
'use strict';
/******************************************************************************/
+import { browser, sendMessage } from './ext.js';
+import { i18n$ } from './i18n.js';
+import { simpleStorage } from './storage.js';
+
+/******************************************************************************/
+
let currentTab = {};
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() {
let url;
try {
@@ -57,7 +45,7 @@ async function toggleTrustedSiteDirective() {
}
if ( url instanceof URL === false ) { return; }
const targetTrustedState = document.body.classList.contains('off');
- const newTrustedState = await chrome.runtime.sendMessage({
+ const newTrustedState = await sendMessage({
what: 'toggleTrustedSiteDirective',
origin: url.origin,
state: targetTrustedState,
@@ -75,7 +63,7 @@ async function toggleTrustedSiteDirective() {
/******************************************************************************/
function reloadTab(ev) {
- chrome.tabs.reload(currentTab.id, {
+ browser.tabs.reload(currentTab.id, {
bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey,
});
document.body.classList.remove('needReload');
@@ -85,7 +73,7 @@ function reloadTab(ev) {
/******************************************************************************/
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; }
currentTab = tab;
@@ -97,7 +85,7 @@ async function init() {
let popupPanelData;
if ( url !== undefined ) {
- popupPanelData = await chrome.runtime.sendMessage({
+ popupPanelData = await sendMessage({
what: 'popupPanelData',
origin: url.origin,
});
@@ -126,7 +114,9 @@ async function init() {
h1.textContent = details.name;
parent.append(h1);
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);
}
}
@@ -189,12 +179,12 @@ async function toggleSections(more) {
}
if ( newBits === currentBits ) { return; }
sectionBitsToAttribute(newBits);
- safeLocalStorage.setItem('popupPanelSections', newBits);
+ simpleStorage.setItem('popupPanelSections', newBits);
}
-sectionBitsToAttribute(
- parseInt(safeLocalStorage.getItem('popupPanelSections'), 10)
-);
+simpleStorage.getItem('popupPanelSections').then(s => {
+ sectionBitsToAttribute(parseInt(s, 10) || 0);
+});
document.querySelector('#moreButton').addEventListener('click', ( ) => {
toggleSections(true);
diff --git a/platform/mv3/extension/js/storage.js b/platform/mv3/extension/js/storage.js
new file mode 100644
index 000000000..221a91ac3
--- /dev/null
+++ b/platform/mv3/extension/js/storage.js
@@ -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) {
+ }
+ }
+}
diff --git a/platform/mv3/extension/manifest.json b/platform/mv3/extension/manifest.json
index 0fa64e855..e9fe94d28 100644
--- a/platform/mv3/extension/manifest.json
+++ b/platform/mv3/extension/manifest.json
@@ -16,6 +16,7 @@
"rule_resources": [
]
},
+ "default_locale": "en",
"description": "uBO Minus is permission-less experimental MV3-based network request blocker",
"icons": {
"16": "img/icon_16.png",
@@ -26,6 +27,7 @@
"manifest_version": 3,
"minimum_chrome_version": "101.0",
"name": "uBlock Origin Lite",
+ "options_page": "dashboard.html",
"permissions": [
"activeTab",
"declarativeNetRequest"
diff --git a/platform/mv3/extension/popup.html b/platform/mv3/extension/popup.html
index 4acf0cf99..d592f2e08 100644
--- a/platform/mv3/extension/popup.html
+++ b/platform/mv3/extension/popup.html
@@ -19,7 +19,7 @@
lock
eraser
-