解除网页限制

破解禁止复制/剪切/粘贴/选择/右键菜单的网站

ของเมื่อวันที่ 15-10-2019 ดู เวอร์ชันล่าสุด

// ==UserScript==
// @name         解除网页限制
// @namespace    http://github.com/rxliuli
// @version      1.0
// @description  破解禁止复制/剪切/粘贴/选择/右键菜单的网站
// @author       rxliuli
// @include      *
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// 这里的 @run-at 非常重要,设置在文档开始时就载入脚本
// @run-at       document-start
// @require      https://greasyfork.org/scripts/382120-rx-util/code/rx-util.js
// ==/UserScript==

;(() => {
  const { safeExec } = rx
  /**
   * 兼容异步函数的返回值
   * @param res 返回值
   * @param callback 同步/异步结果的回调函数
   * @typeparam T 处理参数的类型,如果是 Promise 类型,则取出其泛型类型
   * @typeparam Param 处理参数具体的类型,如果是 Promise 类型,则指定为原类型
   * @typeparam R 返回值具体的类型,如果是 Promise 类型,则指定为 Promise 类型,否则为原类型
   * @returns 处理后的结果,如果是同步的,则返回结果是同步的,否则为异步的
   */
  function compatibleAsync(res, callback) {
    return res instanceof Promise ? res.then(callback) : callback(res)
  }
  /**
   * 在固定时间周期内只执行函数一次
   * @param {Function} fn 执行的函数
   * @param {Number} time 时间周期
   * @returns {Function} 包装后的函数
   */
  function onceOfCycle(fn, time) {
    const get = window.GM_getValue
    const set = window.GM_setValue
    const LastUpdateKey = 'LastUpdate'
    const LastValueKey = 'LastValue'
    return new Proxy(fn, {
      apply(_, _this, args) {
        const now = Date.now()
        const last = get(LastUpdateKey)
        if (last !== null && now - last < time) {
          return safeExec(() => JSON.parse(get(LastValueKey)))
        }
        return compatibleAsync(Reflect.apply(_, _this, args), res => {
          set(LastUpdateKey, now)
          set(LastValueKey, JSON.stringify(res))
          return res
        })
      },
    })
  }
  /**
   * 监听 event 的添加
   * 注:必须及早运行
   */
  function watchEventListener() {
    /**
     * 用来保存监听到的事件信息
     */
    class Event {
      constructor(el, type, listener, useCapture) {
        this.el = el
        this.type = type
        this.listener = listener
        this.useCapture = useCapture
      }
    }
    /**
     * 监听所有的 addEventListener, removeEventListener 事件
     */
    const documentAddEventListener = document.addEventListener
    const eventTargetAddEventListener = EventTarget.prototype.addEventListener
    const documentRemoveEventListener = document.removeEventListener
    const eventTargetRemoveEventListener =
      EventTarget.prototype.removeEventListener
    const events = []
    unsafeWindow.events = events
    /**
     * 自定义的添加事件监听函数
     * @param type 事件类型
     * @param listener 事件监听函数
     * @param [useCapture] 是否需要捕获事件冒泡,默认为 false
     */
    function addEventListener(type, listener, useCapture = false) {
      const $addEventListener =
        // @ts-ignore
        this === document
          ? documentAddEventListener
          : eventTargetAddEventListener
      // @ts-ignore
      events.push(new Event(this, type, listener, useCapture))
      // @ts-ignore
      $addEventListener.apply(this, arguments)
    }
    /**
     * 自定义的根据类型删除事件函数
     * 该方法会删除这个类型下面全部的监听函数,不管数量
     * @param type 事件类型
     */
    function removeEventListenerByType(type) {
      const $removeEventListener =
        // @ts-ignore
        this === document
          ? documentRemoveEventListener
          : eventTargetRemoveEventListener
      const removeIndexs = events
        // @ts-ignore
        .map((e, i) => (e.el === this || e.type === arguments[0] ? i : -1))
        .filter(i => i !== -1)
      removeIndexs.forEach(i => {
        const 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))
    }
    document.addEventListener = EventTarget.prototype.addEventListener = addEventListener
    // @ts-ignore
    document.removeEventListenerByType = EventTarget.prototype.removeEventListenerByType = removeEventListenerByType
  }
  watchEventListener()

  // 注册菜单
  function registerMenu() {
    const domain = location.host
    const isIncludes = GM_getValue(domain) === true
    GM_registerMenuCommand(isIncludes ? '恢复默认' : '解除限制', () => {
      if (isIncludes) {
        GM_setValue(domain, false)
      } else {
        GM_setValue(domain, true)
      }
      location.reload()
    })
  }
  registerMenu()

  const eventTypes = [
    'copy',
    'cut',
    'select',
    'selectstart',
    'contextmenu',
    'dragstart',
  ]
  // 清除网页添加的事件
  function clearEvent() {
    document.querySelectorAll('*').forEach(el => {
      eventTypes.forEach(type => el.removeEventListenerByType(type))
    })
  }
  // 清理或还原DOM节点的onxxx属性
  function clearLoop() {
    let type
    let prop
    let c = [document, document.body, ...document.getElementsByTagName('div')]
    let e = document.querySelector('iframe[src="about:blank"]')
    if (e && e.clientWidth > 99 && e.clientHeight > 11) {
      e = e.contentWindow.document
      c.push(e, e.body)
    }

    for (e of c) {
      if (!e) continue
      e = e.wrappedJSObject || e
      for (type of eventTypes) {
        prop = 'on' + type
        e[prop] = null
      }
    }
  }
  // 清理掉网页添加的全局防止复制/选择的 CSS
  function clearCSS() {
    GM_addStyle(
      `html, * {
        -webkit-user-select:text !important;
        -moz-user-select:text !important;
        user-select:text !important;
      }
      ::-moz-selection {color:#111 !important; background:#05D3F9 !important;}
      ::selection {color:#111 !important; background:#05D3F9 !important;}`,
    )
  }
  // 更新支持的网站列表
  function updateHostList() {
    onceOfCycle(function() {
      GM_xmlhttpRequest({
        method: 'GET',
        url:
          'https://raw.githubusercontent.com/rxliuli/userjs/master/src/UnblockWebRestrictions/blockList.json',
        onload(res) {
          JSON.parse(res.responseText)
            .filter(domain => GM_getValue(domain) !== undefined)
            .forEach(domain => GM_setValue(domain, true))
        },
      })
    }, 1000 * 60 * 60 * 24)()
  }
  updateHostList()

  window.onload = function() {
    if (GM_getValue(location.host) === true) {
      clearEvent()
      clearLoop()
      clearCSS()
    }
  }
})()