From e31637af7861da56c63a96d6c50ce37e2aced9f4 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 13 Sep 2022 17:44:24 -0400 Subject: [PATCH] [mv3] Add ability to enable/disable filter lists --- .jshintrc | 1 - Makefile | 6 +- dist/firefox/publish-signed-beta.py | 5 +- platform/common/vapi-common.js | 12 - platform/mv3/extension/3p-filters.html | 48 ++ .../mv3/extension/_locales/en/messages.json | 202 +++++++ platform/mv3/extension/about.html | 42 ++ platform/mv3/extension/css/3p-filters.css | 186 ++++++ .../mv3/extension/css/dashboard-common.css | 55 ++ platform/mv3/extension/dashboard.html | 35 ++ platform/mv3/{ => extension/img}/ublock.svg | 0 platform/mv3/extension/js/3p-filters.js | 379 +++++++++++++ platform/mv3/extension/js/about.js | 35 ++ platform/mv3/extension/js/background.js | 323 +++++++++-- platform/mv3/extension/js/dashboard-common.js | 32 ++ platform/mv3/extension/js/dashboard.js | 135 +++++ platform/mv3/extension/js/dom.js | 121 ++++ platform/mv3/extension/js/ext.js | 64 +++ platform/mv3/extension/js/popup.js | 50 +- platform/mv3/extension/js/storage.js | 44 ++ platform/mv3/extension/manifest.json | 2 + platform/mv3/extension/popup.html | 10 +- platform/mv3/make-rulesets.js | 278 ++++++--- platform/mv3/ruleset-config.js | 74 --- src/1p-filters.html | 4 +- src/3p-filters.html | 6 +- src/about.html | 2 +- src/advanced-settings.html | 2 +- src/asset-viewer.html | 2 +- src/background.html | 2 +- src/dashboard.html | 2 +- src/devtools.html | 2 +- src/document-blocked.html | 4 +- src/dyna-rules.html | 4 +- src/js/1p-filters.js | 3 +- src/js/3p-filters.js | 21 +- src/js/assets.js | 3 +- src/js/cloud-ui.js | 6 +- src/js/contextmenu.js | 11 +- src/js/dashboard.js | 10 - src/js/document-blocked.js | 8 +- src/js/dyna-rules.js | 9 +- src/js/i18n.js | 532 +++++++++--------- src/js/logger-ui.js | 23 +- src/js/messaging.js | 5 +- src/js/popup-fenix.js | 7 +- src/js/reverselookup.js | 3 +- src/js/settings.js | 20 +- src/js/static-dnr-filtering.js | 6 +- src/js/static-filtering-parser.js | 52 +- src/js/storage.js | 5 +- src/js/tab.js | 3 +- src/js/whitelist.js | 8 +- src/logger-ui.html | 2 +- src/no-dashboard.html | 2 +- src/popup-fenix.html | 2 +- src/settings.html | 4 +- src/shortcuts.html | 2 +- src/support.html | 2 +- src/web_accessible_resources/epicker-ui.html | 2 +- src/whitelist.html | 6 +- tools/import-crowdin.sh | 154 +---- tools/make-mv3.sh | 6 + 63 files changed, 2344 insertions(+), 742 deletions(-) create mode 100644 platform/mv3/extension/3p-filters.html create mode 100644 platform/mv3/extension/_locales/en/messages.json create mode 100644 platform/mv3/extension/about.html create mode 100644 platform/mv3/extension/css/3p-filters.css create mode 100644 platform/mv3/extension/css/dashboard-common.css create mode 100644 platform/mv3/extension/dashboard.html rename platform/mv3/{ => extension/img}/ublock.svg (100%) create mode 100644 platform/mv3/extension/js/3p-filters.js create mode 100644 platform/mv3/extension/js/about.js create mode 100644 platform/mv3/extension/js/dashboard-common.js create mode 100644 platform/mv3/extension/js/dashboard.js create mode 100644 platform/mv3/extension/js/dom.js create mode 100644 platform/mv3/extension/js/ext.js create mode 100644 platform/mv3/extension/js/storage.js delete mode 100644 platform/mv3/ruleset-config.js 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 -
+