Skip to content

Fork Sync: Update from parent repository - #336

Open
github-actions[bot] wants to merge 85 commits into
masterfrom
mirror
Open

Fork Sync: Update from parent repository#336
github-actions[bot] wants to merge 85 commits into
masterfrom
mirror

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

This PR was automatically created by a GitHub Action triggered by a cron schedule. Please review the changes and merge if appropriate.

gorhill and others added 30 commits August 12, 2026 13:11
As discussed internally with team.
…mplate` tags

As discussed internally with team.
Related commit:
gorhill@25d4138
* Add set/unset/closed/given cookie values

* Closed already added
Brave can't be told apart from Chrome through the user agent string, so
detection uses `navigator.brave` with `navigator.userAgentData.brands`
as fallback. Both are synchronous, unlike `navigator.brave.isBrave()` --
the flavor must be settled before filter lists are compiled and cached.

Brave still offers uBO in MV2, and a considerable number of Brave users
run Brave Shields and uBO at the same time. Filters which are safe on
their own can conflict when both blockers apply them, and list
maintainers currently have no way to express that:
uBlockOrigin/uAssets#34162
All supported browsers support user stylesheets, and in any case
filter list maintainers shouldn't have to worry about whether
user stylesheets are supported or not.
antonok-edm
antonok-edm previously approved these changes Sep 4, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Author

anthropic debug - [puLL-Merge] - brave/uBlock@336

Diff
diff --git assets/assets.json assets/assets.json
index 8c183004546ca..bc9f004f5da04 100644
--- assets/assets.json
+++ assets/assets.json
@@ -754,11 +754,11 @@
 		"content": "filters",
 		"group": "regions",
 		"off": true,
-		"title": "🇰🇷kr: List-KR Classic",
+		"title": "🇰🇷kr: 한국어 (Korean)",
 		"tags": "ads korean 한국어",
 		"lang": "ko",
-		"contentURL": "https://cdn.jsdelivr.net/npm/@list-kr/filterslists@latest/dist/filterslist-uBlockOrigin-classic.txt",
-		"supportURL": "https://github.com/List-KR/List-KR#readme"
+		"contentURL": "https://cdn.jsdelivr.net/npm/@filteringdev/filterslists-ko@latest/dist/filterslist-uBlockOrigin-classic.txt",
+		"supportURL": "https://github.com/FilteringDev/filterslists-KO#filteringdevfilterslists-ko"
 	},
 	"LTU-0": {
 		"content": "filters",
diff --git src/js/redirect-resources.js src/js/redirect-resources.js
index d9d835d81c1db..313d50ba6a0f0 100644
--- src/js/redirect-resources.js
+++ src/js/redirect-resources.js
@@ -99,6 +99,11 @@ export default new Map([
         alias: 'google-ima3',                       /* adguard compatibility */
         data: 'text',
     } ],
+    [ 'google-ima-dai.js', {
+        alias: 'google-ima3-dai',                   /* adguard compatibility */
+        data: 'text',
+        requiresTrust: true,
+    } ],
     [ 'googlesyndication_adsbygoogle.js', {
         alias: [
             'googlesyndication.com/adsbygoogle.js',
diff --git src/js/resources/cookie.js src/js/resources/cookie.js
index 69169071056ec..79cda65b6d82f 100644
--- src/js/resources/cookie.js
+++ src/js/resources/cookie.js
@@ -49,6 +49,8 @@ export function getSafeCookieValuesFn() {
         'decline', 'declined',
         'closed', 'next', 'mandatory',
         'disagree', 'agree',
+        'set', 'unset',
+        'given',
     ];
 }
 registerScriptlet(getSafeCookieValuesFn, {
diff --git src/js/resources/prevent-addeventlistener.js src/js/resources/prevent-addeventlistener.js
index 7a9666ab71de1..1321edacaf9d5 100644
--- src/js/resources/prevent-addeventlistener.js
+++ src/js/resources/prevent-addeventlistener.js
@@ -91,8 +91,8 @@ function preventAddEventListener(
         return parts.join('');
     };
     const shouldPrevent = (thisArg, type, handler) => {
-        const matchesType = safe.RegExp_test.call(reType, type);
-        const matchesHandler = safe.RegExp_test.call(rePattern, handler);
+        const matchesType = safe.RegExp_test(reType, type);
+        const matchesHandler = safe.RegExp_test(rePattern, handler);
         const matchesEither = matchesType || matchesHandler;
         const matchesBoth = matchesType && matchesHandler;
         if ( safe.logLevel > 1 && matchesEither ) {
diff --git src/js/resources/prevent-clipboard-write.js src/js/resources/prevent-clipboard-write.js
index 73b562dd64db2..a95308fe9e7c1 100644
--- src/js/resources/prevent-clipboard-write.js
+++ src/js/resources/prevent-clipboard-write.js
@@ -54,40 +54,51 @@ function preventClipboardWrite(matches = '', ...varargs) {
     const extraArgs = safe.parseVarargs(varargs);
     const excludePattern = extraArgs.excludeMatches &&
         safe.initPattern(extraArgs.excludeMatches);
+    const htmlTemplate = [
+        '<div style="background-color:beige;color:black;border:1px solid black;display:flex;font-family:sans-serif;font-size:medium;position:fixed;top:0;white-space:pre-wrap;width:100%;z-index:2147483647">',
+            '<span style="flex-grow:1;padding:0.5em 0 0.5em 0.5em;">${warning}</span>\n',
+            '<button style="font-size:32px;padding:0.5em">×</button>',
+        '</div>',
+    ].join('');
     const domAlert = clipboardText => {
         const doc = document;
-        const div = doc.createElement('div');
-        const span = doc.createElement('span');
-        span.style = 'flex-grow:1;padding:0.5em 0 0.5em 0.5em;';
-        const { domAlert } = extraArgs;
-        const placeholder = /\$\{text\}/.exec(domAlert);
-        if ( placeholder ) {
+        const domAlert = extraArgs.domAlert.replace(/\\n/g, '\n');
+        let html;
+        if ( domAlert.includes('${text}') ) {
             const code = doc.createElement('code');
-            code.style = 'background-color:#ddc;font-family:monospace;padding:0.25em;user-select:none;word-break:break-all';
+            const styles = [
+                'background-color: #ddc',
+                'display: inline-block',
+                'font-family: monospace',
+                'font-size: 100%',
+                'max-height: 8em',
+                'overflow: auto',
+                'padding: 0.25em',
+                'width: 100%;',
+                'word-break: break-all'
+            ];
+            if ( Boolean(extraArgs.selectable ?? true) === false ) {
+                styles.push('user-select: none');
+            }
+            code.style = styles.join(';');
             code.textContent = clipboardText;
-            span.append(
-                domAlert.slice(0, placeholder.index),
-                code,
-                domAlert.slice(placeholder.index + placeholder[0].length)
+            html = htmlTemplate.replace('${warning}',
+                domAlert.replace('${text}', code.outerHTML)
             );
         } else {
-            span.append(domAlert);
+            html = htmlTemplate.replace('${warning}', domAlert);
         }
-        const button = doc.createElement('button');
-        button.style = 'padding:1em';
-        button.textContent = '×';
+        if ( currentAlert ) { currentAlert.remove(); }
+        const domParser = new DOMParser();
+        const fragment = domParser.parseFromString(html, 'text/html');
+        currentAlert = fragment.querySelector('div');
+        const button = currentAlert.querySelector('button');
         button.addEventListener('click', ( ) => {
             if ( currentAlert === null ) { return; }
             currentAlert.remove();
             currentAlert = null;
         });
-        div.append(span, button);
-        div.style = 'background-color:beige;color:black;border:1px solid black;display:flex;font-size:medium;position:fixed;text-align:center;top:0;width:100%;z-index:2147483647';
-        doc.documentElement.append(div);
-        if ( currentAlert ) {
-            currentAlert.remove();
-        }
-        currentAlert = div;
+        doc.documentElement.append(currentAlert);
     };
     let currentAlert = null;
     const prevent = text => {
diff --git src/js/resources/prevent-xhr.js src/js/resources/prevent-xhr.js
index e0bce3b8c9840..478470872a70f 100644
--- src/js/resources/prevent-xhr.js
+++ src/js/resources/prevent-xhr.js
@@ -25,7 +25,10 @@ import {
     matchObjectPropertiesFn,
     parsePropertiesToMatchFn,
 } from './utils.js';
-import { proxyApplyFn } from './proxy-apply.js';
+import {
+    proxyApplyFn,
+    proxyToStringFn,
+} from './proxy-apply.js';
 import { registerScriptlet } from './base.js';
 import { safeSelf } from './safe-self.js';
 
@@ -34,6 +37,62 @@ import { safeSelf } from './safe-self.js';
 
 /******************************************************************************/
 
+export function modifyXhrResponseFn(
+    propsToMatch = '',
+    modifierFn = ''
+) {
+    if ( typeof propsToMatch !== 'string' ) { return; }
+    const safe = safeSelf();
+    if ( modifyXhrResponseFn.xhrInstances === undefined ) {
+        modifyXhrResponseFn.xhrInstances = new WeakMap();
+    }
+    const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
+    const NativeXMLHttpRequest = self.XMLHttpRequest;
+    const TrappedXMLHttpRequest = class XMLHttpRequest extends NativeXMLHttpRequest {
+        open(method, url, ...args) {
+            const haystack = { method, url };
+            if ( propsToMatch === '' ) {
+                safe.uboLog(`modifyXhrResponseFn() / Called: ${safe.JSON_stringify(haystack, null, 2)}`);
+            } else if ( matchObjectPropertiesFn(propNeedles, haystack) ) {
+                modifyXhrResponseFn.xhrInstances.set(this, modifierFn);
+            }
+            return super.open(method, url, ...args);
+        }
+        get response() {
+            const modifierFn = modifyXhrResponseFn.xhrInstances.get(this);
+            return modifierFn
+                ? modifierFn(this, super.response)
+                : super.response;
+        }
+        get responseText() {
+            const modifierFn = modifyXhrResponseFn.xhrInstances.get(this);
+            return modifierFn
+                ? modifierFn(this, super.responseText)
+                : super.responseText;
+        }
+        get responseXML() {
+            const modifierFn = modifyXhrResponseFn.xhrInstances.get(this);
+            return modifierFn
+                ? modifierFn(this, super.responseXML)
+                : super.responseXML;
+        }
+    };
+    proxyToStringFn(TrappedXMLHttpRequest.prototype.open, NativeXMLHttpRequest.prototype.open);
+    proxyToStringFn(TrappedXMLHttpRequest, NativeXMLHttpRequest);
+    self.XMLHttpRequest = TrappedXMLHttpRequest;
+}
+registerScriptlet(modifyXhrResponseFn, {
+    name: 'modify-xhr-response.fn',
+    dependencies: [
+        matchObjectPropertiesFn,
+        parsePropertiesToMatchFn,
+        proxyToStringFn,
+        safeSelf,
+    ],
+});
+
+/******************************************************************************/
+
 function preventXhrFn(
     trusted = false,
     propsToMatch = '',
diff --git src/js/resources/proxy-apply.js src/js/resources/proxy-apply.js
index c64b043e341f9..9f32a643ff387 100644
--- src/js/resources/proxy-apply.js
+++ src/js/resources/proxy-apply.js
@@ -24,6 +24,32 @@ import { registerScriptlet } from './base.js';
 
 /******************************************************************************/
 
+export function proxyToStringFn(proxiedFn, nativeFn) {
+    if ( proxyToStringFn.proxies === undefined ) {
+        proxyToStringFn.proxies = new WeakMap();
+        proxyToStringFn.nativeToString = Function.prototype.toString;
+        const proxiedToString = new Proxy(Function.prototype.toString, {
+            apply(target, thisArg) {
+                let proxied = thisArg;
+                for(;;) {
+                    const fn = proxyToStringFn.proxies.get(proxied);
+                    if ( fn === undefined ) { break; }
+                    proxied = fn;
+                }
+                return proxyToStringFn.nativeToString.call(proxied);
+            }
+        });
+        proxyToStringFn.proxies.set(proxiedToString, proxyToStringFn.nativeToString);
+        Function.prototype.toString = proxiedToString;
+    }
+    proxyToStringFn.proxies.set(proxiedFn, nativeFn);
+}
+registerScriptlet(proxyToStringFn, {
+    name: 'proxy-tostring.fn',
+});
+
+/******************************************************************************/
+
 export function proxyApplyFn(
     target = '',
     handler = '',
diff --git src/js/resources/replace-argument.js src/js/resources/replace-argument.js
index 783fac37abd1d..0c98217a44e02 100644
--- src/js/resources/replace-argument.js
+++ src/js/resources/replace-argument.js
@@ -109,7 +109,7 @@ export function trustedReplaceArgument(
         }
         const argBefore = getArg(context);
         if ( extraArgs.condition !== undefined ) {
-            if ( safe.RegExp_test.call(reCondition, argBefore) === false ) {
+            if ( safe.RegExp_test(reCondition, argBefore) === false ) {
                 return context.reflect();
             }
         }
diff --git src/js/resources/safe-self.js src/js/resources/safe-self.js
index 4a48fd8a119e9..19a06f9404916 100644
--- src/js/resources/safe-self.js
+++ src/js/resources/safe-self.js
@@ -35,8 +35,7 @@ export function safeSelf() {
     const safe = {
         'Array_from': Array.from,
         'Error': self.Error,
-        'Function_toStringFn': self.Function.prototype.toString,
-        'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
+        'Function_toString': Function.prototype.call.bind(self.Function.prototype.toString),
         'Math_floor': Math.floor,
         'Math_max': Math.max,
         'Math_min': Math.min,
@@ -49,7 +48,7 @@ export function safeSelf() {
         'Object_hasOwn': Object.hasOwn.bind(Object),
         'Object_toString': Object.prototype.toString,
         'RegExp': self.RegExp,
-        'RegExp_test': self.RegExp.prototype.test,
+        'RegExp_test': Function.prototype.call.bind(self.RegExp.prototype.test),
         'RegExp_exec': self.RegExp.prototype.exec,
         'Request_clone': self.Request.prototype.clone,
         'String': self.String,
@@ -60,10 +59,8 @@ export function safeSelf() {
         'removeEventListener': self.EventTarget.prototype.removeEventListener,
         'fetch': self.fetch,
         'JSON': self.JSON,
-        'JSON_parseFn': self.JSON.parse,
-        'JSON_stringifyFn': self.JSON.stringify,
-        'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args),
-        'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args),
+        'JSON_parse': Function.prototype.call.bind(self.JSON.parse, self.JSON),
+        'JSON_stringify': Function.prototype.call.bind(self.JSON.stringify, self.JSON),
         'log': console.log.bind(console),
         // Properties
         logLevel: 0,
@@ -116,7 +113,7 @@ export function safeSelf() {
         testPattern(details, haystack) {
             if ( details.matchAll ) { return true; }
             if ( details.re ) {
-                return this.RegExp_test.call(details.re, haystack) === details.expect;
+                return this.RegExp_test(details.re, haystack) === details.expect;
             }
             return haystack.includes(details.pattern) === details.expect;
         },
diff --git src/js/resources/scriptlets.js src/js/resources/scriptlets.js
index 6950bed588110..cc733487d75aa 100755
--- src/js/resources/scriptlets.js
+++ src/js/resources/scriptlets.js
@@ -36,9 +36,9 @@ import './prevent-fetch.js';
 import './prevent-innerHTML.js';
 import './prevent-navigation.js';
 import './prevent-settimeout.js';
-import './prevent-xhr.js';
 import './replace-argument.js';
 import './spoof-css.js';
+import './vod.js';
 
 import {
     collateFetchArgumentsFn,
@@ -49,11 +49,15 @@ import {
     onIdleFn,
     parsePropertiesToMatchFn,
 } from './utils.js';
-import { runAt, runAtHtmlElementFn } from './run-at.js';
+import {
+    runAt,
+    runAtHtmlElementFn,
+} from './run-at.js';
 
 import { getAllCookiesFn } from './cookie.js';
 import { getAllLocalStorageFn } from './localstorage.js';
 import { matchesStackTraceFn } from './stack-trace.js';
+import { modifyXhrResponseFn } from './prevent-xhr.js';
 import { proxyApplyFn } from './proxy-apply.js';
 import { registeredScriptlets } from './base.js';
 import { safeSelf } from './safe-self.js';
@@ -97,15 +101,6 @@ function replaceNodeTextFn(
     const reExcludes = extraArgs.excludes
         ? safe.patternToRegex(extraArgs.excludes, 'ms')
         : null;
-    const stop = (takeRecord = true) => {
-        if ( takeRecord ) {
-            handleMutations(observer.takeRecords());
-        }
-        observer.disconnect();
-        if ( safe.logLevel > 1 ) {
-            safe.uboLog(logPrefix, 'Quitting');
-        }
-    };
     const textContentFactory = (( ) => {
         const out = { createScript: s => s };
         const { trustedTypes: tt } = self;
@@ -118,19 +113,19 @@ function replaceNodeTextFn(
         }
         return out;
     })();
-    let sedCount = extraArgs.sedCount || 0;
+    let sedCount = extraArgs.sedCount ?? Number.MAX_SAFE_INTEGER;
     const handleNode = node => {
         const before = node.textContent;
         if ( reIncludes ) {
             reIncludes.lastIndex = 0;
-            if ( safe.RegExp_test.call(reIncludes, before) === false ) { return true; }
+            if ( safe.RegExp_test(reIncludes, before) === false ) { return; }
         }
         if ( reExcludes ) {
             reExcludes.lastIndex = 0;
-            if ( safe.RegExp_test.call(reExcludes, before) ) { return true; }
+            if ( safe.RegExp_test(reExcludes, before) ) { return; }
         }
         rePattern.lastIndex = 0;
-        if ( safe.RegExp_test.call(rePattern, before) === false ) { return true; }
+        if ( safe.RegExp_test(rePattern, before) === false ) { return; }
         rePattern.lastIndex = 0;
         const after = pattern !== ''
             ? before.replace(rePattern, replacement)
@@ -142,44 +137,65 @@ function replaceNodeTextFn(
             safe.uboLog(logPrefix, `Text before:\n${before.trim()}`);
         }
         safe.uboLog(logPrefix, `Text after:\n${after.trim()}`);
-        return sedCount === 0 || (sedCount -= 1) !== 0;
+        sedCount -= 1;
     };
-    const handleMutations = mutations => {
-        for ( const mutation of mutations ) {
-            for ( const node of mutation.addedNodes ) {
-                if ( reNodeName.test(node.nodeName) === false ) { continue; }
-                if ( handleNode(node) ) { continue; }
-                stop(false); return;
-            }
-        }
-    };
-    const observer = new MutationObserver(handleMutations);
-    observer.observe(document, { childList: true, subtree: true });
-    if ( document.documentElement ) {
-        const treeWalker = document.createTreeWalker(
-            document.documentElement,
+    const handleTree = root => {
+        const treeWalker = document.createTreeWalker(root,
             NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
         );
+        const { currentScript } = document;
         let count = 0;
         for (;;) {
             const node = treeWalker.nextNode();
-            count += 1;
             if ( node === null ) { break; }
-            if ( reNodeName.test(node.nodeName) === false ) { continue; }
-            if ( node === document.currentScript ) { continue; }
-            if ( handleNode(node) ) { continue; }
-            stop(); break;
+            count += 1;
+            if ( node === currentScript ) { continue; }
+            if ( reNodeName.test(node.nodeName) ) {
+                handleNode(node);
+            } else if ( node.nodeName === 'TEMPLATE' ) {
+                count += handleTree(node.content);
+            } else {
+                continue;
+            }
+            if ( sedCount === 0 ) { break; }
         }
+        return count;
+    };
+    if ( document.documentElement ) {
+        const count = handleTree(document.documentElement);
         safe.uboLog(logPrefix, `${count} nodes present before installing mutation observer`);
     }
-    if ( extraArgs.stay ) { return; }
-    runAt(( ) => {
-        const quitAfter = extraArgs.quitAfter || 0;
-        if ( quitAfter !== 0 ) {
-            setTimeout(( ) => { stop(); }, quitAfter);
-        } else {
-            stop();
+    const stay = Boolean(extraArgs.stay);
+    if ( sedCount === 0 && stay === false ) { return; }
+    const stop = (takeRecord = true) => {
+        const mutations = takeRecord ? observer.takeRecords() : [];
+        observer.disconnect();
+        handleMutations(mutations);
+        if ( safe.logLevel > 1 ) {
+            safe.uboLog(logPrefix, 'Quitting');
+        }
+    };
+    const handleMutations = mutations => {
+        for ( const mutation of mutations ) {
+            for ( const node of mutation.addedNodes ) {
+                if ( reNodeName.test(node.nodeName) ) {
+                    handleNode(node);
+                } else if ( node.nodeName === 'TEMPLATE' ) {
+                    handleTree(node.content);
+                } else {
+                    continue;
+                }
+                if ( sedCount === 0 ) { return stop(false); }
+            }
         }
+    };
+    const observer = new MutationObserver(handleMutations);
+    observer.observe(document, { childList: true, subtree: true });
+    if ( stay ) { return; }
+    runAt(( ) => {
+        const quitAfter = extraArgs.quitAfter ?? 0;
+        if ( quitAfter === 0 ) { return stop(); }
+        setTimeout(( ) => { stop(); }, quitAfter);
     }, 'interactive');
 }
 
@@ -984,6 +1000,7 @@ builtinScriptlets.push({
     name: 'xml-prune.js',
     fn: xmlPrune,
     dependencies: [
+        'modify-xhr-response.fn',
         'safe-self.fn',
     ],
 });
@@ -1089,41 +1106,14 @@ function xmlPrune(
             });
         }
     });
-    self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, {
-        apply: async (target, thisArg, args) => {
-            if ( reUrl.test(urlFromArg(args[1])) === false ) {
-                return Reflect.apply(target, thisArg, args);
-            }
-            thisArg.addEventListener('readystatechange', function() {
-                if ( thisArg.readyState !== 4 ) { return; }
-                const type = thisArg.responseType;
-                if (
-                    type === 'document' ||
-                    type === '' && thisArg.responseXML instanceof XMLDocument
-                ) {
-                    pruneFromDoc(thisArg.responseXML);
-                    const serializer = new XMLSerializer();
-                    const textout = serializer.serializeToString(thisArg.responseXML);
-                    Object.defineProperty(thisArg, 'responseText', { value: textout });
-                    if ( typeof thisArg.response === 'string' ) {
-                        Object.defineProperty(thisArg, 'response', { value: textout });
-                    }
-                    return;
-                }
-                if (
-                    type === 'text' ||
-                    type === '' && typeof thisArg.responseText === 'string'
-                ) {
-                    const textin = thisArg.responseText;
-                    const textout = pruneFromText(textin);
-                    if ( textout === textin ) { return; }
-                    Object.defineProperty(thisArg, 'response', { value: textout });
-                    Object.defineProperty(thisArg, 'responseText', { value: textout });
-                    return;
-                }
-            });
-            return Reflect.apply(target, thisArg, args);
+    modifyXhrResponseFn(urlPattern, (xhr, before) => {
+        if ( before instanceof XMLDocument ) {
+            return pruneFromDoc(before);
+        }
+        if ( typeof before === 'string' ) {
+            return pruneFromText(before);
         }
+        return before;
     });
 }
 
@@ -1935,7 +1925,7 @@ function trustedSuppressNativeMethod(
                 }
             }
             if ( signatureArg.type === 'pattern' ) {
-                if ( safe.RegExp_test.call(signatureArg.re, targetArg) === false ) {
+                if ( safe.RegExp_test(signatureArg.re, targetArg) === false ) {
                     return context.reflect();
                 }
             }
--- /dev/null
+++ src/js/resources/vod.js
@@ -0,0 +1,175 @@
+/*******************************************************************************
+
+    uBlock Origin - a comprehensive, efficient content blocker
+    Copyright (C) 2026-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 { modifyXhrResponseFn } from './prevent-xhr.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+export function mpegdashPrune(
+    selector = '',
+    propsToMatch = ''
+) {
+    if ( typeof selector !== 'string' ) { return; }
+    if ( selector === '' ) { return; }
+    const safe = safeSelf();
+    const logPrefix = safe.makeLogPrefix('mpegdash-prune', selector, propsToMatch);
+    const queryAll = (xmlDoc, selector) => {
+        if ( selector.startsWith('xpath:') === false ) {
+            return Array.from(xmlDoc.querySelectorAll(selector));
+        }
+        const xpr = xmlDoc.evaluate(
+            selector.slice(6),
+            xmlDoc,
+            null,
+            XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+            null
+        );
+        const out = [];
+        for ( let i = 0; i < xpr.snapshotLength; i++ ) {
+            const node = xpr.snapshotItem(i);
+            out.push(node);
+        }
+        return out;
+    };
+    const rePTparse = /^PT(\d+D)?(\d+H)?(\d+M)?([\d.]+S)?$/;
+    const secondsPerDay = 24 * 60 * 60;
+    const secondsPerHour = 60 * 60;
+    const secondsPerMinute = 60;
+    const secondsFromPT = pt => {
+        const match = rePTparse.exec(pt);
+        if ( match === null ) { return; }
+        let seconds = 0;
+        if ( match[1] ) {
+            const d = parseFloat(match[1].slice(0, -1));
+            if ( isNaN(d) ) { return; }
+            seconds += d * secondsPerDay;
+        }
+        if ( match[2] ) {
+            const h = parseFloat(match[2].slice(0, -1));
+            if ( isNaN(h) ) { return; }
+            seconds += h * secondsPerHour;
+        }
+        if ( match[3] ) {
+            const m = parseFloat(match[3].slice(0, -1));
+            if ( isNaN(m) ) { return; }
+            seconds += m * secondsPerMinute;
+        }
+        if ( match[4] ) {
+            const s = parseFloat(match[4].slice(0, -1));
+            if ( isNaN(s) ) { return; }
+            seconds += s;
+        }
+        return seconds;
+    };
+    const ptFromSeconds = seconds => {
+        const parts = [ 'PT' ];
+        const d = Math.floor(seconds / secondsPerDay);
+        if ( d ) {
+            parts.push(`${d}D`);
+            seconds -= d * secondsPerDay;
+        }
+        const h = Math.floor(seconds / secondsPerHour);
+        if ( h ) {
+            parts.push(`${h}H`);
+            seconds -= h * secondsPerHour;
+        }
+        const m = Math.floor(seconds / secondsPerMinute);
+        if ( m ) {
+            parts.push(`${m}M`);
+            seconds -= m * secondsPerMinute;
+        }
+        parts.push(`${seconds}S`);
+        return parts.join('');
+    };
+    const fixTimeAttributes = xmlDoc => {
+        try {
+            const periods = queryAll(xmlDoc, 'MPD > Period');
+            if ( periods.length === 0 ) { return; }
+            let seconds = 0;
+            for ( const period of periods ) {
+                const startAttrBefore = period.getAttribute('start');
+                const durAttr = period.getAttribute('duration');
+                if ( startAttrBefore === null || durAttr === null ) { continue; }
+                const startAttrAfter = ptFromSeconds(seconds);
+                period.setAttribute('start', startAttrAfter);
+                if ( period.hasAttribute('id') ) {
+                    const idAttr = period.getAttribute('id');
+                    period.setAttribute('id', idAttr.replace(startAttrBefore, startAttrAfter));
+                }
+                seconds += secondsFromPT(durAttr);
+            }
+            const mpds = queryAll(xmlDoc, 'MPD[mediaPresentationDuration]');
+            if ( mpds.length !== 1 ) { return; }
+            mpds[0].setAttribute('mediaPresentationDuration', ptFromSeconds(seconds));
+        } catch {
+        }
+    };
+    const pruneFromDoc = xmlDoc => {
+        try {
+            if ( selector === '' ) {
+                const serializer = new XMLSerializer();
+                safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`);
+            }
+            const items = queryAll(xmlDoc, selector);
+            if ( items.length === 0 ) { return xmlDoc; }
+            safe.uboLog(logPrefix, `Patching ${items.length} items`);
+            for ( const item of items ) {
+                if ( item.nodeType !== 1 ) { continue; }
+                item.setAttribute('duration', 'PT0S');
+            }
+            fixTimeAttributes(xmlDoc);
+        } catch(ex) {
+            safe.uboErr(logPrefix, `Error: ${ex}`);
+        }
+        return xmlDoc;
+    };
+    const pruneFromText = text => {
+        if ( (/^\s*</.test(text) && />\s*$/.test(text)) === false ) {
+            return text;
+        }
+        try {
+            const xmlParser = new DOMParser();
+            const xmlDoc = xmlParser.parseFromString(text, 'text/xml');
+            pruneFromDoc(xmlDoc);
+            const serializer = new XMLSerializer();
+            text = serializer.serializeToString(xmlDoc);
+        } catch {
+        }
+        return text;
+    };
+    modifyXhrResponseFn(propsToMatch, (xhr, before) => {
+        if ( before instanceof XMLDocument ) {
+            return pruneFromDoc(before);
+        }
+        if ( typeof before === 'string' ) {
+            return pruneFromText(before);
+        }
+        return before;
+    });
+}
+registerScriptlet(mpegdashPrune, {
+    name: 'mpegdash-prune.js',
+    dependencies: [
+        modifyXhrResponseFn,
+        safeSelf,
+    ],
+});
--- /dev/null
+++ src/web_accessible_resources/google-ima-dai.js
@@ -0,0 +1,630 @@
+// Repo: https://github.com/AdguardTeam/Scriptlets
+// Source: https://github.com/AdguardTeam/Scriptlets/blob/master/src/redirects/google-ima3-dai.ts
+// License: https://github.com/AdguardTeam/Scriptlets/blob/master/LICENSE (GPLv3)
+// Commit: https://github.com/AdguardTeam/Scriptlets/blob/eedd995c41/src/redirects/google-ima3-dai.ts
+/* eslint-disable */ 
+(function(source, args) {
+    const flag = "done";
+    const uniqueIdentifier = source.uniqueId + source.name + "_" + (Array.isArray(args) ? args.join("_") : "");
+    if (source.uniqueId) {
+        if (Window.prototype.toString[uniqueIdentifier] === flag) {
+            return;
+        }
+    }
+    function GoogleIma3Dai(source) {
+        var streamEventTypes = {
+            AD_BREAK_ENDED: "adBreakEnded",
+            AD_BREAK_STARTED: "adBreakStarted",
+            AD_PERIOD_ENDED: "adPeriodEnded",
+            AD_PERIOD_STARTED: "adPeriodStarted",
+            AD_PROGRESS: "adProgress",
+            CLICK: "click",
+            COMPLETE: "complete",
+            CUEPOINTS_CHANGED: "cuepointsChanged",
+            ERROR: "error",
+            FIRST_QUARTILE: "firstquartile",
+            HIDE_AD_UI: "hideAdUi",
+            LOADED: "loaded",
+            MIDPOINT: "midpoint",
+            PAUSED: "paused",
+            RESUMED: "resumed",
+            SHOW_AD_UI: "showAdUi",
+            SKIPPABLE_STATE_CHANGED: "skippableStateChanged",
+            SKIPPED: "skip",
+            STARTED: "started",
+            STREAM_INITIALIZED: "streamInitialized",
+            THIRD_QUARTILE: "thirdquartile",
+            VIDEO_CLICKED: "videoClicked"
+        };
+        var isRecord = function isRecord(value) {
+            return typeof value === "object" && value !== null;
+        };
+        var schedule = function schedule(callback) {
+            if (typeof requestAnimationFrame === "function") {
+                requestAnimationFrame(callback);
+                return;
+            }
+            setTimeout(callback, 0);
+        };
+        var toStringRecord = function toStringRecord(value) {
+            if (!isRecord(value)) {
+                return {};
+            }
+            var result = {};
+            var propertyNames = Object.keys(value);
+            for (var propertyIndex = 0; propertyIndex < propertyNames.length; propertyIndex += 1) {
+                var propertyName = propertyNames[propertyIndex];
+                result[propertyName] = String(value[propertyName]);
+            }
+            return result;
+        };
+        var getStringValue = function getStringValue(value) {
+            return typeof value === "string" && value.length > 0 ? value : null;
+        };
+        var initializeEventHandler = function initializeEventHandler(instance) {
+            instance.listeners = new Map;
+        };
+        var normalizeEventTypes = function normalizeEventTypes(type) {
+            if (typeof type === "string") {
+                return [ type ];
+            }
+            if (!Array.isArray(type)) {
+                return [];
+            }
+            var eventTypes = [];
+            for (var eventTypeIndex = 0; eventTypeIndex < type.length; eventTypeIndex += 1) {
+                var eventType = type[eventTypeIndex];
+                if (typeof eventType === "string") {
+                    eventTypes.push(eventType);
+                }
+            }
+            return eventTypes;
+        };
+        var initializeStreamRequest = function initializeStreamRequest(instance, streamRequest) {
+            instance.adTagParameters = {};
+            instance.apiKey = null;
+            instance.authToken = null;
+            instance.format = "hls";
+            instance.networkCode = null;
+            instance.omidAccessModeRules = null;
+            instance.streamActivityMonitorId = null;
+            if (isRecord(streamRequest)) {
+                Object.assign(instance, streamRequest);
+            }
+            instance.adTagParameters = toStringRecord(instance.adTagParameters);
+            if (typeof instance.format !== "string" || instance.format.length === 0) {
+                instance.format = "hls";
+            }
+        };
+        var initializePodStreamRequest = function initializePodStreamRequest(instance, podStreamRequest) {
+            initializeStreamRequest(instance, podStreamRequest);
+            instance.customAssetKey = typeof instance.customAssetKey === "string" ? instance.customAssetKey : "";
+        };
+        var EventHandler = function EventHandler() {
+            initializeEventHandler(this);
+        };
+        EventHandler.prototype.addEventListener = function(type, listener) {
+            if (typeof listener !== "function") {
+                return;
+            }
+            var eventTypes = normalizeEventTypes(type);
+            for (var eventTypeIndex = 0; eventTypeIndex < eventTypes.length; eventTypeIndex += 1) {
+                var eventType = eventTypes[eventTypeIndex];
+                if (!this.listeners.has(eventType)) {
+                    this.listeners.set(eventType, new Set);
+                }
+                var listeners = this.listeners.get(eventType);
+                if (listeners) {
+                    listeners.add(listener);
+                }
+            }
+        };
+        EventHandler.prototype.removeEventListener = function(type, listener) {
+            if (typeof listener !== "function") {
+                return;
+            }
+            var eventTypes = normalizeEventTypes(type);
+            for (var eventTypeIndex = 0; eventTypeIndex < eventTypes.length; eventTypeIndex += 1) {
+                var listeners = this.listeners.get(eventTypes[eventTypeIndex]);
+                if (!listeners) {
+                    continue;
+                }
+                listeners.delete(listener);
+            }
+        };
+        EventHandler.prototype.dispatchEvent = function(streamEvent) {
+            var listeners = this.listeners.get(streamEvent.type);
+            if (!listeners) {
+                return;
+            }
+            for (var _i = 0, _Array$from = Array.from(listeners); _i < _Array$from.length; _i++) {
+                var _listener = _Array$from[_i];
+                try {
+                    _listener(streamEvent);
+                } catch (error) {
+                    logMessage(source, error);
+                }
+            }
+        };
+        var StreamRequest = function StreamRequest(streamRequest) {
+            initializeStreamRequest(this, streamRequest);
+        };
+        var LiveStreamRequest = function LiveStreamRequest(liveStreamRequest) {
+            initializeStreamRequest(this, liveStreamRequest);
+            this.assetKey = typeof this.assetKey === "string" ? this.assetKey : "";
+        };
+        Object.setPrototypeOf(LiveStreamRequest.prototype, StreamRequest.prototype);
+        var PodStreamRequest = function PodStreamRequest(podStreamRequest) {
+            initializePodStreamRequest(this, podStreamRequest);
+        };
+        Object.setPrototypeOf(PodStreamRequest.prototype, StreamRequest.prototype);
+        var VideoStitcherLiveStreamRequest = function VideoStitcherLiveStreamRequest(videoStitcherLiveStreamRequest) {
+            initializePodStreamRequest(this, videoStitcherLiveStreamRequest);
+            this.liveStreamEventId = typeof this.liveStreamEventId === "string" ? this.liveStreamEventId : "";
+            this.oAuthToken = typeof this.oAuthToken === "string" ? this.oAuthToken : null;
+            this.projectNumber = typeof this.projectNumber === "string" ? this.projectNumber : null;
+            this.region = typeof this.region === "string" ? this.region : null;
+            this.videoStitcherSessionOptions = isRecord(this.videoStitcherSessionOptions) ? this.videoStitcherSessionOptions : null;
+        };
+        Object.setPrototypeOf(VideoStitcherLiveStreamRequest.prototype, PodStreamRequest.prototype);
+        var VideoStitcherVodStreamRequest = function VideoStitcherVodStreamRequest(videoStitcherVodStreamRequest) {
+            initializeStreamRequest(this, videoStitcherVodStreamRequest);
+            this.adTagUrl = typeof this.adTagUrl === "string" ? this.adTagUrl : "";
+            this.contentSourceUrl = typeof this.contentSourceUrl === "string" ? this.contentSourceUrl : "";
+            this.oAuthToken = typeof this.oAuthToken === "string" ? this.oAuthToken : null;
+            this.projectNumber = typeof this.projectNumber === "string" ? this.projectNumber : null;
+            this.region = typeof this.region === "string" ? this.region : null;
+            this.videoStitcherSessionOptions = isRecord(this.videoStitcherSessionOptions) ? this.videoStitcherSessionOptions : null;
+            this.vodConfigId = typeof this.vodConfigId === "string" ? this.vodConfigId : "";
+        };
+        Object.setPrototypeOf(VideoStitcherVodStreamRequest.prototype, StreamRequest.prototype);
+        var VODStreamRequest = function VODStreamRequest(vodStreamRequest) {
+            initializeStreamRequest(this, vodStreamRequest);
+            this.contentSourceId = typeof this.contentSourceId === "string" ? this.contentSourceId : "";
+            this.videoId = typeof this.videoId === "string" ? this.videoId : "";
+        };
+        Object.setPrototypeOf(VODStreamRequest.prototype, StreamRequest.prototype);
+        var StreamData = function StreamData(streamData) {
+            this.adPeriodData = null;
+            this.adProgressData = null;
+            this.cuepoints = [];
+            this.errorMessage = null;
+            this.manifestFormat = "HLS";
+            this.streamId = null;
+            this.subtitles = [];
+            this.url = "";
+            if (isRecord(streamData)) {
+                Object.assign(this, streamData);
+            }
+        };
+        var StreamEvent = function StreamEvent(type, streamData, ad) {
+            this.type = type;
+            this.streamData = streamData || new StreamData;
+            this.ad = ad || null;
+        };
+        StreamEvent.Type = streamEventTypes;
+        StreamEvent.prototype.getAd = function() {
+            return this.ad;
+        };
+        StreamEvent.prototype.getStreamData = function() {
+            return this.streamData;
+        };
+        var UiSettings = function UiSettings() {
+            this.locale = "";
+        };
+        UiSettings.prototype.getLocale = function() {
+            return this.locale;
+        };
+        UiSettings.prototype.setLocale = function(locale) {
+            this.locale = locale;
+        };
+        var daiSdkFeatureFlagsStorage = new WeakMap;
+        var getStoredDaiSdkFeatureFlags = function getStoredDaiSdkFeatureFlags(instance) {
+            var storedFeatureFlags = daiSdkFeatureFlagsStorage.get(instance);
+            if (storedFeatureFlags) {
+                return storedFeatureFlags;
+            }
+            var nextFeatureFlags = {};
+            daiSdkFeatureFlagsStorage.set(instance, nextFeatureFlags);
+            return nextFeatureFlags;
+        };
+        var DaiSdkSettingsContainer = function DaiSdkSettingsContainer() {
+            daiSdkFeatureFlagsStorage.set(this, {});
+        };
+        DaiSdkSettingsContainer.prototype.getFeatureFlags = function() {
+            return getStoredDaiSdkFeatureFlags(this);
+        };
+        DaiSdkSettingsContainer.prototype.setFeatureFlags = function(featureFlags) {
+            daiSdkFeatureFlagsStorage.set(this, Object.assign({}, featureFlags));
+        };
+        var normalizeStreamRequest = function normalizeStreamRequest(streamRequest) {
+            if (streamRequest instanceof StreamRequest) {
+                return streamRequest;
+            }
+            if (isRecord(streamRequest)) {
+                if (typeof streamRequest.liveStreamEventId === "string") {
+                    return new VideoStitcherLiveStreamRequest(streamRequest);
+                }
+                if (typeof streamRequest.contentSourceUrl === "string" || typeof streamRequest.vodConfigId === "string" || typeof streamRequest.adTagUrl === "string") {
+                    return new VideoStitcherVodStreamRequest(streamRequest);
+                }
+                if (typeof streamRequest.customAssetKey === "string") {
+                    return new PodStreamRequest(streamRequest);
+                }
+                if (typeof streamRequest.assetKey === "string") {
+                    return new LiveStreamRequest(streamRequest);
+                }
+                if (typeof streamRequest.contentSourceId === "string" || typeof streamRequest.videoId === "string") {
+                    return new VODStreamRequest(streamRequest);
+                }
+            }
+            return new StreamRequest;
+        };
+        var hasLiveIdentifiers = function hasLiveIdentifiers(streamRequest) {
+            var liveStreamRequest = streamRequest;
+            return typeof liveStreamRequest.assetKey === "string" && liveStreamRequest.assetKey.length > 0;
+        };
+        var hasVideoStitcherLiveIdentifiers = function hasVideoStitcherLiveIdentifiers(streamRequest) {
+            var videoStitcherLiveStreamRequest = streamRequest;
+            return typeof videoStitcherLiveStreamRequest.liveStreamEventId === "string" && videoStitcherLiveStreamRequest.liveStreamEventId.length > 0;
+        };
+        var hasPodIdentifiers = function hasPodIdentifiers(streamRequest) {
+            var podStreamRequest = streamRequest;
+            return typeof podStreamRequest.networkCode === "string" && podStreamRequest.networkCode.length > 0 && typeof podStreamRequest.customAssetKey === "string" && podStreamRequest.customAssetKey.length > 0;
+        };
+        var hasVideoStitcherVodIdentifiers = function hasVideoStitcherVodIdentifiers(streamRequest) {
+            var videoStitcherVodStreamRequest = streamRequest;
+            var hasContentSourceUrl = typeof videoStitcherVodStreamRequest.contentSourceUrl === "string" && videoStitcherVodStreamRequest.contentSourceUrl.length > 0;
+            var hasVodConfigId = typeof videoStitcherVodStreamRequest.vodConfigId === "string" && videoStitcherVodStreamRequest.vodConfigId.length > 0;
+            return hasContentSourceUrl || hasVodConfigId;
+        };
+        var hasVodIdentifiers = function hasVodIdentifiers(streamRequest) {
+            var vodStreamRequest = streamRequest;
+            return typeof vodStreamRequest.contentSourceId === "string" && vodStreamRequest.contentSourceId.length > 0 && typeof vodStreamRequest.videoId === "string" && vodStreamRequest.videoId.length > 0;
+        };
+        var hasIdentifiers = function hasIdentifiers(streamRequest) {
+            return hasLiveIdentifiers(streamRequest) || hasPodIdentifiers(streamRequest) || hasVideoStitcherLiveIdentifiers(streamRequest) || hasVideoStitcherVodIdentifiers(streamRequest) || hasVodIdentifiers(streamRequest);
+        };
+        var getFallbackStreamId = function getFallbackStreamId(streamRequest) {
+            var liveStreamRequest = streamRequest;
+            var videoStitcherLiveStreamRequest = streamRequest;
+            var videoStitcherVodStreamRequest = streamRequest;
+            var vodStreamRequest = streamRequest;
+            if (typeof videoStitcherVodStreamRequest.vodConfigId === "string" && videoStitcherVodStreamRequest.vodConfigId.length > 0) {
+                return `mock-video-stitcher-vod-${videoStitcherVodStreamRequest.vodConfigId}`;
+            }
+            if (typeof videoStitcherVodStreamRequest.contentSourceUrl === "string" && videoStitcherVodStreamRequest.contentSourceUrl.length > 0) {
+                return "mock-video-stitcher-vod";
+            }
+            if (typeof videoStitcherLiveStreamRequest.liveStreamEventId === "string" && videoStitcherLiveStreamRequest.liveStreamEventId.length > 0) {
+                return `mock-video-stitcher-live-${videoStitcherLiveStreamRequest.liveStreamEventId}`;
+            }
+            if (typeof liveStreamRequest.assetKey === "string" && liveStreamRequest.assetKey.length > 0) {
+                return `mock-live-${liveStreamRequest.assetKey}`;
+            }
+            if (typeof vodStreamRequest.videoId === "string" && vodStreamRequest.videoId.length > 0) {
+                return `mock-vod-${vodStreamRequest.videoId}`;
+            }
+            return "mock-stream";
+        };
+        var getDefaultManifestFormat = function getDefaultManifestFormat(streamRequest) {
+            return typeof streamRequest.format === "string" && streamRequest.format.toLowerCase() === "dash" ? "DASH" : "HLS";
+        };
+        var createStreamData = function createStreamData(streamRequest, cuepoints, errorMessage, streamDataOverrides) {
+            var streamData = new StreamData({
+                cuepoints: cuepoints.slice(),
+                errorMessage: errorMessage,
+                manifestFormat: getDefaultManifestFormat(streamRequest),
+                streamId: getFallbackStreamId(streamRequest),
+                url: ""
+            });
+            if (isRecord(streamDataOverrides)) {
+                Object.assign(streamData, streamDataOverrides);
+            }
+            return streamData;
+        };
+        var appendRequestParameters = function appendRequestParameters(requestUrl, streamRequest) {
+            var parameterNames = Object.keys(streamRequest.adTagParameters);
+            for (var parameterIndex = 0; parameterIndex < parameterNames.length; parameterIndex += 1) {
+                var parameterName = parameterNames[parameterIndex];
+                requestUrl.searchParams.set(parameterName, streamRequest.adTagParameters[parameterName]);
+            }
+            if (streamRequest.apiKey) {
+                requestUrl.searchParams.set("api-key", streamRequest.apiKey);
+            }
+            if (streamRequest.authToken) {
+                requestUrl.searchParams.set("auth-token", streamRequest.authToken);
+            }
+            if (streamRequest.streamActivityMonitorId) {
+                requestUrl.searchParams.set("dai-sam-id", streamRequest.streamActivityMonitorId);
+            }
+        };
+        var buildLiveRequestUrl = function buildLiveRequestUrl(streamRequest, hostName) {
+            var requestUrl = new URL(`${hostName}/ssai/event/${streamRequest.assetKey}/streams`);
+            appendRequestParameters(requestUrl, streamRequest);
+            return requestUrl.toString();
+        };
+        var buildPodRequestUrl = function buildPodRequestUrl(streamRequest, hostName) {
+            var requestUrl = new URL(`${hostName}/ssai/pods/api/v1/network/${streamRequest.networkCode}` + `/custom_asset/${streamRequest.customAssetKey}/stream`);
+            var manifestType = streamRequest.format.toLowerCase() === "dash" ? "dash" : "hls";
+            appendRequestParameters(requestUrl, streamRequest);
+            requestUrl.searchParams.set("manifest-type", manifestType);
+            return requestUrl.toString();
+        };
+        var buildVodRequestUrl = function buildVodRequestUrl(streamRequest, hostName) {
+            var requestFormat = streamRequest.format.toLowerCase() === "dash" ? "dash" : "hls";
+            var requestUrl = new URL(`${hostName}/ondemand/${requestFormat}/content/${streamRequest.contentSourceId}` + `/vid/${streamRequest.videoId}/streams`);
+            appendRequestParameters(requestUrl, streamRequest);
+            return requestUrl.toString();
+        };
+        var MAIN_HOST_NAME = "https://dai.google.com";
+        var FALLBACK_HOST_NAME = "https://pubads.g.doubleclick.net";
+        var getStreamRequestUrls = function getStreamRequestUrls(streamRequest) {
+            if (hasPodIdentifiers(streamRequest)) {
+                return [ buildPodRequestUrl(streamRequest, MAIN_HOST_NAME), buildPodRequestUrl(streamRequest, FALLBACK_HOST_NAME) ];
+            }
+            if (hasLiveIdentifiers(streamRequest)) {
+                return [ buildLiveRequestUrl(streamRequest, MAIN_HOST_NAME), buildLiveRequestUrl(streamRequest, FALLBACK_HOST_NAME) ];
+            }
+            if (hasVodIdentifiers(streamRequest)) {
+                return [ buildVodRequestUrl(streamRequest, MAIN_HOST_NAME), buildVodRequestUrl(streamRequest, FALLBACK_HOST_NAME) ];
+            }
+            return [];
+        };
+        var readFetchResponseData = async function readFetchResponseData(response) {
+            var typedResponse = response;
+            if (typedResponse && typedResponse.ok === false) {
+                throw new Error(`Stream initialization failed with status ${String(typedResponse.status || 0)}`);
+            }
+            if (typedResponse && typeof typedResponse.json === "function") {
+                var jsonResponse = await typedResponse.json();
+                return isRecord(jsonResponse) ? jsonResponse : {};
+            }
+            return isRecord(response) ? response : {};
+        };
+        var getResponseErrorMessage = function getResponseErrorMessage(responseData) {
+            return getStringValue(responseData.errorMessage) || getStringValue(responseData.error_message);
+        };
+        var createStreamDataFromResponse = function createStreamDataFromResponse(streamRequest, cuepoints, responseData) {
+            var podManifestUrl = getStringValue(responseData.pod_manifest_url) || getStringValue(responseData.podManifestUrl) || "";
+            var responseStreamId = getStringValue(responseData.stream_id) || getStringValue(responseData.streamId);
+            var streamUrl = getStringValue(responseData.stream_manifest) || getStringValue(responseData.streamUrl) || podManifestUrl || "";
+            var responseErrorMessage = getResponseErrorMessage(responseData);
+            var hasInitializedStream = streamUrl.length > 0 || responseStreamId !== null && responseStreamId.length > 0;
+            var errorMessage = responseErrorMessage || (hasInitializedStream ? null : "Stream initialization response missing stream URL");
+            var manifestFormat = getStringValue(responseData.manifest_format) || getStringValue(responseData.manifestFormat) || getDefaultManifestFormat(streamRequest);
+            var streamId = responseStreamId || getFallbackStreamId(streamRequest);
+            var subtitles = Array.isArray(responseData.subtitles) ? responseData.subtitles : [];
+            return createStreamData(streamRequest, cuepoints, errorMessage, {
+                manifestFormat: manifestFormat,
+                podManifestUrl: podManifestUrl,
+                pod_manifest_url: podManifestUrl,
+                streamId: streamId,
+                subtitles: subtitles,
+                url: streamUrl
+            });
+        };
+        var getErrorMessage = function getErrorMessage(error) {
+            if (isRecord(error) && typeof error.message === "string" && error.message.length > 0) {
+                return error.message;
+            }
+            return "Stream initialization failed";
+        };
+        var hideAdUiElement = function hideAdUiElement(streamManager) {
+            if (!streamManager.adUiElement) {
+                return;
+            }
+            streamManager.adUiElement.style.display = "none";
+        };
+        var showVideoControls = function showVideoControls(streamManager) {
+            if (!streamManager.videoElement || streamManager.videoElement.controls) {
+                return;
+            }
+            streamManager.videoElement.controls = true;
+        };
+        var handleContentLoaded = function handleContentLoaded(streamManager) {
+            hideAdUiElement(streamManager);
+            showVideoControls(streamManager);
+        };
+        var isContentLoadedEventType = function isContentLoadedEventType(eventType) {
+            return eventType === StreamEvent.Type.LOADED || eventType === StreamEvent.Type.STREAM_INITIALIZED;
+        };
+        var StreamManager = function StreamManager(videoElement, adUiElement, uiSettings) {
+            initializeEventHandler(this);
+            this.videoElement = videoElement || null;
+            this.adUiElement = adUiElement || null;
+            this.uiSettings = uiSettings || new UiSettings;
+            this.clickElement = adUiElement || null;
+            this.streamData = new StreamData;
+            this.streamMonitor = {};
+            this.streamRequest = null;
+            this.cuepoints = [];
+            this.lastMetadata = null;
+            this.lastTimedMetadata = null;
+        };
+        Object.setPrototypeOf(StreamManager.prototype, EventHandler.prototype);
+        StreamManager.prototype.contentTimeForStreamTime = function(streamTime) {
+            return typeof streamTime === "number" ? streamTime : 0;
+        };
+        StreamManager.prototype.destroy = function() {
+            this.reset();
+        };
+        StreamManager.prototype.focus = function() {
+            var clickElement = this.clickElement;
+            if (!clickElement || typeof clickElement.focus !== "function") {
+                return;
+            }
+            try {
+                clickElement.focus();
+            } catch (error) {
+                logMessage(source, error);
+            }
+        };
+        StreamManager.prototype.getAdSkippableState = function() {
+            return true;
+        };
+        StreamManager.prototype.getStreamData = function() {
+            return this.streamData;
+        };
+        StreamManager.prototype.loadStreamMetadata = function() {
+            handleContentLoaded(this);
+            this.dispatchEvent(new StreamEvent(StreamEvent.Type.LOADED, this.streamData));
+        };
+        StreamManager.prototype.onTimedMetadata = function(metadata) {
+            this.lastTimedMetadata = metadata;
+        };
+        StreamManager.prototype.previousCuePointForStreamTime = function(streamTime) {
+            var previousCuePoint = null;
+            for (var cuepointIndex = 0; cuepointIndex < this.cuepoints.length; cuepointIndex += 1) {
+                var cuepoint = this.cuepoints[cuepointIndex];
+                if (typeof cuepoint.start !== "number") {
+                    continue;
+                }
+                if (cuepoint.start <= streamTime) {
+                    previousCuePoint = cuepoint;
+                }
+            }
+            return previousCuePoint;
+        };
+        StreamManager.prototype.processMetadata = function(type, data, timestamp) {
+            this.lastMetadata = {
+                data: data,
+                timestamp: timestamp,
+                type: type
+            };
+        };
+        StreamManager.prototype.replaceAdTagParameters = function(adTagParameters) {
+            if (!this.streamRequest) {
+                this.streamRequest = new StreamRequest;
+            }
+            this.streamRequest.adTagParameters = toStringRecord(adTagParameters);
+        };
+        StreamManager.prototype.requestStream = function(streamRequest) {
+            var _this = this;
+            var normalizedRequest = normalizeStreamRequest(streamRequest);
+            this.streamRequest = normalizedRequest;
+            var dispatchStreamEvent = function dispatchStreamEvent(eventType, streamData) {
+                _this.streamData = streamData;
+                if (isContentLoadedEventType(eventType)) {
+                    handleContentLoaded(_this);
+                }
+                schedule((function() {
+                    _this.dispatchEvent(new StreamEvent(StreamEvent.Type.STREAM_INITIALIZED, streamData));
+                    _this.dispatchEvent(new StreamEvent(eventType, streamData));
+                }));
+            };
+            if (!hasIdentifiers(normalizedRequest)) {
+                dispatchStreamEvent(StreamEvent.Type.ERROR, createStreamData(normalizedRequest, this.cuepoints, "Missing stream request identifiers"));
+                return;
+            }
+            var requestUrls = getStreamRequestUrls(normalizedRequest);
+            if (requestUrls.length > 0) {
+                var activeRequest = normalizedRequest;
+                var _fetchStreamData = async function fetchStreamData(requestIndex) {
+                    try {
+                        var fetchResponse = await fetch(requestUrls[requestIndex], {
+                            method: "POST",
+                            credentials: "include"
+                        });
+                        var responseData = await readFetchResponseData(fetchResponse);
+                        if (_this.streamRequest !== activeRequest) {
+                            return;
+                        }
+                        var _streamData = createStreamDataFromResponse(activeRequest, _this.cuepoints, responseData);
+                        var eventType = _streamData.errorMessage ? StreamEvent.Type.ERROR : StreamEvent.Type.LOADED;
+                        dispatchStreamEvent(eventType, _streamData);
+                    } catch (error) {
+                        if (_this.streamRequest !== activeRequest) {
+                            return;
+                        }
+                        if (requestIndex + 1 < requestUrls.length) {
+                            await _fetchStreamData(requestIndex + 1);
+                            return;
+                        }
+                        dispatchStreamEvent(StreamEvent.Type.ERROR, createStreamData(activeRequest, _this.cuepoints, getErrorMessage(error)));
+                    }
+                };
+                _fetchStreamData(0);
+                return;
+            }
+            dispatchStreamEvent(StreamEvent.Type.LOADED, createStreamData(normalizedRequest, this.cuepoints, null));
+        };
+        StreamManager.prototype.reset = function() {
+            this.cuepoints = [];
+            this.lastMetadata = null;
+            this.lastTimedMetadata = null;
+            this.streamData = new StreamData;
+            this.streamRequest = null;
+        };
+        StreamManager.prototype.setClickElement = function(clickElement) {
+            if (this.adUiElement) {
+                return;
+            }
+            this.clickElement = clickElement;
+        };
+        StreamManager.prototype.streamTimeForContentTime = function(contentTime) {
+            return typeof contentTime === "number" ? contentTime : 0;
+        };
+        var api = {
+            DaiSdkSettings: new DaiSdkSettingsContainer,
+            LiveStreamRequest: LiveStreamRequest,
+            PodStreamRequest: PodStreamRequest,
+            StreamData: StreamData,
+            StreamEvent: StreamEvent,
+            StreamManager: StreamManager,
+            StreamRequest: StreamRequest,
+            UiSettings: UiSettings,
+            VideoStitcherLiveStreamRequest: VideoStitcherLiveStreamRequest,
+            VideoStitcherVodStreamRequest: VideoStitcherVodStreamRequest,
+            VODStreamRequest: VODStreamRequest
+        };
+        var globalWindow = window;
+        var googleNamespace = isRecord(globalWindow.google) ? globalWindow.google : {};
+        if (!isRecord(globalWindow.google)) {
+            globalWindow.google = googleNamespace;
+        }
+        var imaNamespace = isRecord(googleNamespace.ima) ? googleNamespace.ima : {};
+        googleNamespace.ima = imaNamespace;
+        var daiNamespace = isRecord(imaNamespace.dai) ? imaNamespace.dai : {};
+        imaNamespace.dai = daiNamespace;
+        var apiNamespace = isRecord(daiNamespace.api) ? daiNamespace.api : {};
+        daiNamespace.api = apiNamespace;
+        Object.assign(apiNamespace, api);
+        hit(source);
+    }
+    function hit(e) {
+        if (e.verbose) {
+            try {
+                var n = console.trace.bind(console), i = "[AdGuard] ";
+                "corelibs" === e.engine ? i += e.ruleText : (e.domainName && (i += `${e.domainName}`), 
+                e.args ? i += `#%#//scriptlet('${e.name}', '${e.args.join("', '")}')` : i += `#%#//scriptlet('${e.name}')`), 
+                n && n(i);
+            } catch (e) {}
+            "function" == typeof window.__debug && window.__debug(e);
+        }
+    }
+    function logMessage(e, o) {
+        var r = arguments.length > 2 && void 0 !== arguments[2] && arguments[2], a = !(arguments.length > 3 && void 0 !== arguments[3]) || arguments[3], {name: n, verbose: g} = e;
+        if (r || g) {
+            var i = console.log;
+            a ? i(`${n}: ${o}`) : Array.isArray(o) ? i(`${n}:`, ...o) : i(`${n}:`, o);
+        }
+    }
+    const updatedArgs = args ? [].concat(source).concat(args) : [ source ];
+    try {
+        GoogleIma3Dai.apply(this, updatedArgs);
+        if (source.uniqueId) {
+            Object.defineProperty(Window.prototype.toString, uniqueIdentifier, {
+                value: flag,
+                enumerable: false,
+                writable: false,
+                configurable: false
+            });
+        }
+    } catch (e) {
+        console.log(e);
+    }
+})({
+    name: "google-ima3-dai",
+    args: []
+}, []);

