louk78 / CSDN免登录复制脚本

/* jshint esversion: 8 */
// ==UserScript==
// @name         CSDN免登录复制脚本
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  破解CSDN登录限制,自由复制代码和文本,保持格式整洁
// @author       louk78
// @license      MIT
// @match        *://*.csdn.net/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ========== 工具函数 ==========
    const cleanText = (text) => {
        // 移除开头和结尾的空行,并将连续多个空行压缩为最多两个
        return text.replace(/^\s*\n/, '').replace(/\n{3,}/g, '\n\n').trim();
    };

    // 安全移除元素(带白名单检查)
    const safeRemove = (el) => {
        if (!el || !el.parentNode) return;
        // 白名单:某些包含“login”但不是遮罩的元素(如用户信息)
        const whiteList = [
            'user-login-status',
            'login-info',
            'user-login'
        ];
        const id = el.id || '';
        const cls = el.className || '';
        if (whiteList.some(key => id.includes(key) || cls.includes(key))) {
            return;
        }
        el.remove();
    };

    const nukeLoginElements = () => {
        const killSelectors = [
            '.passport-login-container',
            '.login-mark',
            '.signin',
            '.login-box',
            '.login-modal',
            '.modal-login',
            '.passport-login-box',
            '.hljs-signin',
            '.meiqia-chat',
            '#passportbox',
            '.csdn-side-toolbar',
            '.toolbar-login',
            '.unlogin-box',
            '.hide-article-box',
            '.article_content_unlogin',
            '.needslogin',
            '.content-not-login',
            '.read-soon',
            '.blur-content',
            '.blur-box',
            '.blur-mask',
            '.login-blur',
            '.hljs-button',        // 原复制按钮(禁用状态)
            '.btn-copy[disabled]',
            '.code-snippet-btn[disabled]',
            '.copy-btn-disabled',
            '.mask',
            '.modal-backdrop',
            '.overlay',
            '.fixed-layer'
        ];

        killSelectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(safeRemove);
        });

        // 移除固定定位的高zIndex遮罩
        document.querySelectorAll('*').forEach(el => {
            try {
                const style = window.getComputedStyle(el);
                if (style.position === 'fixed' && parseInt(style.zIndex) > 1000) {
                    safeRemove(el);
                }
            } catch (e) {}
        });

        // 恢复页面滚动
        document.body.style.overflow = 'auto';
        document.documentElement.style.overflow = 'auto';
        document.body.classList.remove('no-scroll', 'overflow-hidden');
    };

    // ========== 2. 注入最小必要样式 ==========
    const injectCoreStyles = () => {
        const coreCSS = `
            /* 解除选择限制(仅作用于正文和代码区域) */
            #content_views, #article_content, .article_content, .blog-content-box,
            .htmledit_views, .content_views, pre, code, .hljs, .prettyprint {
                user-select: auto !important;
                -webkit-user-select: auto !important;
                -moz-user-select: auto !important;
                -ms-user-select: auto !important;
                pointer-events: auto !important;
            }

            /* 移除模糊效果 */
            * {
                filter: none !important;
                -webkit-filter: none !important;
            }

            /* 自定义复制按钮样式 */
            .nuclear-copy-btn {
                position: absolute;
                top: 8px;
                right: 8px;
                padding: 4px 12px;
                background: #4CAF50;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
                font-family: sans-serif;
                z-index: 9999;
                opacity: 0.7;
                transition: opacity 0.2s;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .nuclear-copy-btn:hover {
                opacity: 1;
            }
            .nuclear-copy-btn.copied {
                background: #2196F3;
            }
            pre {
                position: relative !important;
                padding-top: 35px !important;  /* 为按钮留出空间 */
            }
        `;
        const style = document.createElement('style');
        style.id = 'nuclear-csdn-styles';
        style.textContent = coreCSS;
        if (!document.getElementById('nuclear-csdn-styles')) {
            document.head.appendChild(style);
        }
    };

    // ========== 3. 劫持事件,允许复制 ==========
    const hijackEvents = () => {
        // 阻止网站禁止复制的所有事件
        const blockEvents = ['copy', 'cut', 'selectstart', 'contextmenu'];
        blockEvents.forEach(eventType => {
            document.addEventListener(eventType, (e) => {
                e.stopPropagation();
                // 允许默认行为
            }, true);
            window.addEventListener(eventType, (e) => {
                e.stopPropagation();
            }, true);
        });

        // 覆盖事件属性
        Object.defineProperties(document, {
            oncopy: { value: null, writable: false },
            oncut: { value: null, writable: false },
            onselectstart: { value: null, writable: false }
        });
    };

    // ========== 4. 增强复制按钮:提取干净代码 ==========
    const addCleanCopyButtons = () => {
        // 为每个pre添加复制按钮
        document.querySelectorAll('pre').forEach((pre) => {
            if (pre.querySelector('.nuclear-copy-btn')) return; // 避免重复

            pre.style.position = 'relative';
            pre.style.paddingTop = '35px';

            const btn = document.createElement('button');
            btn.className = 'nuclear-copy-btn';
            btn.textContent = '复制代码';

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

                // 获取代码元素
                let codeEl = pre.querySelector('code');
                if (!codeEl) codeEl = pre; // 如果没有code标签,直接用pre

                // 克隆节点以移除可能存在的按钮干扰
                const clone = codeEl.cloneNode(true);
                // 移除克隆节点内的任何按钮
                clone.querySelectorAll('.nuclear-copy-btn, .hljs-button, .btn-copy').forEach(el => el.remove());

                // 提取文本:使用innerText保留格式
                let codeText = clone.innerText || clone.textContent;
                codeText = cleanText(codeText);

                try {
                    await navigator.clipboard.writeText(codeText);
                    btn.textContent = '已复制';
                    btn.classList.add('copied');
                    setTimeout(() => {
                        btn.textContent = '复制代码';
                        btn.classList.remove('copied');
                    }, 1500);
                } catch (err) {
                    // 降级方案
                    const textarea = document.createElement('textarea');
                    textarea.value = codeText;
                    textarea.style.position = 'fixed';
                    textarea.style.left = '-9999px';
                    document.body.appendChild(textarea);
                    textarea.select();
                    document.execCommand('copy');
                    document.body.removeChild(textarea);
                    btn.textContent = '已复制(降级)';
                    setTimeout(() => {
                        btn.textContent = '复制代码';
                    }, 1500);
                }
            });

            pre.appendChild(btn);
        });

        // 为独立的code块(无pre)添加包裹和按钮
        document.querySelectorAll('code:not(pre code)').forEach(code => {
            if (code.closest('.nuclear-copy-wrapper')) return;
            const wrapper = document.createElement('div');
            wrapper.className = 'nuclear-copy-wrapper';
            wrapper.style.position = 'relative';
            wrapper.style.margin = '10px 0';
            wrapper.style.padding = '15px';
            wrapper.style.background = '#f6f8fa';
            wrapper.style.borderRadius = '5px';
            code.parentNode.insertBefore(wrapper, code);
            wrapper.appendChild(code);

            const btn = document.createElement('button');
            btn.className = 'nuclear-copy-btn';
            btn.textContent = '复制代码';
            btn.style.position = 'absolute';
            btn.style.top = '5px';
            btn.style.right = '5px';
            btn.addEventListener('click', async () => {
                const text = cleanText(code.innerText || code.textContent);
                await navigator.clipboard.writeText(text);
                btn.textContent = '已复制';
                setTimeout(() => {
                    btn.textContent = '复制代码';
                }, 1500);
            });
            wrapper.appendChild(btn);
        });
    };

    // ========== 5. 清理普通文本复制格式 ==========
    const enhancePlainCopy = () => {
        document.addEventListener('copy', (e) => {
            // 如果用户是通过复制按钮复制,这里不处理(按钮已处理)
            // 如果是用户直接选择文本复制,可以清理HTML
            setTimeout(() => {
                // 无法直接修改剪贴板内容,但可以尝试通过设置自定义数据?实际上copy事件中可以修改
                // 这里我们不做强制修改,因为用户可能想保留格式。如果需要可以在这里干预。
                // 可选:如果选中区域包含代码块,可以尝试提取纯文本。
            }, 0);
        }, true);
    };

    // ========== 6. 监控动态内容 ==========
    const startMonitoring = () => {
        const observer = new MutationObserver(() => {
            nukeLoginElements();
            addCleanCopyButtons();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    };

    // ========== 7. 反检测(精简) ==========
    const antiDetection = () => {
        Object.defineProperty(navigator, 'webdriver', { get: () => false });
        // 可选:屏蔽控制台中与脚本相关的日志
        const originalLog = console.log;
        console.log = function(...args) {
            if (args[0] && typeof args[0] === 'string' && args[0].includes('Tampermonkey')) return;
            originalLog.apply(console, args);
        };
    };

    // ========== 8. 网络请求拦截(保留,但不影响复制格式) ==========
    const interceptNetwork = () => {
        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            const url = args[0];
            if (typeof url === 'string' && /login|signin|passport|user\/status/i.test(url)) {
                return Promise.reject(new Error('Blocked'));
            }
            return originalFetch.apply(this, args);
        };
        const originalXHR = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(...args) {
            const url = args[1];
            if (typeof url === 'string' && /login|signin|passport|user\/status/i.test(url)) {
                args[1] = 'about:blank';
            }
            return originalXHR.apply(this, args);
        };
    };

    // ========== 主启动函数 ==========
    const main = () => {
        nukeLoginElements();
        injectCoreStyles();
        hijackEvents();
        antiDetection();
        interceptNetwork();

        // 延迟执行以确保DOM就绪
        setTimeout(() => {
            addCleanCopyButtons();
            startMonitoring();
        }, 500);
        setTimeout(addCleanCopyButtons, 1500);
        setInterval(addCleanCopyButtons, 5000); // 定期检查新代码块
    };

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