Linux Do Authorize Helper

Linux Do 授权增强:语义化按钮定位、GM存储保障安全、XSS防御、可视化管理、支持通配符与编辑功能。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Linux Do Authorize Helper
// @namespace    http://tampermonkey.net/
// @version      3.5
// @description  Linux Do 授权增强:语义化按钮定位、GM存储保障安全、XSS防御、可视化管理、支持通配符与编辑功能。
// @author       blackzero358
// @license      AGPLv3
// @match        https://connect.linux.do/oauth2/authorize*
// @icon         data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔑</text></svg>
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // ================= 配置常量 =================
    const CONFIG = {
        STORAGE_MODE: 'ld_auth_mode',
        STORAGE_LIST: 'ld_trusted_list',
        STORAGE_AUTO_ADD: 'ld_auto_add_manual',
        STORAGE_TRUST_OFFICIAL: 'ld_trust_official_subdomains',
        SELECTORS: {
            APPROVE_BTN: 'a[href*="/oauth2/approve/"]',
            APPROVE_TEXT_XPATH: "//a[contains(text(), '允许') or contains(text(), 'Authorize') or contains(text(), 'Approve')]",
            SITE_LINK: '.bg-white p.mb-2 a[href^="http"]'
        }
    };

    // ================= 样式定义 (Dark Mode) =================
    const styles = `
        /* 主面板 */
        #ld-auth-panel {
            position: fixed; bottom: 20px; right: 20px;
            background: #1f2937; color: #e5e7eb;
            padding: 16px; border-radius: 12px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.6);
            z-index: 9999; font-size: 14px;
            border: 1px solid #374151; width: 260px;
            font-family: system-ui, -apple-system, sans-serif;
        }
        .ld-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
        .ld-title { font-weight: 700; color: #f3f4f6; font-size: 15px; }

        /* 控件通用 */
        .ld-select, .ld-input {
            width: 100%; padding: 8px; margin-bottom: 10px;
            background: #374151; border: 1px solid #4b5563;
            color: white; border-radius: 6px; outline: none;
            box-sizing: border-box;
        }
        .ld-input:focus { border-color: #60a5fa; }

        .ld-checkbox-row {
            display: flex; align-items: center; gap: 8px;
            margin-bottom: 8px; font-size: 12px; color: #9ca3af;
            transition: opacity 0.3s;
        }
        .ld-checkbox-row.hidden { display: none; }
        .ld-checkbox-row input { accent-color: #60a5fa; cursor: pointer; }
        .ld-checkbox-row label { cursor: pointer; user-select: none; }

        /* 状态条 */
        .ld-status {
            padding: 10px; border-radius: 6px; margin-bottom: 12px; margin-top: 5px;
            background: #111827; border: 1px solid #374151;
            font-size: 13px; line-height: 1.4;
        }
        .ld-status.safe { color: #34d399; border-color: #059669; }
        .ld-status.warn { color: #fbbf24; border-color: #d97706; }
        .ld-status.danger { color: #f87171; border-color: #dc2626; animation: pulse 2s infinite; }

        /* 按钮 */
        .ld-btn {
            width: 100%; padding: 8px; background: #374151; color: #e5e7eb;
            border: 1px solid #4b5563; border-radius: 6px; cursor: pointer;
            transition: all 0.2s; display: inline-flex; justify-content: center; align-items: center; gap: 5px;
        }
        .ld-btn:hover { background: #4b5563; }
        .ld-btn.primary { background: #2563eb; border-color: #1d4ed8; }
        .ld-btn.primary:hover { background: #1d4ed8; }
        .ld-btn.danger { color: #fca5a5; border-color: #7f1d1d; }
        .ld-btn.danger:hover { background: #991b1b; color: white; }
        .ld-btn-sm { padding: 4px 8px; font-size: 12px; width: auto; }

        /* 模态框 */
        #ld-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.75); z-index: 10000;
            display: none; justify-content: center; align-items: center;
            backdrop-filter: blur(3px);
        }
        #ld-modal {
            background: #1f2937; width: 500px; max-width: 90%;
            border-radius: 12px; border: 1px solid #4b5563;
            display: flex; flex-direction: column; max-height: 80vh; overflow: hidden;
        }
        .ld-modal-header { padding: 15px; border-bottom: 1px solid #374151; display: flex; justify-content: space-between; align-items: center; background: #1f2937; }
        .ld-modal-body { flex: 1; overflow-y: auto; padding: 0; position: relative; }

        /* 列表项 */
        .ld-list-item {
            padding: 12px 15px; border-bottom: 1px solid #374151;
            display: flex; justify-content: space-between; align-items: center;
        }
        .ld-list-item:hover { background: #2d3748; }
        .ld-site-info { display: flex; flex-direction: column; gap: 2px; flex: 1; margin-right: 10px; overflow: hidden; }
        .ld-site-url { font-size: 14px; color: #e5e7eb; word-break: break-all; }
        .ld-site-date { font-size: 11px; color: #9ca3af; }
        .ld-actions { display: flex; gap: 5px; }

        /* 编辑页面 */
        .ld-editor-view { padding: 20px; }
        .ld-help-text { font-size: 12px; color: #9ca3af; margin-bottom: 15px; line-height: 1.5; background: #111827; padding: 10px; border-radius: 6px; border: 1px dashed #4b5563; }
        .ld-help-text code { background: #374151; padding: 2px 4px; border-radius: 3px; color: #60a5fa; font-family: monospace; }
        .ld-editor-actions { display: flex; gap: 10px; margin-top: 15px; }

        .ld-action-btn { background: none; border: none; cursor: pointer; opacity: 0.7; font-size: 16px; padding: 4px; color: #e5e7eb; }
        .ld-action-btn:hover { opacity: 1; transform: scale(1.1); }
        .ld-action-btn.edit:hover { color: #60a5fa; }
        .ld-action-btn.del:hover { color: #f87171; }

        @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } }
    `;
    GM_addStyle(styles);

    // ================= 核心逻辑工具 =================

    const Store = {
        getMode: () => GM_getValue(CONFIG.STORAGE_MODE, 'manual'),
        setMode: (val) => GM_setValue(CONFIG.STORAGE_MODE, val),

        getList: () => GM_getValue(CONFIG.STORAGE_LIST, []),
        setList: (list) => GM_setValue(CONFIG.STORAGE_LIST, list),

        getAutoAdd: () => GM_getValue(CONFIG.STORAGE_AUTO_ADD, false),
        setAutoAdd: (val) => GM_setValue(CONFIG.STORAGE_AUTO_ADD, val),

        getTrustOfficial: () => GM_getValue(CONFIG.STORAGE_TRUST_OFFICIAL, false),
        setTrustOfficial: (val) => GM_setValue(CONFIG.STORAGE_TRUST_OFFICIAL, val)
    };

    const Utils = {
        normalizeUrl: (url) => {
            if (!url) return '';
            try { return url.trim().replace(/\/+$/, ''); } catch (e) { return url; }
        },
        getTargetSite: () => {
            try {
                const strongTags = Array.from(document.querySelectorAll('strong'));
                const siteLabel = strongTags.find(el => el.textContent.includes('网站'));
                if (siteLabel && siteLabel.parentElement) {
                    const link = siteLabel.parentElement.querySelector('a');
                    if (link) return link.href;
                }
                const link = document.querySelector(CONFIG.SELECTORS.SITE_LINK);
                return link ? link.href : null;
            } catch (e) { return null; }
        },
        getApproveBtn: () => {
            let btn = document.querySelector(CONFIG.SELECTORS.APPROVE_BTN);
            if (!btn) {
                const result = document.evaluate(CONFIG.SELECTORS.APPROVE_TEXT_XPATH, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
                btn = result.singleNodeValue;
            }
            return btn;
        },
        formatDate: (timestamp) => {
            if (!timestamp) return '手动添加';
            return new Date(timestamp).toLocaleString('zh-CN', { hour12: false });
        },
        createSafeElement: (tag, className, text) => {
            const el = document.createElement(tag);
            if (className) el.className = className;
            if (text) el.textContent = text;
            return el;
        },
        isLinuxDoSubdomain: (urlStr) => {
            try {
                const hostname = new URL(urlStr).hostname;
                return hostname === 'linux.do' || hostname.endsWith('.linux.do');
            } catch (e) { return false; }
        },
        isWildcardMatch: (pattern, targetUrl) => {
            if (!pattern || !targetUrl) return false;
            const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
            const regex = new RegExp(`^${escaped}$`, 'i');
            return regex.test(targetUrl);
        }
    };

    // ================= 业务逻辑 =================

    const Logic = {
        addSite: (url) => {
            if (!url) return;
            const normUrl = Utils.normalizeUrl(url);
            const list = Store.getList();
            if (list.some(item => Utils.normalizeUrl(item.url) === normUrl)) return;

            list.push({ url: normUrl, addedAt: Date.now() });
            Store.setList(list);
            console.log('[LD Auth] Added:', normUrl);
            UI.updateStatus();
        },
        updateSite: (oldUrl, newUrl) => {
            const list = Store.getList();
            const normOld = Utils.normalizeUrl(oldUrl);
            const normNew = Utils.normalizeUrl(newUrl);

            const index = list.findIndex(item => Utils.normalizeUrl(item.url) === normOld);
            if (index !== -1) {
                list[index].url = normNew;
                list[index].addedAt = Date.now();
            } else {
                list.push({ url: normNew, addedAt: Date.now() });
            }
            Store.setList(list);
            UI.updateStatus();
        },
        removeSite: (url) => {
            const normUrl = Utils.normalizeUrl(url);
            const list = Store.getList().filter(item => Utils.normalizeUrl(item.url) !== normUrl);
            Store.setList(list);
            UI.updateStatus();
        },
        checkTrust: (targetUrl) => {
            if (!targetUrl) return false;
            const normTarget = Utils.normalizeUrl(targetUrl);

            if (Store.getTrustOfficial() && Utils.isLinuxDoSubdomain(normTarget)) {
                return true;
            }

            const list = Store.getList();
            return list.some(item => Utils.isWildcardMatch(item.url, normTarget));
        },
        performClick: (btn, reason) => {
            if (!btn) return;
            const statusEl = document.getElementById('ld-status-text');
            if (statusEl) {
                statusEl.innerHTML = `正在自动授权...<br><span style="opacity:0.7;font-size:12px">(${reason})</span>`;
            }
            setTimeout(() => { btn.click(); }, 800);
        }
    };

    // ================= UI 渲染 =================

    const UI = {
        state: {
            currentView: 'list',
            editingItem: null
        },

        init: () => {
            const panel = document.createElement('div');
            panel.id = 'ld-auth-panel';
            panel.innerHTML = `
                <div class="ld-header">
                    <span class="ld-title">🛡️ Linux Do Auth Helper</span>
                </div>

                <select id="ld-mode-select" class="ld-select">
                    <option value="manual">🔵 手动模式</option>
                    <option value="safe">🟢 安全模式</option>
                    <option value="dangerous">🔴 危险模式</option>
                </select>

                <div id="ld-trust-official-container" class="ld-checkbox-row">
                    <input type="checkbox" id="ld-trust-official-check">
                    <label for="ld-trust-official-check" title="在安全模式下,自动允许 *.linux.do 的请求">自动信任 *.linux.do</label>
                </div>

                <div id="ld-auto-add-container" class="ld-checkbox-row" title="手动点击允许按钮时,自动将该站点加入白名单">
                    <input type="checkbox" id="ld-auto-add-check">
                    <label for="ld-auto-add-check">手动授权时自动信任</label>
                </div>

                <div id="ld-status-text" class="ld-status">初始化中...</div>

                <button id="ld-manage-btn" class="ld-btn">📋 管理信任列表</button>
            `;
            document.body.appendChild(panel);

            UI.bindEvents();
            UI.updateStatus();
            UI.buildModal();
            UI.toggleControlsVisibility(Store.getMode());
        },

        toggleControlsVisibility: (mode) => {
            const autoAddContainer = document.getElementById('ld-auto-add-container');
            const officialTrustContainer = document.getElementById('ld-trust-official-container');

            // 手动模式下:显示“自动记录”,隐藏“官方信任”
            if (mode === 'manual') {
                autoAddContainer?.classList.remove('hidden');
                officialTrustContainer?.classList.add('hidden');
            }
            // 安全模式下:显示“官方信任”,隐藏“自动记录”
            else if (mode === 'safe') {
                autoAddContainer?.classList.add('hidden');
                officialTrustContainer?.classList.remove('hidden');
            }
            // 危险模式下:全部隐藏
            else {
                autoAddContainer?.classList.add('hidden');
                officialTrustContainer?.classList.add('hidden');
            }
        },

        bindEvents: () => {
            const modeSelect = document.getElementById('ld-mode-select');
            modeSelect.value = Store.getMode();
            modeSelect.addEventListener('change', (e) => {
                const val = e.target.value;
                if (val === 'dangerous') {
                    if (!confirm('⚠️ 高危警告:\n危险模式将自动同意【所有】网站的授权请求。\n\n请确认风险!')) {
                        modeSelect.value = Store.getMode();
                        return;
                    }
                }
                Store.setMode(val);
                UI.toggleControlsVisibility(val);
                UI.updateStatus();
                Main.runAutoLogic();
            });

            const officialCheck = document.getElementById('ld-trust-official-check');
            officialCheck.checked = Store.getTrustOfficial();
            officialCheck.addEventListener('change', (e) => {
                Store.setTrustOfficial(e.target.checked);
                UI.updateStatus();
                Main.runAutoLogic();
            });

            const autoAddCheck = document.getElementById('ld-auto-add-check');
            autoAddCheck.checked = Store.getAutoAdd();
            autoAddCheck.addEventListener('change', (e) => {
                Store.setAutoAdd(e.target.checked);
            });

            document.getElementById('ld-manage-btn').addEventListener('click', () => {
                UI.switchView('list');
                document.getElementById('ld-overlay').style.display = 'flex';
            });
        },

        updateStatus: () => {
            const mode = Store.getMode();
            const targetUrl = Utils.getTargetSite();
            const isLinuxDo = Utils.isLinuxDoSubdomain(targetUrl);
            const trustOfficial = Store.getTrustOfficial();
            const isTrusted = Logic.checkTrust(targetUrl);
            const statusEl = document.getElementById('ld-status-text');

            statusEl.className = 'ld-status';

            if (mode === 'manual') {
                statusEl.textContent = '手动模式:请手动点击允许。';
            } else if (mode === 'dangerous') {
                statusEl.classList.add('danger');
                statusEl.innerHTML = '危险模式运行中<br>自动同意所有请求!';
            } else if (mode === 'safe') {
                if (isTrusted) {
                    statusEl.classList.add('safe');
                    if (trustOfficial && isLinuxDo) {
                        statusEl.innerHTML = '✅ 官方域名(自动匹配)<br>准备自动授权...';
                    } else {
                        statusEl.innerHTML = '✅ 已匹配信任规则<br>准备自动授权...';
                    }
                } else {
                    statusEl.classList.add('warn');
                    statusEl.innerHTML = '⚠️ 未信任的新站点<br>等待手动确认';
                }
            }
        },

        buildModal: () => {
            const overlay = document.createElement('div');
            overlay.id = 'ld-overlay';
            overlay.innerHTML = `
                <div id="ld-modal">
                    <div class="ld-modal-header">
                        <span class="ld-title" id="ld-modal-title">信任站点管理</span>
                        <div style="display:flex; gap:10px; align-items:center;">
                             <button id="ld-add-btn" class="ld-btn ld-btn-sm primary" title="新建规则">✚ 新建</button>
                             <button id="ld-modal-close" class="ld-action-btn" style="font-size:20px">×</button>
                        </div>
                    </div>

                    <div id="ld-view-list" style="display:flex; flex-direction:column; flex:1; overflow:hidden;">
                        <div style="padding:10px; border-bottom:1px solid #374151;">
                            <input type="text" id="ld-search" class="ld-input" placeholder="🔍 搜索规则..." style="margin:0;">
                        </div>
                        <div id="ld-list-body" class="ld-modal-body"></div>
                        <div style="padding:15px; border-top:1px solid #374151; display:flex; justify-content:flex-end;">
                            <button id="ld-clear-all" class="ld-btn danger" style="width:auto; padding:6px 15px;">🗑️ 清空全部</button>
                        </div>
                    </div>

                    <div id="ld-view-editor" class="ld-modal-body" style="display:none;"></div>
                </div>
            `;
            document.body.appendChild(overlay);

            const close = () => { overlay.style.display = 'none'; };
            document.getElementById('ld-modal-close').addEventListener('click', close);
            overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });

            document.getElementById('ld-search').addEventListener('input', (e) => UI.renderList(e.target.value));
            document.getElementById('ld-clear-all').addEventListener('click', () => {
                if (confirm('确定删除所有信任记录吗?')) {
                    Store.setList([]);
                    UI.renderList();
                    UI.updateStatus();
                }
            });
            document.getElementById('ld-add-btn').addEventListener('click', () => {
                UI.switchView('editor', null);
            });
        },

        switchView: (view, item = null) => {
            UI.state.currentView = view;
            UI.state.editingItem = item;

            const listView = document.getElementById('ld-view-list');
            const editorView = document.getElementById('ld-view-editor');
            const addBtn = document.getElementById('ld-add-btn');
            const title = document.getElementById('ld-modal-title');

            if (view === 'list') {
                listView.style.display = 'flex';
                editorView.style.display = 'none';
                addBtn.style.display = 'block';
                title.textContent = '信任站点管理';
                UI.renderList(document.getElementById('ld-search').value);
            } else {
                listView.style.display = 'none';
                editorView.style.display = 'block';
                addBtn.style.display = 'none';
                title.textContent = item ? '编辑规则' : '新建规则';
                UI.renderEditor(item);
            }
        },

        renderList: (filterText = '') => {
            const container = document.getElementById('ld-list-body');
            container.innerHTML = '';

            let list = Store.getList();
            if (filterText) {
                list = list.filter(item => item.url.toLowerCase().includes(filterText.toLowerCase()));
            }

            if (list.length === 0) {
                container.innerHTML = '<div style="padding:20px; text-align:center; color:#6b7280;">无数据</div>';
                return;
            }

            list.sort((a, b) => b.addedAt - a.addedAt).forEach(item => {
                const row = document.createElement('div');
                row.className = 'ld-list-item';

                const info = document.createElement('div');
                info.className = 'ld-site-info';
                info.appendChild(Utils.createSafeElement('div', 'ld-site-url', item.url));
                info.appendChild(Utils.createSafeElement('div', 'ld-site-date', Utils.formatDate(item.addedAt)));

                const actions = document.createElement('div');
                actions.className = 'ld-actions';

                const editBtn = document.createElement('button');
                editBtn.className = 'ld-action-btn edit';
                editBtn.innerHTML = '✎';
                editBtn.title = '编辑';
                editBtn.onclick = () => UI.switchView('editor', item);

                const delBtn = document.createElement('button');
                delBtn.className = 'ld-action-btn del';
                delBtn.innerHTML = '🗑️';
                delBtn.title = '删除';
                delBtn.onclick = () => {
                    if (confirm(`不再信任 ${item.url}?`)) {
                        Logic.removeSite(item.url);
                        UI.renderList(document.getElementById('ld-search').value);
                    }
                };

                actions.appendChild(editBtn);
                actions.appendChild(delBtn);

                row.appendChild(info);
                row.appendChild(actions);
                container.appendChild(row);
            });
        },

        renderEditor: (item) => {
            const container = document.getElementById('ld-view-editor');
            const initialValue = item ? item.url : '';

            container.innerHTML = `
                <div class="ld-editor-view">
                    <label style="display:block; margin-bottom:5px; color:#e5e7eb;">域名或 URL 规则:</label>
                    <input type="text" id="ld-edit-input" class="ld-input" value="${initialValue}" placeholder="例如: https://example.com" autofocus>

                    <div class="ld-help-text">
                        <strong>💡 规则说明:</strong><br>
                        1. <strong>精准匹配:</strong> 输入完整 URL,如 <code>https://oa.example.com</code><br>
                        2. <strong>通配符匹配:</strong> 使用 <code>*</code> 代表任意字符。<br>
                           - <code>*.example.com</code> 匹配所有二级域名。<br>
                           - <code>*linux.do</code> 匹配所有以 linux.do 结尾的域名。<br>
                        3. <strong>注意:</strong> 匹配时不区分大小写。
                    </div>

                    <div class="ld-editor-actions">
                        <button id="ld-save-btn" class="ld-btn primary">💾 保存</button>
                        <button id="ld-cancel-btn" class="ld-btn">取消</button>
                    </div>
                </div>
            `;

            const input = document.getElementById('ld-edit-input');
            input.focus();

            document.getElementById('ld-save-btn').addEventListener('click', () => {
                const val = input.value.trim();
                if (!val) {
                    alert('内容不能为空');
                    return;
                }

                if (item) {
                    Logic.updateSite(item.url, val);
                } else {
                    Logic.addSite(val);
                }
                UI.switchView('list');
            });

            document.getElementById('ld-cancel-btn').addEventListener('click', () => {
                UI.switchView('list');
            });

            input.addEventListener('keyup', (e) => {
                 if (e.key === 'Enter') document.getElementById('ld-save-btn').click();
            });
        }
    };

    // ================= 主程序 =================

    const Main = {
        init: () => {
            try {
                UI.init();
                Main.hookManualClick();
                Main.runAutoLogic();
            } catch (e) {
                console.error('[LD Auth] Init failed:', e);
            }
        },

        hookManualClick: () => {
            const btn = Utils.getApproveBtn();
            if (!btn) return;

            btn.addEventListener('click', (e) => {
                if (!e.isTrusted) return;

                if (Store.getAutoAdd()) {
                    const url = Utils.getTargetSite();
                    if (url) {
                        Logic.addSite(url);
                        setTimeout(UI.updateStatus, 100);
                    }
                }
            });
        },

        runAutoLogic: () => {
            const btn = Utils.getApproveBtn();
            const targetUrl = Utils.getTargetSite();
            const mode = Store.getMode();

            if (!btn) return;

            if (mode === 'dangerous') {
                Logic.performClick(btn, '危险模式');
            } else if (mode === 'safe') {
                if (Logic.checkTrust(targetUrl)) {
                    const isOfficial = Store.getTrustOfficial() && Utils.isLinuxDoSubdomain(targetUrl);
                    Logic.performClick(btn, isOfficial ? '安全模式-官方域名' : '安全模式-规则匹配');
                }
            }
        }
    };

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

})();