使用 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 监听事件并自行实现复制/剪切/粘贴功能

实现强制复制

思路

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

实现强制剪切

思路

  1. 冒泡监听 cut 事件
  2. 获取当前选中的内容
  3. 设置剪切版的内容
  4. 如果是可编辑内容要删除选中部分
  5. 阻止默认事件处理
可以看到唯一需要增加的就是需要额外处理可编辑内容了,然而代码量瞬间爆炸了哦
/** * 字符串安全的转换为小写 * @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. 阻止默认事件处理
/** * 获取到最后一个获得焦点的元素 */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...,几乎完美实现了解除限制的功能

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

// ==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 脚本这方面用得很多呢 (><)