使用 Greasemonkey 解除網頁複製粘貼限制

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章