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

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

吾輩的博客原文地址: https://blog.rxliuli.com/p/4b2822b2/
吾輩發佈了一個油猴腳本,能夠直接安裝 解除網頁限制 以得到更好的使用體驗。

場景

在瀏覽網頁時常常會出現的一件事,當吾輩想要複製,忽然發現複製好像沒用了?(知乎禁止轉載的文章)亦或者是複製的最後多出了一點內容(簡書),或者乾脆直接不能選中了(360doc)。粘貼時也有可能發現一直粘貼不了(支付寶登陸)。javascript

問題

欲先制敵,必先惑敵。想要解除複製粘貼的限制,就必需要清楚它們是如何實現的。無論如何,瀏覽器上可以運行的都是 JavaScript,它們都是使用 JavaScript 實現的。實現方式大體都是監聽相應的事件(例如 onkeydown 監聽 Ctrl-C),而後作一些特別的操做。html

例如屏蔽複製功能只須要一句代碼java

document.oncopy = event => false

是的,只要返回了 false,那麼 copy 就會失效。還有一個更討厭的方式,直接在 body 元素上加行內事件git

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

解決

能夠看出,通常都是使用 JavaScript 在相應事件中返回 false,來阻止對應事件。那麼,既然事件都被阻止了,是否意味着咱們就一籌莫展了呢?吾輩所能想到的解決方案大體有三種方向github

  • 使用 JavaScript 監聽事件並自行實現複製/剪切/粘貼功能瀏覽器

    • 優勢:實現完成後不論是任何網站都能使用,而且不會影響到監聽以外的事件,也不會刪除監聽的同類型事件,能夠解除瀏覽器自己的限制(密碼框禁止複製)
    • 缺點:某些功能自行實現難度很大,例如選擇文本
  • 從新實現 addEventListener 而後刪除掉網站自定義的事件安全

    • 優勢:事件生效範圍普遍,通用性高,不只 _複製/剪切/粘貼_,其餘類型的事件也能夠解除
    • 缺點:實現起來須要替換 addEventListener 事件夠早,對瀏覽器默認操做不會生效(密碼框禁止複製),並且某些網站也沒法破解
  • 替換元素並刪除 DOM 上的事件屬性app

    • 優勢:可以確保網站 js 的限制被解除,通用性高,事件生效範圍普遍
    • 缺點:可能影響到其餘類型的事件,複製節點時不會複製使用 addEventListener 添加的事件dom

      注:此方法不予演示,缺陷實在過大

總之,若是真的想解除限制,恐怕須要兩種方式並用才能夠呢函數

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

相關文章
相關標籤/搜索