diff --git a/Makefile b/Makefile index c9d92a35c..477a3b3f7 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ run_options := $(filter-out $@,$(MAKECMDGOALS)) .PHONY: all clean cleanassets test lint chromium opera firefox npm dig \ - mv3-chromium mv3-firefox mv3-edge \ + mv3-chromium mv3-firefox mv3-edge mv3-safari \ compare maxcost medcost mincost modifiers record wasm sources := $(wildcard assets/* assets/*/* dist/version src/* src/*/* src/*/*/* src/*/*/*/*) @@ -80,6 +80,11 @@ dist/build/uBOLite.edge: tools/make-mv3.sh tools/make-edge.mjs $(sources) $(plat mv3-edge: dist/build/uBOLite.edge +dist/build/uBOLite.safari: tools/make-mv3.sh $(sources) $(platform) $(mv3-data) dist/build/mv3-data + tools/make-mv3.sh safari + +mv3-safari: dist/build/uBOLite.safari + dist/build/uAssets: tools/pull-assets.sh diff --git a/platform/mv3/extension/js/admin.js b/platform/mv3/extension/js/admin.js index 843285d89..c9fa5f5b9 100644 --- a/platform/mv3/extension/js/admin.js +++ b/platform/mv3/extension/js/admin.js @@ -37,7 +37,7 @@ import { } from './mode-manager.js'; import { broadcastMessage } from './utils.js'; -import { dnr } from './ext.js'; +import { dnr } from './ext-compat.js'; import { registerInjectables } from './scripting-manager.js'; import { rulesetConfig } from './config.js'; import { ubolLog } from './debug.js'; diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 6499c9d5a..bc2ba932b 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -44,7 +44,6 @@ import { import { browser, - dnr, localRead, localRemove, localWrite, runtime, windows, @@ -75,11 +74,12 @@ import { saveRulesetConfig, } from './config.js'; +import { dnr } from './ext-compat.js'; import { registerInjectables } from './scripting-manager.js'; /******************************************************************************/ -const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, ''); +const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, '').toLowerCase(); const canShowBlockedCount = typeof dnr.setExtensionActionOptions === 'function'; @@ -205,7 +205,9 @@ function onMessage(request, sender, callback) { // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender // Firefox API does not set `sender.origin` - if ( sender.origin !== undefined && sender.origin !== UBOL_ORIGIN ) { return; } + if ( sender.origin !== undefined ) { + if ( sender.origin.toLowerCase() !== UBOL_ORIGIN ) { return; } + } switch ( request.what ) { diff --git a/platform/mv3/extension/js/debug.js b/platform/mv3/extension/js/debug.js index 0a6ecaffe..ea5dafbed 100644 --- a/platform/mv3/extension/js/debug.js +++ b/platform/mv3/extension/js/debug.js @@ -19,11 +19,19 @@ Home: https://github.com/gorhill/uBlock */ -import { dnr } from './ext.js'; +import { INITIATOR_DOMAINS, dnr } from './ext-compat.js'; +import { browser } from './ext.js'; /******************************************************************************/ -export const isSideloaded = dnr.onRuleMatchedDebug instanceof Object; +const isModern = dnr.onRuleMatchedDebug instanceof Object; + +export const isSideloaded = (( ) => { + if ( isModern ) { return true; } + if ( typeof dnr.getMatchedRules === 'function' ) { return true; } + const manifest = browser.runtime.getManifest(); + return manifest.permissions?.includes('declarativeNetRequestFeedback') ?? false; +})(); /******************************************************************************/ @@ -67,8 +75,8 @@ const getRuleset = async rulesetId => { if ( condition.requestDomains ) { condition.requestDomains = pruneLongLists(condition.requestDomains); } - if ( condition.initiatorDomains ) { - condition.initiatorDomains = pruneLongLists(condition.initiatorDomains); + if ( condition[INITIATOR_DOMAINS] ) { + condition[INITIATOR_DOMAINS] = pruneLongLists(condition[INITIATOR_DOMAINS]); } } const ruleId = rule.id; @@ -92,20 +100,32 @@ export const getMatchedRules = (( ) => { const noopFn = ( ) => Promise.resolve([]); if ( isSideloaded !== true ) { return noopFn; } - return async tabId => { - const promises = []; - for ( let i = 0; i < bufferSize; i++ ) { - const j = (writePtr + i) % bufferSize; - const ruleInfo = matchedRules[j]; - if ( ruleInfo === null ) { continue; } - if ( ruleInfo.request.tabId !== -1 ) { - if ( ruleInfo.request.tabId !== tabId ) { continue; } + if ( isModern ) { + return async tabId => { + const promises = []; + for ( let i = 0; i < bufferSize; i++ ) { + const j = (writePtr + i) % bufferSize; + const ruleInfo = matchedRules[j]; + if ( ruleInfo === null ) { continue; } + if ( ruleInfo.request.tabId !== -1 ) { + if ( ruleInfo.request.tabId !== tabId ) { continue; } + } + const promise = getRuleDetails(ruleInfo); + if ( promise === undefined ) { continue; } + promises.unshift(promise); } - const promise = getRuleDetails(ruleInfo); - if ( promise === undefined ) { continue; } - promises.unshift(promise); + return Promise.all(promises); + }; + } + + return async tabId => { + const matchedRules = await dnr.getMatchedRules({ tabId }); + if ( matchedRules instanceof Object === false ) { return []; } + const out = []; + for ( const ruleInfo of matchedRules.rulesMatchedInfo ) { + out.push({ request: ruleInfo.request }); } - return Promise.all(promises); + return out; }; })(); @@ -118,6 +138,7 @@ const matchedRuleListener = ruleInfo => { export const toggleDeveloperMode = state => { if ( isSideloaded !== true ) { return; } + if ( isModern === false ) { return; } if ( state ) { dnr.onRuleMatchedDebug.addListener(matchedRuleListener); } else { diff --git a/platform/mv3/extension/js/ext-compat.js b/platform/mv3/extension/js/ext-compat.js new file mode 100644 index 000000000..9e8469ce7 --- /dev/null +++ b/platform/mv3/extension/js/ext-compat.js @@ -0,0 +1,96 @@ +/******************************************************************************* + + uBlock Origin Lite - a comprehensive, MV3-compliant content blocker + 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 +*/ + +export const webext = self.browser || self.chrome; +export const dnr = webext.declarativeNetRequest; +export const INITIATOR_DOMAINS = 'initiatorDomains'; +export const EXCLUDED_INITIATOR_DOMAINS = 'excludedInitiatorDomains'; + +/******************************************************************************/ + +const ruleCompare = (a, b) => a.id - b.id; + +const isSameRules = (a, b) => { + a.sort(ruleCompare); + b.sort(ruleCompare); + return JSON.stringify(a) === JSON.stringify(b); +}; + +/******************************************************************************/ + +dnr.setAllowAllRules = async function(id, allowed, notAllowed, reverse) { + const [ + beforeDynamicRules, + beforeSessionRules, + ] = await Promise.all([ + dnr.getDynamicRules({ ruleIds: [ id+0 ] }), + dnr.getSessionRules({ ruleIds: [ id+1 ] }), + ]); + const addDynamicRules = []; + const addSessionRules = []; + if ( reverse || allowed.length || notAllowed.length ) { + const rule0 = { + id: id+0, + action: { type: 'allowAllRequests' }, + condition: { + resourceTypes: [ 'main_frame' ], + }, + priority: 1000000, + }; + if ( allowed.length ) { + rule0.condition.requestDomains = allowed.slice(); + } else if ( notAllowed.length ) { + rule0.condition.excludedRequestDomains = notAllowed.slice(); + } + addDynamicRules.push(rule0); + // https://github.com/uBlockOrigin/uBOL-home/issues/114 + // https://github.com/uBlockOrigin/uBOL-home/issues/247 + const rule1 = { + id: id+1, + action: { type: 'allow' }, + condition: { + tabIds: [ webext.tabs.TAB_ID_NONE ], + }, + priority: 1000000, + }; + if ( allowed.length ) { + rule1.condition.initiatorDomains = allowed.slice(); + } else if ( notAllowed.length ) { + rule1.condition.excludedInitiatorDomains = notAllowed.slice(); + } + addSessionRules.push(rule1); + } + if ( isSameRules(addDynamicRules, beforeDynamicRules) ) { return false; } + return Promise.all([ + dnr.updateDynamicRules({ + addRules: addDynamicRules, + removeRuleIds: beforeDynamicRules.map(r => r.id), + }), + dnr.updateSessionRules({ + addRules: addSessionRules, + removeRuleIds: beforeSessionRules.map(r => r.id), + }), + ]).then(( ) => + true + ).catch(( ) => + false + ); +}; diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index 2f3285388..ee735b21b 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -19,16 +19,13 @@ Home: https://github.com/gorhill/uBlock */ -export const browser = - self.browser instanceof Object && - self.browser instanceof Element === false - ? self.browser - : self.chrome; +import { webext } from './ext-compat.js'; -export const dnr = browser.declarativeNetRequest; +/******************************************************************************/ + +export const browser = webext; export const i18n = browser.i18n; export const runtime = browser.runtime; -export const TAB_ID_NONE = browser.tabs.TAB_ID_NONE; export const windows = browser.windows; /******************************************************************************/ diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index a50c889b6..2cb89045b 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -347,6 +347,7 @@ async function init() { } tabURL.href = url.href || ''; } catch { + return false; } if ( url !== undefined ) { diff --git a/platform/mv3/extension/js/report.js b/platform/mv3/extension/js/report.js index 1350edcd4..eda30f97f 100644 --- a/platform/mv3/extension/js/report.js +++ b/platform/mv3/extension/js/report.js @@ -19,8 +19,9 @@ Home: https://github.com/gorhill/uBlock */ -import { dnr, runtime } from './ext.js'; import { dom, qs$ } from './dom.js'; +import { dnr } from './ext-compat.js'; +import { runtime } from './ext.js'; import { sendMessage } from './ext.js'; /******************************************************************************/ diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js index add3c5e25..ae5ef7f32 100644 --- a/platform/mv3/extension/js/ruleset-manager.js +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -20,8 +20,6 @@ */ import { - TAB_ID_NONE, - dnr, i18n, localRead, localRemove, localWrite, runtime, @@ -33,6 +31,7 @@ import { saveRulesetConfig, } from './config.js'; +import { dnr } from './ext-compat.js'; import { fetchJSON } from './fetch.js'; import { getAdminRulesets } from './admin.js'; import { hasBroadHostPermissions } from './utils.js'; @@ -440,124 +439,30 @@ async function updateSessionRules() { /******************************************************************************/ async function filteringModesToDNR(modes) { - const [ - dynamicRules, - sessionRules, - ] = await Promise.all([ - dnr.getDynamicRules({ ruleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID+0 ] }), - dnr.getSessionRules({ ruleIds: [ TRUSTED_DIRECTIVE_BASE_RULE_ID+1 ] }), - ]); - const dynamicRule = dynamicRules?.length && dynamicRules[0] || undefined; - const beforeRequestDomainSet = new Set(dynamicRule?.condition.requestDomains); - const beforeExcludedRrequestDomainSet = new Set(dynamicRule?.condition.excludedRequestDomains); - if ( dynamicRule !== undefined && beforeRequestDomainSet.size === 0 ) { - beforeRequestDomainSet.add('all-urls'); - } else { - beforeExcludedRrequestDomainSet.add('all-urls'); - } - const noneHostnames = new Set([ ...modes.none ]); const notNoneHostnames = new Set([ ...modes.basic, ...modes.optimal, ...modes.complete ]); - let afterRequestDomainSet = new Set(); - let afterExcludedRequestDomainSet = new Set(); - if ( noneHostnames.has('all-urls') ) { - afterRequestDomainSet = new Set([ 'all-urls' ]); - afterExcludedRequestDomainSet = notNoneHostnames; + const requestDomains = []; + const excludedRequestDomains = []; + const allowEverywhere = noneHostnames.has('all-urls'); + if ( allowEverywhere ) { + excludedRequestDomains.push(...notNoneHostnames); } else { - afterRequestDomainSet = noneHostnames; - afterExcludedRequestDomainSet = new Set(); + requestDomains.push(...noneHostnames); } - - const removeDynamicRuleIds = []; - const removeSessionRuleIds = []; - if ( dynamicRule ) { - removeDynamicRuleIds.push(TRUSTED_DIRECTIVE_BASE_RULE_ID+0); - removeSessionRuleIds.push(TRUSTED_DIRECTIVE_BASE_RULE_ID+1); - } - - const allowEverywhere = afterRequestDomainSet.delete('all-urls'); - const addDynamicRules = []; - const addSessionRules = []; - if ( - allowEverywhere || - afterRequestDomainSet.size !== 0 || - afterExcludedRequestDomainSet.size !== 0 - ) { - const rule0 = { - id: TRUSTED_DIRECTIVE_BASE_RULE_ID+0, - action: { type: 'allowAllRequests' }, - condition: { - resourceTypes: [ 'main_frame' ], - }, - priority: 100, - }; - if ( afterRequestDomainSet.size !== 0 ) { - rule0.condition.requestDomains = - Array.from(afterRequestDomainSet).sort(); - } else if ( afterExcludedRequestDomainSet.size !== 0 ) { - rule0.condition.excludedRequestDomains = - Array.from(afterExcludedRequestDomainSet).sort(); - } - addDynamicRules.push(rule0); - // https://github.com/uBlockOrigin/uBOL-home/issues/114 - // https://github.com/uBlockOrigin/uBOL-home/issues/247 - const rule1 = { - id: TRUSTED_DIRECTIVE_BASE_RULE_ID+1, - action: { type: 'allow' }, - condition: { - tabIds: [ TAB_ID_NONE ], - }, - priority: 100, - }; - if ( rule0.condition.requestDomains ) { - rule1.condition.initiatorDomains = - rule0.condition.requestDomains.slice(); - } else if ( rule0.condition.excludedRequestDomains ) { - rule1.condition.excludedInitiatorDomains = - rule0.condition.excludedRequestDomains.slice(); - } - addSessionRules.push(rule1); - } - - const noneCount = noneHostnames.has('all-urls') - ? -notNoneHostnames.size + const noneCount = allowEverywhere + ? notNoneHostnames.size : noneHostnames.size; - - const promises = []; - if ( isDifferentAllowRules(addDynamicRules, dynamicRules) ) { - promises.push(dnr.updateDynamicRules({ - addRules: addDynamicRules, - removeRuleIds: removeDynamicRuleIds, - })); - ubolLog(`Add "allowAllRequests" dynamic rule for ${noneCount} sites`); - } - if ( isDifferentAllowRules(addSessionRules, sessionRules) ) { - promises.push(dnr.updateSessionRules({ - addRules: addSessionRules, - removeRuleIds: removeSessionRuleIds, - })); - ubolLog(`Add "allow" session rule for ${noneCount} sites`); - } - if ( promises.length === 0 ) { return; } - return Promise.all(promises); + return dnr.setAllowAllRules( + TRUSTED_DIRECTIVE_BASE_RULE_ID, + requestDomains.sort(), + excludedRequestDomains.sort(), + allowEverywhere + ).then(modified => { + if ( modified === false ) { return; } + ubolLog(`${allowEverywhere ? 'Enabled' : 'Disabled'} DNR filtering for ${noneCount} sites`); + }); } -const isDifferentAllowRules = (a = [], b = []) => { - if ( a.length !== b.length ) { return true; } - const pp = [ - 'requestDomains', - 'excludedRequestDomains', - 'initiatorDomains', - 'excludedInitiatorDomains', - ]; - for ( const p of pp ) { - const ac = a.length && a[0].condition[p] || []; - const bc = b.length && b[0].condition[p] || []; - if ( ac.join() !== bc.join() ) { return true; } - } - return false; -}; - /******************************************************************************/ async function defaultRulesetsFromLanguage() { diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index e22a7c530..8ff9a5c36 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -68,7 +68,9 @@ const arrayEq = (a = [], b = [], sort = true) => { const normalizeMatches = matches => { if ( matches.length <= 1 ) { return; } - if ( matches.includes('') === false ) { return; } + if ( matches.includes('') === false ) { + if ( matches.includes('*://*/*') === false ) { return; } + } matches.length = 0; matches.push(''); }; @@ -555,6 +557,9 @@ function registerScriptlet(context, scriptletDetails) { /******************************************************************************/ +// Issue: Safari appears to completely ignore excludeMatches +// https://github.com/radiolondra/ExcludeMatches-Test + async function registerInjectables() { if ( browser.scripting === undefined ) { return false; } diff --git a/platform/mv3/extension/js/scripting/zapper.js b/platform/mv3/extension/js/scripting/zapper.js index fb37ee1cf..5a0e81c58 100644 --- a/platform/mv3/extension/js/scripting/zapper.js +++ b/platform/mv3/extension/js/scripting/zapper.js @@ -28,11 +28,13 @@ const zapper = self.uBOLZapper = self.uBOLZapper || {}; if ( zapper.injected ) { return; } zapper.injected = true; +const webext = typeof browser === 'object' ? browser : chrome; + /******************************************************************************/ const sendMessage = msg => { try { - chrome.runtime.sendMessage(msg).catch(( ) => { }); + webext.runtime.sendMessage(msg).catch(( ) => { }); } catch { } }; @@ -365,11 +367,10 @@ const onFrameMessage = function(msg) { // can remove the iframe. const bootstrap = async ( ) => { - const dynamicURL = new URL(chrome.runtime.getURL('/zapper-ui.html')); + const dynamicURL = new URL(webext.runtime.getURL('/zapper-ui.html')); return new Promise(resolve => { const frame = document.createElement('iframe'); frame.setAttribute(zapperSecret, ''); - document.documentElement.append(frame); frame.onload = ( ) => { frame.onload = null; frame.setAttribute(`${zapperSecret}-loaded`, ''); @@ -382,7 +383,7 @@ const bootstrap = async ( ) => { quitZapper(); }; const realURL = new URL(dynamicURL); - realURL.hostname = chrome.i18n.getMessage('@@extension_id'); + realURL.hostname = webext.i18n.getMessage('@@extension_id'); frame.contentWindow.postMessage( { what: 'zapperStart' }, realURL.origin, @@ -394,7 +395,13 @@ const bootstrap = async ( ) => { zapperFramePort: port, }); }; - frame.contentWindow.location = dynamicURL.href; + if ( dynamicURL.protocol !== 'safari-web-extension:' ) { + document.documentElement.append(frame); + frame.contentWindow.location = dynamicURL.href; + } else { + frame.setAttribute('src', dynamicURL.href); + document.documentElement.append(frame); + } }); }; diff --git a/platform/mv3/extension/js/utils.js b/platform/mv3/extension/js/utils.js index ff96e9ee6..00451f15f 100644 --- a/platform/mv3/extension/js/utils.js +++ b/platform/mv3/extension/js/utils.js @@ -116,7 +116,7 @@ const matchesFromHostnames = hostnames => { const hostnamesFromMatches = origins => { const out = []; for ( const origin of origins ) { - if ( origin === '' ) { + if ( origin === '' || origin === '*://*/*' ) { out.push('all-urls'); continue; } @@ -141,7 +141,10 @@ const broadcastMessage = message => { // most browsers treat host_permissions as optional." async function hasBroadHostPermissions() { - return browser.permissions.contains({ origins: [ '' ] }); + return browser.permissions.getAll().then(permissions => + permissions.origins.includes('') || + permissions.origins.includes('*://*/*') + ).catch(( ) => false); } /******************************************************************************/ diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 930aa99b7..b69e27a59 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -71,7 +71,7 @@ const env = [ 'user_stylesheet', ]; -if ( platform === 'edge' ) { +if ( platform === 'edge' || platform === 'safari' ) { env.push('chromium'); } @@ -318,6 +318,38 @@ const isURLSkip = rule => /******************************************************************************/ +function patchRuleset(ruleset) { + if ( platform !== 'safari' ) { return ruleset; } + const out = []; + for ( const rule of ruleset ) { + const condition = rule.condition; + if ( rule.action.type === 'modifyHeaders' ) { + log(`Safari's incomplete API: ${JSON.stringify(rule)}`, true); + continue; + } + if ( Array.isArray(condition.requestMethods) ) { + log(`Safari's incomplete API: ${JSON.stringify(rule)}`, true); + continue; + } + if ( Array.isArray(condition.excludedRequestMethods) ) { + log(`Safari's incomplete API: ${JSON.stringify(rule)}`, true); + continue; + } + if ( Array.isArray(condition.initiatorDomains) ) { + condition.domains = condition.initiatorDomains; + delete condition.initiatorDomains; + } + if ( Array.isArray(condition.excludedInitiatorDomains) ) { + condition.excludedDomains = condition.excludedInitiatorDomains; + delete condition.excludedInitiatorDomains; + } + out.push(rule); + } + return out; +} + +/******************************************************************************/ + // Two distinct hostnames: // www.example.com // example.com @@ -489,7 +521,9 @@ async function processNetworkFilters(assetDetails, network) { } } - const plainGood = rules.filter(rule => isSafe(rule) && isRegex(rule) === false); + const plainGood = patchRuleset( + rules.filter(rule => isSafe(rule) && isRegex(rule) === false) + ); log(`\tPlain good: ${plainGood.length}`); log(plainGood .filter(rule => Array.isArray(rule._warning)) @@ -497,12 +531,16 @@ async function processNetworkFilters(assetDetails, network) { .join('\n'), true ); - const regexes = rules.filter(rule => isSafe(rule) && isRegex(rule)); + const regexes = patchRuleset( + rules.filter(rule => isSafe(rule) && isRegex(rule)) + ); log(`\tMaybe good (regexes): ${regexes.length}`); - const redirects = rules.filter(rule => - isUnsupported(rule) === false && - isRedirect(rule) + const redirects = patchRuleset( + rules.filter(rule => + isUnsupported(rule) === false && + isRedirect(rule) + ) ); redirects.forEach(rule => { if ( rule.action.redirect.extensionPath === undefined ) { return; } @@ -512,17 +550,23 @@ async function processNetworkFilters(assetDetails, network) { }); log(`\tredirect=: ${redirects.length}`); - const removeparamsGood = rules.filter(rule => - isUnsupported(rule) === false && isRemoveparam(rule) + const removeparamsGood = patchRuleset( + rules.filter(rule => + isUnsupported(rule) === false && isRemoveparam(rule) + ) ); - const removeparamsBad = rules.filter(rule => - isUnsupported(rule) && isRemoveparam(rule) + const removeparamsBad = patchRuleset( + rules.filter(rule => + isUnsupported(rule) && isRemoveparam(rule) + ) ); log(`\tremoveparams= (accepted/discarded): ${removeparamsGood.length}/${removeparamsBad.length}`); - const modifyHeaders = rules.filter(rule => - isUnsupported(rule) === false && - isModifyHeaders(rule) + const modifyHeaders = patchRuleset( + rules.filter(rule => + isUnsupported(rule) === false && + isModifyHeaders(rule) + ) ); log(`\tmodifyHeaders=: ${modifyHeaders.length}`); @@ -1401,10 +1445,10 @@ async function main() { // Patch web_accessible_resources key manifest.web_accessible_resources = manifest.web_accessible_resources || []; const web_accessible_resources = { - resources: Array.from(requiredRedirectResources).map(path => `/${path}`), + resources: Array.from(requiredRedirectResources).map(path => `${path}`), matches: [ '' ], }; - if ( env.includes('chromium') ) { + if ( env.includes('chromium') && env.includes('safari') === false ) { web_accessible_resources.use_dynamic_url = true; } manifest.web_accessible_resources.push(web_accessible_resources); diff --git a/platform/mv3/safari/ext-compat.js b/platform/mv3/safari/ext-compat.js new file mode 100644 index 000000000..c7f02984d --- /dev/null +++ b/platform/mv3/safari/ext-compat.js @@ -0,0 +1,131 @@ +/******************************************************************************* + + uBlock Origin Lite - a comprehensive, MV3-compliant content blocker + 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 +*/ + + +export const webext = self.browser; +export const INITIATOR_DOMAINS = 'domains'; +export const EXCLUDED_INITIATOR_DOMAINS = 'excludedDomains'; + +/******************************************************************************/ + +// https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest/ + +const nativeDNR = webext.declarativeNetRequest; + +const isSupportedRule = r => { + if ( r.action?.responseHeaders ) { return false; } + if ( r.condition?.tabIds !== undefined ) { return false; } + return true; +}; + +const ruleCompare = (a, b) => a.id - b.id; + +const isSameRules = (a, b) => { + a.sort(ruleCompare); + b.sort(ruleCompare); + return JSON.stringify(a) === JSON.stringify(b); +}; + +/******************************************************************************/ + +export const dnr = { + DYNAMIC_RULESET_ID: '_dynamic', + MAX_NUMBER_OF_ENABLED_STATIC_RULESETS: nativeDNR.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS, + MAX_NUMBER_OF_REGEX_RULES: nativeDNR.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES, + async getAvailableStaticRuleCount() { + return 150000; + }, + getDynamicRules({ ruleIds } = {}) { + return new Promise(resolve => { + nativeDNR.getDynamicRules(rules => { + if ( Array.isArray(rules) === false ) { return resolve([]); } + if ( Array.isArray(ruleIds) === false ) { return resolve(rules); } + return resolve(rules.filter(rule => ruleIds.includes(rule.id))); + }); + }); + }, + getEnabledRulesets(...args) { + return nativeDNR.getEnabledRulesets(...args); + }, + getMatchedRules(...args) { + return nativeDNR.getMatchedRules(...args); + }, + getSessionRules({ ruleIds } = {}) { + return new Promise(resolve => { + nativeDNR.getSessionRules(rules => { + if ( Array.isArray(rules) === false ) { return resolve([]); } + if ( Array.isArray(ruleIds) === false ) { return resolve(rules); } + return resolve(rules.filter(rule => ruleIds.includes(rule.id))); + }); + }); + }, + isRegexSupported(...args) { + return nativeDNR.isRegexSupported(...args); + }, + async updateDynamicRules(optionsBefore) { + const { addRules, removeRuleIds } = optionsBefore; + const addRulesAfter = addRules?.filter(isSupportedRule); + if ( Boolean(addRulesAfter?.length || removeRuleIds?.length) === false ) { return; } + const optionsAfter = {}; + if ( addRulesAfter?.length ) { optionsAfter.addRules = addRulesAfter; } + if ( removeRuleIds?.length ) { optionsAfter.removeRuleIds = removeRuleIds; } + return nativeDNR.updateDynamicRules(optionsAfter); + }, + updateEnabledRulesets(...args) { + return nativeDNR.updateEnabledRulesets(...args); + }, + async updateSessionRules(optionsBefore) { + const { addRules, removeRuleIds } = optionsBefore; + const addRulesAfter = addRules?.filter(isSupportedRule); + if ( Boolean(addRulesAfter?.length || removeRuleIds?.length) === false ) { return; } + const optionsAfter = {}; + if ( optionsAfter?.length ) { optionsAfter.addRules = addRulesAfter; } + if ( removeRuleIds?.length ) { optionsAfter.removeRuleIds = removeRuleIds; } + return nativeDNR.updateSessionRules(optionsAfter); + }, + async setAllowAllRules(id, allowed, notAllowed, reverse) { + const beforeRules = await this.getDynamicRules({ ruleIds: [ id+0 ] }); + const addRules = []; + if ( reverse || allowed.length || notAllowed.length ) { + const rule0 = { + id: id+0, + action: { type: 'allow' }, + condition: { urlFilter: '*' }, + priority: 1000000, + }; + if ( allowed.length ) { + rule0.condition.domains = allowed; + } else if ( notAllowed.length ) { + rule0.condition.excludedDomains = notAllowed; + } + addRules.push(rule0); + } + if ( isSameRules(addRules, beforeRules) ) { return false; } + return this.updateDynamicRules({ + addRules, + removeRuleIds: beforeRules.map(r => r.id), + }).then(( ) => + true + ).catch(( ) => + false + ); + }, +}; diff --git a/platform/mv3/safari/manifest.json b/platform/mv3/safari/manifest.json new file mode 100644 index 000000000..ba7cc7715 --- /dev/null +++ b/platform/mv3/safari/manifest.json @@ -0,0 +1,64 @@ +{ + "action": { + "default_icon": "/img/icon_64.png", + "default_popup": "popup.html" + }, + "author": "Raymond Hill", + "background": { + "scripts": [ "/js/background.js" ], + "type": "module" + }, + "commands": { + "enter-zapper-mode": { + "description": "__MSG_zapperTipEnter__" + } + }, + "declarative_net_request": { + "rule_resources": [ + ] + }, + "default_locale": "en", + "description": "__MSG_extShortDesc__", + "icons": { + "16": "/img/icon_16.png", + "32": "/img/icon_32.png", + "64": "/img/icon_64.png", + "128": "/img/icon_128.png", + "512": "/img/icon_512.png" + }, + "manifest_version": 3, + "name": "__MSG_extName__", + "options_ui": { + "page": "dashboard.html" + }, + "host_permissions": [ + "" + ], + "permissions": [ + "activeTab", + "declarativeNetRequest", + "declarativeNetRequestWithHostAccess", + "scripting", + "storage" + ], + "short_name": "uBO Lite", + "version": "1.0", + "web_accessible_resources": [ + { + "resources": [ + "strictblock.html" + ], + "matches": [ + "" + ] + }, + { + "resources": [ + "zapper-ui.html" + ], + "matches": [ + "" + ] + } + ] +} diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index df23d863c..6da12ae45 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -437,6 +437,20 @@ function finalizeRuleset(context, network) { mergeRules(rulesetMap, 'requestDomains'); mergeRules(rulesetMap, 'responseHeaders'); + // Convert back single-entry requestDomains into pattern-based filters + // https://github.com/uBlockOrigin/uBOL-home/issues/327 + // TODO: Remove when (if) Safari is changed to interpret requestDomains as + // in other browsers. + for ( const rule of rulesetMap.values() ) { + const { condition } = rule; + if ( condition?.requestDomains === undefined ) { continue; } + if ( condition.requestDomains.length !== 1 ) { continue; } + if ( condition.urlFilter !== undefined ) { continue; } + if ( condition.regexFilter !== undefined ) { continue; } + condition.urlFilter = `||${condition.requestDomains[0]}^`; + condition.requestDomains = undefined; + } + // Patch id const rulesetFinal = []; { diff --git a/tools/make-mv3.sh b/tools/make-mv3.sh index 6d2364930..7a9988433 100755 --- a/tools/make-mv3.sh +++ b/tools/make-mv3.sh @@ -23,6 +23,9 @@ for i in "$@"; do edge) PLATFORM="edge" ;; + safari) + PLATFORM="safari" + ;; uBOLite_+([0-9]).+([0-9]).+([0-9]).+([0-9])) TAGNAME="$i" FULL="yes" @@ -82,13 +85,12 @@ cp -R "$UBO_DIR/src/img/flags-of-the-world" "$DES"/img cp LICENSE.txt "$DES"/ echo "*** uBOLite.mv3: Copying mv3-specific files" -if [ "$PLATFORM" = "firefox" ]; then - cp platform/mv3/firefox/background.html "$DES"/ -fi +cp platform/mv3/"$PLATFORM"/manifest.json "$DES"/ cp platform/mv3/extension/*.html "$DES"/ cp platform/mv3/extension/*.json "$DES"/ cp platform/mv3/extension/css/* "$DES"/css/ cp -R platform/mv3/extension/js/* "$DES"/js/ +cp platform/mv3/"$PLATFORM"/ext-compat.js "$DES"/js/ 2>/dev/null || : cp platform/mv3/extension/img/* "$DES"/img/ cp -R platform/mv3/extension/_locales "$DES"/ cp platform/mv3/README.md "$DES/" @@ -96,11 +98,6 @@ cp platform/mv3/README.md "$DES/" echo "*** uBOLite.mv3: Generating rulesets" TMPDIR=$(mktemp -d) mkdir -p "$TMPDIR" -if [ "$PLATFORM" = "chromium" ] || [ "$PLATFORM" = "edge" ]; then - cp platform/mv3/chromium/manifest.json "$DES"/ -elif [ "$PLATFORM" = "firefox" ]; then - cp platform/mv3/firefox/manifest.json "$DES"/ -fi ./tools/make-nodejs.sh "$TMPDIR" cp platform/mv3/*.json "$TMPDIR"/ cp platform/mv3/*.js "$TMPDIR"/