Verdict

pass

Risk

low

Summary

The diff is dominated by legitimate content-blocking work: a new mpegdash-prune scriptlet for stripping ad periods from DASH manifests, a refactored XHR-response-modification helper, Function.prototype.toString proxying (standard anti-detection), safe-self hardening refactors, and a cosmetic rewrite of the clipboard-write warning banner. The only item worth noting is the new google-ima-dai.js redirect stub (imported from AdGuard, gated behind requiresTrust: true), which reimplements the DAI stream-request call to Google's servers with credentials: "include"; this appears necessary to keep server-side-stitched video playable and is not exfiltration of user data by the extension.

Findings

  • File: src/web_accessible_resources/google-ima-dai.js
  • Severity: low
  • Category: Third-party network request / cookie transmission from a redirect stub
  • Concern: Unlike typical inert redirect stubs, this mock performs fetch(requestUrls[i], { method: "POST", credentials: "include" }) against hardcoded https://dai.google.com and https://pubads.g.doubleclick.net, forwarding page-supplied adTagParameters, api-key, auth-token and dai-sam-id, with cookies attached. This re-enables contact with an ad/SSAI endpoint rather than neutralizing it.
  • Evidence: var MAIN_HOST_NAME = "https://dai.google.com"; var FALLBACK_HOST_NAME = "https://pubads.g.doubleclick.net"; and var fetchResponse = await fetch(requestUrls[requestIndex], { method: "POST", credentials: "include" }); plus appendRequestParameters() copying streamRequest.adTagParameters into the query string.
  • Recommendation: Acceptable as-is given DAI streams cannot be played without the stream-manifest request (the accompanying mpegdash-prune scriptlet is what removes the stitched ad periods), and the resource is requiresTrust: true. Consider dropping credentials: "include" (or documenting why cookies are required) so the stub does not attach third-party ad-server cookies, and confirm the file matches the cited upstream AdGuard commit byte-for-byte before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants