// ==UserScript== // @name 学习通绕过解锁粘贴限制 // @namespace https://scriptcat.org/zh-CN/script-show-page/4835 // @version 1.7.0 // @description 解锁学习通 代码编辑区、UE编辑器、讨论区 的粘贴限制,以及小部分改进 // @author Muyue // @match *://*.chaoxing.com/* // @match *://mooc1-*.chaoxing.com/* // @connect sso.chaoxing.com // @grant unsafeWindow // @run-at document-start // @tag 学习通 // @tag 绕过粘贴限制 // @tag 粘贴 // @license CC BY-NC-SA 4.0 // ==/UserScript== (function () { 'use strict'; const TAG = '[Unified-Paste]'; const log = (...a) => console.log(TAG, ...a); const w = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const MARK = '只能录入不能粘贴'; function showNotice(msg) { if (w.$ && w.$.toast) { w.$.toast({ type: 'notice', content: msg }); } else { log(msg); } } function initDiscussionLogic() { const checkObj = setInterval(() => { if (w.obj) { if (w.obj.circle) { w.obj.circle.prohibitReplyPasting = 0; log('Discussion config bypassed (prohibitReplyPasting=0)'); } w.obj.isManager = 1; clearInterval(checkObj); } }, 500); setTimeout(() => clearInterval(checkObj), 10000); w.addEventListener('paste', function (e) { const target = e.target; if (target.tagName === 'TEXTAREA' || target.contentEditable === 'true') { e.stopImmediatePropagation(); } }, true); } function isBlocker(fn) { if (typeof fn !== 'function' || fn.__unlocked) return false; try { return Function.prototype.toString.call(fn).indexOf(MARK) !== -1; } catch (e) { return false; } } function passthrough() { return true; } passthrough.__unlocked = true; // 作业页 allowPaste=="1" 时 editor.addListener('beforepaste', editorPaste) // editorPaste 清空 html.html 并 toast。全局拦截,覆盖之后创建的每一个 UE 实例。 function initUEditorLogic() { let current = passthrough; const apply = (fn) => { if (isBlocker(fn)) { current = passthrough; log('editorPaste blocked'); return; } if (typeof fn === 'function') current = fn; }; try { apply(w.editorPaste); Object.defineProperty(w, 'editorPaste', { configurable: true, enumerable: true, get() { return current; }, set: apply }); } catch (e) { if (isBlocker(w.editorPaste)) w.editorPaste = passthrough; } const wrapAddListener = (owner) => { if (!owner || typeof owner.addListener !== 'function' || owner.addListener.__pasteWrapped) return; const raw = owner.addListener; const wrapped = function (type, fn) { if (type === 'beforepaste' && isBlocker(fn)) { log('dropped beforepaste', this && (this.key || this.uid)); return this; } return raw.apply(this, arguments); }; wrapped.__pasteWrapped = true; owner.addListener = wrapped; }; const stripEditor = (editor) => { if (!editor) return; wrapAddListener(editor); const lists = []; if (editor.__allListeners && Array.isArray(editor.__allListeners.beforepaste)) { lists.push(editor.__allListeners.beforepaste); } if (editor._listeners && Array.isArray(editor._listeners.beforepaste)) { lists.push(editor._listeners.beforepaste); } lists.forEach((list) => { for (let i = list.length - 1; i >= 0; i--) { const fn = list[i]; if (!isBlocker(fn)) continue; if (typeof editor.removeListener === 'function') { try { editor.removeListener('beforepaste', fn); } catch (err) {} } if (list[i] === fn) list.splice(i, 1); log('removed beforepaste', editor.key || editor.uid); } }); }; const sweep = () => { const UE = w.UE; if (!UE) return; wrapAddListener(UE.Editor && UE.Editor.prototype); const map = UE.instants; if (!map) return; Object.keys(map).forEach((key) => { try { stripEditor(map[key]); } catch (err) {} }); }; let ueRef = w.UE; try { Object.defineProperty(w, 'UE', { configurable: true, enumerable: true, get() { return ueRef; }, set(value) { ueRef = value; sweep(); } }); } catch (e) {} sweep(); const timer = setInterval(sweep, 400); setTimeout(() => clearInterval(timer), 180000); // 页面 "1"==1 时写死 disablePasteImage / disableDraggable。 // pasteimage 因此直接 return,粘贴文件不会上传再插入。 const enablePasteFile = () => { const cfg = w.UEDITOR_CONFIG; if (cfg) { cfg.disablePasteImage = false; cfg.disableDraggable = false; } const UE = w.UE; const map = UE && UE.instants; if (!map) return; Object.keys(map).forEach((key) => { const editor = map[key]; if (!editor || !editor.options) return; editor.options.disablePasteImage = false; editor.options.disableDraggable = false; }); }; enablePasteFile(); const fileTimer = setInterval(enablePasteFile, 400); setTimeout(() => clearInterval(fileTimer), 180000); // 粘贴 doc/pdf 等非图片:按附件对话框同一条上传链,插入 insertCloud 卡片。 const hookedBodies = new WeakSet(); const val = (id) => { const el = w.document.getElementById(id); return el && el.value ? el.value : ''; }; const fileSizeText = (n) => { n = Number(n) || 0; if (n < 1024) return n + 'B'; if (n < 1024 * 1024) return (n / 1024).toFixed(2) + 'K'; return (n / 1024 / 1024).toFixed(2) + 'M'; }; const attachHtml = (rep) => { const dataType = String(rep.fileType || '').replace('.', '').toLowerCase(); const url = String(rep.url || ''); const mark = 'objectId='; const at = url.indexOf(mark); const objId = rep.objectid || (at >= 0 ? url.slice(at + mark.length) : ''); if (!objId) return ''; const fileName = rep.original || 'file'; const size = fileSizeText(rep.size); const data = btoa(encodeURIComponent(JSON.stringify({ objectid: objId, name: fileName, type: dataType, size: size }))); return '
'; }; const uploadPastedFile = (editor, file) => { const base = editor.getOpt && editor.getOpt('fileUrl'); const enc = val('uploadEnc') || w.uploadEnc; const stamp = val('uploadTimeStamp') || w.currentTime; const uid = val('userId') || w.uid; if (!base || !enc || !stamp || !uid) { log('attachment upload skipped, missing auth'); return; } const joiner = base.indexOf('?') >= 0 ? '&' : '?'; const url = base + joiner + 'source=1&enc2=' + enc + '&t=' + stamp + '&uid=' + uid; const fd = new FormData(); fd.append('upfile', file, file.name || 'paste.bin'); fetch(url, { method: 'POST', body: fd, credentials: 'include' }) .then((r) => r.json()) .then((rep) => { if (!rep || rep.state !== 'SUCCESS') { log('attachment upload failed', rep && (rep.state || rep.status)); return; } const html = attachHtml(rep); if (!html) return; editor.focus(); editor.execCommand('insertHTML', html, true); showNotice('已粘贴附件 ' + (rep.original || file.name)); }) .catch((err) => log('attachment upload error', err)); }; const hookDocPaste = () => { const UE = w.UE; const map = UE && UE.instants; if (!map) return; Object.keys(map).forEach((key) => { const editor = map[key]; const body = editor && editor.body; if (!body || hookedBodies.has(body)) return; body.addEventListener('paste', function (e) { const cd = e.clipboardData; if (!cd || !cd.items) return; const files = []; for (let i = 0; i < cd.items.length; i++) { const item = cd.items[i]; if (item.kind !== 'file') continue; if (item.type && item.type.indexOf('image/') === 0) continue; const file = item.getAsFile && item.getAsFile(); if (file) files.push(file); } if (!files.length) return; e.preventDefault(); e.stopPropagation(); files.forEach((file) => uploadPastedFile(editor, file)); }); hookedBodies.add(body); log('doc paste upload hooked', editor.key || editor.uid); }); }; hookDocPaste(); const docTimer = setInterval(hookDocPaste, 400); setTimeout(() => clearInterval(docTimer), 180000); } function initCodeEditorLogic() { const checkCodeEditor = setInterval(() => { var codeEditors = w.codeEditors || {}; if (Object.keys(codeEditors).length === 0) return; clearInterval(checkCodeEditor); log('CodeMirror detected, unlocking paste...'); for (let editorId in codeEditors) { let editor = codeEditors[editorId]; if (editor && !editor._isUnlocked) { editor.on('beforeChange', function (cm, change) { if (change.origin === 'paste') { change.cancel(); const pastedText = (event.clipboardData || w.clipboardData).getData('text'); const cursor = cm.getCursor(); cm.operation(function () { cm.replaceRange(pastedText, cursor); }); showNotice('已绕过代码粘贴限制 By.Muyue -imuyue.com'); } }); editor._isUnlocked = true; } } }, 1000); } initDiscussionLogic(); initUEditorLogic(); w.addEventListener('load', () => { initCodeEditorLogic(); }); })();