Hack olm.vn

Hack olm

// ==UserScript==
// @name         Hack olm.vn
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Hack olm
// @author       Trần Bảo Ngọc
// @match        *://*.olm.vn/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    const HOOK_CODE = `(() => {
      if (window.__olmHooked) return; window.__olmHooked = true;

      const tryParseJSON = (text) => { try { return JSON.parse(text); } catch { return null; } };
      const looksLikeBase64 = (s) => typeof s === 'string' && s.length > 16 && /^[A-Za-z0-9+/=]+$/.test(s);
      const collectContents = (obj, out) => {
        if (!obj) return;
        if (Array.isArray(obj)) { for (const it of obj) collectContents(it, out); return; }
        if (typeof obj === 'object') {
          for (const [k, v] of Object.entries(obj)) {
            if (k === 'content' && looksLikeBase64(v)) out.push(v);
            collectContents(v, out);
          }
        }
      };

      const postIfHasContents = (meta, text) => {
        const json = tryParseJSON(text); if (!json) return;
        const contents = []; collectContents(json, contents);
        if (contents.length) {
          window.postMessage({ type: 'OLM_SNIF_CONTENTS', meta, contents }, '*');
        }
      };

      // XHR hook
      (() => {
        const origOpen = XMLHttpRequest.prototype.open;
        const origSend = XMLHttpRequest.prototype.send;
        const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
        XMLHttpRequest.prototype.open = function(method, url, ...rest) {
          this.__olm = { method, url, headers: {}, body: null };
          try { this.addEventListener('load', () => {
            if (this.responseType && this.responseType !== 'text') return;
            postIfHasContents(this.__olm, this.responseText || '');
          }); } catch {}
          return origOpen.call(this, method, url, ...rest);
        };
        XMLHttpRequest.prototype.setRequestHeader = function(h, v) {
          try { if (this.__olm) this.__olm.headers[h] = v; } catch {}
          return origSetHeader.call(this, h, v);
        };
        XMLHttpRequest.prototype.send = function(body) {
          try { if (this.__olm) this.__olm.body = body; } catch {}
          return origSend.call(this, body);
        };
      })();

      // fetch hook
      (() => {
        const origFetch = window.fetch;
        window.fetch = function(input, init) {
            const method = (init && init.method) || (input && input.method) || 'GET';
            const url = typeof input === 'string' ? input : (input && input.url) || '';
            const headers = (init && init.headers) || (input && input.headers) || {};
            const body = (init && init.body) || (input && input.body) || null;
          const meta = { method, url, headers, body };
            const p = origFetch(input, init);
          p.then(r => { try { const c = r.clone(); c.text().then(t => postIfHasContents(meta, t)).catch(()=>{}); } catch {} }).catch(()=>{});
            return p;
        };
      })();
    })();`;

    const inject = () => {
    const s = document.createElement('script');
    s.textContent = HOOK_CODE;
    (document.head || document.documentElement).appendChild(s);
    s.remove();
    };
    inject();
    const state = { isVisible: true, items: [], originals: [], firstMeta: null };

    // Lazy loader for html beautifier
    let beautifierLoading = null;
    function ensureBeautifier() {
        if (window.html_beautify) return Promise.resolve();
        if (beautifierLoading) return beautifierLoading;
        beautifierLoading = new Promise((resolve) => {
            const s = document.createElement('script');
            s.src = 'https://cdn.jsdelivr.net/npm/[email protected]/js/lib/beautify-html.min.js';
            s.async = true;
            s.onload = () => resolve();
            s.onerror = () => resolve();
            document.head.appendChild(s);
        });
        return beautifierLoading;
    }

    // Lazy loader for MathJax
    let mathjaxLoading = null;
    function ensureMathJax() {
        if (window.MathJax && window.MathJax.typesetPromise) return Promise.resolve();
        if (mathjaxLoading) return mathjaxLoading;
        window.MathJax = {
            tex: {
                inlineMath: [["$","$"],["\\(","\\)"]],
                displayMath: [["$$","$$"],["\\[","\\]"]],
                processEscapes: true,
                packages: { '[+]': ['mhchem'] }
            },
            loader: { load: ['[tex]/mhchem'] }
        };
        mathjaxLoading = new Promise((resolve) => {
            const s = document.createElement('script');
            s.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js';
            s.async = true;
            s.onload = () => resolve();
            s.onerror = () => resolve();
            document.head.appendChild(s);
        });
        return mathjaxLoading;
    }

    const utils = {
        safeDecode(base64) {
            try { return decodeURIComponent(escape(atob(base64))); } catch { return null; }
        }
    };

    // Fuzzy matching utilities
    const fuzzyMatch = {
        normalize(text) {
            return text.toLowerCase()
                .replace(/\s+/g, ' ')
                .replace(/[^\w\s]/g, '')
                .trim();
        },
        similarity(s1, s2) {
            const n1 = this.normalize(s1);
            const n2 = this.normalize(s2);
            if (n1 === n2) return 1;
            if (!n1 || !n2) return 0;
            const len1 = n1.length;
            const len2 = n2.length;
            const maxLen = Math.max(len1, len2);
            if (n1.includes(n2) || n2.includes(n1)) {
                return 0.8 + (0.2 * Math.min(len1, len2) / maxLen);
            }
            const chars1 = new Set(n1.split(''));
            const chars2 = new Set(n2.split(''));
            const intersection = new Set([...chars1].filter(x => chars2.has(x)));
            const union = new Set([...chars1, ...chars2]);
            return intersection.size / union.size;
        },
        findBestMatch(searchText) {
            const candidates = [];
            const minLength = 3;
            const walker = document.createTreeWalker(
                document.body,
                NodeFilter.SHOW_TEXT,
                {
                    acceptNode: (node) => {
                        const text = node.textContent.trim();
                        if (text.length < minLength) return NodeFilter.FILTER_REJECT;
                        const parent = node.parentElement;
                        if (!parent || parent.closest('#olm-answers-container')) return NodeFilter.FILTER_REJECT;
                        return NodeFilter.FILTER_ACCEPT;
                    }
                }
            );
            let node;
            while (node = walker.nextNode()) {
                const text = node.textContent.trim();
                const parent = node.parentElement;
                if (parent) {
                    candidates.push({ element: parent, text });
                }
            }
            let bestMatch = null;
            let bestScore = 0;
            for (const candidate of candidates) {
                const score = this.similarity(searchText, candidate.text);
                if (score > bestScore) {
                    bestScore = score;
                    bestMatch = candidate.element;
                }
            }
            return bestScore > 0.3 ? bestMatch : null;
        }
    };

    // Scroll and highlight element
    function scrollToElement(element) {
        if (!element) return false;
        document.querySelectorAll('.olm-highlight').forEach(el => {
            el.classList.remove('olm-highlight');
        });
        element.classList.add('olm-highlight');
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        setTimeout(() => {
            element.classList.remove('olm-highlight');
        }, 3000);
        return true;
    }

    // Process HTML snippets
    async function processAndAppend(htmlSnippets) {
        await ensureBeautifier();
        const beautify = window.html_beautify || ((s)=>s);
        const options = {
            indent_size: 2,
            preserve_newlines: true,
            max_preserve_newlines: 2,
            wrap_line_length: 0,
            content_unformatted: ['pre','code','textarea']
        };
        for (const html of htmlSnippets) {
            const container = document.createElement('div');
            container.innerHTML = html;
            try {
                state.originals.push(beautify(container.innerHTML, options));
            } catch {}
            const segments = [];
            let current = document.createElement('div');
            const pushCurrentIfAny = () => {
                const s = current.innerHTML.trim();
                if (s) segments.push(beautify(s, options));
                current = document.createElement('div');
            };
            Array.from(container.childNodes).forEach((node) => {
                if (node.nodeType === 1 && node.classList && node.classList.contains('exp')) {
                    pushCurrentIfAny();
                    return;
                }
                current.appendChild(node.cloneNode(true));
            });
            pushCurrentIfAny();
            if (segments.length) {
                state.items.push(...segments);
            } else {
                container.querySelectorAll('.exp').forEach(el => el.remove());
                state.items.push(beautify(container.innerHTML, options));
            }
        }
        ui.update();
    }

    window.addEventListener('message', (ev) => {
        const msg = ev.data;
        if (!msg || msg.type !== 'OLM_SNIF_CONTENTS') return;
        if (!state.firstMeta) state.firstMeta = msg.meta || null;
        const decoded = (msg.contents || []).map(utils.safeDecode).filter(Boolean);
        if (!decoded.length) return;
        processAndAppend(decoded);
    }, false);

    async function downloadWordFile(event) {
        const button = event.target;
        const originalText = button.textContent;
        button.textContent = 'Đang xử lý...';
        button.disabled = true;
        try {
            const match = window.location.pathname.match(/(\d+)$/);
            if (!match || !match[0]) {
                alert('Lỗi: Không tìm thấy ID chủ đề (dãy số ở cuối link) trong URL.');
                throw new Error('Không tìm thấy ID chủ đề trong pathname.');
            }
            const id_cate = match[0];
            button.textContent = 'Đang lấy link...';
            const apiUrl = `https://olm.vn/download-word-for-user?id_cate=${id_cate}&showAns=1&questionNotApproved=0`;
            const response = await fetch(apiUrl);
            if (!response.ok) {
                throw new Error(`Lỗi server OLM: ${response.statusText}`);
            }
            const data = await response.json();
            if (!data || !data.file) {
                throw new Error('Response JSON không hợp lệ hoặc không có link file.');
            }
            const fileUrl = data.file;
            button.textContent = 'Đang tải về...';
            const link = document.createElement('a');
            link.href = fileUrl;
            link.target = '_blank';
            let filename = fileUrl.split('/').pop();
            if (!filename || !filename.includes('.')) {
                filename = `olm-answers-${id_cate}.docx`;
            }
            link.download = filename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        } catch (error) {
            console.error('Lỗi khi tải file Word:', error);
            alert(`Đã xảy ra lỗi: ${error.message}`);
        } finally {
            button.textContent = originalText;
            button.disabled = false;
        }
    }

    const ui = {
        el: {},
        // THÊM: Quản lý trạng thái kéo
        dragState: { isDragging: false, startX: 0, startY: 0, initialX: 0, initialY: 0 },

        // THÊM: Hàm xử lý kéo
        onMouseDown(event) {
            // Bỏ qua nếu nhấn vào nút resize
            if (event.target.classList.contains('olm-mini-resize')) return;

            this.dragState.isDragging = true;
            const rect = this.el.panel.getBoundingClientRect();

            // Quan trọng: Phải GỠ BỎ 'right' và 'bottom' nếu đang dùng
            // Script này set 'top' và 'right' ban đầu. Ta phải đổi 'right' -> 'left'.
            this.el.panel.style.right = 'auto';
            this.el.panel.style.left = `${rect.left}px`;

            this.dragState.initialX = rect.left;
            this.dragState.initialY = rect.top;
            this.dragState.startX = event.clientX;
            this.dragState.startY = event.clientY;

            // Phải bind 'this' cho các listener này vì chúng được gọi bởi 'window'
            this.boundOnMouseMove = this.onMouseMove.bind(this);
            this.boundOnMouseUp = this.onMouseUp.bind(this);

            window.addEventListener('mousemove', this.boundOnMouseMove);
            window.addEventListener('mouseup', this.boundOnMouseUp);
        },

        // THÊM: Hàm xử lý di chuột
        onMouseMove(event) {
            if (!this.dragState.isDragging) return;
            event.preventDefault();
            const dx = event.clientX - this.dragState.startX;
            const dy = event.clientY - this.dragState.startY;
            this.el.panel.style.left = `${this.dragState.initialX + dx}px`;
            this.el.panel.style.top = `${this.dragState.initialY + dy}px`;
        },

        // THÊM: Hàm xử lý nhả chuột
        onMouseUp() {
            this.dragState.isDragging = false;
            window.removeEventListener('mousemove', this.boundOnMouseMove);
            window.removeEventListener('mouseup', this.boundOnMouseUp);
        },

        init() {
            const style = document.createElement('style');
            style.textContent = `
                @keyframes gradient-animation {
                    0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; }
                }
                #olm-answers-container {
                    position: fixed; top: 10px; right: 10px; width: 450px; max-height: 90vh;
                    border: 1px solid #cccccc; border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
                    z-index: 10000; display: flex; flex-direction: column; 
                    font-size: 16px; 
                    /* THAY ĐỔI: Bỏ 'resize: both;' để dùng tay cầm tùy chỉnh */
                    overflow: hidden; 
                    transition: opacity 0.3s ease-out, transform 0.3s ease-out;
                    color: #333;
                    background: linear-gradient(45deg, #e0f7fa, #d1c4e9, #fce4ec, #e0f7fa);
                    background-size: 400% 400%; animation: gradient-animation 20s ease infinite;
                    backdrop-filter: blur(5px);
                    -webkit-backdrop-filter: blur(5px);
                }
                #olm-answers-container.hidden { 
                    opacity: 0; 
                    transform: scale(0.95) translateX(20px); 
                    pointer-events: none; 
                }
                .olm-answers-header {
                    padding: 10px 15px; background-color: rgba(255, 255, 255, 0.4); border-bottom: 1px solid #cccccc;
                    cursor: move; /* THÊM: Con trỏ di chuyển */
                    user-select: none; font-weight: bold; text-align: center;
                    backdrop-filter: blur(8px);
                    -webkit-backdrop-filter: blur(8px);
                    color: #000;
                }
                #olm-answers-content {
                    padding: 10px; margin: 0; flex-grow: 1; overflow-y: auto; background-color: rgba(255, 255, 255, 0.8);
                }
                #olm-answers-footer {
                    padding: 8px; background-color: rgba(255, 255, 255, 0.4); border-top: 1px solid #cccccc; text-align: center;
                    backdrop-filter: blur(8px);
                }
                #olm-dl-word {
                    padding: 8px 16px; border: none; background-color: #28a745; color: white;
                    border-radius: 6px; cursor: pointer; font-weight: bold; transition: background-color 0.2s, transform 0.1s;
                    font-size: 15px;
                    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                }
                #olm-dl-word:hover { 
                    background-color: #218838; 
                    transform: translateY(-1px);
                }
                #olm-dl-word:disabled { 
                    background-color: #9e9e9e; 
                    cursor: not-allowed;
                    transform: none;
                }
                
                #olm-answers-content::-webkit-scrollbar { width: 8px; }
                #olm-answers-content::-webkit-scrollbar-track { background: #f1f1f1; }
                #olm-answers-content::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
                #olm-answers-content::-webkit-scrollbar-thumb:hover { background: #555; }

                /* Style cho highlight từ Click-to-find */
                .olm-highlight {
                    background-color: rgba(255, 255, 0, 0.4) !important;
                    outline: 2px solid #ffd700 !important;
                    outline-offset: 2px;
                    transition: all 0.3s ease;
                }
                .olm-item{
                    background:#f8f8f8;
                    border-left:3px solid #007bff;
                    padding:10px;
                    border-radius:0 6px 6px 0;
                    margin-bottom:12px;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
                }
                .olm-item .question-content {
                    font-weight: bold;
                    color: #0056b3;
                    margin-bottom: 8px;
                }
                .olm-item .content-container {
                    padding: 8px 0 0 15px;
                    border-top: 1px dashed #e0e0e0;
                    margin-top: 8px;
                }
                .olm-item .content-container[data-type="answer"] { color: #dc3545; font-weight: bold; }
                .olm-item .content-container[data-type="solution"] { color: #28a745; }
                .olm-item .content-container[data-type="not-found"] { color: #777; font-style: italic; }
                .olm-item img{max-width:100%;height:auto;}
                .olm-item li.correctAnswer{color:#48bb78;font-weight:600}
                .olm-item .fill-answer{color:#48bb7Ghi}
                .olm-item [dir="ltr"]{cursor:pointer;transition:background 0.2s;padding:2px;border-radius:3px}
                .olm-item [dir="ltr"]:hover{background:#d0eaff}
                .olm-item ol.quiz-list{margin:0 0 6px 20px;padding:0}
                .olm-item ol.quiz-list li{margin:0;padding:0}

                /* THÊM LẠI: CSS cho tay cầm resize đã bị mất */
                .olm-mini-resize {
                    position:absolute; bottom:0; left:0; 
                    width:28px; height:28px; 
                    cursor:nesw-resize; opacity:.7; 
                    touch-action:none; -webkit-user-select:none; user-select:none;
                    background:linear-gradient(135deg,transparent 60%, rgba(255,255,255,.35) 60%, rgba(255,255,255,.35) 65%, transparent 65%)
                }
                .olm-mini-resize:active{opacity:1}
            `;
            document.head.appendChild(style);

            const panel = document.createElement('div');
            panel.id = 'olm-answers-container';
            // THAY ĐỔI: Xóa class 'hidden' để hiện mặc định
            // panel.classList.add('hidden'); 

            panel.innerHTML = `
              <div class="olm-answers-header">Code by Trần Bảo Ngọc (Nhấn Shift phải để Ẩn/Hiện)</div>
              <div id="olm-answers-content"><div>Không có dữ liệu. Bắt đầu một bài tập hoặc trắc nghiệm.</div></div>
              <div id="olm-answers-footer">
                <button id="olm-dl-word">Tải Đáp ÁN (Word)</button>
              </div>
            `;
            document.body.appendChild(panel);

            const resize = document.createElement('div');
            resize.className = 'olm-mini-resize';
            panel.appendChild(resize);

            // Gắn sự kiện lắng nghe phím Shift phải
            window.addEventListener('keydown', (event) => {
                if (event.code === 'ShiftRight') {
                    ui.toggleVisibility();
                }
            });

            this.el = {
                panel,
                header: panel.querySelector('.olm-answers-header'), // THÊM: Chọn header
                body: panel.querySelector('#olm-answers-content'),
                dlWord: panel.querySelector('#olm-dl-word'),
                resize
            };

            // THÊM: Gắn sự kiện kéo-thả cho header
            this.onMouseDown = this.onMouseDown.bind(this); // Bind 'this' một lần
            this.el.header.addEventListener('mousedown', this.onMouseDown);

            // Gắn sự kiện cho nút tải
            this.el.dlWord.onclick = downloadWordFile;

            // "Click-to-find"
            this.el.body.addEventListener('click', (e) => {
                const ltrEl = e.target.closest('[dir="ltr"]');
                if (!ltrEl) return;
                const itemEl = ltrEl.closest('.olm-item');
                if (!itemEl) return;
                e.stopPropagation();
                const searchText = ltrEl.textContent.trim();
                if (!searchText) return;
                const matchEl = fuzzyMatch.findBestMatch(searchText);
                if (matchEl) {
                    scrollToElement(matchEl);
                } else {
                    const origBg = ltrEl.style.backgroundColor;
                    ltrEl.style.backgroundColor = 'rgba(255, 136, 136, 0.3)';
                    setTimeout(() => { ltrEl.style.backgroundColor = origBg; }, 300);
                }
            });

            // Resize handle
            (function setupResize(panelEl, handle){
                let isResizing = false; let startX = 0; let startY = 0; let startW = 0; let startH = 0;
                const minW = 220; const minH = 180; const maxW = Math.round(window.innerWidth * 0.9); const maxH = Math.round(window.innerHeight * 0.9);
                const getSizes = () => {
                    const cs = window.getComputedStyle(panelEl);
                    return { w: parseInt(cs.width, 10) || panelEl.offsetWidth, h: parseInt(cs.height, 10) || panelEl.offsetHeight };
                };
                const onStart = (cx, cy) => { isResizing = true; const s = getSizes(); startW = s.w; startH = s.h; startX = cx; startY = cy; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); document.addEventListener('touchmove', onTouchMove, { passive: false }); document.addEventListener('touchend', onEnd); };
                const onMove = (e) => { if (!isResizing) return; e.preventDefault(); const cx = e.clientX; const cy = e.clientY; applyResize(cx, cy); };
                const onTouchMove = (e) => { if (!isResizing) return; e.preventDefault(); const t = e.touches[0]; applyResize(t.clientX, t.clientY); };
                const applyResize = (cx, cy) => {
                    const newW = Math.max(minW, Math.min(maxW, startW + (startX - cx)));
                    const newH = Math.max(minH, Math.min(maxH, startH + (cy - startY)));
                    panelEl.style.width = newW + 'px';
                    panelEl.style.height = newH + 'px';
                };
                const onEnd = () => { isResizing = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onEnd); };
                handle.addEventListener('mousedown', (e) => { onStart(e.clientX, e.clientY); });
                handle.addEventListener('touchstart', (e) => { if (!e.touches || !e.touches[0]) return; e.preventDefault(); const t = e.touches[0]; onStart(t.clientX, t.clientY); }, { passive: false });
            })(panel, resize);
        },
        
        toggleVisibility() {
            state.isVisible = !state.isVisible;
            this.el.panel.classList.toggle('hidden', !state.isVisible);
            if (state.isVisible) {
                this.update();
            }
        },
        
        update() {
            if (!this.el.body) return;
            if (!state.items.length) { this.el.body.innerHTML = '<div>Không có dữ liệu. Bắt đầu một bài tập hoặc trắc nghiệm.</div>'; return; }
            const renderSegment = (html) => {
                const tmp = document.createElement('div');
                tmp.innerHTML = html;
                if (!tmp.querySelector('li.correctAnswer')) {
                    tmp.querySelectorAll('input[data-accept]').forEach(inp => {
                        const v = inp.getAttribute('data-accept') || '';
                        const span = document.createElement('span');
                        span.className = 'fill-answer';
                        span.textContent = v;
                        inp.replaceWith(span);
                    });
                }
                return tmp.innerHTML;
            };
            this.el.body.innerHTML = state.items.map((html, i) => `
                <div class="olm-item">
                    <div style="opacity:.7;margin-bottom:4px; font-size: 13px; color: #555;">Câu ${i+1}</div>
                    ${renderSegment(html)}
                </div>
            `).join('');
            ensureMathJax().then(() => {
                try { window.MathJax.typesetPromise && window.MathJax.typesetPromise([this.el.body]); } catch {}
            });
        }
    };
    const onReady = () => ui.init();
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', onReady);
    else onReady();
})();