使用 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 腳本這方面用得很多呢 (๑>ᴗ<๑)