Greasy Fork

Greasy Fork is available in English.

解除网页限制

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

当前为 2020-01-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         解除网页限制
// @namespace    http://github.com/rxliuli
// @version      1.2
// @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
// ==/UserScript==

;(() => {
  /**
   * 安全执行某个函数
   * 支持异步函数
   * @param fn 需要执行的函数
   * @param defaultVal 发生异常后的默认返回值,默认为 null
   * @param args 可选的函数参数
   * @returns 函数执行的结果,或者其默认值
   */
  function safeExec(fn, defaultVal, ...args) {
    const defRes = defaultVal === undefined ? null : defaultVal
    try {
      const res = fn(...args)
      return res instanceof Promise ? res.catch(() => defRes) : res
    } catch (err) {
      return defRes
    }
  }

  const isBlock = GM_listValues().some(
    host => location.host.includes(host) && GM_getValue(host) === true,
  )
  /**
   * 兼容异步函数的返回值
   * @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
        })
      },
    })
  }

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

  const eventTypes = [
    'copy',
    'cut',
    'paste',
    'select',
    'selectstart',
    'contextmenu',
    'dragstart',
  ]

  /**
   * 监听 event 的添加
   * 注:必须及早运行
   */
  function watchEventListener() {
    /**
     * 监听所有的 addEventListener, removeEventListener 事件
     */
    const documentAddEventListener = document.addEventListener
    const eventTargetAddEventListener = EventTarget.prototype.addEventListener
    /**
     * 自定义的添加事件监听函数
     * @param type 事件类型
     * @param listener 事件监听函数
     * @param [useCapture] 是否需要捕获事件冒泡,默认为 false
     */
    function addEventListener(type, listener, useCapture = false) {
      const $addEventListener =
        // @ts-ignore
        this === document
          ? documentAddEventListener
          : eventTargetAddEventListener

      // 在这里阻止会更合适一点
      if (eventTypes.includes(type)) {
        console.log('拦截 addEventListener: ', type, this)
        return
      }
      // @ts-ignore
      $addEventListener.apply(this, arguments)
    }
    document.addEventListener = EventTarget.prototype.addEventListener = addEventListener
  }
  // 清理使用 onXXX 添加到事件
  function clearJsOnXXXEvent() {
    const emptyFunc = () => {}
    function modifyPrototype(prototype, type) {
      Object.defineProperty(prototype, `on${type}`, {
        get() {
          return emptyFunc
        },
        set() {
          return true
        },
      })
    }
    eventTypes.forEach(type => {
      modifyPrototype(HTMLElement.prototype, type)
      modifyPrototype(document, type)
    })
  }

  if (isBlock) {
    watchEventListener()
    clearJsOnXXXEvent()
  }
  // 清理或还原DOM节点的onXXX 属性
  function clearDomOnXXXEvent() {
    function _innerClear() {
      eventTypes.forEach(type => {
        document
          .querySelectorAll(`[on${type}]`)
          .forEach(el => el.setAttribute(`on${type}`, 'return true'))
      })
    }
    setInterval(_innerClear, 3000)
  }
  // 清理掉网页添加的全局防止复制/选择的 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 => {
              console.log('更新了屏蔽域名: ', domain)
              GM_setValue(domain, true)
            })
        },
      })
    }, 1000 * 60 * 60 * 24)()
  }
  updateHostList()

  window.addEventListener('load', function() {
    if (isBlock) {
      clearDomOnXXXEvent()
      clearCSS()
    }
  })
})()