diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ce73dd51063..528e2c9d59b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +- [Fix regression in `prevent-fetch` scriptlet](https://github.com/gorhill/uBlock/commit/be78200c2f) +- [Add `prevent-dialog` scriptlet](https://github.com/gorhill/uBlock/commit/fd12d01928) +- [[firefox] Change minimum required version to 128](https://github.com/gorhill/uBlock/commit/015ddcde29) + +---------- + +# 1.68.0 + +- [Improve `prevent-fetch` scriptlet](https://github.com/gorhill/uBlock/commit/b46572e938) +- [Fix regression in `trusted-replace-argument` scriptlet](https://github.com/gorhill/uBlock/commit/2e509d42fc) +- [Add web-accessible resource for sensors analytics](https://github.com/gorhill/uBlock/commit/cd0f5be12c) +- [Fix custom prefixes unduly assigning trust to external lists](https://github.com/gorhill/uBlock/commit/b5f74456a4) +- [Improve `m3u-prune` scriptlet](https://github.com/gorhill/uBlock/commit/53d60ac36c) - [Improve `prevent-fetch` scriptlet](https://github.com/gorhill/uBlock/commit/60e15cb6e1) - [Fix regex-matching in `JSONPath`](https://github.com/gorhill/uBlock/commit/8491e9c476) - [Ignore negated request types when validating `redirect` option](https://github.com/gorhill/uBlock/commit/50e898b847) diff --git a/Makefile b/Makefile index ba0bce9e0081c..ae772675d1409 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,8 @@ publish-edge: ghasset=chromium \ datebasedmajor=1 \ storeid=odfafepnkmbhccpbejgmiehpchacaeak \ - productid=$(shell secret-tool lookup token ubo_edge_id) + productid=$(shell secret-tool lookup token ubo_edge_id) \ + notes="See release notes at https://github.com/gorhill/uBlock/releases" # Usage: make publish-firefox version=? publish-firefox: diff --git a/dist/firefox/updates.json b/dist/firefox/updates.json index 4f18f9b6ecccd..96c3693c8b8b2 100644 --- a/dist/firefox/updates.json +++ b/dist/firefox/updates.json @@ -3,13 +3,13 @@ "uBlock0@raymondhill.net": { "updates": [ { - "version": "1.66.5.2", + "version": "1.68.1.0", "browser_specific_settings": { "gecko": { "strict_min_version": "92.0" } }, - "update_link": "https://github.com/gorhill/uBlock/releases/download/1.66.5b2/uBlock0_1.66.5b2.firefox.signed.xpi" + "update_link": "https://github.com/gorhill/uBlock/releases/download/1.68.1b0/uBlock0_1.68.1b0.firefox.signed.xpi" } ] } diff --git a/dist/version b/dist/version index c92c237621cfb..d9204d11c6eae 100644 --- a/dist/version +++ b/dist/version @@ -1 +1 @@ -1.67.1.1 \ No newline at end of file +1.68.1.0 \ No newline at end of file diff --git a/platform/firefox/manifest.json b/platform/firefox/manifest.json index 888dced282b82..84f38e21b1144 100644 --- a/platform/firefox/manifest.json +++ b/platform/firefox/manifest.json @@ -17,10 +17,13 @@ "browser_specific_settings": { "gecko": { "id": "uBlock0@raymondhill.net", - "strict_min_version": "92.0" + "strict_min_version": "115.0", + "data_collection_permissions": { + "required": [ "none" ] + } }, "gecko_android": { - "strict_min_version": "92.0" + "strict_min_version": "115.0" } }, "commands": { diff --git a/platform/mv3/edge/patch-ruleset.js b/platform/mv3/edge/patch-ruleset.js new file mode 100644 index 0000000000000..656849f9b39dd --- /dev/null +++ b/platform/mv3/edge/patch-ruleset.js @@ -0,0 +1,38 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2025-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 +*/ + +function patchRule(rule, out) { + const { condition } = rule; + if ( Array.isArray(condition.responseHeaders) ) { + if ( condition.regexFilter === undefined ) { return; } + } + out.push(rule); + return rule; +} + +export function patchRuleset(ruleset) { + const out = []; + for ( const rule of ruleset ) { + if ( patchRule(rule, out) ) { continue; } + console.log(`\tReject ${JSON.stringify(rule)}`); + } + return out; +} diff --git a/platform/mv3/extension/_locales/id/messages.json b/platform/mv3/extension/_locales/id/messages.json index f1ab349367232..3d96addb6420f 100644 --- a/platform/mv3/extension/_locales/id/messages.json +++ b/platform/mv3/extension/_locales/id/messages.json @@ -332,7 +332,7 @@ "description": "Label for the menu entry to delete cosmetic filters" }, "developDropdownLabel": { - "message": "Melihat", + "message": "Melihat:", "description": "A label of a dropdown list" }, "developOptionFilteringModeDetails": { @@ -348,7 +348,7 @@ "description": "A section header in a dropdown list" }, "developOptionDynamicRuleset": { - "message": "Aturan dinamis ", + "message": "Aturan dinamis", "description": "An option in a dropdown list" }, "developOptionSessionRuleset": { diff --git a/platform/mv3/extension/_locales/it/messages.json b/platform/mv3/extension/_locales/it/messages.json index 76f0ec65da3ea..555752c82a500 100644 --- a/platform/mv3/extension/_locales/it/messages.json +++ b/platform/mv3/extension/_locales/it/messages.json @@ -4,7 +4,7 @@ "description": "extension name." }, "extShortDesc": { - "message": "Un efficiente bloccatore dei contenuti. Blocca annunci, tracker, minatori e altro ancora subito dopo l'installazione.", + "message": "Un efficiente bloccatore di contenuti. Blocca annunci, tracker, minatori e altro ancora subito dopo l'installazione.", "description": "this will be in the Chrome web store: must be 132 characters or less" }, "perRulesetStats": { @@ -184,7 +184,7 @@ "description": "An entry in the widget used to select the type of issue" }, "supportS6Checkbox1": { - "message": "Contrassegna la pagina web come “NSFW”. (“Not Safe For Work”)", + "message": "Contrassegna la pagina web come “NSFW”. (“non adatta ai minori”)", "description": "A checkbox to use for NSFW sites" }, "supportReportSpecificButton": { diff --git a/platform/mv3/extension/_locales/ko/messages.json b/platform/mv3/extension/_locales/ko/messages.json index 63cd0904984dc..24df7497bd82e 100644 --- a/platform/mv3/extension/_locales/ko/messages.json +++ b/platform/mv3/extension/_locales/ko/messages.json @@ -4,7 +4,7 @@ "description": "extension name." }, "extShortDesc": { - "message": "실험적이고 권한이 적은 콘텐츠 차단기. 광고, 추적기, 채굴기 등을 설치 직후 차단합니다.", + "message": "효율적인 콘텐츠 차단기입니다. 설치 직후 광고, 추적기, 채굴기 등을 즉시 차단합니다.", "description": "this will be in the Chrome web store: must be 132 characters or less" }, "perRulesetStats": { @@ -20,7 +20,7 @@ "description": "appears as tab name in dashboard" }, "customFiltersPageName": { - "message": "Custom filters", + "message": "커스텀 필터", "description": "appears as tab name in dashboard" }, "developPageName": { @@ -40,7 +40,7 @@ "description": "Label in the popup panel for the current filtering mode" }, "popupLocalToolsLabel": { - "message": "On this website", + "message": "이 웹사이트에서는", "description": "Label in the popup panel for the local tools section" }, "popupTipReport": { @@ -88,11 +88,11 @@ "description": "Header for a ruleset section in 'Filter lists pane'" }, "customFiltersImportExportLabel": { - "message": "Import / Export", + "message": "가져오기 / 내보내기", "description": "Text label heading the import/export area of custom filters" }, "customFiltersImportTextareaPlaceholder": { - "message": "Paste here specific cosmetic filters to add", + "message": "여기에 추가하려는 표면 필터 붙여넣기", "description": "Placeholder text which describes the purpose of the textarea widget" }, "aboutChangelog": { @@ -136,11 +136,11 @@ "description": "Label of 'Troubleshooting information' section in 'Report a filter issue' page" }, "supportS6P1S1": { - "message": "봉사자들이 중복 신고로 인해 부담을 겪지 않도록, 해당 이슈가 이미 신고되지는 않았는지 확인해주시기 바랍니다.", + "message": "자원봉사자들이 중복 신고로 인해 부담을 겪지 않도록, 해당 이슈가 이미 신고되지는 않았는지 확인해 주시기 바랍니다.", "description": "A paragraph in the filter issue reporter section" }, "supportFindSpecificButton": { - "message": "유사한 신고 탐색", + "message": "비슷한 신고 내역 찾기", "description": "A clickable link in the filter issue reporter section" }, "supportS6URL": { @@ -172,7 +172,7 @@ "description": "An entry in the widget used to select the type of issue" }, "supportS6Select1Option5": { - "message": "uBO Lite를 켜면 깨집니다", + "message": "uBO Lite를 켜면 오작동합니다", "description": "An entry in the widget used to select the type of issue" }, "supportS6Select1Option6": { @@ -264,15 +264,15 @@ "description": "Short description for a checkbox in the options page" }, "settingsBackupRestoreLabel": { - "message": "Backup", + "message": "백업", "description": "The header text for the back up/restore section" }, "settingsBackupRestoreSummary": { - "message": "Back up your custom settings to a file, or restore your custom settings from a file.", + "message": "사용자 지정 설정을 파일에 백업하거나 파일에서 사용자 지정 설정을 복원합니다.", "description": "A summary description of the back up/restore section." }, "settingsBackupRestoreLegend": { - "message": "Restoring will overwrite all your current custom settings.", + "message": "복원 시 현재 설정이 모두 덮어씌워집니다.", "description": "Important information about the back up/restore section." }, "findListsPlaceholder": { @@ -324,11 +324,11 @@ "description": "Tooltip for the button used to exit zapper mode" }, "pickerTipEnter": { - "message": "Create a custom filter", + "message": "커스텀 필터 생성", "description": "Label for the menu entry to create cosmetic filters" }, "unpickerTipEnter": { - "message": "Remove a custom filter", + "message": "커스텀 필터 삭제", "description": "Label for the menu entry to delete cosmetic filters" }, "developDropdownLabel": { @@ -364,7 +364,7 @@ "description": "Text for buttons used to revert changes" }, "addButton": { - "message": "Add", + "message": "추가", "description": "Text for buttons used to add content" }, "importAndAppendButton": { @@ -376,11 +376,11 @@ "description": "Text for buttons used to export content" }, "backupButton": { - "message": "Back up…", + "message": "백업…", "description": "Text for buttons used to back up content" }, "restoreButton": { - "message": "Restore…", + "message": "복원…", "description": "Text for buttons used to restore content" }, "dnrRulesWarning": { @@ -392,23 +392,23 @@ "description": "Short sentence to report the number of currently registered DNR rules" }, "pickerSliderLabel": { - "message": "Move the slider to select the best match", + "message": "슬라이더를 움직여 가장 적합한 항목을 선택하세요", "description": "Label to describe the purpose of the slider" }, "pickerPick": { - "message": "Pick", + "message": "선택", "description": "Text for the button to re-enter element-picking mode" }, "pickerPreview": { - "message": "Preview", + "message": "미리보기", "description": "Text for the button to activate preview mode" }, "pickerCreate": { - "message": "Create", + "message": "생성", "description": "Text for the button to create the filter" }, "unpickerUsage": { - "message": "Select a filter below to highlight matching elements in the webpage. Click the trash can to remove a filter.", + "message": "아래의 필터를 선택하면 웹페이지의 일치하는 요소가 강조 표시됩니다. 필터를 삭제하려면 휴지통을 클릭합니다.", "description": "Summary description on how to use the tool to remove custom filters" } } diff --git a/platform/mv3/extension/_locales/lv/messages.json b/platform/mv3/extension/_locales/lv/messages.json index 5d96cb3dcbfcb..1d2143344f536 100644 --- a/platform/mv3/extension/_locales/lv/messages.json +++ b/platform/mv3/extension/_locales/lv/messages.json @@ -4,7 +4,7 @@ "description": "extension name." }, "extShortDesc": { - "message": "Bezatļauju satura aizturētājs. Aiztur reklāmas, izsekotājus, kriptoracējus un daudz ko citu uzreiz pēc uzstādīšanas.", + "message": "Iedarbīgs satura aizturētājs. Aiztur reklāmas, izsekotājus, kriptoracējus un vēl uzreiz pēc uzstādīšanas.", "description": "this will be in the Chrome web store: must be 132 characters or less" }, "perRulesetStats": { diff --git a/platform/mv3/extension/_locales/tr/messages.json b/platform/mv3/extension/_locales/tr/messages.json index 5d2b445476118..e6af931730170 100644 --- a/platform/mv3/extension/_locales/tr/messages.json +++ b/platform/mv3/extension/_locales/tr/messages.json @@ -228,7 +228,7 @@ "description": "This describes the 'complete' filtering mode" }, "noFilteringModeDescription": { - "message": "Filtreleme yapılmayacak alan alarının listesi", + "message": "Filtreleme yapılmayacak web sitelerinin listesi.", "description": "A short description for the editable field which lists trusted sites" }, "noFilteringModePlaceholder": { diff --git a/platform/mv3/extension/css/picker-ui.css b/platform/mv3/extension/css/picker-ui.css index 8e86f93d0a10a..8f2fae5aee248 100644 --- a/platform/mv3/extension/css/picker-ui.css +++ b/platform/mv3/extension/css/picker-ui.css @@ -38,6 +38,7 @@ #ubol-picker textarea { border: 0; box-sizing: border-box; + font-size: var(--monospace-size); min-height: 5em; resize: none; width: 100%; @@ -49,7 +50,7 @@ color: var(--ink-2); display: flex; flex-direction: column; - font-size: small; + font-size: var(--monospace-size); gap: 0.25em; } #ubol-picker .resultsetWidgets > span:first-of-type { @@ -75,7 +76,7 @@ #ubol-picker #candidateFilters { font-family: monospace; - font-size: small; + font-size: var(--monospace-size); max-height: min(20em, 30vh); min-height: 6em; overflow-y: auto; @@ -111,7 +112,7 @@ color: var(--ink-2); column-gap: 0; display: grid; - font-size: small; + font-size: var(--font-size-smaller); grid-template: auto / 1fr 1fr; justify-items: stretch; user-select: none; diff --git a/platform/mv3/extension/css/settings.css b/platform/mv3/extension/css/settings.css index fa5cef0048436..d0efeb1b675f8 100644 --- a/platform/mv3/extension/css/settings.css +++ b/platform/mv3/extension/css/settings.css @@ -23,13 +23,19 @@ label + legend { body[data-forbid~="dashboard"] #dashboard-nav [data-pane="settings"], body[data-forbid~="dashboard"] section[data-pane="settings"], body[data-forbid~="dashboard"] #dashboard-nav [data-pane="rulesets"], -body[data-forbid~="dashboard"] section[data-pane="rulesets"] { +body[data-forbid~="dashboard"] section[data-pane="rulesets"], +body[data-forbid~="dashboard"] #dashboard-nav [data-pane="filters"], +body[data-forbid~="dashboard"] section[data-pane="filters"] { display: none; } body[data-forbid~="filteringMode"] section[data-pane="settings"] > div:has(> h3[data-i18n="defaultFilteringModeSectionLabel"]), body[data-forbid~="filteringMode"] section[data-pane="settings"] > div:has(> h3[data-i18n="filteringMode0Name"]) { display: none; } +body[data-forbid~="picker"] #dashboard-nav [data-pane="filters"], +body[data-forbid~="picker"] section[data-pane="filters"] { + display: none; + } body[data-forbid~="develop"] #developerMode { display: none; } diff --git a/platform/mv3/extension/dashboard.html b/platform/mv3/extension/dashboard.html index 464e53cf19b88..fba5fbe63a267 100644 --- a/platform/mv3/extension/dashboard.html +++ b/platform/mv3/extension/dashboard.html @@ -167,7 +167,7 @@

_


-

+            

             
diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index d83abf48ed871..dff4325240bf6 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -26,6 +26,7 @@ import { getDefaultFilteringMode, getFilteringMode, getFilteringModeDetails, + persistHostPermissions, setDefaultFilteringMode, setFilteringMode, setFilteringModeDetails, @@ -54,6 +55,7 @@ import { broadcastMessage, gotoURL, hasBroadHostPermissions, + hostnameFromMatch, hostnamesFromMatches, } from './utils.js'; @@ -116,42 +118,75 @@ function getCurrentVersion() { /******************************************************************************/ -async function onPermissionsRemoved() { - const modified = await syncWithBrowserPermissions(); - if ( modified === false ) { return false; } - registerInjectables(); - return true; +async function reloadTab(tabId, url = '') { + return new Promise(resolve => { + self.setTimeout(( ) => { + if ( url !== '' ) { + browser.tabs.update(tabId, { url }); + } else { + browser.tabs.reload(tabId); + } + resolve(); + }, 437); + }); } -// https://github.com/uBlockOrigin/uBOL-home/issues/280 -async function onPermissionsAdded(permissions) { - const details = pendingPermissionRequest; - pendingPermissionRequest = undefined; - if ( details === undefined ) { - const modified = await syncWithBrowserPermissions(); - if ( modified === false ) { return; } - return Promise.all([ - updateSessionRules(), - registerInjectables(), - ]); - } +// When a new host permission is granted through the popup panel +async function onPermissionGrantedThruExtension(details, origins) { + await persistHostPermissions(); const defaultMode = await getDefaultFilteringMode(); if ( defaultMode >= MODE_OPTIMAL ) { return; } - if ( Array.isArray(permissions.origins) === false ) { return; } - const hostnames = hostnamesFromMatches(permissions.origins); + if ( Array.isArray(origins) === false ) { return; } + const hostnames = hostnamesFromMatches(origins); if ( hostnames.includes(details.hostname) === false ) { return; } const beforeLevel = await getFilteringMode(details.hostname); if ( beforeLevel === details.afterLevel ) { return; } const afterLevel = await setFilteringMode(details.hostname, details.afterLevel); if ( afterLevel !== details.afterLevel ) { return; } await registerInjectables(); - if ( rulesetConfig.autoReload ) { - self.setTimeout(( ) => { - browser.tabs.update(details.tabId, { - url: details.url, - }); - }, 437); - } + if ( rulesetConfig.autoReload !== true ) { return; } + await reloadTab(details.tabId, details.url); +} + +// When a new host permission is granted through the browser +async function onPermissionGrantedThruBrowser(origins) { + const modified = await syncWithBrowserPermissions(); + if ( modified === false ) { return; } + await registerInjectables(); + if ( rulesetConfig.autoReload !== true ) { return; } + if ( origins.length !== 1 ) { return; } + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + const tabId = tabs?.[0]?.id; + if ( typeof tabId !== 'number' || tabId === -1 ) { return; } + const results = await browser.scripting.executeScript({ + target: { tabId, frameIds: [ 0 ] }, + func: ( ) => document.location.hostname, + }).catch(( ) => { + }); + const tabHostname = results?.[0]?.result; + if ( typeof tabHostname !== 'string' ) { return; } + const hostname = hostnameFromMatch(origins[0]); + if ( tabHostname.endsWith(hostname) === false ) { return; } + const pos = tabHostname.length - hostname.length; + if ( pos !== 0 && tabHostname.charAt(pos-1) !== '.' ) { return; } + await reloadTab(tabId); +} + +// https://github.com/uBlockOrigin/uBOL-home/issues/280 +async function onPermissionsAdded(permissions) { + const details = pendingPermissionRequest; + pendingPermissionRequest = undefined; + const { origins = [] } = permissions; + return details !== undefined + ? onPermissionGrantedThruExtension(details, origins) + : onPermissionGrantedThruBrowser(origins); +} + +async function onPermissionsRemoved() { + const modified = await syncWithBrowserPermissions(); + if ( modified === false ) { return false; } + registerInjectables(); + return true; } async function onPermissionsChanged(op, permissions) { diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index 18c88f8788cb3..78fb2c53b9623 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -33,6 +33,8 @@ export const webextFlavor = (( ) => { return extURL.startsWith('moz-extension:') ? 'firefox' : 'chromium'; })(); +const notAnObject = a => typeof a !== 'object' || a === null; + /******************************************************************************/ // The extension's service worker can be evicted at any time, so when we @@ -47,72 +49,64 @@ export function sendMessage(msg) { /******************************************************************************/ export async function localRead(key) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.local instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.local) ) { return; } try { const bin = await browser.storage.local.get(key); - if ( bin instanceof Object === false ) { return; } + if ( notAnObject(bin) ) { return; } return bin[key] ?? undefined; } catch { } } export async function localWrite(key, value) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.local instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.local) ) { return; } return browser.storage.local.set({ [key]: value }); } export async function localRemove(key) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.local instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.local) ) { return; } return browser.storage.local.remove(key); } export async function localKeys() { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.local instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.local) ) { return; } if ( browser.storage.local.getKeys ) { return browser.storage.local.getKeys(); } const bin = await browser.storage.local.get(null); - if ( bin instanceof Object === false ) { return; } + if ( notAnObject(bin) ) { return; } return Object.keys(bin); } /******************************************************************************/ export async function sessionRead(key) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.session instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.session) ) { return; } try { const bin = await browser.storage.session.get(key); - if ( bin instanceof Object === false ) { return; } + if ( notAnObject(bin) ) { return; } return bin[key] ?? undefined; } catch { } } export async function sessionWrite(key, value) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.session instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.session) ) { return; } return browser.storage.session.set({ [key]: value }); } export async function sessionRemove(key) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.session instanceof Object === false ) { return; } + if ( notAnObject(browser?.storage?.session) ) { return; } return browser.storage.session.remove(key); } /******************************************************************************/ export async function adminRead(key) { - if ( browser.storage instanceof Object === false ) { return; } - if ( browser.storage.managed instanceof Object === false ) { return; } + if ( browser?.storage?.managed instanceof Object === false ) { return; } try { const bin = await browser.storage.managed.get(key); - if ( bin instanceof Object === false ) { return; } + if ( notAnObject(bin) ) { return; } return bin[key] ?? undefined; } catch { } diff --git a/platform/mv3/extension/js/mode-manager.js b/platform/mv3/extension/js/mode-manager.js index 4367ff6c2b2dd..416613b5fd350 100644 --- a/platform/mv3/extension/js/mode-manager.js +++ b/platform/mv3/extension/js/mode-manager.js @@ -29,7 +29,7 @@ import { import { browser, - localRead, localWrite, + localRead, localRemove, localWrite, sessionRead, sessionWrite, } from './ext.js'; @@ -337,16 +337,33 @@ export function setDefaultFilteringMode(afterLevel) { /******************************************************************************/ +export async function persistHostPermissions(iter) { + if ( iter === undefined ) { + const permissions = await browser.permissions.getAll(); + iter = hostnamesFromMatches(permissions.origins) || []; + } + const hostnames = Array.from(iter); + return hostnames.length !== 0 + ? localWrite('permissions.hostnames', hostnames) + : localRemove('permissions.hostnames'); +} + +/******************************************************************************/ + export async function syncWithBrowserPermissions() { const [ - permissions, + beforePermissions, + afterPermissions, beforeMode, ] = await Promise.all([ + localRead('permissions.hostnames'), browser.permissions.getAll(), getDefaultFilteringMode(), ]); - const allowedHostnames = new Set(hostnamesFromMatches(permissions.origins || [])); - const hasBroadHostPermissions = allowedHostnames.has('all-urls'); + const beforeAllowedHostnames = new Set(beforePermissions); + const afterAllowedHostnames = new Set(hostnamesFromMatches(afterPermissions.origins || [])); + await persistHostPermissions(afterAllowedHostnames); + const hasBroadHostPermissions = afterAllowedHostnames.has('all-urls'); const broadHostPermissionsToggled = hasBroadHostPermissions !== rulesetConfig.hasBroadHostPermissions; let modified = false; @@ -364,13 +381,15 @@ export async function syncWithBrowserPermissions() { const afterMode = await getDefaultFilteringMode(); if ( afterMode > MODE_BASIC ) { return afterMode !== beforeMode; } const filteringModes = await getFilteringModeDetails(); - if ( allowedHostnames.has('all-urls') === false ) { + if ( afterAllowedHostnames.has('all-urls') === false ) { const { none, basic, optimal, complete } = filteringModes; for ( const hn of new Set([ ...optimal, ...complete ]) ) { + if ( afterAllowedHostnames.has(hn) ) { continue; } applyFilteringMode(filteringModes, hn, afterMode); modified = true; } - for ( const hn of allowedHostnames ) { + for ( const hn of afterAllowedHostnames ) { + if ( beforeAllowedHostnames.has(hn) ) { continue; } if ( optimal.has(hn) || complete.has(hn) ) { continue; } if ( basic.has(hn) || none.has(hn) ) { continue; } applyFilteringMode(filteringModes, hn, MODE_OPTIMAL); diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js index b6393ea06da56..3bbdca991e657 100644 --- a/platform/mv3/extension/js/ruleset-manager.js +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -168,19 +168,20 @@ async function updateRegexRules(currentRules, addRules, removeRuleIds) { async function updateDynamicRules() { const currentRules = await dnr.getDynamicRules(); - const addRules = []; - const removeRuleIds = []; // Remove potentially left-over rules from previous version + const removeRuleIds = []; for ( const rule of currentRules ) { if ( rule.id >= SPECIAL_RULES_REALM ) { continue; } removeRuleIds.push(rule.id); rule.id = 0; } + const addRules = []; await updateRegexRules(currentRules, addRules, removeRuleIds); if ( addRules.length === 0 && removeRuleIds.length === 0 ) { return; } + const dynamicRegexCountBefore = dynamicRegexCount; dynamicRegexCount = 0; let ruleId = 1; for ( const rule of addRules ) { @@ -190,6 +191,11 @@ async function updateDynamicRules() { if ( dynamicRegexCount !== 0 ) { ubolLog(`Using ${dynamicRegexCount}/${dnr.MAX_NUMBER_OF_REGEX_RULES} dynamic regex-based DNR rules`); } + // If we increase the number of dynamic regex rules, reset session rules to + // reduce risk of hitting maximum regex count + if ( dynamicRegexCount > dynamicRegexCountBefore ) { + await clearSessionRules(); + } const response = {}; @@ -339,8 +345,11 @@ async function updateSessionRules() { let regexCount = dynamicRegexCount; let ruleId = 1; for ( const rule of addRulesUnfiltered ) { - if ( rule?.condition.regexFilter ) { regexCount += 1; } - rule.id = regexCount < maxRegexCount ? ruleId++ : 0; + rule.id = ruleId++; + if ( Boolean(rule?.condition.regexFilter) === false ) { continue; } + regexCount += 1; + if ( regexCount < maxRegexCount ) { continue; } + rule.id = 0; } sessionRegexCount = regexCount - dynamicRegexCount; const addRules = addRulesUnfiltered.filter(a => a.id !== 0); @@ -367,6 +376,13 @@ async function updateSessionRules() { return response; } +async function clearSessionRules() { + const currentRules = await dnr.getSessionRules(); + if ( currentRules.length === 0 ) { return; } + const removeRuleIds = currentRules.map(a => a.id); + return dnr.updateSessionRules({ removeRuleIds }); +} + /******************************************************************************/ async function getEffectiveSessionRules() { diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 78200d47bcc49..43b02efc9a294 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -434,29 +434,24 @@ function registerScriptlet(context, scriptletDetails) { ]; for ( const rulesetId of rulesetsDetails.map(v => v.id) ) { - const scriptletList = scriptletDetails.get(rulesetId); - if ( scriptletList === undefined ) { continue; } - - for ( const [ token, details ] of scriptletList ) { - const id = `${rulesetId}.${token}`; - const registered = before.get(id); + const worlds = scriptletDetails.get(rulesetId); + if ( worlds === undefined ) { continue; } + for ( const world of Object.keys(worlds) ) { + const id = `${rulesetId}.${world.toLowerCase()}`; const matches = []; const excludeMatches = []; + const hostnames = worlds[world]; let targetHostnames = []; if ( hasBroadHostPermission ) { excludeMatches.push(...permissionRevokedMatches); - if ( details.hostnames.length > 100 ) { - targetHostnames = [ '*' ]; - } else { - targetHostnames = details.hostnames; - } + targetHostnames = hostnames; } else if ( permissionGrantedHostnames.length !== 0 ) { - if ( details.hostnames.includes('*') ) { + if ( hostnames.includes('*') ) { targetHostnames = permissionGrantedHostnames; } else { targetHostnames = ut.intersectHostnameIters( - details.hostnames, + hostnames, permissionGrantedHostnames ); } @@ -465,16 +460,17 @@ function registerScriptlet(context, scriptletDetails) { matches.push(...ut.matchesFromHostnames(targetHostnames)); normalizeMatches(matches); + const registered = before.get(id); before.delete(id); // Important! const directive = { id, - js: [ `/rulesets/scripting/scriptlet/${id}.js` ], + js: [ `/rulesets/scripting/scriptlet/${world.toLowerCase()}/${rulesetId}.js` ], matches, allFrames: true, matchOriginAsFallback: true, runAt: 'document_start', - world: details.world, + world, }; if ( excludeMatches.length !== 0 ) { directive.excludeMatches = excludeMatches; diff --git a/platform/mv3/extension/js/scripting/css-procedural.js b/platform/mv3/extension/js/scripting/css-procedural.js index 28c2d516b36a2..a17da0020e6b5 100644 --- a/platform/mv3/extension/js/scripting/css-procedural.js +++ b/platform/mv3/extension/js/scripting/css-procedural.js @@ -30,42 +30,47 @@ self.proceduralImports = undefined; /******************************************************************************/ -const selectors = []; -const exceptions = []; - -const lookupHostname = (hostname, details, out) => { - let seqi = details.hostnamesMap.get(hostname); - if ( seqi === undefined ) { return; } - const { argsList, argsSeqs } = details; - for (;;) { - const argi = argsSeqs[seqi++]; - const done = argi > 0; - out.push(...JSON.parse(argsList[done ? argi : -argi])); - if ( done ) { break; } +const isolatedAPI = self.isolatedAPI; +const selectors = new Set(); +const exceptions = new Set(); + +const lookupHostname = (hostname, details) => { + const listref = isolatedAPI.binarySearch(details.hostnames, hostname); + if ( listref === -1 ) { return; } + if ( Array.isArray(details.selectorLists) === false ) { + details.selectorLists = details.selectorLists.split(';'); + details.selectorListRefs = JSON.parse(`[${details.selectorListRefs}]`); + } + const ilist = details.selectorListRefs[listref]; + const list = JSON.parse(`[${details.selectorLists[ilist]}]`); + for ( const iselector of list ) { + if ( iselector >= 0 ) { + selectors.add(details.selectors[iselector]); + } else { + exceptions.add(details.selectors[~iselector]); + } } }; const lookupAll = hostname => { for ( const details of proceduralImports ) { - lookupHostname(hostname, details, selectors); - const matches = []; - lookupHostname(`~${hostname}`, details, matches); - if ( matches.length === 0 ) { continue; } - exceptions.push(...matches.map(a => JSON.stringify(a))); + lookupHostname(hostname, details); } }; -self.isolatedAPI.forEachHostname(lookupAll, { +isolatedAPI.forEachHostname(lookupAll, { hasEntities: proceduralImports.some(a => a.hasEntities) }); + proceduralImports.length = 0; -if ( selectors.length === 0 ) { return; } +for ( const selector of exceptions ) { + selectors.delete(selector); +} + +if ( selectors.size === 0 ) { return; } -const exceptedSelectors = exceptions.length !== 0 - ? selectors.filter(a => exceptions.includes(JSON.stringify(a)) === false) - : selectors; -if ( exceptedSelectors.length === 0 ) { return; } +const exceptedSelectors = Array.from(selectors).map(a => JSON.parse(a)); const declaratives = exceptedSelectors.filter(a => a.cssable); if ( declaratives.length !== 0 ) { diff --git a/platform/mv3/extension/js/scripting/css-specific.js b/platform/mv3/extension/js/scripting/css-specific.js index e0117e7c30d9e..4f2c878e18dea 100644 --- a/platform/mv3/extension/js/scripting/css-specific.js +++ b/platform/mv3/extension/js/scripting/css-specific.js @@ -30,42 +30,48 @@ self.specificImports = undefined; /******************************************************************************/ -const selectors = []; -const exceptions = []; - -const lookupHostname = (hostname, details, out) => { - let seqi = details.hostnamesMap.get(hostname); - if ( seqi === undefined ) { return; } - const { argsList, argsSeqs } = details; - for (;;) { - const argi = argsSeqs[seqi++]; - const done = argi > 0; - out.push(...argsList[done ? argi : -argi].split('\n')); - if ( done ) { break; } +const isolatedAPI = self.isolatedAPI; +const selectors = new Set(); +const exceptions = new Set(); + +const lookupHostname = (hostname, details) => { + const listref = isolatedAPI.binarySearch(details.hostnames, hostname); + if ( listref === -1 ) { return; } + if ( Array.isArray(details.selectorLists) === false ) { + details.selectorLists = details.selectorLists.split(';'); + details.selectorListRefs = JSON.parse(`[${details.selectorListRefs}]`); + } + const ilist = details.selectorListRefs[listref]; + const list = JSON.parse(`[${details.selectorLists[ilist]}]`); + for ( const iselector of list ) { + if ( iselector >= 0 ) { + selectors.add(details.selectors[iselector]); + } else { + exceptions.add(details.selectors[~iselector]); + } } }; const lookupAll = hostname => { for ( const details of specificImports ) { - lookupHostname(hostname, details, selectors); - lookupHostname(`~${hostname}`, details, exceptions); + lookupHostname(hostname, details); } }; -self.isolatedAPI.forEachHostname(lookupAll, { +isolatedAPI.forEachHostname(lookupAll, { hasEntities: specificImports.some(a => a.hasEntities) }); specificImports.length = 0; -if ( selectors.length === 0 ) { return; } +for ( const selector of exceptions ) { + selectors.delete(selector); +} -const exceptedSelectors = exceptions.length !== 0 - ? selectors.filter(a => exceptions.includes(a) === false) - : selectors; -if ( exceptedSelectors.length === 0 ) { return; } +if ( selectors.size === 0 ) { return; } -self.cssAPI.insert(`${exceptedSelectors.join(',')}{display:none!important;}`); +const css = `${Array.from(selectors).join(',\n')}{display:none!important;}`; +self.cssAPI.insert(css); /******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting/isolated-api.js b/platform/mv3/extension/js/scripting/isolated-api.js index 4781f935c251f..4cf251548a8e8 100644 --- a/platform/mv3/extension/js/scripting/isolated-api.js +++ b/platform/mv3/extension/js/scripting/isolated-api.js @@ -73,6 +73,28 @@ if ( r !== undefined ) { return r; } } }; + + isolatedAPI.binarySearch = (sorted, target) => { + let l = 0, i = 0, d = 0; + let r = sorted.length; + let candidate; + while ( l < r ) { + i = l + r >>> 1; + candidate = sorted[i]; + d = target.length - candidate.length; + if ( d === 0 ) { + if ( target === candidate ) { return i; } + d = target < candidate ? -1 : 1; + } + if ( d < 0 ) { + r = i; + } else { + l = i + 1; + } + } + return -1; + }; + })(self.isolatedAPI); /******************************************************************************/ diff --git a/platform/mv3/extension/js/utils.js b/platform/mv3/extension/js/utils.js index 22aa5631a1c3d..e9ebee6016fd8 100644 --- a/platform/mv3/extension/js/utils.js +++ b/platform/mv3/extension/js/utils.js @@ -118,7 +118,7 @@ export const matchesFromHostnames = hostnames => { export const hostnameFromMatch = origin => { if ( origin === '' || origin === '*://*/*' ) { return 'all-urls'; } - const match = /^\*:\/\/(?:\*\.)?([^/]+)\/\*/.exec(origin); + const match = /^[^:]+:\/\/(?:\*\.)?([^/]+)\/\*/.exec(origin); if ( match === null ) { return ''; } return match[1]; }; diff --git a/platform/mv3/extension/report.html b/platform/mv3/extension/report.html index 9dc3f3e2a14b1..cde7614191306 100644 --- a/platform/mv3/extension/report.html +++ b/platform/mv3/extension/report.html @@ -54,7 +54,7 @@


-

+    

     
diff --git a/platform/mv3/firefox/manifest.json b/platform/mv3/firefox/manifest.json index 444b3f52004bd..e2ca46cdbb2e2 100644 --- a/platform/mv3/firefox/manifest.json +++ b/platform/mv3/firefox/manifest.json @@ -16,7 +16,10 @@ "browser_specific_settings": { "gecko": { "id": "uBOLiteRedux@raymondhill.net", - "strict_min_version": "128.0" + "strict_min_version": "128.0", + "data_collection_permissions": { + "required": [ "none" ] + } }, "gecko_android": { "strict_min_version": "128.0" diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index d4b5587d557ff..9443e5ac9701f 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -90,17 +90,13 @@ const jsonSetMapReplacer = (k, v) => { return v; }; -const uidint32 = (s) => { - const h = createHash('sha256').update(s).digest('hex').slice(0,8); - return parseInt(h,16) & 0x7FFFFFFF; -}; - /******************************************************************************/ const consoleLog = console.log; const stdOutput = []; const log = (text, silent = true) => { + silent = silent && text.startsWith('!!!') === false; stdOutput.push(text); if ( silent === false ) { consoleLog(text); @@ -118,7 +114,7 @@ const logProgress = text => { /******************************************************************************/ async function fetchText(url, cacheDir) { - logProgress(`Reading locally cached ${url}`); + logProgress(`Reading locally cached ${path.basename(url)}`); const fname = url .replace(/^https?:\/\//, '') .replace(/\//g, '_');(url); @@ -130,7 +126,7 @@ async function fetchText(url, cacheDir) { log(`\tFetched local ${url}`); return { url, content }; } - logProgress(`Fetching remote ${url}`); + logProgress(`Fetching remote ${path.basename(url)}`); log(`\tFetching remote ${url}`); const response = await fetch(url).catch(( ) => { }); if ( response === undefined ) { @@ -152,7 +148,7 @@ async function fetchText(url, cacheDir) { async function fallbackFetchText(url) { const match = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/master\/([^?]+)/.exec(url); if ( match === null ) { return; } - logProgress(`\tGitHub CLI-fetching remote ${url}`); + logProgress(`\tGitHub CLI-fetching remote ${path.basename(url)}`); // https://docs.github.com/en/rest/repos/contents const content = execSync(`gh api \ -H "Accept: application/vnd.github.raw+json" \ @@ -811,84 +807,6 @@ const globalHighlyGenericExceptionSet = new Set(); /******************************************************************************/ -// This merges selectors which are used by the same hostnames - -function groupSelectorsByHostnames(mapin) { - if ( mapin === undefined ) { return []; } - const merged = new Map(); - for ( const [ selector, details ] of mapin ) { - if ( details.rejected ) { continue; } - const json = JSON.stringify(details); - let entries = merged.get(json); - if ( entries === undefined ) { - entries = new Set(); - merged.set(json, entries); - } - entries.add(selector); - } - const out = []; - for ( const [ json, entries ] of merged ) { - const details = JSON.parse(json); - details.selectors = Array.from(entries).sort(); - out.push(details); - } - return out; -} - -// This merges hostnames which have the same set of selectors. -// -// Also, we sort the hostnames to increase likelihood that selector with -// same hostnames will end up in same generated scriptlet. - -function groupHostnamesBySelectors(arrayin) { - const contentMap = new Map(); - for ( const entry of arrayin ) { - const id = uidint32(JSON.stringify(entry.selectors)); - let details = contentMap.get(id); - if ( details === undefined ) { - details = { a: entry.selectors }; - contentMap.set(id, details); - } - if ( entry.matches !== undefined ) { - if ( details.y === undefined ) { - details.y = new Set(); - } - for ( const hn of entry.matches ) { - details.y.add(hn); - } - } - if ( entry.excludeMatches !== undefined ) { - if ( details.n === undefined ) { - details.n = new Set(); - } - for ( const hn of entry.excludeMatches ) { - details.n.add(hn); - } - } - } - const out = Array.from(contentMap).map(a => [ - a[0], { - a: a[1].a, - y: a[1].y ? Array.from(a[1].y) : undefined, - n: a[1].n ? Array.from(a[1].n) : undefined, - } - ]); - return out; -} - -const scriptletHostnameToIdMap = (hostnames, id, map) => { - for ( const hn of hostnames ) { - const existing = map.get(hn); - if ( existing === undefined ) { - map.set(hn, id); - } else if ( Array.isArray(existing) ) { - existing.push(id); - } else { - map.set(hn, [ existing, id ]); - } - } -}; - const scriptletJsonReplacer = (k, v) => { if ( k === 'n' ) { if ( v === undefined || v.size === 0 ) { return; } @@ -903,175 +821,86 @@ const scriptletJsonReplacer = (k, v) => { /******************************************************************************/ -function argsMap2List(argsMap, hostnamesMap) { - const argsList = [ '' ]; - const indexMap = new Map(); - for ( const [ id, details ] of argsMap ) { - indexMap.set(id, argsList.length); - argsList.push(details); - } - const argsSeqs = [ 0 ]; - const argsSeqsIndices = new Map(); - for ( const [ hn, ids ] of hostnamesMap ) { - const seqKey = JSON.stringify(ids); - if ( argsSeqsIndices.has(seqKey) ) { - hostnamesMap.set(hn, argsSeqsIndices.get(seqKey)); - continue; - } - const seqIndex = argsSeqs.length; - argsSeqsIndices.set(seqKey, seqIndex); - hostnamesMap.set(hn, seqIndex); - if ( typeof ids === 'number' ) { - argsSeqs.push(indexMap.get(ids)); - continue; - } - for ( let i = 0; i < ids.length; i++ ) { - argsSeqs.push(-indexMap.get(ids[i])); - } - argsSeqs[argsSeqs.length-1] = -argsSeqs[argsSeqs.length-1]; - } - return { argsList, argsSeqs }; -} - -/******************************************************************************/ - -async function processCosmeticFilters(assetDetails, mapin) { +async function processCosmeticFilters(assetDetails, realm, mapin) { if ( mapin === undefined ) { return 0; } if ( mapin.size === 0 ) { return 0; } - const domainBasedEntries = groupHostnamesBySelectors( - groupSelectorsByHostnames(mapin) - ); - // We do not want more than n CSS files per subscription, so we will - // group multiple unrelated selectors in the same file, and distinct - // css declarations will be injected programmatically according to the - // hostname of the current document. - // - // The cosmetic filters will be injected programmatically as content - // script and the decisions to activate the cosmetic filters will be - // done at injection time according to the document's hostname. - const generatedFiles = []; - - const argsMap = domainBasedEntries.map(entry => [ - entry[0], - entry[1].a ? entry[1].a.join('\n') : undefined, - ]); - const hostnamesMap = new Map(); + // Collate all distinct selectors + const allSelectors = new Map(); + const allHostnames = new Map(); let hasEntities = false; - for ( const [ id, details ] of domainBasedEntries ) { - if ( details.y ) { - scriptletHostnameToIdMap(details.y, id, hostnamesMap); - hasEntities ||= details.y.some(a => a.endsWith('.*')); + for ( const [ selector, details ] of mapin ) { + if ( details.rejected ) { continue; } + if ( allSelectors.has(selector) === false ) { + allSelectors.set(selector, allSelectors.size); } - if ( details.n ) { - scriptletHostnameToIdMap(details.n.map(a => `~${a}`), id, hostnamesMap); - hasEntities ||= details.n.some(a => a.endsWith('.*')); + const iSelector = allSelectors.get(selector); + if ( details.matches ) { + for ( const hn of details.matches ) { + if ( allHostnames.has(hn) === false ) { + allHostnames.set(hn, new Set()); + } + allHostnames.get(hn).add(iSelector); + hasEntities ||= hn.endsWith('.*'); + } + } + if ( details.excludeMatches ) { + for ( const hn of details.excludeMatches ) { + if ( allHostnames.has(hn) === false ) { + allHostnames.set(hn, new Set()); + } + allHostnames.get(hn).add(~iSelector); + hasEntities ||= hn.endsWith('.*'); + } } } - const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap); + const allSelectorLists = new Map(); + for ( const [ hn, selectorSet ] of allHostnames ) { + const list = JSON.stringify(Array.from(selectorSet).sort()).slice(1, -1); + if ( allSelectorLists.has(list) === false ) { + allSelectorLists.set(list, allSelectorLists.size); + } + allHostnames.set(hn, allSelectorLists.get(list)); + } + // The cosmetic filters will be injected programmatically as content + // script and the decisions to activate the cosmetic filters will be + // done at injection time according to the document's hostname. const originalScriptletMap = await loadAllSourceScriptlets(); - let patchedScriptlet = originalScriptletMap.get('css-specific').replace( + let patchedScriptlet = originalScriptletMap.get(`css-${realm}`).replace( '$rulesetId$', assetDetails.id ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$argsList\$/, - `${JSON.stringify(argsList, scriptletJsonReplacer)}` + /\bself\.\$selectors\$/, + `/* ${allSelectors.size} */ ${JSON.stringify(Array.from(allSelectors.keys()))}` ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$argsSeqs\$/, - `${JSON.stringify(argsSeqs, scriptletJsonReplacer)}` + /\bself\.\$selectorLists\$/, + `/* ${allSelectorLists.size} */ ${JSON.stringify(Array.from(allSelectorLists.keys()).join(';'))}` ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$hostnamesMap\$/, - `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` - ); - patchedScriptlet = safeReplace(patchedScriptlet, - 'self.$hasEntities$', - JSON.stringify(hasEntities) - ); - writeFile(`${scriptletDir}/specific/${assetDetails.id}.js`, patchedScriptlet); - generatedFiles.push(`${assetDetails.id}`); - - if ( generatedFiles.length !== 0 ) { - log(`CSS-specific: ${mapin.size} distinct filters`); - log(`\tCombined into ${hostnamesMap.size} distinct hostnames`); - } - - return hostnamesMap.size; -} - -/******************************************************************************/ - -async function processProceduralCosmeticFilters(assetDetails, mapin) { - if ( mapin === undefined ) { return 0; } - if ( mapin.size === 0 ) { return 0; } - - const procedurals = new Map(); - mapin.forEach((details, jsonSelector) => { - procedurals.set(jsonSelector, details); + const sortedHostnames = Array.from(allHostnames.keys()).toSorted((a, b) => { + const d = a.length - b.length; + if ( d !== 0 ) { return d; } + return a < b ? -1 : 1; }); - if ( procedurals.size === 0 ) { return 0; } - - const contentArray = groupHostnamesBySelectors( - groupSelectorsByHostnames(procedurals) - ); - - const argsMap = contentArray.map(entry => [ - entry[0], - entry[1].a, - ]); - const hostnamesMap = new Map(); - let hasEntities = false; - for ( const [ id, details ] of contentArray ) { - if ( details.y ) { - scriptletHostnameToIdMap(details.y, id, hostnamesMap); - hasEntities ||= details.y.some(a => a.endsWith('.*')); - } - if ( details.n ) { - scriptletHostnameToIdMap(details.n.map(a => `~${a}`), id, hostnamesMap); - hasEntities ||= details.n.some(a => a.endsWith('.*')); - } - } - const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap); - const argsListAfter = []; - for ( const a of argsList ) { - const aAfter = []; - for ( let b of a ) { - aAfter.push(JSON.parse(b)); - } - argsListAfter.push(JSON.stringify(aAfter)); - } - const originalScriptletMap = await loadAllSourceScriptlets(); - let patchedScriptlet = originalScriptletMap.get('css-procedural').replace( - '$rulesetId$', - assetDetails.id - ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$argsList\$/, - `${JSON.stringify(argsListAfter, scriptletJsonReplacer)}` - ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$argsSeqs\$/, - `${JSON.stringify(argsSeqs, scriptletJsonReplacer)}` + /\bself\.\$selectorListRefs\$/, + `/* ${sortedHostnames.length} */ "${JSON.stringify(sortedHostnames.map(a => allHostnames.get(a))).slice(1, -1)}"` ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$hostnamesMap\$/, - `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}` + /\bself\.\$hostnames\$/, + `/* ${sortedHostnames.length} */ ${JSON.stringify(sortedHostnames)}` ); patchedScriptlet = safeReplace(patchedScriptlet, 'self.$hasEntities$', JSON.stringify(hasEntities) ); - writeFile(`${scriptletDir}/procedural/${assetDetails.id}.js`, patchedScriptlet); + writeFile(`${scriptletDir}/${realm}/${assetDetails.id}.js`, patchedScriptlet); - if ( contentArray.length !== 0 ) { - log(`Procedural-related distinct filters: ${procedurals.size} distinct combined selectors`); - log(`\tCombined into ${hostnamesMap.size} distinct hostnames`); - } + log(`CSS-${realm}: ${allSelectors.size} distinct filters for ${allHostnames.size} distinct hostnames`); - return hostnamesMap.size; + return sortedHostnames.length; } /******************************************************************************/ @@ -1212,10 +1041,13 @@ async function rulesetFromURLs(assetDetails) { ); const specificCosmeticStats = await processCosmeticFilters( assetDetails, + 'specific', declarativeCosmetic ); - const proceduralStats = await processProceduralCosmeticFilters( + + const proceduralStats = await processCosmeticFilters( assetDetails, + 'procedural', proceduralCosmetic ); const scriptletStats = await processScriptletFilters( diff --git a/platform/mv3/make-scriptlets.js b/platform/mv3/make-scriptlets.js index cf356c5ea2160..67226e5c460ea 100644 --- a/platform/mv3/make-scriptlets.js +++ b/platform/mv3/make-scriptlets.js @@ -27,24 +27,37 @@ import { safeReplace } from './safe-replace.js'; const resourceDetails = new Map(); const resourceAliases = new Map(); -const scriptletFiles = new Map(); +const worldTemplate = { + scriptletFunctions: new Map(), + allFunctions: new Map(), + args: new Map(), + arglists: new Map(), + hostnames: new Map(), + matches: new Set(), + hasEntities: false, + hasAncestors: false, +}; +const worlds = { + ISOLATED: structuredClone(worldTemplate), + MAIN: structuredClone(worldTemplate), +}; /******************************************************************************/ -function createScriptletCoreCode(scriptletToken) { - const details = resourceDetails.get(scriptletToken); - const components = new Map([ [ scriptletToken, details.code ] ]); - const dependencies = details.dependencies && details.dependencies.slice() || []; +function createScriptletCoreCode(worldDetails, resourceEntry) { + const { allFunctions } = worldDetails; + allFunctions.set(resourceEntry.name, resourceEntry.code); + const dependencies = resourceEntry.dependencies && + resourceEntry.dependencies.slice() || []; while ( dependencies.length !== 0 ) { const token = dependencies.shift(); - if ( components.has(token) ) { continue; } const details = resourceDetails.get(token); if ( details === undefined ) { continue; } - components.set(token, details.code); + if ( allFunctions.has(details.name) ) { continue; } + allFunctions.set(details.name, details.code); if ( Array.isArray(details.dependencies) === false ) { continue; } dependencies.push(...details.dependencies); } - return Array.from(components.values()).join('\n\n'); } /******************************************************************************/ @@ -70,7 +83,8 @@ export function init() { /******************************************************************************/ export function reset() { - scriptletFiles.clear(); + worlds.ISOLATED = structuredClone(worldTemplate); + worlds.MAIN = structuredClone(worldTemplate); } /******************************************************************************/ @@ -85,56 +99,58 @@ export function compile(assetDetails, details) { const scriptletToken = details.args[0]; const resourceEntry = resourceDetails.get(scriptletToken); if ( resourceEntry === undefined ) { return; } - const argsToken = JSON.stringify(details.args.slice(1)); if ( resourceEntry.requiresTrust && details.trustedSource !== true ) { - console.log(`Rejecting +js(${scriptletToken},${argsToken.slice(1,-1)}): ${assetDetails.id} is not trusted`); + console.log(`Rejecting +js(${details.args.join()}): ${assetDetails.id} is not trusted`); return; } - if ( scriptletFiles.has(scriptletToken) === false ) { - scriptletFiles.set(scriptletToken, { - name: resourceEntry.name, - code: createScriptletCoreCode(scriptletToken), - world: resourceEntry.world, - args: new Map(), - hostnames: new Map(), - exceptions: new Map(), - hasEntities: false, - hasAncestors: false, - matches: new Set(), - }); + const worldDetails = worlds[resourceEntry.world]; + const { scriptletFunctions } = worldDetails; + if ( scriptletFunctions.has(resourceEntry.name) === false ) { + scriptletFunctions.set(resourceEntry.name, scriptletFunctions.size); + createScriptletCoreCode(worldDetails, resourceEntry); } - const scriptletDetails = scriptletFiles.get(scriptletToken); - if ( scriptletDetails.args.has(argsToken) === false ) { - scriptletDetails.args.set(argsToken, scriptletDetails.args.size); + // Convert args to arg indices + const arglist = details.args.slice(); + arglist[0] = scriptletFunctions.get(resourceEntry.name); + for ( let i = 1; i < arglist.length; i++ ) { + const arg = arglist[i]; + if ( worldDetails.args.has(arg) === false ) { + worldDetails.args.set(arg, worldDetails.args.size); + } + arglist[i] = worldDetails.args.get(arg); + } + const arglistKey = JSON.stringify(arglist).slice(1, -1); + if ( worldDetails.arglists.has(arglistKey) === false ) { + worldDetails.arglists.set(arglistKey, worldDetails.arglists.size); } - const iArgs = scriptletDetails.args.get(argsToken); + const arglistIndex = worldDetails.arglists.get(arglistKey); if ( details.matches ) { for ( const hn of details.matches ) { const isEntity = hn.endsWith('.*') || hn.endsWith('.*>>'); - scriptletDetails.hasEntities ||= isEntity; + worldDetails.hasEntities ||= isEntity; const isAncestor = hn.endsWith('>>') - scriptletDetails.hasAncestors ||= isAncestor; + worldDetails.hasAncestors ||= isAncestor; if ( isEntity || isAncestor ) { - scriptletDetails.matches.clear(); - scriptletDetails.matches.add('*'); + worldDetails.matches.clear(); + worldDetails.matches.add('*'); } - if ( scriptletDetails.matches.has('*') === false ) { - scriptletDetails.matches.add(hn); + if ( worldDetails.matches.has('*') === false ) { + worldDetails.matches.add(hn); } - if ( scriptletDetails.hostnames.has(hn) === false ) { - scriptletDetails.hostnames.set(hn, new Set()); + if ( worldDetails.hostnames.has(hn) === false ) { + worldDetails.hostnames.set(hn, new Set()); } - scriptletDetails.hostnames.get(hn).add(iArgs); + worldDetails.hostnames.get(hn).add(arglistIndex); } } else { - scriptletDetails.matches.add('*'); + worldDetails.matches.add('*'); } if ( details.excludeMatches ) { for ( const hn of details.excludeMatches ) { - if ( scriptletDetails.exceptions.has(hn) === false ) { - scriptletDetails.exceptions.set(hn, []); + if ( worldDetails.hostnames.has(hn) === false ) { + worldDetails.hostnames.set(hn, new Set()); } - scriptletDetails.exceptions.get(hn).push(iArgs); + worldDetails.hostnames.get(hn).add(~arglistIndex); } } } @@ -146,51 +162,47 @@ export async function commit(rulesetId, path, writeFn) { './scriptlets/scriptlet.template.js', { encoding: 'utf8' } ); - const patchHnMap = hnmap => { - const out = Array.from(hnmap); - out.forEach(a => { - const values = Array.from(a[1]); - a[1] = values.length === 1 ? values[0] : values; - }); - return out; - }; - const scriptletStats = []; - for ( const [ name, details ] of scriptletFiles ) { - let content = safeReplace(scriptletTemplate, - 'function $scriptletName$(){}', - details.code + const stats = {}; + for ( const world of Object.keys(worlds) ) { + const worldDetails = worlds[world]; + const { scriptletFunctions, allFunctions, args, arglists } = worldDetails; + if ( scriptletFunctions.size === 0 ) { continue; } + const hostnames = Array.from(worldDetails.hostnames).toSorted((a, b) => { + const d = a[0].length - b[0].length; + if ( d !== 0 ) { return d; } + return a[0] < b[0] ? -1 : 1; + }).map(a => ([ a[0], JSON.stringify(Array.from(a[1]).map(a => JSON.parse(a))).slice(1,-1)])); + let content = safeReplace(scriptletTemplate, /\$rulesetId\$/, rulesetId, 0); + content = safeReplace(content, 'self.$hasEntities$', 'true'); + content = safeReplace(content, 'self.$hasAncestors$', 'true'); + content = safeReplace(content, + 'self.$scriptletHostnames$', + `/* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[0]))}` ); - content = safeReplace(content, /\$rulesetId\$/, rulesetId, 0); - content = safeReplace(content, /\$scriptletName\$/, details.name, 0); content = safeReplace(content, - 'self.$argsList$', - JSON.stringify(Array.from(details.args.keys()).map(a => JSON.parse(a))) + 'self.$scriptletArglistRefs$', + `/* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[1]).join(';'))}` ); content = safeReplace(content, - 'self.$hostnamesMap$', - JSON.stringify(patchHnMap(details.hostnames)) + 'self.$scriptletArglists$', + `/* ${arglists.size} */ ${JSON.stringify(Array.from(arglists.keys()).join(';'))}` ); content = safeReplace(content, - 'self.$hasEntities$', - JSON.stringify(details.hasEntities) + 'self.$scriptletArgs$', + `/* ${args.size} */ ${JSON.stringify(Array.from(args.keys()))}` ); content = safeReplace(content, - 'self.$hasAncestors$', - JSON.stringify(details.hasAncestors) + 'self.$scriptletFunctions$', + `/* ${scriptletFunctions.size} */\n[${Array.from(scriptletFunctions.keys()).join(',')}]` ); content = safeReplace(content, - 'self.$exceptionsMap$', - JSON.stringify(Array.from(details.exceptions)) + 'self.$scriptletCode$', + Array.from(allFunctions.values()).join('\n\n') ); - writeFn(`${path}/${rulesetId}.${name}`, content); - scriptletStats.push([ - name.slice(0, -3), { - hostnames: Array.from(details.matches).sort(), - world: details.world, - } - ]); + writeFn(`${path}/${world.toLowerCase()}/${rulesetId}.js`, content); + stats[world] = Array.from(worldDetails.matches).sort(); } - return scriptletStats; + return stats; } /******************************************************************************/ diff --git a/platform/mv3/safari/patch-ruleset.js b/platform/mv3/safari/patch-ruleset.js index 953475d150c7a..88932de1932b6 100644 --- a/platform/mv3/safari/patch-ruleset.js +++ b/platform/mv3/safari/patch-ruleset.js @@ -19,6 +19,26 @@ Home: https://github.com/gorhill/uBlock */ +// https://github.com/uBlockOrigin/uBOL-home/issues/539 +function patchRuleForIssue539(rule) { + const { condition } = rule; + if ( Array.isArray(condition.requestDomains) === false ) { return; } + if ( Array.isArray(condition.initiatorDomains) ) { return; } + if ( Array.isArray(condition.excludedRequestDomains) ) { + if ( Array.isArray(condition.excludedInitiatorDomains) ) { return; } + } + if ( Array.isArray(condition.resourceTypes) === false ) { return; } + if ( condition.resourceTypes.length !== 1 ) { return; } + if ( condition.resourceTypes.includes('main_frame') === false ) { return; } + if ( condition.regexFilter === undefined ) { return; } + condition.initiatorDomains = condition.requestDomains; + delete condition.requestDomains; + if ( Array.isArray(condition.excludedRequestDomains) ) { + condition.excludedInitiatorDomains = condition.excludedRequestDomains; + delete condition.excludedRequestDomains; + } +} + function patchRule(rule, out) { const copy = structuredClone(rule); const condition = copy.condition; @@ -40,6 +60,7 @@ function patchRule(rule, out) { condition.excludedDomains = condition.excludedInitiatorDomains; delete condition.excludedInitiatorDomains; } + patchRuleForIssue539(copy); // https://github.com/uBlockOrigin/uBOL-home/issues/434 let { urlFilter } = condition; if ( urlFilter?.endsWith('^') ) { diff --git a/platform/mv3/scriptlets/css-procedural.template.js b/platform/mv3/scriptlets/css-procedural.template.js index 1928abdd488cc..1456dc1bedbc4 100644 --- a/platform/mv3/scriptlets/css-procedural.template.js +++ b/platform/mv3/scriptlets/css-procedural.template.js @@ -27,13 +27,15 @@ /******************************************************************************/ -const argsList = self.$argsList$; -const argsSeqs = self.$argsSeqs$; -const hostnamesMap = new Map(self.$hostnamesMap$); + +const selectors = self.$selectors$; +const selectorLists = self.$selectorLists$; +const selectorListRefs = self.$selectorListRefs$; +const hostnames = self.$hostnames$; const hasEntities = self.$hasEntities$; self.proceduralImports = self.proceduralImports || []; -self.proceduralImports.push({ argsList, argsSeqs, hostnamesMap, hasEntities }); +self.proceduralImports.push({ selectors, selectorLists, selectorListRefs, hostnames, hasEntities }); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-specific.template.js b/platform/mv3/scriptlets/css-specific.template.js index f4c93cdf0f752..cfe84f38593ca 100644 --- a/platform/mv3/scriptlets/css-specific.template.js +++ b/platform/mv3/scriptlets/css-specific.template.js @@ -27,13 +27,14 @@ /******************************************************************************/ -const argsList = self.$argsList$; -const argsSeqs = self.$argsSeqs$; -const hostnamesMap = new Map(self.$hostnamesMap$); +const selectors = self.$selectors$; +const selectorLists = self.$selectorLists$; +const selectorListRefs = self.$selectorListRefs$; +const hostnames = self.$hostnames$; const hasEntities = self.$hasEntities$; self.specificImports = self.specificImports || []; -self.specificImports.push({ argsList, argsSeqs, hostnamesMap, hasEntities }); +self.specificImports.push({ selectors, selectorLists, selectorListRefs, hostnames, hasEntities }); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/scriptlet.template.js b/platform/mv3/scriptlets/scriptlet.template.js index fb5ace81870f0..b8dedca6b9576 100644 --- a/platform/mv3/scriptlets/scriptlet.template.js +++ b/platform/mv3/scriptlets/scriptlet.template.js @@ -26,53 +26,30 @@ // Isolate from global scope // Start of local scope -(function uBOL_$scriptletName$() { +(function uBOL_scriptlets() { /******************************************************************************/ -function $scriptletName$(){} +self.$scriptletCode$ /******************************************************************************/ const scriptletGlobals = {}; // eslint-disable-line -const argsList = self.$argsList$; -const hostnamesMap = new Map(self.$hostnamesMap$); -const exceptionsMap = new Map(self.$exceptionsMap$); -const hasEntities = self.$hasEntities$; -const hasAncestors = self.$hasAncestors$; - -const collectArgIndices = (hn, map, out) => { - let argsIndices = map.get(hn); - if ( argsIndices === undefined ) { return; } - if ( typeof argsIndices !== 'number' ) { - for ( const argsIndex of argsIndices ) { - out.add(argsIndex); - } - } else { - out.add(argsIndices); - } -}; -const indicesFromHostname = (hostname, suffix = '') => { - const hnParts = hostname.split('.'); - const hnpartslen = hnParts.length; - if ( hnpartslen === 0 ) { return; } - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = `${hnParts.slice(i).join('.')}${suffix}`; - collectArgIndices(hn, hostnamesMap, todoIndices); - collectArgIndices(hn, exceptionsMap, tonotdoIndices); - } - if ( hasEntities ) { - const n = hnpartslen - 1; - for ( let i = 0; i < n; i++ ) { - for ( let j = n; j > i; j-- ) { - const en = `${hnParts.slice(i,j).join('.')}.*${suffix}`; - collectArgIndices(en, hostnamesMap, todoIndices); - collectArgIndices(en, exceptionsMap, tonotdoIndices); - } - } - } -}; +const $scriptletFunctions$ = self.$scriptletFunctions$; + +const $scriptletArgs$ = self.$scriptletArgs$; + +const $scriptletArglists$ = self.$scriptletArglists$; + +const $scriptletArglistRefs$ = self.$scriptletArglistRefs$; + +const $scriptletHostnames$ = self.$scriptletHostnames$; + +const $hasEntities$ = self.$hasEntities$; +const $hasAncestors$ = self.$hasAncestors$; + +/******************************************************************************/ const entries = (( ) => { const docloc = document.location; @@ -81,31 +58,107 @@ const entries = (( ) => { origins.push(...docloc.ancestorOrigins); } return origins.map((origin, i) => { - const beg = origin.lastIndexOf('://'); + const beg = origin.indexOf('://'); if ( beg === -1 ) { return; } - const hn = origin.slice(beg+3) - const end = hn.indexOf(':'); - return { hn: end === -1 ? hn : hn.slice(0, end), i }; + const hn1 = origin.slice(beg+3) + const end = hn1.indexOf(':'); + const hn2 = end === -1 ? hn1 : hn1.slice(0, end); + const hnParts = hn2.split('.'); + if ( hn2.length === 0 ) { return; } + const hns = []; + for ( let i = 0; i < hnParts.length; i++ ) { + hns.push(`${hnParts.slice(i).join('.')}`); + } + const ens = []; + if ( $hasEntities$ ) { + const n = hnParts.length - 1; + for ( let i = 0; i < n; i++ ) { + for ( let j = n; j > i; j-- ) { + ens.push(`${hnParts.slice(i,j).join('.')}.*`); + } + } + ens.sort((a, b) => { + const d = b.length - a.length; + if ( d !== 0 ) { return d; } + return a > b ? -1 : 1; + }); + } + return { hns, ens, i }; }).filter(a => a !== undefined); })(); if ( entries.length === 0 ) { return; } -const todoIndices = new Set(); -const tonotdoIndices = new Set(); +const collectArglistRefIndices = (out, hn, r) => { + let l = 0, i = 0, d = 0; + let candidate = ''; + while ( l < r ) { + i = l + r >>> 1; + candidate = $scriptletHostnames$[i]; + d = hn.length - candidate.length; + if ( d === 0 ) { + if ( hn === candidate ) { + out.add(i); break; + } + d = hn < candidate ? -1 : 1; + } + if ( d < 0 ) { + r = i; + } else { + l = i + 1; + } + } + return i; +}; + +const indicesFromHostname = (out, hnDetails, suffix = '') => { + if ( hnDetails.hns.length === 0 ) { return; } + let r = $scriptletHostnames$.length; + for ( const hn of hnDetails.hns ) { + r = collectArglistRefIndices(out, `${hn}${suffix}`, r); + } + if ( $hasEntities$ ) { + let r = $scriptletHostnames$.length; + for ( const en of hnDetails.ens ) { + r = collectArglistRefIndices(out, `${en}${suffix}`, r); + } + } +}; -indicesFromHostname(entries[0].hn); -if ( hasAncestors ) { +const todoIndices = new Set(); +indicesFromHostname(todoIndices, entries[0]); +if ( $hasAncestors$ ) { for ( const entry of entries ) { if ( entry.i === 0 ) { continue; } - indicesFromHostname(entry.hn, '>>'); + indicesFromHostname(todoIndices, entry, '>>'); } } +$scriptletHostnames$.length = 0; -// Apply scriplets -for ( const i of todoIndices ) { - if ( tonotdoIndices.has(i) ) { continue; } - try { $scriptletName$(...argsList[i]); } - catch { } +if ( todoIndices.size === 0 ) { return; } + +// Collect arglist references +const todo = new Set(); +{ + const arglistRefs = $scriptletArglistRefs$.split(';'); + for ( const i of todoIndices ) { + for ( const ref of JSON.parse(`[${arglistRefs[i]}]`) ) { + todo.add(ref); + } + } +} + +// Execute scriplets +{ + const arglists = $scriptletArglists$.split(';'); + const args = $scriptletArgs$; + for ( const ref of todo ) { + if ( ref < 0 ) { continue; } + if ( todo.has(~ref) ) { continue; } + const arglist = JSON.parse(`[${arglists[ref]}]`); + const fn = $scriptletFunctions$[arglist[0]]; + try { fn(...arglist.slice(1).map(a => args[a])); } + catch { } + } } /******************************************************************************/ diff --git a/publish-extension b/publish-extension index 138d1e158bd2a..b245171eac43e 160000 --- a/publish-extension +++ b/publish-extension @@ -1 +1 @@ -Subproject commit 138d1e158bd2a1d59ebfc97f6a93f121dc52373a +Subproject commit b245171eac43ed1faeecff4b5d277044cdbcdbea diff --git a/src/_locales/id/messages.json b/src/_locales/id/messages.json index 85a37b3d749e3..0268d73230abd 100644 --- a/src/_locales/id/messages.json +++ b/src/_locales/id/messages.json @@ -484,7 +484,7 @@ "description": "Filter lists section name" }, "3pGroupSocial": { - "message": "Widget sosial", + "message": "Gawai sosial", "description": "Filter lists section name" }, "3pGroupCookies": { @@ -1012,7 +1012,7 @@ "description": "An entry in the widget used to select the type of issue" }, "supportS6Select1Option7": { - "message": "Mengarah ke perangkat lunak jahat, phishing", + "message": "Mengarah ke perangkat lunak jahat, pengelabuan", "description": "An entry in the widget used to select the type of issue" }, "supportS6Checkbox1": { diff --git a/src/_locales/ko/messages.json b/src/_locales/ko/messages.json index 53c728da7c375..31132589b0814 100644 --- a/src/_locales/ko/messages.json +++ b/src/_locales/ko/messages.json @@ -1284,7 +1284,7 @@ "description": "Label for buttons used to select all text in editor" }, "toggleCosmeticFiltering": { - "message": "표면 필터 토글", + "message": "시각 요소 필터 토글", "description": "Label for keyboard shortcut used to toggle cosmetic filtering" }, "toggleJavascript": { diff --git a/src/_locales/pt_BR/messages.json b/src/_locales/pt_BR/messages.json index 647bf2408e403..59a7772f2d6cc 100644 --- a/src/_locales/pt_BR/messages.json +++ b/src/_locales/pt_BR/messages.json @@ -4,7 +4,7 @@ "description": "extension name." }, "extShortDesc": { - "message": "Até que enfim, um bloqueador eficiente. Bem otimizado no uso de CPU e memória.", + "message": "Até que enfim, um bloqueador eficiente. Bem otimizado para CPU e memória.", "description": "this will be in the Chrome web store: must be 132 characters or less" }, "dashboardName": { @@ -1080,7 +1080,7 @@ "description": "English: Restore from file..." }, "aboutResetDataButton": { - "message": "Redefinir pras configurações padrão…", + "message": "Reconfigurar tudo para o padrão…", "description": "English: Reset to default settings..." }, "aboutRestoreDataConfirm": { @@ -1092,7 +1092,7 @@ "description": "Message to display when an error occurred during restore" }, "aboutResetDataConfirm": { - "message": "Todas as suas configurações serão removidas, e o uBlock₀ reiniciará.\n\nRedefinir o uBlock₀ para as configurações de fábrica?", + "message": "Todas as suas configurações serão removidas, e o uBlock₀ reiniciará.\n\nReconfigurar o uBlock₀ para as configurações de fábrica?", "description": "Message asking user to confirm reset" }, "errorCantConnectTo": { diff --git a/src/_locales/vi/messages.json b/src/_locales/vi/messages.json index b6dc92403476f..7f5300ee5d240 100644 --- a/src/_locales/vi/messages.json +++ b/src/_locales/vi/messages.json @@ -480,7 +480,7 @@ "description": "Filter lists section name" }, "3pGroupMalware": { - "message": "Bảo mật, bảo vệ khỏi phần mềm độc hại", + "message": "Bảo vệ phần mềm độc hại, bảo mật", "description": "Filter lists section name" }, "3pGroupSocial": { @@ -512,7 +512,7 @@ "description": "The label for the checkbox used to import external filter lists" }, "3pExternalListsHint": { - "message": "Một URL cho mỗi dòng. URL không hợp lệ sẽ âm thầm bị bỏ qua.", + "message": "Một URL mỗi dòng. URL không hợp lệ sẽ âm thầm bị bỏ qua.", "description": "Short information about how to use the textarea to import external filter lists by URL" }, "3pExternalListObsolete": { @@ -1068,7 +1068,7 @@ "description": "Shown in the About pane" }, "aboutBackupDataButton": { - "message": "Sao lưu vào tập tin…", + "message": "Sao lưu vào tệp…", "description": "Text for button to create a backup of all settings" }, "aboutBackupFilename": { @@ -1080,7 +1080,7 @@ "description": "English: Restore from file..." }, "aboutResetDataButton": { - "message": "Đặt lại cấu hình mặc định…", + "message": "Đặt lại cài đặt mặc định…", "description": "English: Reset to default settings..." }, "aboutRestoreDataConfirm": { diff --git a/src/js/redirect-resources.js b/src/js/redirect-resources.js index f1dd5c27ded58..26cb0b6540bb4 100644 --- a/src/js/redirect-resources.js +++ b/src/js/redirect-resources.js @@ -190,4 +190,7 @@ export default new Map([ [ 'scorecardresearch_beacon.js', { alias: 'scorecardresearch.com/beacon.js', } ], + [ 'sensors-analytics.js', { + data: 'text', + } ], ]); diff --git a/src/js/resources/json-edit.js b/src/js/resources/json-edit.js index e5d72c60772aa..a95bc8c63921c 100644 --- a/src/js/resources/json-edit.js +++ b/src/js/resources/json-edit.js @@ -193,6 +193,7 @@ function editInboundObjectFn( const argPos = parseInt(argPosRaw, 10); if ( isNaN(argPos) ) { return; } const getArgPos = args => { + if ( Array.isArray(args) === false ) { return; } if ( argPos >= 0 ) { if ( args.length <= argPos ) { return; } return argPos; diff --git a/src/js/resources/localstorage.js b/src/js/resources/localstorage.js index 8a6b7629323c6..3e33ca2397a56 100644 --- a/src/js/resources/localstorage.js +++ b/src/js/resources/localstorage.js @@ -47,6 +47,7 @@ export function setLocalStorageItemFn( trusted = false, key = '', value = '', + options = {} ) { if ( key === '' ) { return; } @@ -86,6 +87,8 @@ export function setLocalStorageItemFn( } } + let modified = false; + try { const storage = self[`${which}Storage`]; if ( value === '$remove$' ) { @@ -96,14 +99,25 @@ export function setLocalStorageItemFn( const key = storage.key(i); if ( pattern.test(key) ) { toRemove.push(key); } } + modified = toRemove.length !== 0; for ( const key of toRemove ) { storage.removeItem(key); } } else { - storage.setItem(key, `${value}`); + + const before = storage.getItem(key); + const after = `${value}`; + modified = after !== before; + if ( modified ) { + storage.setItem(key, after); + } } } catch { } + + if ( modified && typeof options.reload === 'number' ) { + setTimeout(( ) => { window.location.reload(); }, options.reload); + } } registerScriptlet(setLocalStorageItemFn, { name: 'set-local-storage-item.fn', @@ -178,23 +192,29 @@ registerScriptlet(removeCacheStorageItem, { **/ export function setLocalStorageItem(key = '', value = '') { - setLocalStorageItemFn('local', false, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('local', false, key, value, options); } registerScriptlet(setLocalStorageItem, { name: 'set-local-storage-item.js', world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); export function setSessionStorageItem(key = '', value = '') { - setLocalStorageItemFn('session', false, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('session', false, key, value, options); } registerScriptlet(setSessionStorageItem, { name: 'set-session-storage-item.js', world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); @@ -211,25 +231,31 @@ registerScriptlet(setSessionStorageItem, { **/ export function trustedSetLocalStorageItem(key = '', value = '') { - setLocalStorageItemFn('local', true, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('local', true, key, value, options); } registerScriptlet(trustedSetLocalStorageItem, { name: 'trusted-set-local-storage-item.js', requiresTrust: true, world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); export function trustedSetSessionStorageItem(key = '', value = '') { - setLocalStorageItemFn('session', true, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('session', true, key, value, options); } registerScriptlet(trustedSetSessionStorageItem, { name: 'trusted-set-session-storage-item.js', requiresTrust: true, world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); diff --git a/src/js/resources/prevent-dialog.js b/src/js/resources/prevent-dialog.js new file mode 100644 index 0000000000000..462870c26a176 --- /dev/null +++ b/src/js/resources/prevent-dialog.js @@ -0,0 +1,72 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2025-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 + +*/ + +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/** + * @scriptlet prevent-dialog + * + * @description + * Programmatically close `dialog` elements. + * + * @param [selector] + * Optional. The dialog element must matches `dialog{selector}` for the + * prevention to take place. + * + * @usage: + * example.com##+js(prevent-dialog) + * + * */ + +export function preventDialog( + selector = '', +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-dialog', selector); + const prevent = ( ) => { + debouncer = undefined; + const elems = document.querySelectorAll(`dialog${selector}`); + for ( const elem of elems ) { + if ( typeof elem.close !== 'function' ) { continue; } + if ( elem.open === false ) { continue; } + elem.close(); + safe.uboLog(logPrefix, 'Closed'); + } + }; + let debouncer; + const observer = new MutationObserver(( ) => { + if ( debouncer !== undefined ) { return; } + debouncer = requestAnimationFrame(prevent); + }); + observer.observe(document, { + attributes: true, + childList: true, + subtree: true, + }); +} +registerScriptlet(preventDialog, { + name: 'prevent-dialog.js', + dependencies: [ + safeSelf, + ], +}); diff --git a/src/js/resources/prevent-fetch.js b/src/js/resources/prevent-fetch.js index ba2565a69b48b..5bf03e0818f95 100644 --- a/src/js/resources/prevent-fetch.js +++ b/src/js/resources/prevent-fetch.js @@ -20,7 +20,11 @@ */ -import { generateContentFn } from './utils.js'; +import { + generateContentFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, +} from './utils.js'; import { proxyApplyFn } from './proxy-apply.js'; import { registerScriptlet } from './base.js'; import { safeSelf } from './safe-self.js'; @@ -43,20 +47,7 @@ function preventFetchFn( responseType ); const extraArgs = safe.getExtraArgs(Array.from(arguments), 4); - const needles = []; - for ( const condition of safe.String_split.call(propsToMatch, /\s+/) ) { - if ( condition === '' ) { continue; } - const pos = condition.indexOf(':'); - let key, value; - if ( pos !== -1 ) { - key = condition.slice(0, pos); - value = condition.slice(pos + 1); - } else { - key = 'url'; - value = condition; - } - needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) }); - } + const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url'); const validResponseProps = { ok: [ false, true ], statusText: [ '', 'Not Found' ], @@ -84,43 +75,41 @@ function preventFetchFn( responseProps.type = { value: responseType }; } } + const fetchProps = (src, out) => { + if ( typeof src !== 'object' || src === null ) { return; } + const props = [ + 'body', 'cache', 'credentials', 'duplex', 'headers', + 'integrity', 'keepalive', 'method', 'mode', 'priority', + 'redirect', 'referrer', 'referrerPolicy', 'signal', + ]; + for ( const prop of props ) { + if ( src[prop] === undefined ) { continue; } + out[prop] = src[prop]; + } + }; + const fetchDetails = args => { + const out = {}; + if ( args[0] instanceof self.Request ) { + out.url = `${args[0].url}`; + fetchProps(args[0], out); + } else { + out.url = `${args[0]}`; + } + fetchProps(args[1], out); + return out; + }; proxyApplyFn('fetch', function fetch(context) { const { callArgs } = context; - const details = callArgs[0] instanceof self.Request - ? callArgs[0] - : Object.assign({ url: callArgs[0] }, callArgs[1]); - let proceed = true; - try { - const props = new Map(); - for ( const prop in details ) { - let v = details[prop]; - if ( typeof v !== 'string' ) { - try { v = safe.JSON_stringify(v); } - catch { } - } - if ( typeof v !== 'string' ) { continue; } - props.set(prop, v); - } - if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) { - const out = Array.from(props).map(a => `${a[0]}:${a[1]}`); - safe.uboLog(logPrefix, `Called: ${out.join('\n')}`); - } - if ( propsToMatch === '' && responseBody === '' ) { - return context.reflect(); - } - proceed = needles.length === 0; - for ( const { key, pattern } of needles ) { - if ( - pattern.expect && props.has(key) === false || - safe.testPattern(pattern, props.get(key)) === false - ) { - proceed = true; - break; - } - } - } catch { + const details = fetchDetails(callArgs); + if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) { + const out = Array.from(Object.entries(details)).map(a => `${a[0]}:${a[1]}`); + safe.uboLog(logPrefix, `Called: ${out.join('\n')}`); + } + if ( propsToMatch === '' && responseBody === '' ) { + return context.reflect(); } - if ( proceed ) { + const matched = matchObjectPropertiesFn(propNeedles, details); + if ( matched === undefined || matched.length === 0 ) { return context.reflect(); } return Promise.resolve(generateContentFn(trusted, responseBody)).then(text => { @@ -148,6 +137,8 @@ registerScriptlet(preventFetchFn, { name: 'prevent-fetch.fn', dependencies: [ generateContentFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, proxyApplyFn, safeSelf, ], diff --git a/src/js/resources/replace-argument.js b/src/js/resources/replace-argument.js index 62867cbf261d0..1a305389bf5ce 100644 --- a/src/js/resources/replace-argument.js +++ b/src/js/resources/replace-argument.js @@ -72,6 +72,7 @@ export function trustedReplaceArgument( const parsed = parseReplaceFn(argraw.slice(5)); if ( parsed === undefined ) { return; } replacer = arg => `${arg}`.replace(replacer.re, replacer.replacement); + Object.assign(replacer, parsed); } else if ( argraw.startsWith('add:') ) { const delta = parseFloat(argraw.slice(4)); if ( isNaN(delta) ) { return; } diff --git a/src/js/resources/scriptlets.js b/src/js/resources/scriptlets.js index 19fc628d51739..c935fdb9e92db 100755 --- a/src/js/resources/scriptlets.js +++ b/src/js/resources/scriptlets.js @@ -27,6 +27,7 @@ import './json-edit.js'; import './json-prune.js'; import './noeval.js'; import './object-prune.js'; +import './prevent-dialog.js'; import './prevent-fetch.js'; import './prevent-innerHTML.js'; import './prevent-settimeout.js'; @@ -1589,6 +1590,7 @@ builtinScriptlets.push({ name: 'm3u-prune.js', fn: m3uPrune, dependencies: [ + 'proxy-apply.fn', 'safe-self.fn', ], }); @@ -1704,28 +1706,30 @@ function m3uPrune( if ( arg instanceof Request ) { return arg.url; } return String(arg); }; - const realFetch = self.fetch; - self.fetch = new Proxy(self.fetch, { - apply: function(target, thisArg, args) { - if ( reUrl.test(urlFromArg(args[0])) === false ) { - return Reflect.apply(target, thisArg, args); - } - return realFetch(...args).then(realResponse => - realResponse.text().then(text => { - const response = new Response(pruner(text), { - status: realResponse.status, - statusText: realResponse.statusText, - headers: realResponse.headers, - }); - if ( toLog.length !== 0 ) { - toLog.unshift(logPrefix); - safe.uboLog(toLog.join('\n')); - } - return response; - }) - ); + proxyApplyFn('fetch', async function fetch(context) { + const args = context.callArgs; + const fetchPromise = context.reflect(); + if ( reUrl.test(urlFromArg(args[0])) === false ) { return fetchPromise; } + const responseBefore = await fetchPromise; + const responseClone = responseBefore.clone(); + const textBefore = await responseClone.text(); + const textAfter = pruner(textBefore); + if ( textAfter === textBefore ) { return responseBefore; } + const responseAfter = new Response(textAfter, { + status: responseBefore.status, + statusText: responseBefore.statusText, + headers: responseBefore.headers, + }); + Object.defineProperties(responseAfter, { + url: { value: responseBefore.url }, + type: { value: responseBefore.type }, + }); + if ( toLog.length !== 0 ) { + toLog.unshift(logPrefix); + safe.uboLog(toLog.join('\n')); } - }); + return responseAfter; + }) self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, { apply: async (target, thisArg, args) => { if ( reUrl.test(urlFromArg(args[1])) === false ) { diff --git a/src/js/s14e-serializer.js b/src/js/s14e-serializer.js index c74a4f2e303e2..d57c1ca8c1718 100644 --- a/src/js/s14e-serializer.js +++ b/src/js/s14e-serializer.js @@ -1132,34 +1132,6 @@ export const isSerialized = s => export const isCompressed = s => typeof s === 'string' && s.startsWith(MAGICLZ4PREFIX); -/******************************************************************************* - * - * Configuration - * - * */ - -const defaultConfig = { - threadTTL: 3000, -}; - -const validateConfig = { - threadTTL: val => val > 0, -}; - -const currentConfig = Object.assign({}, defaultConfig); - -export const getConfig = ( ) => Object.assign({}, currentConfig); - -export const setConfig = config => { - for ( const key in Object.keys(config) ) { - if ( Object.hasOwn(defaultConfig, key) === false ) { continue; } - const val = config[key]; - if ( typeof val !== typeof defaultConfig[key] ) { continue; } - if ( (validateConfig[key])(val) === false ) { continue; } - currentConfig[key] = val; - } -}; - /******************************************************************************* * * Asynchronous APIs @@ -1169,270 +1141,13 @@ export const setConfig = config => { * * */ -const THREAD_AREYOUREADY = 1; -const THREAD_IAMREADY = 2; -const THREAD_SERIALIZE = 3; -const THREAD_DESERIALIZE = 4; - -class MainThread { - constructor() { - this.name = 'main'; - this.jobs = []; - this.workload = 0; - this.timer = undefined; - this.busy = 2; - } - - process() { - if ( this.jobs.length === 0 ) { return; } - const job = this.jobs.shift(); - this.workload -= job.size; - const result = job.what === THREAD_SERIALIZE - ? serialize(job.data, job.options) - : deserialize(job.data); - job.resolve(result); - this.processAsync(); - if ( this.jobs.length === 0 ) { - this.busy = 2; - } else if ( this.busy > 2 ) { - this.busy -= 1; - } - } - - processAsync() { - if ( this.timer !== undefined ) { return; } - if ( this.jobs.length === 0 ) { return; } - this.timer = globalThis.requestIdleCallback(deadline => { - this.timer = undefined; - globalThis.queueMicrotask(( ) => { - this.process(); - }); - if ( deadline.timeRemaining() === 0 ) { - this.busy += 1; - } - }, { timeout: 5 }); - } - - serialize(data, options) { - return new Promise(resolve => { - this.workload += 1; - this.jobs.push({ what: THREAD_SERIALIZE, data, options, size: 1, resolve }); - this.processAsync(); - }); - } - - deserialize(data, options) { - return new Promise(resolve => { - const size = data.length; - this.workload += size; - this.jobs.push({ what: THREAD_DESERIALIZE, data, options, size, resolve }); - this.processAsync(); - }); - } - - get queueSize() { - return this.jobs.length; - } - - get workSize() { - return this.workload * this.busy; - } -} - -class Thread { - constructor(gcer) { - this.name = 'worker'; - this.jobs = new Map(); - this.jobIdGenerator = 1; - this.workload = 0; - this.workerAccessTime = 0; - this.workerTimer = undefined; - this.gcer = gcer; - this.workerPromise = new Promise(resolve => { - let worker = null; - try { - worker = new Worker('js/s14e-serializer.js', { type: 'module' }); - worker.onmessage = ev => { - const msg = ev.data; - if ( isInstanceOf(msg, 'Object') === false ) { return; } - if ( msg.what === THREAD_IAMREADY ) { - worker.onmessage = ev => { this.onmessage(ev); }; - worker.onerror = null; - resolve(worker); - } - }; - worker.onerror = ( ) => { - worker.onmessage = worker.onerror = null; - resolve(null); - }; - worker.postMessage({ - what: THREAD_AREYOUREADY, - config: currentConfig, - }); - } catch(ex) { - console.info(ex); - worker.onmessage = worker.onerror = null; - resolve(null); - } - }); - } - - countdownWorker() { - if ( this.workerTimer !== undefined ) { return; } - this.workerTimer = setTimeout(async ( ) => { - this.workerTimer = undefined; - if ( this.jobs.size !== 0 ) { return; } - const idleTime = Date.now() - this.workerAccessTime; - if ( idleTime < currentConfig.threadTTL ) { - return this.countdownWorker(); - } - const worker = await this.workerPromise; - if ( this.jobs.size !== 0 ) { return; } - this.gcer(this); - if ( worker === null ) { return; } - worker.onmessage = worker.onerror = null; - worker.terminate(); - }, currentConfig.threadTTL); - } - - onmessage(ev) { - this.ondone(ev.data); - } - - ondone(job) { - const resolve = this.jobs.get(job.id); - if ( resolve === undefined ) { return; } - this.jobs.delete(job.id); - resolve(job.result); - this.workload -= job.size; - if ( this.jobs.size !== 0 ) { return; } - this.countdownWorker(); - } - - async serialize(data, options) { - return new Promise(resolve => { - const id = this.jobIdGenerator++; - this.workload += 1; - this.jobs.set(id, resolve); - return this.workerPromise.then(worker => { - this.workerAccessTime = Date.now(); - if ( worker === null ) { - this.ondone({ id, result: serialize(data, options), size: 1 }); - } else { - worker.postMessage({ what: THREAD_SERIALIZE, id, data, options, size: 1 }); - } - }); - }); - } - - async deserialize(data, options) { - return new Promise(resolve => { - const id = this.jobIdGenerator++; - const size = data.length; - this.workload += size; - this.jobs.set(id, resolve); - return this.workerPromise.then(worker => { - this.workerAccessTime = Date.now(); - if ( worker === null ) { - this.ondone({ id, result: deserialize(data, options), size }); - } else { - worker.postMessage({ what: THREAD_DESERIALIZE, id, data, options, size }); - } - }); - }); - } - - get queueSize() { - return this.jobs.size; - } - - get workSize() { - return this.workload; - } -} - -const threads = { - pool: [ new MainThread() ], - thread(maxPoolSize) { - const poolSize = this.pool.length; - if ( poolSize !== 0 && poolSize >= maxPoolSize ) { - if ( poolSize === 1 ) { return this.pool[0]; } - return this.pool.reduce((a, b) => { - //console.log(`${a.name}: q=${a.queueSize} w=${a.workSize} ${b.name}: q=${b.queueSize} w=${b.workSize}`); - if ( b.queueSize === 0 ) { return b; } - if ( a.queueSize === 0 ) { return a; } - return b.workSize < a.workSize ? b : a; - }); - } - const thread = new Thread(thread => { - const pos = this.pool.indexOf(thread); - if ( pos === -1 ) { return; } - this.pool.splice(pos, 1); - }); - this.pool.push(thread); - return thread; - }, -}; - export async function serializeAsync(data, options = {}) { - const maxThreadCount = options.multithreaded || 0; - if ( maxThreadCount === 0 ) { - return serialize(data, options); - } - const thread = threads.thread(maxThreadCount); - //console.log(`serializeAsync: thread=${thread.name} workload=${thread.workSize}`); - const result = await thread.serialize(data, options); - if ( result !== undefined ) { return result; } return serialize(data, options); } export async function deserializeAsync(data, options = {}) { if ( isSerialized(data) === false ) { return data; } - const maxThreadCount = options.multithreaded || 0; - if ( maxThreadCount === 0 ) { - return deserialize(data, options); - } - const thread = threads.thread(maxThreadCount); - //console.log(`deserializeAsync: thread=${thread.name} data=${data.length} workload=${thread.workSize}`); - const result = await thread.deserialize(data, options); - if ( result !== undefined ) { return result; } return deserialize(data, options); } -/******************************************************************************* - * - * Worker-only code - * - * */ - -if ( isInstanceOf(globalThis, 'DedicatedWorkerGlobalScope') ) { - globalThis.onmessage = ev => { - const msg = ev.data; - switch ( msg.what ) { - case THREAD_AREYOUREADY: - setConfig(msg.config); - globalThis.postMessage({ what: THREAD_IAMREADY }); - break; - case THREAD_SERIALIZE: { - const result = serialize(msg.data, msg.options); - globalThis.postMessage({ id: msg.id, size: msg.size, result }); - break; - } - case THREAD_DESERIALIZE: { - let result; - try { - result = deserialize(msg.data); - } catch(ex) { - console.error(ex); - } finally { - globalThis.postMessage({ id: msg.id, size: msg.size, result }); - } - break; - } - default: - break; - } - }; -} - /******************************************************************************/ diff --git a/src/js/storage.js b/src/js/storage.js index 758ae9bd3e593..66a3352394ab2 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -433,7 +433,10 @@ onBroadcast(msg => { } }).filter(prefix => prefix !== undefined); } + const match = /^[a-z-]+:\/\/[^/]+\//.exec(assetKey); + const assetOrigin = match && match[0]; for ( const prefix of this.parsedTrustedListPrefixes ) { + if ( assetOrigin && prefix.length < assetOrigin.length ) { continue; } if ( assetKey.startsWith(prefix) ) { return true; } } return false; diff --git a/src/web_accessible_resources/sensors-analytics.js b/src/web_accessible_resources/sensors-analytics.js new file mode 100644 index 0000000000000..74049ec43e607 --- /dev/null +++ b/src/web_accessible_resources/sensors-analytics.js @@ -0,0 +1,32 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2025-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 +*/ + +(function() { + 'use strict'; + const noopfn = function() { + }; + window.sensorsDataAnalytic201505 = { + init: noopfn, + quick: noopfn, + register: noopfn, + track: noopfn, + }; +})();