使用 Greasemonkey 解除网页复制粘贴限制

使用 Greasemonkey 解除网页复制粘贴限制

场景

在浏览网页时经常会出现的一件事,当吾辈想要复制,突然发现复制好像没用了?(知乎禁止转载的文章)亦或者是复制的最后多出了一点内容(简书),或者干脆直接不能选中了(360doc)。粘贴时也有可能发现一直粘贴不了(支付宝登录)。

问题

欲先制敌,必先惑敌。想要解除复制粘贴的限制,就必须要清楚它们是如何实现的。不管如何,浏览器上能够运行的都是 JavaScript,它们都是使用 JavaScript 实现的。实现方式大致都是监听相应的事件(例如 onkeydown 监听 Ctrl-C),然后做一些特别的操作。

例如屏蔽复制功能只需要一句代码

1
document.oncopy = event => false

是的,只要返回了 false,那么 copy 就会失效。还有一个更讨厌的方式,直接在 body 元素上加行内事件

1
<body oncopy="javascript: return false" />

解决

可以看出,一般都是使用 JavaScript 在相应事件中返回 false,来阻止对应事件。那么,既然事件都被阻止了,是否意味着我们就束手无策了呢?吾辈所能想到的大致有三种方向

  • 使用 JavaScript 监听事件并自行实现复制/剪切/粘贴功能
    • 优点:实现完成后不管是任何网站都能使用,并且不会影响到监听之外的事件,也不会删除监听的同类型事件,可以解除浏览器本身的限制(密码框禁止复制)
    • 缺点:某些功能自行实现难度很大,例如选择文本
  • 重新实现 addEventListener 然后删除掉网站自定义的事件
    • 优点:事件生效范围广泛,通用性高,不仅 复制/剪切/粘贴,其他类型的事件也可以解除
    • 缺点:实现起来需要替换 addEventListener 事件够早,对浏览器默认操作不会生效(密码框禁止复制),而且某些网站也无法破解
  • 替换元素并删除 DOM 上的事件属性
    • 优点:能够确保网站 js 的限制被解除,通用性高,事件生效范围广泛
    • 缺点:可能影响到其他类型的事件,复制节点时不会复制使用 addEventListener 添加的事件

      注:此方法不予演示,缺陷实在过大

总之,如果真的想解除限制,恐怕需要两种方式并用才可以呢

使用 JavaScript 监听事件并自行实现复制/剪切/粘贴功能

实现强制复制

思路

  1. 冒泡监听 copy 事件
  2. 获取当前选中的内容
  3. 设置剪切版的内容
  4. 阻止默认事件处理
1
2
3
4
5
6
7
8
9
10
11
12
13
// 强制复制
document.addEventListener(
'copy',
event => {
event.clipboardData.setData(
'text/plain',
document.getSelection().toString()
)
// 阻止默认的事件处理
event.preventDefault()
},
true
)

实现强制剪切

思路

  1. 冒泡监听 cut 事件
  2. 获取当前选中的内容
  3. 设置剪切版的内容
  4. 如果是可编辑内容要删除选中部分
  5. 阻止默认事件处理

可以看到唯一需要增加的就是需要额外处理可编辑内容了,然而代码量瞬间爆炸了哦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 字符串安全的转换为小写
* @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
)

实现强制粘贴

  1. 冒泡监听 focus/blur,以获得最后一个获得焦点的可编辑元素
  2. 冒泡监听 paste 事件
  3. 获取剪切版的内容
  4. 获取最后一个获得焦点的可编辑元素
  5. 删除当前选中的文本
  6. 在当前光标处插入文本
  7. 阻止默认事件处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* 获取到最后一个获得焦点的元素
*/
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
)

总结

脚本全貌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
;(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/scripts/41075,几乎完美实现了解除限制的功能

原理很简单,修改原型,重新实现 EventTargetdocuementaddEventListener 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// ==UserScript==
// @name force copy
// @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 脚本这方面用得很多呢 (๑>ᴗ<๑)