// ==UserScript== // @name 剪贴板权限控制 // @description 控制网站的写入剪贴板操作,提供允许/拒绝选项 // @version 1.0 // @author WJ // @match *://*/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_registerMenuCommand // @run-at document-start // @namespace https://greasyfork.org/users/914996 // @downloadURL none // ==/UserScript== (() => { 'use strict'; /* ---------- 数据 ---------- */ let whitelist = GM_getValue('whitelist', []); let blacklist = GM_getValue('blacklist', []); const domain = location.hostname; const save = () => { GM_setValue('whitelist', whitelist); GM_setValue('blacklist', blacklist); }; /* ---------- 工具 ---------- */ const toast = (msg) => { const t = document.createElement('div'); t.className = 'WJ_toast'; t.textContent = msg; document.body.append(t); setTimeout(() => t.remove(), 3000); }; const addWarning = () => { const header = document.querySelector('.WJ_modal .WJ_header'); if (!header || header.querySelector('.WJ_warning')) return; const warning = document.createElement('div'); warning.className = 'WJ_warning'; warning.textContent = '⚠️ 此网站授权期多次尝试写入剪贴板 已拒绝后续写入 ⚠️'; warning.style.color = '#FFD700'; warning.style.fontSize = '16px'; warning.style.marginTop = '8px'; header.appendChild(warning); }; const handleDecision = (action, onAllow, onDeny) => { ({ allow: () => (toast('允许本次复制'), onAllow?.()), deny: () => (toast('拒绝本次复制'), onDeny?.()), 'always-allow': () => (!whitelist.includes(domain) && (whitelist.push(domain), save()), toast(`添加白名单 ${domain}`), onAllow?.()), 'always-deny': () => (!blacklist.includes(domain) && (blacklist.push(domain), save()), toast(`添加黑名单 ${domain}`), onDeny?.()) }[action] || (() => {}))(); }; /* ---------- 拦截逻辑 ---------- */ let isModalOpen = false; const decide = (text, onAllow, onDeny) => { if (isModalOpen) return addWarning(), onDeny?.(); if (whitelist.includes(domain)) return onAllow(); if (blacklist.includes(domain)) return (toast('已拦截复制'), onDeny?.()); isModalOpen = true; showModal(text, onAllow, onDeny); }; /* 1. Clipboard API writeText */ navigator.clipboard.writeText = (text) => new Promise((res, rej) => decide(text, () => { GM_setClipboard(text); res(); }, () => rej(new Error('User denied'))) ); /* 2. Clipboard API write (富文本) */ navigator.clipboard.write = d => new Promise((r, j) => { const fallback = () => decide('富文本内容?图片/表格 无法显示', () => (GM_setClipboard(''), r()), j); (d[0]?.types?.includes('text/plain') ? d[0].getType('text/plain') .then(b => new Response(b).text()) : Promise.reject()) .then(t => decide(t, () => (GM_setClipboard(t), r()), j)) .catch(fallback); }); /* 3. copy 事件 */ const rawAdd = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function(t, l, o) { if (t === 'copy' && this === document) return; if (!o && ['touchstart','touchmove','wheel'].includes(t)) o = { passive: true }; rawAdd.call(this, t, l, o); }; const onCopy = (e) => { e?.preventDefault?.(); e?.stopImmediatePropagation?.(); const text = getSelection().toString(); decide(text, () => GM_setClipboard(text)); return true; }; /* 4. execCommand('copy') */ document.oncopy = onCopy; document.execCommand = function (c, u, v) { if (c.toLowerCase() === 'copy') { onCopy({}); return true; } return Document.prototype.execCommand.call(this, c, u, v); }; /* ---------- 弹窗 ---------- */ const showModal = (text, onAllow, onDeny) => { const overlay = document.createElement('div'); overlay.className = 'WJ_overlay'; const modal = document.createElement('div'); modal.className = 'WJ_modal'; modal.innerHTML = `
${domain} 请求写入剪贴板
${text}
`; document.documentElement.append(overlay, modal); const close = () => (overlay.remove(), modal.remove(), isModalOpen = false); const clickHandler = e => { const a = e.target.dataset.action; if (a) handleDecision(a, () => (close(), onAllow?.()), () => (close(), onDeny?.())); }; modal.onclick = clickHandler; overlay.onclick = () => (handleDecision('deny', null, onDeny), close()); }; /* ---------- 管理面板 ---------- */ const showPanel = () => { const overlay = document.createElement('div'); overlay.className = 'WJ_overlay'; const panel = document.createElement('div'); panel.className = 'WJ_modal'; panel.innerHTML = `
黑白名单-管理面板
白名单
${whitelist.map(d => `
${d}
`).join('') || '
白名单为空
'}
黑名单
${blacklist.map(d => `
${d}
`).join('') || '
黑名单为空
'}
`; document.documentElement.append(overlay, panel); panel.addEventListener('click', e => { if (e.target.id === 'WJ_close') return overlay.remove(), panel.remove(); if (e.target.classList.contains('WJ_delete')) { const { list, domain } = e.target.dataset; list === 'whitelist' ? whitelist = whitelist.filter(d => d !== domain) : blacklist = blacklist.filter(d => d !== domain); save(); overlay.remove(); panel.remove(); showPanel(); } }); }; /* ---------- 样式 ---------- */ GM_addStyle(` .WJ_modal,.WJ_overlay+div{position:fixed;top:65%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:560px;background:#121212;z-index:99999;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.3);font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;overflow:hidden;} .WJ_header{background:linear-gradient(135deg,#4a6fa5,#3a5a8a);color:#fff;padding:20px;font-size:20px;font-weight:600;text-align:center;letter-spacing:.5px;} .WJ_text{white-space:pre-wrap;word-break:break-word;background:#1F2021;padding:5px;border-radius:8px;height:220px;overflow-y:auto;font-family:Consolas,monospace;font-size:15px;line-height:1.5;color:#ccc;margin:15px;border:1px solid #333;} .WJ_footer{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:15px;background:#1F1F1F;} .WJ_btn{padding:12px 5px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:14px;color:#fff;text-align:center;transition:transform .1s,filter .1s;} .WJ_btn:active{transform:scale(0.98);} .WJ_allow{background:linear-gradient(135deg,#4CAF50,#2E7D32);} .WJ_deny{background:linear-gradient(135deg,#FF9800,#EF6C00);} .WJ_always-allow{background:linear-gradient(135deg,#2196F3,#1565C0);} .WJ_always-deny{background:linear-gradient(135deg,#F44336,#C62828);} .WJ_close{background:#3D5E90;padding:14px 40px;margin:0 auto;} .WJ_toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.9);color:#fff;padding:16px 32px;border-radius:8px;border:2px solid #CCC;z-index:99999;font-size:16px;font-weight:500;} .WJ_overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:99998;backdrop-filter:blur(8px);} .WJ_panel-content{display:flex;padding:0;max-height:60vh;} .WJ_list{flex:1;padding:20px;overflow-y:auto;border-right:1px solid #333;background:#121212;} .WJ_list:last-child{border-right:none;} .WJ_list-title{font-weight:600;margin:0 0 15px;padding-bottom:10px;border-bottom:2px solid #4a6fa5;color:#AAA;text-align:center;font-size:18px;} .WJ_list-item{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;background:#1F2021;margin-bottom:10px;border-radius:8px;border:1px solid #333;} .WJ_list-item span{overflow:hidden;text-overflow:ellipsis;color:#ddd;} .WJ_delete{background:linear-gradient(135deg,#dc3545,#c82333);color:#fff;border:none;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:14px;font-weight:500;margin-left:10px;} .WJ_close-box{padding:20px;text-align:center;border-top:1px solid #333;background:#1F2021;} .WJ_empty{text-align:center;padding:20px;color:#6c757d;font-style:italic;}` ); GM_registerMenuCommand('黑白名单管理', showPanel); })();