// ==UserScript== // @name 移除外联脚本 // @version 1.0 // @description 拦截并移除第三方/黑名单外联脚本,提供独立白名单与黑名单管理。 // @author DeepSeek (提取自网页广告拦截器) // @match *://*/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_addStyle // @run-at document-start // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0NSIgZmlsbD0iI2UxMWQ0OCIvPjxwYXRoIGQ9Ik0zNSAzMCBMNTAgNTAgTDM1IDcwIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjciIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik02NSAzMCBMNTAgNTAgTDY1IDcwIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjciIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxsaW5lIHgxPSIyNSIgeTE9IjI1IiB4Mj0iNzUiIHkyPSI3NSIgc3Ryb2tlPSIjZmFjYzE1IiBzdHJva2Utd2lkdGg9IjgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg== // ==/UserScript== (function () { 'use strict'; // ---------- 安全引用 ---------- const _globals = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const _location = _globals.location; const _document = _globals.document; const _URL = _globals.URL; const _MutationObserver = _globals.MutationObserver; const _Element = _globals.Element; const _Set = _globals.Set; const _Map = _globals.Map; const _WeakSet = _globals.WeakSet; const _getComputedStyle = _globals.getComputedStyle; // ---------- 配置键 ---------- const HOST = _location.hostname; const KEY_ENABLED = `extscript_enabled_${HOST}`; const KEY_BLACKLIST = 'extscript_global_blacklist'; // 关键词数组(匹配src) const KEY_WHITELIST = 'extscript_global_whitelist'; // 域名或URL模式(支持通配符*) // ---------- 工具函数 ---------- function matchRule(str, rule) { if (!rule) return false; const escapeRegex = (s) => s.replace(/([.*+?^${}()|[\]\\])/g, "\\$1"); // 完整URL模式 if (/^https?:\/\//i.test(rule)) { const pattern = "^" + rule.split("*").map(escapeRegex).join(".*") + "$"; return new RegExp(pattern, "i").test(str); } // 路径匹配(以/开头) if (rule.startsWith('/')) { const pattern = "^" + rule.split("*").map(escapeRegex).join(".*"); return new RegExp(pattern, "i").test(str); } // 包含匹配(默认) const pattern = ".*" + rule.split("*").map(escapeRegex).join(".*") + ".*"; return new RegExp(pattern, "i").test(str); } function isThirdParty(url) { if (!url || typeof url !== 'string') return false; try { const target = new _URL(url, _location.href); return target.hostname !== HOST; } catch (e) { return false; } } function isWhitelisted(url) { const wl = GM_getValue(KEY_WHITELIST, []); if (!wl.length) return false; try { const target = new _URL(url, _location.href); const host = target.hostname; const full = target.href; return wl.some(rule => { if (/^https?:\/\//i.test(rule)) { return matchRule(full, rule); } else if (rule.startsWith('/')) { return matchRule(target.pathname + target.search, rule); } else { // 默认按域名匹配(支持通配符) return matchRule(host, rule); } }); } catch (e) { return false; } } function isBlacklisted(url) { const bl = GM_getValue(KEY_BLACKLIST, []); if (!bl.length) return false; return bl.some(rule => matchRule(url, rule)); } function shouldBlockScript(src) { if (!src || src.startsWith('data:') || src.startsWith('blob:') || src.startsWith('javascript:')) return false; // 如果白名单命中,放行 if (isWhitelisted(src)) return false; // 第三方且黑名单命中 -> 拦截 if (isThirdParty(src) && isBlacklisted(src)) return true; // 第三方但未命中黑名单 -> 默认拦截(只允许白名单内的第三方) if (isThirdParty(src)) return true; // 同源但黑名单命中 -> 也拦截(用户可手动加黑) if (isBlacklisted(src)) return true; return false; } // ---------- 日志管理 ---------- let logs = []; const MAX_LOGS = 200; function addLog(src, reason) { logs.unshift({ src, reason, time: Date.now() }); if (logs.length > MAX_LOGS) logs.pop(); } function getLogs() { return logs; } // ---------- 核心拦截 ---------- let enabled = false; // 已处理元素集合(防止重复处理) const processedSet = new _WeakSet(); function neutralizeScript(el) { if (!el || el.tagName !== 'SCRIPT') return; if (processedSet.has(el)) return; // 移除src并清空内容 try { el.removeAttribute('src'); el.textContent = ''; el.setAttribute('type', 'text/plain'); processedSet.add(el); } catch (e) {} } function removeScriptElement(el) { if (!el || el.tagName !== 'SCRIPT') return; if (processedSet.has(el)) return; const src = el.src || el.getAttribute('src') || ''; if (!src) return; if (!shouldBlockScript(src)) return; // 记录日志 addLog(src, '第三方/黑名单'); // 阻止加载 neutralizeScript(el); // 从DOM移除(可选) try { if (el.parentNode) el.parentNode.removeChild(el); } catch (e) {} processedSet.add(el); } // ---------- 拦截入口 ---------- function checkAndIntercept(el) { if (!enabled) return; if (!el || el.tagName !== 'SCRIPT') return; if (processedSet.has(el)) return; // 如果已经设置了src(静态或动态已有src),直接检查 const src = el.src || el.getAttribute('src') || ''; if (src) { removeScriptElement(el); return; } // 否则等待src设置时通过hook拦截 } // 劫持src属性setter function hookScriptSrc() { try { const proto = _globals.HTMLScriptElement.prototype; const desc = Object.getOwnPropertyDescriptor(proto, 'src'); if (!desc || !desc.set) return; const origSet = desc.set; Object.defineProperty(proto, 'src', { get: desc.get, set: function (v) { if (enabled && typeof v === 'string') { // 先设置,再检查(因为可能后续还会修改) origSet.call(this, v); // 立即检查并移除 removeScriptElement(this); return; } return origSet.call(this, v); }, configurable: true, enumerable: true }); } catch (e) {} } // 劫持setAttribute function hookSetAttribute() { try { const orig = _Element.prototype.setAttribute; _Element.prototype.setAttribute = function (name, value) { if (enabled && this.tagName === 'SCRIPT' && name.toLowerCase() === 'src' && typeof value === 'string') { // 先调用原方法设置 orig.call(this, name, value); // 检查并移除 removeScriptElement(this); return; } return orig.call(this, name, value); }; } catch (e) {} } // 劫持appendChild/insertBefore(动态创建) function hookDOMInsertion() { try { const origAppend = _Node.prototype.appendChild; _Node.prototype.appendChild = function (child) { if (enabled && child && child.tagName === 'SCRIPT') { // 先检查现有src removeScriptElement(child); // 如果是空src,等后续setter触发 } return origAppend.call(this, child); }; const origInsert = _Node.prototype.insertBefore; _Node.prototype.insertBefore = function (newNode, refNode) { if (enabled && newNode && newNode.tagName === 'SCRIPT') { removeScriptElement(newNode); } return origInsert.call(this, newNode, refNode); }; } catch (e) {} } // MutationObserver监听新增script let mo = null; function startMutationObserver() { if (mo) mo.disconnect(); mo = new _MutationObserver(mutations => { if (!enabled) return; for (const m of mutations) { for (const n of m.addedNodes) { if (n.nodeType === 1 && n.tagName === 'SCRIPT') { removeScriptElement(n); } } if (m.type === 'attributes' && m.attributeName === 'src' && m.target.tagName === 'SCRIPT') { removeScriptElement(m.target); } } }); mo.observe(_document.documentElement || _document, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); } // 扫描现有脚本 function scanExisting() { if (!enabled) return; try { const scripts = _document.querySelectorAll('script[src]'); for (const s of scripts) { removeScriptElement(s); } } catch (e) {} } // ---------- 初始化拦截 ---------- function initInterception() { enabled = GM_getValue(KEY_ENABLED, false); if (!enabled) return; // 尽早hook hookScriptSrc(); hookSetAttribute(); hookDOMInsertion(); // 页面加载后扫描 if (_document.readyState === 'loading') { _document.addEventListener('DOMContentLoaded', scanExisting); } else { scanExisting(); } startMutationObserver(); // 也劫持document.write中的script try { const origWrite = _document.write; _document.write = function (html) { if (enabled && typeof html === 'string') { // 简单处理:提取script[src]并检查 const div = _document.createElement('div'); div.innerHTML = html; const scripts = div.querySelectorAll('script[src]'); for (const s of scripts) { const src = s.src || s.getAttribute('src'); if (src && shouldBlockScript(src)) { // 移除该script标签 s.remove(); addLog(src, 'document.write拦截'); } } // 写入修改后的html html = div.innerHTML; } return origWrite.call(this, html); }; } catch (e) {} } // ---------- 管理面板 (Shadow DOM) ---------- let panelObserver = null; function showManagerUI() { if (_document.getElementById('extscr-host-container')) return; const hostContainer = _document.createElement('div'); hostContainer.id = 'extscr-host-container'; Object.assign(hostContainer.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', zIndex: '2147483647', pointerEvents: 'none', all: 'initial' }); hostContainer.style.setProperty('z-index', '2147483647', 'important'); _document.documentElement.appendChild(hostContainer); const shadowRoot = hostContainer.attachShadow({ mode: 'closed' }); const style = _document.createElement('style'); style.textContent = ` :host { all: initial !important; } #extscr-overlay { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background: rgba(0, 0, 0, 0.65) !important; backdrop-filter: blur(2px) !important; z-index: 2147483646 !important; display: block !important; pointer-events: auto !important; } #extscr-panel { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; width: 90vw !important; max-width: 450px !important; max-height: 85vh !important; overflow-y: auto !important; background-color: #ffffff !important; border: 1px solid #ccc !important; box-shadow: 0 10px 40px rgba(0,0,0,0.4) !important; z-index: 2147483647 !important; padding: 18px !important; border-radius: 12px !important; font-family: -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif !important; box-sizing: border-box !important; display: block !important; pointer-events: auto !important; } #extscr-panel * { box-sizing: border-box !important; } #extscr-panel h3 { margin: 0 0 12px 0 !important; font-size: 18px !important; color: #222 !important; border-bottom: 2px solid #eee !important; padding-bottom: 8px !important; display: flex !important; justify-content: space-between !important; font-weight: 700 !important; } #extscr-panel label { display: block !important; margin: 10px 0 5px 0 !important; font-weight: 600 !important; font-size: 13px !important; color: #444 !important; } #extscr-panel textarea { width: 100% !important; height: 85px !important; margin: 0 0 5px 0 !important; border: 1px solid #ccc !important; border-radius: 6px !important; padding: 8px !important; font-size: 13px !important; background-color: #fafafa !important; color: #111 !important; resize: vertical !important; display: block !important; } #extscr-panel .buttons { display: flex !important; justify-content: flex-end !important; gap: 8px !important; margin-top: 15px !important; } #extscr-panel button { padding: 10px 14px !important; border-radius: 6px !important; cursor: pointer !important; border: none !important; font-weight: 500 !important; font-size: 13px !important; display: inline-block !important; text-align: center !important; } #extscr-panel .save-btn { background-color: #007bff !important; color: white !important; flex-grow: 1 !important; } #extscr-panel .cancel-btn { background-color: #eee !important; color: #333 !important; } #extscr-panel .log-area { background-color: #f8f8f8 !important; border: 1px solid #ddd !important; border-radius: 6px !important; padding: 6px !important; max-height: 120px !important; overflow-y: auto !important; font-size: 12px !important; font-family: monospace !important; white-space: pre-wrap !important; word-break: break-all !important; } `; shadowRoot.appendChild(style); const overlay = _document.createElement('div'); overlay.id = 'extscr-overlay'; const panel = _document.createElement('div'); panel.id = 'extscr-panel'; const blacklist = GM_getValue(KEY_BLACKLIST, []).join('\n'); const whitelist = GM_getValue(KEY_WHITELIST, []).join('\n'); const logText = getLogs().map(l => `${new Date(l.time).toLocaleTimeString()} ${l.src} (${l.reason})`).join('\n'); panel.innerHTML = `