// ==UserScript== // @name 解除粘贴 // @namespace https://github.com/unlock-paste // @version 1.1.0 // @description 解除网页“禁止粘贴”的限制:剥离 onpaste/onbeforepaste 属性,并在 window 捕获阶段拦截 paste/beforepaste 与粘贴型 beforeinput,让浏览器默认粘贴正常发生。支持菜单切换(全局/本站/状态/强制刷新),默认全站开启,并覆盖 iframe。 // @author Unlock-Paste // @license MIT // @match *://*/* // @match file:///* // @all-frames // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-start // ==/UserScript== /* * 解除粘贴 专版 * * 作用:只针对「粘贴」这一件事。很多网页通过 onpaste="return false" 或 * addEventListener('paste', e => e.preventDefault()) 禁止粘贴。本脚本: * 1. 在 window 捕获阶段拦截 paste/beforepaste 与粘贴型 beforeinput(insertFromPaste), * 只 stopImmediatePropagation,不 preventDefault,从而让站点自己的拦截处理器不再执行, * 浏览器默认粘贴仍会发生。 * 2. 移除内联 onpaste/onbeforepaste 属性,并用 MutationObserver(childList + 属性过滤) * 处理后续动态插入、或站点动态重设的拦截。 * 3. 默认全站开启,并在所有 iframe 生效;菜单只在顶层窗口注册,避免干扰。 * * 使用:脚本管理器(脚本猫/油猴)菜单项切换: * - 「解除粘贴:切换全局」全站开启/关闭 * - 「解除粘贴:切换本站」当前站点开启/关闭(按 hostname 精确记录) * - 「解除粘贴:查看状态」查看全局/本站/当前是否生效 * - 「解除粘贴:强制刷新」立即重新扫描并剥离一遍 * * 边界:只处理 paste,不改 copy/cut/selectstart/contextmenu/user-select, * 也不改 readonly/disabled,尽量保留页面其它行为原样。 * * 稳定性: * - @all-frames 覆盖 iframe 内嵌表单/编辑器;菜单仅在顶层窗口显示。 * - window 哨兵避免同一上下文重复注入;MutationObserver 逐项 try/catch 兜底。 * - 配置带 schemaVersion,便于后续升级迁移。 * * 仅供个人提升效率自用。 */ (function () { 'use strict'; // 防重复注入:同一窗口上下文只跑一次。 if (window.__unlockPasteV1) return; window.__unlockPasteV1 = true; // ==================== 常量 ==================== const SCRIPT_VERSION = '1.1.0'; const CFG_KEY = 'unlock_paste_config'; const SCHEMA_VERSION = 1; const DEFAULT_CFG = { enabled: true, disabledSites: [], schemaVersion: SCHEMA_VERSION }; // 是否为顶层窗口;跨域访问 window.top 需保护。 let isTop = true; try { isTop = (window.top === window); } catch (e) { isTop = true; } // ==================== 状态 ==================== const STATE = { cfg: null, active: false, // 本页当前是否已挂上解除逻辑 pasteHandler: null, beforePasteHandler: null, beforeInputHandler: null, observer: null, stripped: new Map() // el -> { onpaste, onbeforepaste } 原始值,用于停用时还原 }; // ==================== 配置持久化 ==================== function loadCfg() { let cfg = null; try { cfg = GM_getValue(CFG_KEY, null); } catch (e) { cfg = null; } if (!cfg || typeof cfg !== 'object') { cfg = { enabled: DEFAULT_CFG.enabled, disabledSites: [], schemaVersion: DEFAULT_CFG.schemaVersion }; } if (typeof cfg.enabled !== 'boolean') cfg.enabled = DEFAULT_CFG.enabled; if (!Array.isArray(cfg.disabledSites)) cfg.disabledSites = []; cfg.disabledSites = cfg.disabledSites.map(String).map(function (s) { return s.toLowerCase(); }); if (typeof cfg.schemaVersion !== 'number') cfg.schemaVersion = SCHEMA_VERSION; return cfg; } function saveCfg() { try { GM_setValue(CFG_KEY, STATE.cfg); } catch (e) { /* 忽略持久化失败 */ } } // ==================== 辅助 ==================== function host() { return (location.hostname || '').toLowerCase(); } function isSiteDisabled() { return STATE.cfg.disabledSites.indexOf(host()) !== -1; } function shouldApply() { return STATE.cfg.enabled && !isSiteDisabled(); } function notify(msg) { try { GM_notification('[解除粘贴] ' + msg, '解除粘贴'); } catch (e) { /* 忽略 */ } } function safeLog(msg) { try { console.log('[解除粘贴] ' + msg); } catch (e) { /* 忽略 */ } } // ==================== 粘贴拦截 ==================== // 只阻断传播,不取消默认行为;这样站点注册的拦截处理器不会被触发。 function blockPaste(e) { e.stopImmediatePropagation(); } // 富文本/部分编辑器用 beforeinput(insertFromPaste) 阻止粘贴,这里只拦粘贴来源。 function blockBeforeInput(e) { if (e.inputType && e.inputType.indexOf('insertFromPaste') === 0) { e.stopImmediatePropagation(); } } // ==================== 属性剥离 + 还原 ==================== function stripPasteInContainer(root) { if (!root) return; let els; if (root.nodeType === 1) { els = [root].concat(Array.prototype.slice.call(root.querySelectorAll('[onpaste],[onbeforepaste]'))); } else { els = Array.prototype.slice.call(root.querySelectorAll('[onpaste],[onbeforepaste]')); } for (let i = 0; i < els.length; i++) { const el = els[i]; if (!STATE.stripped.has(el)) { STATE.stripped.set(el, { onpaste: el.getAttribute('onpaste'), onbeforepaste: el.getAttribute('onbeforepaste') }); } el.removeAttribute('onpaste'); el.removeAttribute('onbeforepaste'); } } function restoreAttributes() { STATE.stripped.forEach(function (rec, el) { if (!el || !el.isConnected) return; if (rec.onpaste != null) el.setAttribute('onpaste', rec.onpaste); else el.removeAttribute('onpaste'); if (rec.onbeforepaste != null) el.setAttribute('onbeforepaste', rec.onbeforepaste); else el.removeAttribute('onbeforepaste'); }); STATE.stripped.clear(); } // ==================== MutationObserver 兜底 ==================== function startObserver() { if (STATE.observer) return; STATE.observer = new MutationObserver(function (muts) { for (let i = 0; i < muts.length; i++) { const m = muts[i]; try { if (m.type === 'attributes') { if (m.target && m.target.nodeType === 1) stripPasteInContainer(m.target); } else { const added = m.addedNodes; for (let j = 0; j < added.length; j++) { const n = added[j]; if (n.nodeType === 1 || n.nodeType === 9) stripPasteInContainer(n); } } } catch (e) { /* 单个变更异常不阻塞 */ } } }); // childList 处理动态插入的元素;attributes 过滤处理站点动态重设 onpaste。 STATE.observer.observe(document.documentElement || document, { childList: true, subtree: true, attributes: true, attributeFilter: ['onpaste', 'onbeforepaste'] }); } // ==================== 应用 / 停用 ==================== function apply() { if (STATE.active) return; STATE.pasteHandler = blockPaste; STATE.beforePasteHandler = blockPaste; STATE.beforeInputHandler = blockBeforeInput; window.addEventListener('paste', STATE.pasteHandler, true); window.addEventListener('beforepaste', STATE.beforePasteHandler, true); window.addEventListener('beforeinput', STATE.beforeInputHandler, true); stripPasteInContainer(document); startObserver(); STATE.active = true; safeLog('已启用 ' + host() + (isTop ? '' : '(子框架)')); } function remove() { if (!STATE.active) return; if (STATE.pasteHandler) window.removeEventListener('paste', STATE.pasteHandler, true); if (STATE.beforePasteHandler) window.removeEventListener('beforepaste', STATE.beforePasteHandler, true); if (STATE.beforeInputHandler) window.removeEventListener('beforeinput', STATE.beforeInputHandler, true); if (STATE.observer) { STATE.observer.disconnect(); STATE.observer = null; } STATE.pasteHandler = null; STATE.beforePasteHandler = null; STATE.beforeInputHandler = null; STATE.active = false; restoreAttributes(); safeLog('已停用 ' + host() + (isTop ? '' : '(子框架)')); } // ==================== 菜单命令 ==================== function toggleGlobal() { STATE.cfg.enabled = !STATE.cfg.enabled; saveCfg(); if (STATE.cfg.enabled) apply(); else remove(); notify('全局解除粘贴已' + (STATE.cfg.enabled ? '开启' : '关闭')); } function toggleSite() { const h = host(); const idx = STATE.cfg.disabledSites.indexOf(h); if (idx === -1) { STATE.cfg.disabledSites.push(h); } else { STATE.cfg.disabledSites.splice(idx, 1); } saveCfg(); if (shouldApply()) apply(); else remove(); notify('本站解除粘贴已' + (idx === -1 ? '关闭' : '开启') + '(' + h + ')'); } function showStatus() { notify( '全局' + (STATE.cfg.enabled ? '开' : '关') + ' | 本站' + (isSiteDisabled() ? '关' : '开') + ' | 已生效' + (STATE.active ? '是' : '否') + '(' + host() + (isTop ? '' : ',子框架') + ')' ); } function refresh() { if (shouldApply()) { stripPasteInContainer(document); safeLog('已强制刷新当前页面'); } notify('已强制刷新当前页面'); } function registerMenus() { // 菜单只在顶层窗口注册,避免在 iframe 里刷屏。 if (!isTop) return; try { GM_registerMenuCommand('解除粘贴:切换全局', toggleGlobal); GM_registerMenuCommand('解除粘贴:切换本站', toggleSite); GM_registerMenuCommand('解除粘贴:查看状态', showStatus); GM_registerMenuCommand('解除粘贴:强制刷新', refresh); } catch (e) { /* 忽略菜单注册失败 */ } } // ==================== 启动 ==================== function init() { STATE.cfg = loadCfg(); registerMenus(); if (shouldApply()) apply(); safeLog('v' + SCRIPT_VERSION + ' 已启动 ' + (isTop ? '顶层' : '子框架') + ',' + (STATE.active ? '已生效' : '未生效')); } init(); })();