diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9cd1db1cb..0361fb5ab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,13 +39,17 @@ jobs: tag_name: ${{ steps.release_info.outputs.VERSION }} release_name: ${{ steps.release_info.outputs.VERSION }} prerelease: true - - name: Build all packages + - name: Build MV2 packages run: | tools/make-chromium.sh ${{ steps.release_info.outputs.VERSION }} tools/make-firefox.sh ${{ steps.release_info.outputs.VERSION }} tools/make-thunderbird.sh ${{ steps.release_info.outputs.VERSION }} tools/make-npm.sh ${{ steps.release_info.outputs.VERSION }} tools/make-mv3.sh all + - name: Build MV3 packages + run: | + tools/make-mv3.sh + echo ::set-output name=MV3PACKAGE::$(basename $(ls dist/build/uBlock0_*.mv3.zip)) - name: Upload Chromium package uses: actions/upload-release-asset@v1 env: @@ -88,6 +92,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: dist/build/uBlock0.mv3.zip - asset_name: uBlock0.mv3.zip + asset_path: dist/build/${{ env.MV3PACKAGE }} + asset_name: ${{ env.MV3PACKAGE }} asset_content_type: application/octet-stream diff --git a/Makefile b/Makefile index 77af4aa02..41b600af7 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ run_options := $(filter-out $@,$(MAKECMDGOALS)) compare maxcost medcost mincost modifiers record wasm sources := $(wildcard assets/resources/* src/* src/*/* src/*/*/* src/*/*/*/*) -platform := $(wildcard platform/* platform/*/*) +platform := $(wildcard platform/* platform/*/* platform/*/*/*) assets := $(wildcard submodules/uAssets/* \ submodules/uAssets/*/* \ submodules/uAssets/*/*/* \ @@ -52,10 +52,11 @@ dig: dist/build/uBlock0.dig dig-snfe: dig cd dist/build/uBlock0.dig && npm run snfe $(run_options) -dist/build/uBlock0.mv3: tools/make-mv3.sh $(sources) $(platform) - tools/make-mv3.sh all +mv3: tools/make-mv3.sh $(sources) $(platform) + tools/make-mv3.sh -mv3: dist/build/uBlock0.mv3 +mv3-quick: tools/make-mv3.sh $(sources) $(platform) + tools/make-mv3.sh quick # Update submodules. update-submodules: diff --git a/platform/mv3/extension/background.js b/platform/mv3/extension/background.js deleted file mode 100644 index 4c7ef6cd9..000000000 --- a/platform/mv3/extension/background.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -import regexRulesets from '/rulesets/regexes.js'; - -const dnr = chrome.declarativeNetRequest; - -dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: true }); - -(async ( ) => { - const allRules = []; - const toCheck = []; - for ( const regexRuleset of regexRulesets ) { - if ( regexRuleset.enabled !== true ) { continue; } - for ( const rule of regexRuleset.rules ) { - const regex = rule.condition.regexFilter; - const isCaseSensitive = rule.condition.isUrlFilterCaseSensitive === true; - allRules.push(rule); - toCheck.push(dnr.isRegexSupported({ regex, isCaseSensitive })); - } - } - const results = await Promise.all(toCheck); - const newRules = []; - for ( let i = 0; i < allRules.length; i++ ) { - const rule = allRules[i]; - const result = results[i]; - if ( result instanceof Object && result.isSupported ) { - newRules.push(rule); - } else { - console.info(`${result.reason}: ${rule.condition.regexFilter}`); - } - } - const oldRules = await dnr.getDynamicRules(); - const oldRuleMap = new Map(oldRules.map(rule => [ rule.id, rule ])); - const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ])); - const addRules = []; - const removeRuleIds = []; - for ( const oldRule of oldRules ) { - const newRule = newRuleMap.get(oldRule.id); - if ( newRule === undefined ) { - removeRuleIds.push(oldRule.id); - } else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) { - removeRuleIds.push(oldRule.id); - addRules.push(newRule); - } - } - for ( const newRule of newRuleMap.values() ) { - if ( oldRuleMap.has(newRule.id) ) { continue; } - addRules.push(newRule); - } - if ( addRules.length !== 0 || removeRuleIds.length !== 0 ) { - await dnr.updateDynamicRules({ addRules, removeRuleIds }); - } - - const dynamicRules = await dnr.getDynamicRules(); - console.log(`Dynamic rule count: ${dynamicRules.length}`); - - const enabledRulesets = await dnr.getEnabledRulesets(); - console.log(`Enabled rulesets: ${enabledRulesets}`); - - console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - dynamicRules.length}`); - - dnr.getAvailableStaticRuleCount().then(count => { - console.log(`Available static rule count: ${count}`); - }); -})(); diff --git a/platform/mv3/extension/css/popup.css b/platform/mv3/extension/css/popup.css new file mode 100644 index 000000000..fc3eb2d2d --- /dev/null +++ b/platform/mv3/extension/css/popup.css @@ -0,0 +1,227 @@ + /* External CSS values override */ +.fa-icon.fa-icon-badged > .fa-icon-badge { + bottom: auto; + top: -20%; + } + +/* Internal CSS values */ +:root body { + overflow: hidden; + } +:root body, +:root.mobile body { + --font-size: 14px; + --popup-gap: var(--font-size); + --popup-gap-thin: calc(0.5 * var(--popup-gap)); + --popup-gap-extra-thin: calc(0.25 * var(--popup-gap)); + --popup-main-min-width: 18em; + --popup-firewall-min-width: 30em; + --popup-rule-cell-width: 5em; + font-size: var(--font-size); + line-height: 20px; + } +:root body.loading { + opacity: 0; + } +a { + color: var(--ink-1); + fill: var(--ink-1); + text-decoration: none; + } +:focus { + outline: 0; + } + +#main { + align-self: flex-start; + max-width: 340px; + min-width: var(--popup-main-min-width); + } +:root.portrait #main { + align-self: inherit; + } +hr { + border: 0; + border-top: 1px solid var(--hr-ink); + margin: 0; + padding: 0; + } + +#sticky { + background-color: var(--surface-1); + position: sticky; + top: 0; + z-index: 100; + } +#stickyTools { + align-items: stretch; + display: flex; + justify-content: space-between; + } +#switch { + color: var(--popup-power-ink); + cursor: pointer; + display: flex; + fill: var(--popup-power-ink); + flex-grow: 1; + font-size: 96px; + justify-content: center; + margin: var(--popup-gap-thin) var(--popup-gap-thin) 0; + padding: 0; + stroke: none; + stroke-width: 64; + } +body.off #switch { + fill: var(--surface-1); + stroke: var(--checkbox-ink); + } +.rulesetTools { + background-color: transparent; + border: 0; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: space-evenly; + width: 25%; + } +.rulesetTools [id] { + background-color: var(--popup-ruleset-tool-surface); + border-radius: 4px; + cursor: pointer; + fill: var(--popup-ruleset-tool-ink); + flex-grow: 1; + font-size: 2.2em; + padding: 0; + visibility: hidden; + } +.rulesetTools [id]:not(:first-of-type) { + margin-block-start: 1px; + } +.rulesetTools [id] > svg { + fill: var(--ink-4); + } +body.needReload #refresh, +body.needSave #saveRules, +body.needSave #revertRules { + visibility: visible; + } +#hostname { + margin: var(--popup-gap) var(--popup-gap-extra-thin); + text-align: center; + } +#hostname > span { + word-break: break-all; + } +#hostname > span + span { + font-weight: 600; + } + +.itemRibbon { + column-gap: var(--popup-gap); + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + grid-template: auto / 1fr 1fr; + margin: var(--popup-gap); + } +.itemRibbon > span + span { + text-align: end; + } + +.toolRibbon { + align-items: start; + background-color: var(--popup-toolbar-surface); + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + grid-template: auto / repeat(4, 1fr); + justify-items: center; + margin: 0; + white-space: normal; + } +.toolRibbon .tool { + cursor: pointer; + display: flex; + flex-direction: column; + font-size: 1.4em; + min-width: 32px; + padding: var(--popup-gap) + var(--popup-gap-thin); + unicode-bidi: embed; + visibility: hidden; + } +.toolRibbon .tool:hover { + color: var(--ink-1); + fill: var(--ink-1); + } +.toolRibbon .tool.enabled { + visibility: visible; + } +.toolRibbon .tool .caption { + font: 10px/12px sans-serif; + margin-top: 6px; + text-align: center; + } +body.mobile.no-tooltips .toolRibbon .tool { + font-size: 1.6em; + } + +#basicTools { + margin-top: var(--default-gap); + } + +/* configurable UI elements */ +:root:not(.mobile) .toolRibbon .caption, +:root.mobile body.no-tooltips .toolRibbon .caption, +:root.mobile body[data-ui~="-captions"] .toolRibbon .caption { + display: none; + } +:root.mobile .toolRibbon .caption, +:root:not(.mobile) body[data-ui~="+captions"] .toolRibbon .caption { + display: inherit; + } +:root:not(.mobile) .toolRibbon .tool, +:root.mobile body.no-tooltips .toolRibbon .tool, +:root.mobile body[data-ui~="-captions"] .toolRibbon .tool { + padding: var(--popup-gap) var(--popup-gap-thin); + } + +/* horizontally-constrained viewport */ +:root.portrait body { + overflow-y: auto; + width: 100%; + } +:root.portrait #main { + max-width: unset; + } +/* mouse-driven devices */ +:root.desktop { + display: flex; + justify-content: flex-end; + } +:root.desktop body { + --popup-gap: calc(var(--font-size) * 0.875); + } +:root.desktop body:not(.off) #switch:hover { + fill: rgb(var(--popup-power-ink-rgb) / 90%); + } +:root.desktop body.off #switch:hover { + stroke: var(--popup-power-ink); + } +:root.desktop .rulesetTools [id]:hover { + background-color: var(--popup-ruleset-tool-surface-hover); + } +:root.desktop .rulesetTools [id]:hover > svg { + fill: var(--ink-2); + } +:root.desktop #firewall { + direction: rtl; + line-height: 1.4; + } +:root.desktop .tool:hover { + background-color: var(--popup-toolbar-surface-hover); + } +:root.desktop #moreOrLess > span:hover { + background-color: var(--surface-2); + /* background-color: var(--popup-toolbar-surface-hover); */ + } diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js new file mode 100644 index 000000000..85307962b --- /dev/null +++ b/platform/mv3/extension/js/background.js @@ -0,0 +1,188 @@ +'use strict'; + +import regexRulesets from '/rulesets/regexes.js'; + +/******************************************************************************/ + +const dnr = chrome.declarativeNetRequest; +const TRUSTED_DIRECTIVE_BASE_RULE_ID = 1000000; +const dynamicRuleMap = new Map(); + +/******************************************************************************/ + +async function updateRegexRules() { + const allRules = []; + const toCheck = []; + for ( const regexRuleset of regexRulesets ) { + if ( regexRuleset.enabled !== true ) { continue; } + for ( const rule of regexRuleset.rules ) { + const regex = rule.condition.regexFilter; + const isCaseSensitive = rule.condition.isUrlFilterCaseSensitive === true; + allRules.push(rule); + toCheck.push(dnr.isRegexSupported({ regex, isCaseSensitive })); + } + } + const results = await Promise.all(toCheck); + const newRules = []; + for ( let i = 0; i < allRules.length; i++ ) { + const rule = allRules[i]; + const result = results[i]; + if ( result instanceof Object && result.isSupported ) { + newRules.push(rule); + } else { + console.info(`${result.reason}: ${rule.condition.regexFilter}`); + } + } + 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; } + const newRule = newRuleMap.get(oldRule.id); + if ( newRule === undefined ) { + removeRuleIds.push(oldRule.id); + dynamicRuleMap.delete(oldRule.id); + } else if ( JSON.stringify(oldRule) !== JSON.stringify(newRule) ) { + removeRuleIds.push(oldRule.id); + addRules.push(newRule); + dynamicRuleMap.set(oldRule.id, newRule); + } + } + for ( const newRule of newRuleMap.values() ) { + if ( dynamicRuleMap.has(newRule.id) ) { continue; } + addRules.push(newRule); + dynamicRuleMap.set(newRule.id, newRule); + } + if ( addRules.length !== 0 || removeRuleIds.length !== 0 ) { + return dnr.updateDynamicRules({ addRules, removeRuleIds }); + } +} + +/******************************************************************************/ + +async function matchesTrustedSiteDirective(details) { + const url = new URL(details.origin); + let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + if ( rule === undefined ) { return false; } + const domainSet = new Set(rule.condition.requestDomains); + let hostname = url.hostname; + for (;;) { + if ( domainSet.has(hostname) ) { return true; } + const pos = hostname.indexOf('.'); + if ( pos === -1 ) { break; } + hostname = hostname.slice(pos+1); + } + return false; +} + +async function addTrustedSiteDirective(details) { + const url = new URL(details.origin); + let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + if ( rule !== undefined ) { + rule.condition.initiatorDomains = undefined; + if ( Array.isArray(rule.condition.requestDomains) === false ) { + rule.condition.requestDomains = []; + } + } + if ( rule === undefined ) { + rule = { + id: TRUSTED_DIRECTIVE_BASE_RULE_ID, + action: { + type: 'allowAllRequests', + }, + condition: { + requestDomains: [ url.hostname ], + resourceTypes: [ 'main_frame' ], + }, + priority: TRUSTED_DIRECTIVE_BASE_RULE_ID, + }; + dynamicRuleMap.set(TRUSTED_DIRECTIVE_BASE_RULE_ID, rule); + } else if ( rule.condition.requestDomains.includes(url.hostname) === false ) { + rule.condition.requestDomains.push(url.hostname); + } + await dnr.updateDynamicRules({ + addRules: [ rule ], + removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], + }); + return true; +} + +async function removeTrustedSiteDirective(details) { + const url = new URL(details.origin); + let rule = dynamicRuleMap.get(TRUSTED_DIRECTIVE_BASE_RULE_ID); + if ( rule === undefined ) { return false; } + rule.condition.initiatorDomains = undefined; + if ( Array.isArray(rule.condition.requestDomains) === false ) { + rule.condition.requestDomains = []; + } + const domainSet = new Set(rule.condition.requestDomains); + const beforeCount = domainSet.size; + let hostname = url.hostname; + for (;;) { + domainSet.delete(hostname); + const pos = hostname.indexOf('.'); + if ( pos === -1 ) { break; } + hostname = hostname.slice(pos+1); + } + if ( domainSet.size === beforeCount ) { return false; } + if ( domainSet.size === 0 ) { + dynamicRuleMap.delete(TRUSTED_DIRECTIVE_BASE_RULE_ID); + await dnr.updateDynamicRules({ + removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ] + }); + return false; + } + rule.condition.requestDomains = Array.from(domainSet); + await dnr.updateDynamicRules({ + addRules: [ rule ], + removeRuleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID ], + }); + return false; +} + +async function toggleTrustedSiteDirective(details) { + return details.state + ? removeTrustedSiteDirective(details) + : addTrustedSiteDirective(details); +} + +/******************************************************************************/ + +(async ( ) => { + const dynamicRules = await dnr.getDynamicRules(); + for ( const rule of dynamicRules ) { + dynamicRuleMap.set(rule.id, rule); + } + + await updateRegexRules(); + + console.log(`Dynamic rule count: ${dynamicRuleMap.size}`); + + 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 'matchesTrustedSiteDirective': + matchesTrustedSiteDirective(request).then(response => { + callback(response); + }); + return true; + case 'toggleTrustedSiteDirective': + toggleTrustedSiteDirective(request).then(response => { + callback(response); + }); + return true; + default: + break; + } + }); +})(); diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js new file mode 100644 index 000000000..a252cf7d2 --- /dev/null +++ b/platform/mv3/extension/js/popup.js @@ -0,0 +1,114 @@ +/******************************************************************************* + + 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'; + +/******************************************************************************/ + +let currentTab = {}; +let originalTrustedState = false; + +/******************************************************************************/ + +async function toggleTrustedSiteDirective() { + let url; + try { + url = new URL(currentTab.url); + } catch(ex) { + return; + } + if ( url instanceof URL === false ) { return; } + const targetTrustedState = document.body.classList.contains('off'); + const newTrustedState = await chrome.runtime.sendMessage({ + what: 'toggleTrustedSiteDirective', + origin: url.origin, + state: targetTrustedState, + tabId: currentTab.id, + }).catch(( ) => targetTrustedState === false); + document.body.classList.toggle('off', newTrustedState === true); + document.body.classList.toggle( + 'needReload', + newTrustedState !== originalTrustedState + ); +} + +/******************************************************************************/ + +function reloadTab(ev) { + chrome.tabs.reload(currentTab.id, { + bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey, + }); + document.body.classList.remove('needReload'); + originalTrustedState = document.body.classList.contains('off'); +} + +/******************************************************************************/ + +async function init() { + const [ tab ] = await chrome.tabs.query({ active: true }); + if ( tab instanceof Object === false ) { return true; } + currentTab = tab; + + let url; + try { + url = new URL(currentTab.url); + } catch(ex) { + } + + if ( url !== undefined ) { + originalTrustedState = await chrome.runtime.sendMessage({ + what: 'matchesTrustedSiteDirective', + origin: url.origin, + }) === true; + } + + const body = document.body; + body.classList.toggle('off', originalTrustedState); + const elemHn = document.querySelector('#hostname'); + + elemHn.textContent = url && url.hostname || ''; + + document.querySelector('#switch').addEventListener( + 'click', + toggleTrustedSiteDirective + ); + + document.querySelector('#refresh').addEventListener( + 'click', + reloadTab + ); + + document.body.classList.remove('loading'); + + return true; +} + +async function tryInit() { + try { + await init(); + } catch(ex) { + setTimeout(tryInit, 100); + } +} + +tryInit(); + +/******************************************************************************/ diff --git a/platform/mv3/extension/manifest.json b/platform/mv3/extension/manifest.json index 8a618a8bf..851314eca 100644 --- a/platform/mv3/extension/manifest.json +++ b/platform/mv3/extension/manifest.json @@ -1,7 +1,15 @@ { + "action": { + "default_icon": { + "16": "img/icon_16.png", + "32": "img/icon_32.png", + "64": "img/icon_64.png" + }, + "default_popup": "popup.html" + }, "author": "Raymond Hill", - "background": { - "service_worker": "background.js", + "background": { + "service_worker": "/js/background.js", "type": "module" }, "declarative_net_request": { @@ -19,7 +27,8 @@ "minimum_chrome_version": "101.0", "name": "uBO Minus (MV3)", "permissions": [ + "activeTab", "declarativeNetRequest" ], - "version": "0.1.0" + "version": "0.1" } diff --git a/platform/mv3/extension/popup.html b/platform/mv3/extension/popup.html new file mode 100644 index 000000000..d000a4e20 --- /dev/null +++ b/platform/mv3/extension/popup.html @@ -0,0 +1,57 @@ + + + +
+ + + + + + +