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,
+ };
+})();