brave / adblock-resources

Custom resources and scriptlets used for Brave's adblocker
Mozilla Public License 2.0
68 stars 23 forks source link

Modify trusted-type scriptlet #195

Closed ryanbr closed 2 months ago

ryanbr commented 2 months ago

Address https://github.com/brave/adblock-resources/pull/194#issuecomment-2267106996

ryanbr commented 2 months ago

Has been superseded by https://github.com/gorhill/uBlock/commit/4f0d1301ab48e62afb3424609834c6353a0a5eee

TEMP-ad commented 2 months ago

@ryanbr Oh didn't know they were going to add the trusted types to Youtube , that's why it was confusing to see, but makes sense now they will do that. At least I wasn't wrong about Gorhill just adding it to the scriptlet only, instead of making it globally like the single scriptlet.

BTW, I am commenting here because I noticed this PR with my suggestion, I appreciate that, but so you know, and if you ever need this knowledge when I said:

{function runAt(fn, when) {...}

function safeSelf() {...}

I didn't mean just that, but the whole functions from uBlock scriptlet https://github.com/gorhill/uBlock/blob/master/assets/resources/scriptlets.js (I had linked the exact lines in my comment), I know this doesn't matter anymore since Gorhill added it to the file, but so you know how it works.

This process is what I do when I sideload/update the scriptlets from https://github.com/uBlockO/uBO-Scriptlets/blob/master/scriptlets.js in resources.json, which is pretty useful for a lot of scriptlets that Gorhill is probably not going to ever add, like trusted-set-attr, which I requested to Gorhill to add but he didn't want to implement it, even if it is useful to fix some stuff about the web like adding autocomplete=off in input elements so they don't get added to the autofill file, and adding dark modes and all that. Since they are not for 'ads and trackers' he said he wasn't interested even if it takes (because I did it) like 3 seconds to remove the limit values set any attribute with it.

Anyway, this is why I said it like that, because it is a long scriptlet, since safeSelf is really a big scriptlet, and the more dependencies the more you have to copy and paste (at least runAt is easy since it is only 2 functions) and didn't want to make my comment long and just to the point.

I should have explained it better, because if this scriptlet was needed, then it would have been also not working correctly.

Anyway take care!

{
    function safeSelf() {
        if (scriptletGlobals.safeSelf) {
            return scriptletGlobals.safeSelf;
        }
        const self = globalThis;
        const safe = {
            'Array_from': Array.from,
            'Error': self.Error,
            'Function_toStringFn': self.Function.prototype.toString,
            'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
            'Math_floor': Math.floor,
            'Math_max': Math.max,
            'Math_min': Math.min,
            'Math_random': Math.random,
            'Object': Object,
            'Object_defineProperty': Object.defineProperty.bind(Object),
            'Object_defineProperties': Object.defineProperties.bind(Object),
            'Object_fromEntries': Object.fromEntries.bind(Object),
            'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object),
            'RegExp': self.RegExp,
            'RegExp_test': self.RegExp.prototype.test,
            'RegExp_exec': self.RegExp.prototype.exec,
            'Request_clone': self.Request.prototype.clone,
            'String_fromCharCode': String.fromCharCode,
            'XMLHttpRequest': self.XMLHttpRequest,
            'addEventListener': self.EventTarget.prototype.addEventListener,
            '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),
            'log': console.log.bind(console),
            // Properties
            logLevel: 0,
            // Methods
            makeLogPrefix(...args) {
                return this.sendToLogger && `[${args.join(' \u205D ')}]` || '';
            },
            uboLog(...args) {
                if (this.sendToLogger === undefined) { return; }
                if (args === undefined || args[0] === '') { return; }
                return this.sendToLogger('info', ...args);

            },
            uboErr(...args) {
                if (this.sendToLogger === undefined) { return; }
                if (args === undefined || args[0] === '') { return; }
                return this.sendToLogger('error', ...args);
            },
            escapeRegexChars(s) {
                return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            },
            initPattern(pattern, options = {}) {
                if (pattern === '') {
                    return { matchAll: true };
                }
                const expect = (options.canNegate !== true || pattern.startsWith('!') === false);
                if (expect === false) {
                    pattern = pattern.slice(1);
                }
                const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
                if (match !== null) {
                    return {
                        re: new this.RegExp(
                            match[1],
                            match[2] || options.flags
                        ),
                        expect,
                    };
                }
                if (options.flags !== undefined) {
                    return {
                        re: new this.RegExp(this.escapeRegexChars(pattern),
                            options.flags
                        ),
                        expect,
                    };
                }
                return { pattern, expect };
            },
            testPattern(details, haystack) {
                if (details.matchAll) { return true; }
                if (details.re) {
                    return this.RegExp_test.call(details.re, haystack) === details.expect;
                }
                return haystack.includes(details.pattern) === details.expect;
            },
            patternToRegex(pattern, flags = undefined, verbatim = false) {
                if (pattern === '') { return /^/; }
                const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
                if (match === null) {
                    const reStr = this.escapeRegexChars(pattern);
                    return new RegExp(verbatim ? `^${reStr}$` : reStr, flags);
                }
                try {
                    return new RegExp(match[1], match[2] || undefined);
                }
                catch (ex) {
                }
                return /^/;
            },
            getExtraArgs(args, offset = 0) {
                const entries = args.slice(offset).reduce((out, v, i, a) => {
                    if ((i & 1) === 0) {
                        const rawValue = a[i + 1];
                        const value = /^\d+$/.test(rawValue)
                            ? parseInt(rawValue, 10)
                            : rawValue;
                        out.push([a[i], value]);
                    }
                    return out;
                }, []);
                return this.Object_fromEntries(entries);
            },
            onIdle(fn, options) {
                if (self.requestIdleCallback) {
                    return self.requestIdleCallback(fn, options);
                }
                return self.requestAnimationFrame(fn);
            },
        };
        scriptletGlobals.safeSelf = safe;
        if (scriptletGlobals.bcSecret === undefined) { return safe; }
        // This is executed only when the logger is opened
        const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
        let bcBuffer = [];
        safe.logLevel = scriptletGlobals.logLevel || 1;
        safe.sendToLogger = (type, ...args) => {
            if (args.length === 0) { return; }
            const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
            if (bcBuffer === undefined) {
                return bc.postMessage({ what: 'messageToLogger', type, text });
            }
            bcBuffer.push({ type, text });
        };
        bc.onmessage = ev => {
            const msg = ev.data;
            switch (msg) {
                case 'iamready!':
                    if (bcBuffer === undefined) { break; }
                    bcBuffer.forEach(({ type, text }) =>
                        bc.postMessage({ what: 'messageToLogger', type, text })
                    );
                    bcBuffer = undefined;
                    break;
                case 'setScriptletLogLevelToOne':
                    safe.logLevel = 1;
                    break;
                case 'setScriptletLogLevelToTwo':
                    safe.logLevel = 2;
                    break;
            }
        };
        bc.postMessage('areyouready?');
        return safe;
    }

    function runAt(fn, when) {
        const intFromReadyState = state => {
            const targets = {
                'loading': 1,
                'interactive': 2, 'end': 2, '2': 2,
                'complete': 3, 'idle': 3, '3': 3,
            };
            const tokens = Array.isArray(state) ? state : [state];
            for (const token of tokens) {
                const prop = `${token}`;
                if (targets.hasOwnProperty(prop) === false) { continue; }
                return targets[prop];
            }
            return 0;
        };
        const runAt = intFromReadyState(when);
        if (intFromReadyState(document.readyState) >= runAt) {
            fn(); return;
        }
        const onStateChange = () => {
            if (intFromReadyState(document.readyState) < runAt) { return; }
            fn();
            safe.removeEventListener.apply(document, args);
        };
        const safe = safeSelf();
        const args = ['readystatechange', onStateChange, { capture: true }];
        safe.addEventListener.apply(document, args);
    }

    (function setTrustedTypes() {
        if (window.trustedTypes && window.trustedTypes.createPolicy) {
            window.trustedTypes.createPolicy("default", {
                createScript: string => string
            });
        }
        runAt(() => { }, 'interactive');
    }
        ());
}