// ==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); })();