Untitled

public
1 month ago 29 views javascript
// ==UserScript==
// @name         Quiz.com Slide Debug + Highlight (invisible toggle)
// @namespace    quiz-debug
// @version      2.5
// @description  Detect exposed correct answers from Quiz.com worker messages, invisible toggle
// @match        https://quiz.com/*
// @match        https://*.quiz.com/*
// @run-at       document-start
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    const win = unsafeWindow;

    let correctAnswers = [];
    let lastQuestion = '';
    let highlightVisible = false; // hidden by default

    console.log('[Quiz Debug] v2.5 loaded — invisible toggle, hidden by default');

    // =========================================================
    // Helpers
    // =========================================================

    function normalize(value) {
        return String(value ?? '')
            .replace(/\s+/g, ' ')
            .trim()
            .toLowerCase();
    }

    function setCorrectAnswers(question, answers) {
        const found = answers
            .filter(a => a && a.isCorrect === true)
            .map(a => String(a.text ?? '').trim())
            .filter(Boolean);

        if (!found.length) return;

        const signature =
            String(question || '') + '|' + found.join('|');

        const oldSignature =
            lastQuestion + '|' + correctAnswers.join('|');

        if (signature === oldSignature)
            return;

        lastQuestion = String(question || '');
        correctAnswers = found;

        console.log(
            '%c[Quiz Debug] QUESTION:',
            'color:#00bfff;font-weight:bold',
            lastQuestion
        );

        console.log(
            '%c[Quiz Debug] CORRECT:',
            'color:#00ff66;font-weight:bold',
            correctAnswers
        );

        clearHighlights();
        scheduleHighlight();
    }

    // =========================================================
    // Recursively inspect any object coming from Worker
    // =========================================================

    function inspect(value, depth = 0, seen = new WeakSet()) {
        if (depth > 12 || value == null)
            return;

        if (typeof value === 'string') {
            const str = value.trim();

            if (str.startsWith('{') || str.startsWith('[')) {
                try {
                    inspect(JSON.parse(str), depth + 1, seen);
                } catch (_) {}
            }

            return;
        }

        if (typeof value !== 'object')
            return;

        if (seen.has(value))
            return;

        seen.add(value);

        if (
            Array.isArray(value.answers) &&
            value.answers.some(
                a =>
                    a &&
                    typeof a === 'object' &&
                    'isCorrect' in a
            )
        ) {
            setCorrectAnswers(value.question, value.answers);
        }

        if (
            value.path === '/slideObject' &&
            value.value &&
            Array.isArray(value.value.answers)
        ) {
            setCorrectAnswers(
                value.value.question,
                value.value.answers
            );
        }

        if (Array.isArray(value)) {
            for (const item of value) {
                inspect(item, depth + 1, seen);
            }
        } else {
            for (const key of Object.keys(value)) {
                try {
                    inspect(value[key], depth + 1, seen);
                } catch (_) {}
            }
        }
    }

    // =========================================================
    // Highlight DOM
    // =========================================================

    function applyOutline(el) {
        el.style.setProperty('outline', '6px solid #00ff55', 'important');
        el.style.setProperty('outline-offset', '3px', 'important');
        el.style.setProperty('box-shadow', '0 0 20px #00ff55', 'important');
    }

    function removeOutline(el) {
        el.style.removeProperty('outline');
        el.style.removeProperty('outline-offset');
        el.style.removeProperty('box-shadow');
    }

    function clearHighlights() {
        document
            .querySelectorAll('[data-quiz-correct-highlight]')
            .forEach(el => {
                removeOutline(el);
                el.removeAttribute('data-quiz-correct-highlight');
            });
    }

    function highlightAnswers() {
        if (!correctAnswers.length)
            return;

        const expected = correctAnswers.map(normalize);

        const elements = Array.from(
            document.querySelectorAll(
                'button, [role="button"], div, span, p'
            )
        );

        for (const answer of expected) {
            let matches = elements.filter(el => {
                if (el.children.length > 3) return false;
                if (normalize(el.textContent) !== answer) return false;

                const r = el.getBoundingClientRect();
                return (
                    r.width > 20 &&
                    r.height > 10 &&
                    r.width < window.innerWidth * 0.8 &&
                    r.height < 180
                );
            });

            if (!matches.length) {
                console.log(
                    '[Quiz Debug] Text not found in DOM:',
                    answer
                );
                continue;
            }

            matches.sort((a, b) => {
                const ar = a.getBoundingClientRect();
                const br = b.getBoundingClientRect();
                return ar.width * ar.height - br.width * br.height;
            });

            let target = matches[0];
            let node = target;

            for (
                let i = 0;
                i < 8 && node && node.parentElement;
                i++
            ) {
                const parent = node.parentElement;
                const rect = parent.getBoundingClientRect();
                const text = normalize(parent.textContent);

                if (
                    text === answer &&
                    rect.width >= 120 &&
                    rect.height >= 30 &&
                    rect.height <= 140
                ) {
                    target = parent;
                }

                node = parent;
            }

            target.setAttribute('data-quiz-correct-highlight', 'true');

            if (highlightVisible) {
                applyOutline(target);
            } else {
                removeOutline(target);
            }

            console.log(
                '%c[Quiz Debug] Marked:',
                'color:#00ff55;font-weight:bold',
                answer,
                highlightVisible ? '(visible)' : '(hidden)',
                target
            );
        }
    }

    function scheduleHighlight() {
        [
            0, 50, 150, 300, 600,
            1000, 1500, 2500, 4000
        ].forEach(ms => {
            setTimeout(highlightAnswers, ms);
        });
    }

    // =========================================================
    // Toggle button — invisible until hovered
    // =========================================================

    function makeToggleButton() {
        if (document.getElementById('quiz-debug-toggle-wrap'))
            return;

        if (!document.body) {
            setTimeout(makeToggleButton, 50);
            return;
        }

        // invisible hover zone
        const wrap = document.createElement('div');
        wrap.id = 'quiz-debug-toggle-wrap';
        Object.assign(wrap.style, {
            position: 'fixed',
            bottom: '0',
            right: '0',
            width: '90px',
            height: '90px',
            zIndex: '2147483647',
            background: 'transparent',
            pointerEvents: 'auto'
        });

        const btn = document.createElement('button');
        btn.id = 'quiz-debug-toggle';
        btn.type = 'button';
        btn.textContent = '○';

        Object.assign(btn.style, {
            position: 'absolute',
            bottom: '16px',
            right: '16px',
            width: '32px',
            height: '32px',
            borderRadius: '50%',
            border: '2px solid #777',
            background: 'rgba(0,0,0,0.55)',
            color: '#777',
            fontSize: '16px',
            fontWeight: 'bold',
            cursor: 'pointer',
            padding: '0',
            lineHeight: '1',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            userSelect: 'none',
            opacity: '0',
            transition: 'opacity 0.2s, background 0.2s, color 0.2s, border-color 0.2s',
            pointerEvents: 'none'
        });

        function paintState() {
            if (highlightVisible) {
                btn.style.borderColor = '#00ff55';
                btn.style.color = '#00ff55';
                btn.textContent = '●';
            } else {
                btn.style.borderColor = '#777';
                btn.style.color = '#777';
                btn.textContent = '○';
            }
        }
        paintState();

        wrap.addEventListener('mouseenter', () => {
            btn.style.opacity = '1';
            btn.style.pointerEvents = 'auto';
        });
        wrap.addEventListener('mouseleave', () => {
            btn.style.opacity = '0';
            btn.style.pointerEvents = 'none';
        });

        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();

            highlightVisible = !highlightVisible;

            console.log(
                '[Quiz Debug] highlight visible =',
                highlightVisible
            );

            paintState();

            document
                .querySelectorAll('[data-quiz-correct-highlight]')
                .forEach(el => {
                    if (highlightVisible) {
                        applyOutline(el);
                    } else {
                        removeOutline(el);
                    }
                });
        });

        wrap.appendChild(btn);
        document.body.appendChild(wrap);
    }

    // =========================================================
    // Hook WebSocket — safe wrapper
    // =========================================================

    try {
        const NativeWebSocket = win.WebSocket;

        function PatchedWebSocket(...args) {
            const socket = new NativeWebSocket(...args);
            console.log('[Quiz Debug] window WebSocket:', args[0]);

            socket.addEventListener('message', event => {
                try { inspect(event.data); } catch (_) {}
            });

            const nativeSend = socket.send;
            socket.send = function (data) {
                try { inspect(data); } catch (_) {}
                return nativeSend.call(this, data);
            };

            return socket;
        }

        PatchedWebSocket.prototype = NativeWebSocket.prototype;
        Object.setPrototypeOf(PatchedWebSocket, NativeWebSocket);

        for (const k of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) {
            try {
                Object.defineProperty(PatchedWebSocket, k, {
                    value: NativeWebSocket[k],
                    writable: false,
                    configurable: false
                });
            } catch (_) {}
        }

        win.WebSocket = PatchedWebSocket;
    } catch (error) {
        console.warn('[Quiz Debug] WebSocket hook failed:', error);
    }

    // =========================================================
    // Hook Worker — safe wrapper
    // =========================================================

    try {
        const NativeWorker = win.Worker;

        function PatchedWorker(...args) {
            console.log('[Quiz Debug] Worker created:', args[0]);
            const worker = new NativeWorker(...args);

            worker.addEventListener('message', event => {
                try { inspect(event.data); } catch (_) {}
            });

            const nativePostMessage = worker.postMessage;
            worker.postMessage = function (...msgArgs) {
                try { inspect(msgArgs[0]); } catch (_) {}
                return nativePostMessage.apply(this, msgArgs);
            };

            return worker;
        }

        PatchedWorker.prototype = NativeWorker.prototype;
        Object.setPrototypeOf(PatchedWorker, NativeWorker);

        win.Worker = PatchedWorker;

        console.log('[Quiz Debug] Worker hook installed');
    } catch (error) {
        console.error('[Quiz Debug] Worker hook failed:', error);
    }

    // =========================================================
    // Hook SharedWorker
    // =========================================================

    if (win.SharedWorker) {
        try {
            const NativeSharedWorker = win.SharedWorker;

            function PatchedSharedWorker(...args) {
                console.log('[Quiz Debug] SharedWorker:', args[0]);
                const shared = new NativeSharedWorker(...args);

                if (shared.port) {
                    hookMessagePort(shared.port);
                    try { shared.port.start(); } catch (_) {}
                }

                return shared;
            }

            PatchedSharedWorker.prototype = NativeSharedWorker.prototype;
            Object.setPrototypeOf(PatchedSharedWorker, NativeSharedWorker);

            win.SharedWorker = PatchedSharedWorker;
        } catch (error) {
            console.warn('[Quiz Debug] SharedWorker hook failed:', error);
        }
    }

    // =========================================================
    // Hook MessagePort
    // =========================================================

    function hookMessagePort(port) {
        if (!port || port.__quizDebugHooked) return;

        try {
            Object.defineProperty(port, '__quizDebugHooked', { value: true });
        } catch (_) { return; }

        port.addEventListener('message', event => {
            try { inspect(event.data); } catch (_) {}
        });

        const nativePostMessage = port.postMessage;
        port.postMessage = function (...args) {
            try { inspect(args[0]); } catch (_) {}
            return nativePostMessage.apply(this, args);
        };
    }

    // =========================================================
    // Hook MessageChannel
    // =========================================================

    try {
        const NativeMessageChannel = win.MessageChannel;

        if (NativeMessageChannel) {
            function PatchedMessageChannel(...args) {
                const channel = new NativeMessageChannel(...args);
                hookMessagePort(channel.port1);
                hookMessagePort(channel.port2);
                return channel;
            }

            PatchedMessageChannel.prototype = NativeMessageChannel.prototype;
            Object.setPrototypeOf(PatchedMessageChannel, NativeMessageChannel);

            win.MessageChannel = PatchedMessageChannel;
        }
    } catch (_) {}

    // =========================================================
    // React rerender observer
    // =========================================================

    const observer = new MutationObserver(() => {
        if (correctAnswers.length) {
            highlightAnswers();
        }
    });

    function startObserver() {
        if (!document.documentElement) {
            setTimeout(startObserver, 20);
            return;
        }
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    startObserver();

    // =========================================================
    // Boot the toggle button
    // =========================================================

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', makeToggleButton);
    } else {
        makeToggleButton();
    }

    setInterval(() => {
        if (!document.getElementById('quiz-debug-toggle-wrap')) {
            makeToggleButton();
        }
    }, 1000);

})();
Raw