使用 Greasemonkey 解除网页复制粘贴限制
吾辈的博客原文地址:https://blog.rxliuli.com/p/4b2822b2/
吾辈发布了一个油猴脚本,可以直接安装 解除网页限制 以获得更好的使用体验。
场景
在浏览网页时经常会出现的一件事,当吾辈想要复制,突然发现复制好像没用了?(知乎禁止转载的文章)亦或者是复制的最后多出了一点内容(简书),或者干脆直接不能选中了(360doc)。粘贴时也有可能发现一直粘贴不了(支付宝登录)。
问题
欲先制敌,必先惑敌。想要解除复制粘贴的限制,就必须要清楚它们是如何实现的。不管如何,浏览器上能够运行的都是 JavaScript,它们都是使用 JavaScript 实现的。实现方式大致都是监听相应的事件(例如 onkeydown
监听 Ctrl-C
),然后做一些特别的操作。
例如屏蔽复制功能只需要一句代码
document.oncopy = event => false
是的,只要返回了 false,那么 copy 就会失效。还有一个更讨厌的方式,直接在 body
元素上加行内事件
<body oncopy="javascript: return false" />
解决
可以看出,一般都是使用 JavaScript 在相应事件中返回 false,来阻止对应事件。那么,既然事件都被阻止了,是否意味着我们就束手无策了呢?吾辈所能想到的解决方案大致有三种方向
使用 JavaScript 监听事件并自行实现复制/剪切/粘贴功能
- 优点:实现完成后不管是任何网站都能使用,并且不会影响到监听之外的事件,也不会删除监听的同类型事件,可以解除浏览器本身的限制(密码框禁止复制)
- 缺点:某些功能自行实现难度很大,例如选择文本
重新实现
addEventListener
然后删除掉网站自定义的事件- 优点:事件生效范围广泛,通用性高,不仅 _复制/剪切/粘贴_,其他类型的事件也可以解除
- 缺点:实现起来需要替换
addEventListener
事件够早,对浏览器默认操作不会生效(密码框禁止复制),而且某些网站也无法破解
替换元素并删除 DOM 上的事件属性
- 优点:能够确保网站 js 的限制被解除,通用性高,事件生效范围广泛
缺点:可能影响到其他类型的事件,复制节点时不会复制使用
addEventListener
添加的事件注:此方法不予演示,缺陷实在过大
总之,如果真的想解除限制,恐怕需要两种方式并用才可以呢
使用 JavaScript 监听事件并自行实现复制/剪切/粘贴功能
实现强制复制
思路
- 冒泡监听
copy
事件 - 获取当前选中的内容
- 设置剪切版的内容
- 阻止默认事件处理
// 强制复制document.addEventListener( 'copy', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) // 阻止默认的事件处理 event.preventDefault() }, true,)
实现强制剪切
思路
- 冒泡监听
cut
事件 - 获取当前选中的内容
- 设置剪切版的内容
- 如果是可编辑内容要删除选中部分
- 阻止默认事件处理
可以看到唯一需要增加的就是需要额外处理可编辑内容了,然而代码量瞬间爆炸了哦
/** * 字符串安全的转换为小写 * @param {String} str 字符串 * @returns {String} 转换后得到的全小写字符串 */function toLowerCase(str) { if (!str || typeof str !== 'string') { return str } return str.toLowerCase()}/** * 判断指定元素是否是可编辑元素 * 注:可编辑元素并不一定能够进行编辑,例如只读的 input 元素 * @param {Element} el 需要进行判断的元素 * @returns {Boolean} 是否为可编辑元素 */function isEditable(el) { var inputEls = ['input', 'date', 'datetime', 'select', 'textarea'] return ( el && (el.isContentEditable || inputEls.includes(toLowerCase(el.tagName))) )}/** * 获取输入框中光标所在位置 * @param {Element} el 需要获取的输入框元素 * @returns {Number} 光标所在位置的下标 */function getCusorPostion(el) { return el.selectionStart}/** * 设置输入框中选中的文本/光标所在位置 * @param {Element} el 需要设置的输入框元素 * @param {Number} start 光标所在位置的下标 * @param {Number} {end} 结束位置,默认为输入框结束 */function setCusorPostion(el, start, end = start) { el.focus() el.setSelectionRange(start, end)}/** * 在指定范围内删除文本 * @param {Element} el 需要设置的输入框元素 * @param {Number} {start} 开始位置,默认为当前选中开始位置 * @param {Number} {end} 结束位置,默认为当前选中结束位置 */function removeText(el, start = el.selectionStart, end = el.selectionEnd) { // 删除之前必须要 [记住] 当前光标的位置 var index = getCusorPostion(el) var value = el.value el.value = value.substr(0, start) + value.substr(end, value.length) setCusorPostion(el, index)}// 强制剪切document.addEventListener( 'cut', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) // 如果是可编辑元素还要进行删除 if (isEditable(event.target)) { removeText(event.target) } event.preventDefault() }, true,)
实现强制粘贴
- 冒泡监听
focus/blur
,以获得最后一个获得焦点的可编辑元素 - 冒泡监听
paste
事件 - 获取剪切版的内容
- 获取最后一个获得焦点的可编辑元素
- 删除当前选中的文本
- 在当前光标处插入文本
- 阻止默认事件处理
/** * 获取到最后一个获得焦点的元素 */var getLastFocus = (lastFocusEl => { document.addEventListener( 'focus', event => { lastFocusEl = event.target }, true, ) document.addEventListener( 'blur', event => { lastFocusEl = null }, true, ) return () => lastFocusEl})(null)/** * 字符串安全的转换为小写 * @param {String} str 字符串 * @returns {String} 转换后得到的全小写字符串 */function toLowerCase(str) { if (!str || typeof str !== 'string') { return str } return str.toLowerCase()}/** * 判断指定元素是否是可编辑元素 * 注:可编辑元素并不一定能够进行编辑,例如只读的 input 元素 * @param {Element} el 需要进行判断的元素 * @returns {Boolean} 是否为可编辑元素 */function isEditable(el) { var inputEls = ['input', 'date', 'datetime', 'select', 'textarea'] return ( el && (el.isContentEditable || inputEls.includes(toLowerCase(el.tagName))) )}/** * 获取输入框中光标所在位置 * @param {Element} el 需要获取的输入框元素 * @returns {Number} 光标所在位置的下标 */function getCusorPostion(el) { return el.selectionStart}/** * 设置输入框中选中的文本/光标所在位置 * @param {Element} el 需要设置的输入框元素 * @param {Number} start 光标所在位置的下标 * @param {Number} {end} 结束位置,默认为输入框结束 */function setCusorPostion(el, start, end = start) { el.focus() el.setSelectionRange(start, end)}/** * 在指定位置后插入文本 * @param {Element} el 需要设置的输入框元素 * @param {String} value 要插入的值 * @param {Number} {start} 开始位置,默认为当前光标处 */function insertText(el, text, start = getCusorPostion(el)) { var value = el.value el.value = value.substr(0, start) + text + value.substr(start) setCusorPostion(el, start + text.length)}/** * 在指定范围内删除文本 * @param {Element} el 需要设置的输入框元素 * @param {Number} {start} 开始位置,默认为当前选中开始位置 * @param {Number} {end} 结束位置,默认为当前选中结束位置 */function removeText(el, start = el.selectionStart, end = el.selectionEnd) { // 删除之前必须要 [记住] 当前光标的位置 var index = getCusorPostion(el) var value = el.value el.value = value.substr(0, start) + value.substr(end, value.length) setCusorPostion(el, index)}// 强制粘贴document.addEventListener( 'paste', event => { // 获取当前剪切板内容 var clipboardData = event.clipboardData var items = clipboardData.items var item = items[0] if (item.kind !== 'string') { return } var text = clipboardData.getData(item.type) // 获取当前焦点元素 // 粘贴的时候获取不到焦点? var focusEl = getLastFocus() // input 居然不是 [可编辑] 的元素? if (isEditable(focusEl)) { removeText(focusEl) insertText(focusEl, text) event.preventDefault() } }, true,)
总结
脚本全貌
;(function() { 'use strict' /** * 两种思路: * 1. 自己实现 * 2. 替换元素 */ /** * 获取到最后一个获得焦点的元素 */ var getLastFocus = (lastFocusEl => { document.addEventListener( 'focus', event => { lastFocusEl = event.target }, true, ) document.addEventListener( 'blur', event => { lastFocusEl = null }, true, ) return () => lastFocusEl })(null) /** * 字符串安全的转换为小写 * @param {String} str 字符串 * @returns {String} 转换后得到的全小写字符串 */ function toLowerCase(str) { if (!str || typeof str !== 'string') { return str } return str.toLowerCase() } /** * 字符串安全的转换为大写 * @param {String} str 字符串 * @returns {String} 转换后得到的全大写字符串 */ function toUpperCase(str) { if (!str || typeof str !== 'string') { return str } return str.toUpperCase() } /** * 判断指定元素是否是可编辑元素 * 注:可编辑元素并不一定能够进行编辑,例如只读的 input 元素 * @param {Element} el 需要进行判断的元素 * @returns {Boolean} 是否为可编辑元素 */ function isEditable(el) { var inputEls = ['input', 'date', 'datetime', 'select', 'textarea'] return ( el && (el.isContentEditable || inputEls.includes(toLowerCase(el.tagName))) ) } /** * 获取输入框中光标所在位置 * @param {Element} el 需要获取的输入框元素 * @returns {Number} 光标所在位置的下标 */ function getCusorPostion(el) { return el.selectionStart } /** * 设置输入框中选中的文本/光标所在位置 * @param {Element} el 需要设置的输入框元素 * @param {Number} start 光标所在位置的下标 * @param {Number} {end} 结束位置,默认为输入框结束 */ function setCusorPostion(el, start, end = start) { el.focus() el.setSelectionRange(start, end) } /** * 在指定位置后插入文本 * @param {Element} el 需要设置的输入框元素 * @param {String} value 要插入的值 * @param {Number} {start} 开始位置,默认为当前光标处 */ function insertText(el, text, start = getCusorPostion(el)) { var value = el.value el.value = value.substr(0, start) + text + value.substr(start) setCusorPostion(el, start + text.length) } /** * 在指定范围内删除文本 * @param {Element} el 需要设置的输入框元素 * @param {Number} {start} 开始位置,默认为当前选中开始位置 * @param {Number} {end} 结束位置,默认为当前选中结束位置 */ function removeText(el, start = el.selectionStart, end = el.selectionEnd) { // 删除之前必须要 [记住] 当前光标的位置 var index = getCusorPostion(el) var value = el.value el.value = value.substr(0, start) + value.substr(end, value.length) setCusorPostion(el, index) } // 强制复制 document.addEventListener( 'copy', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) event.preventDefault() }, true, ) // 强制剪切 document.addEventListener( 'cut', event => { event.clipboardData.setData( 'text/plain', document.getSelection().toString(), ) // 如果是可编辑元素还要进行删除 if (isEditable(event.target)) { removeText(event.target) } event.preventDefault() }, true, ) // 强制粘贴 document.addEventListener( 'paste', event => { // 获取当前剪切板内容 var clipboardData = event.clipboardData var items = clipboardData.items var item = items[0] if (item.kind !== 'string') { return } var text = clipboardData.getData(item.type) // 获取当前焦点元素 // 粘贴的时候获取不到焦点? var focusEl = getLastFocus() // input 居然不是 [可编辑] 的元素? if (isEditable(focusEl)) { removeText(focusEl) insertText(focusEl, text) event.preventDefault() } }, true, ) function selection() { var dom document.onmousedown = event => { dom = event.target // console.log('点击: ', dom) debugger console.log('光标所在处: ', getCusorPostion(dom)) } document.onmousemove = event => { console.log('移动: ', dom) } document.onmouseup = event => { console.log('松开: ', dom) } }})()
重新实现 addEventListener
然后删除掉网站自定义的事件
该实现来灵感来源自 https://greasyfork.org/en/scr...,几乎完美实现了解除限制的功能
原理很简单,修改原型,重新实现 EventTarget
和 docuement
的 addEventListener
函数
// ==UserScript==// @name 解除网页限制// @namespace http://github.com/rxliuli// @version 1.0// @description 破解禁止复制/剪切/粘贴/选择/右键菜单的网站// @author rxliuli// @include https://www.jianshu.com/*// @grant GM.getValue// @grant GM.setValue// 这里的 @run-at 非常重要,设置在文档开始时就载入脚本// @run-at document-start// ==/UserScript==;(() => { /** * 监听所有的 addEventListener, removeEventListener 事件 */ var documentAddEventListener = document.addEventListener var eventTargetAddEventListener = EventTarget.prototype.addEventListener var documentRemoveEventListener = document.removeEventListener var eventTargetRemoveEventListener = EventTarget.prototype.removeEventListener var events = [] /** * 用来保存监听到的事件信息 */ class Event { constructor(el, type, listener, useCapture) { this.el = el this.type = type this.listener = listener this.useCapture = useCapture } } /** * 自定义的添加事件监听函数 * @param {String} type 事件类型 * @param {EventListener} listener 事件监听函数 * @param {Boolean} {useCapture} 是否需要捕获事件冒泡,默认为 false */ function addEventListener(type, listener, useCapture = false) { var _this = this var $addEventListener = _this === document ? documentAddEventListener : eventTargetAddEventListener events.push(new Event(_this, type, listener, useCapture)) $addEventListener.apply(this, arguments) } /** * 自定义的根据类型删除事件函数 * 该方法会删除这个类型下面全部的监听函数,不管数量 * @param {String} type 事件类型 */ function removeEventListenerByType(type) { var _this = this var $removeEventListener = _this === document ? documentRemoveEventListener : eventTargetRemoveEventListener var removeIndexs = events .map((e, i) => (e.el === _this || e.type === arguments[0] ? i : -1)) .filter(i => i !== -1) removeIndexs.forEach(i => { var e = events[i] $removeEventListener.apply(e.el, [e.type, e.listener, e.useCapture]) }) removeIndexs.sort((a, b) => b - a).forEach(i => events.splice(i, 1)) } function clearEvent() { var eventTypes = [ 'copy', 'cut', 'select', 'contextmenu', 'selectstart', 'dragstart', ] document.querySelectorAll('*').forEach(el => { eventTypes.forEach(type => el.removeEventListenerByType(type)) }) } ;(function() { document.addEventListener = EventTarget.prototype.addEventListener = addEventListener document.removeEventListenerByType = EventTarget.prototype.removeEventListenerByType = removeEventListenerByType })() window.onload = function() { clearEvent() }})()
最后,JavaScript hook 技巧是真的很多,果然写 Greasemonkey 脚本这方面用得很多呢 (><)