解除网页限制

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

目前为 2020-07-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 解除网页限制
  3. // @namespace http://github.com/rxliuli/userjs
  4. // @version 2.2.2
  5. // @description 破解禁止复制/剪切/粘贴/选择/右键菜单的网站
  6. // @author rxliuli
  7. // @include *
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_deleteValue
  11. // @grant GM_listValues
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_addStyle
  14. // @grant GM_xmlhttpRequest
  15. // @grant unsafeWindow
  16. // 这里的 @run-at 非常重要,设置在文档开始时就载入脚本
  17. // @run-at document-start
  18. // @license MIT
  19. // ==/UserScript==
  20. ;
  21. (() => {
  22. /**
  23. * 安全执行某个函数
  24. * 支持异步函数
  25. * @param fn 需要执行的函数
  26. * @param defaultVal 发生异常后的默认返回值,默认为 null
  27. * @param args 可选的函数参数
  28. * @returns 函数执行的结果,或者其默认值
  29. */
  30. function safeExec(fn, defaultVal, ...args) {
  31. const defRes = (defaultVal === undefined ? null : defaultVal);
  32. try {
  33. const res = fn(...args);
  34. return res instanceof Promise ? res.catch(() => defRes) : res;
  35. }
  36. catch (err) {
  37. return defRes;
  38. }
  39. }
  40. /**
  41. * 兼容异步函数的返回值
  42. * @param res 返回值
  43. * @param callback 同步/异步结果的回调函数
  44. * @typeparam T 处理参数的类型,如果是 Promise 类型,则取出其泛型类型
  45. * @typeparam Param 处理参数具体的类型,如果是 Promise 类型,则指定为原类型
  46. * @typeparam R 返回值具体的类型,如果是 Promise 类型,则指定为 Promise 类型,否则为原类型
  47. * @returns 处理后的结果,如果是同步的,则返回结果是同步的,否则为异步的
  48. */
  49. function compatibleAsync(res, callback) {
  50. return (res instanceof Promise
  51. ? res.then(callback)
  52. : callback(res));
  53. }
  54. /**
  55. * 在固定时间周期内只执行函数一次
  56. * @param {Function} fn 执行的函数
  57. * @param {Number} time 时间周期
  58. * @returns {Function} 包装后的函数
  59. */
  60. function onceOfCycle(fn, time) {
  61. const get = window.GM_getValue.bind(window);
  62. const set = window.GM_setValue.bind(window);
  63. const LastUpdateKey = 'LastUpdate';
  64. const LastValueKey = 'LastValue';
  65. return new Proxy(fn, {
  66. apply(_, _this, args) {
  67. const now = Date.now();
  68. const last = get(LastUpdateKey);
  69. if (![null, undefined, 'null', 'undefined'].includes(last) &&
  70. now - last < time) {
  71. return safeExec(() => JSON.parse(get(LastValueKey)), 1);
  72. }
  73. return compatibleAsync(Reflect.apply(_, _this, args), res => {
  74. set(LastUpdateKey, now);
  75. set(LastValueKey, JSON.stringify(res));
  76. return res;
  77. });
  78. },
  79. });
  80. }
  81. /**
  82. * 解除限制
  83. */
  84. class UnblockLimit {
  85. /**
  86. * 监听 event 的添加
  87. * 注:必须及早运行
  88. */
  89. static watchEventListener() {
  90. const documentAddEventListener = document.addEventListener;
  91. const eventTargetAddEventListener = EventTarget.prototype.addEventListener;
  92. function addEventListener(type, listener, useCapture) {
  93. const $addEventListener = this instanceof Document
  94. ? documentAddEventListener
  95. : eventTargetAddEventListener;
  96. // 在这里阻止会更合适一点
  97. if (UnblockLimit.eventTypes.includes(type)) {
  98. console.log('拦截 addEventListener: ', type, this);
  99. return;
  100. }
  101. Reflect.apply($addEventListener, this, [type, listener, useCapture]);
  102. }
  103. document.addEventListener = EventTarget.prototype.addEventListener = addEventListener;
  104. }
  105. // 代理网页添加的键盘快捷键,阻止自定义 C-C/C-V/C-X 这三个快捷键
  106. static proxyKeyEventListener() {
  107. const documentAddEventListener = document.addEventListener;
  108. const eventTargetAddEventListener = EventTarget.prototype.addEventListener;
  109. function addEventListener(type, listener, useCapture) {
  110. const $addEventListener = this === document
  111. ? documentAddEventListener
  112. : eventTargetAddEventListener;
  113. // 在这里阻止会更合适一点
  114. const listenerHandler = {
  115. apply(target, thisArg, argArray) {
  116. const ev = argArray[0];
  117. const proxyKey = ['c', 'x', 'v', 'a'];
  118. const proxyAssistKey = ['Control', 'Alt'];
  119. if ((ev.ctrlKey && proxyKey.includes(ev.key)) ||
  120. proxyAssistKey.includes(ev.key)) {
  121. console.log('已阻止: ', ev.ctrlKey, ev.altKey, ev.key);
  122. return;
  123. }
  124. if (ev.altKey) {
  125. return;
  126. }
  127. // Reflect.apply(target, thisArg, argArray)
  128. },
  129. };
  130. Reflect.apply($addEventListener, this, [
  131. type,
  132. UnblockLimit.keyEventTypes.includes(type)
  133. ? new Proxy(listener, listenerHandler)
  134. : listener,
  135. useCapture,
  136. ]);
  137. }
  138. document.addEventListener = EventTarget.prototype.addEventListener = addEventListener;
  139. }
  140. // 清理使用 onXXX 添加到事件
  141. static clearJsOnXXXEvent() {
  142. const emptyFunc = () => { };
  143. function modifyPrototype(prototype, type) {
  144. Object.defineProperty(prototype, `on${type}`, {
  145. get() {
  146. return emptyFunc;
  147. },
  148. set() {
  149. return true;
  150. },
  151. });
  152. }
  153. UnblockLimit.eventTypes.forEach(type => {
  154. modifyPrototype(HTMLElement.prototype, type);
  155. modifyPrototype(document, type);
  156. });
  157. }
  158. // 清理或还原DOM节点的onXXX 属性
  159. static clearDomOnXXXEvent() {
  160. function _innerClear() {
  161. UnblockLimit.eventTypes.forEach(type => {
  162. document
  163. .querySelectorAll(`[on${type}]`)
  164. .forEach(el => el.setAttribute(`on${type}`, 'return true'));
  165. });
  166. }
  167. setInterval(_innerClear, 3000);
  168. }
  169. // 清理掉网页添加的全局防止复制/选择的 CSS
  170. static clearCSS() {
  171. GM_addStyle(`html, body, div, span, applet, object, iframe,
  172. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  173. a, abbr, acronym, address, big, cite, code,
  174. del, dfn, em, img, ins, kbd, q, s, samp,
  175. small, strike, strong, sub, sup, tt, var,
  176. b, u, i, center,
  177. dl, dt, dd, ol, ul, li,
  178. fieldset, form, label, legend,
  179. table, caption, tbody, tfoot, thead, tr, th, td,
  180. article, aside, canvas, details, embed,
  181. figure, figcaption, footer, header, hgroup,
  182. menu, nav, output, ruby, section, summary,
  183. time, mark, audio, video, html body * {
  184. -webkit-user-select: text !important;
  185. -moz-user-select: text !important;
  186. user-select: text !important;
  187. }
  188.  
  189. ::-moz-selection {
  190. color: #111 !important;
  191. background: #05d3f9 !important;
  192. }
  193.  
  194. ::selection {
  195. color: #111 !important;
  196. background: #05d3f9 !important;
  197. }
  198. `);
  199. }
  200. }
  201. UnblockLimit.eventTypes = [
  202. 'copy',
  203. 'cut',
  204. 'paste',
  205. 'select',
  206. 'selectstart',
  207. 'contextmenu',
  208. 'dragstart',
  209. ];
  210. UnblockLimit.keyEventTypes = [
  211. 'keydown',
  212. 'keypress',
  213. 'keyup',
  214. ];
  215. //更新屏蔽列表
  216. class BlockHost {
  217. static fetchHostList() {
  218. return new Promise((resolve, reject) => {
  219. GM_xmlhttpRequest({
  220. method: 'GET',
  221. url: 'https://rxliuli.com/userjs/src/UnblockWebRestrictions/blockList.json',
  222. onload(res) {
  223. resolve(JSON.parse(res.responseText));
  224. },
  225. onerror(e) {
  226. reject(e);
  227. },
  228. });
  229. });
  230. }
  231. static updateHostList(hostList) {
  232. hostList
  233. .filter(config => GM_getValue(JSON.stringify(config)) === undefined)
  234. .forEach(domain => {
  235. console.log('更新了屏蔽域名: ', domain);
  236. GM_setValue(JSON.stringify(domain), true);
  237. });
  238. }
  239. static isBlock() {
  240. return GM_listValues()
  241. .filter(config => GM_getValue(config) === true)
  242. .some(configStr => {
  243. const config = safeExec(() => JSON.parse(configStr), {
  244. type: 'domain',
  245. url: configStr,
  246. });
  247. return this.match(new URL(location.href), config);
  248. });
  249. }
  250. static match(href, config) {
  251. if (typeof config === 'string') {
  252. return href.host.includes(config);
  253. }
  254. else {
  255. const { type, url } = config;
  256. switch (type) {
  257. case 'domain':
  258. return href.host.includes(url);
  259. case 'link':
  260. return href.href === url;
  261. case 'linkPrefix':
  262. return href.href.startsWith(url);
  263. case 'regex':
  264. return new RegExp(url).test(href.href);
  265. }
  266. }
  267. }
  268. }
  269. // 更新支持的网站列表
  270. BlockHost.updateBlockHostList = onceOfCycle(async () => {
  271. BlockHost.updateHostList(await BlockHost.fetchHostList());
  272. }, 1000 * 60 * 60 * 24);
  273. //注册(不可用)菜单
  274. class MenuHandler {
  275. static register() {
  276. const domain = location.host;
  277. const isBlock = BlockHost.isBlock();
  278. if (!isBlock) {
  279. console.log('domain: ', domain);
  280. }
  281. GM_registerMenuCommand(isBlock ? '恢复默认' : '解除限制', () => {
  282. const key = JSON.stringify({
  283. type: 'domain',
  284. url: domain,
  285. });
  286. GM_setValue(key, !isBlock);
  287. console.log('isBlock: ', isBlock, GM_getValue(key));
  288. location.reload();
  289. });
  290. }
  291. }
  292. /**
  293. * 屏蔽列表配置 API,用以在指定 API 进行高级配置
  294. */
  295. class ConfigBlockApi {
  296. list() {
  297. return GM_listValues()
  298. .filter(key => key !== 'LastUpdate' && key !== 'LastValue')
  299. .map(config => safeExec(() => JSON.parse(config), {
  300. type: 'domain',
  301. url: config,
  302. }));
  303. }
  304. delete(config) {
  305. GM_deleteValue(JSON.stringify(config));
  306. }
  307. add(config) {
  308. GM_setValue(JSON.stringify(config), true);
  309. }
  310. clear() {
  311. GM_listValues().forEach(GM_deleteValue);
  312. }
  313. async update() {
  314. await BlockHost.updateBlockHostList();
  315. }
  316. }
  317. //启动类
  318. class Application {
  319. start() {
  320. MenuHandler.register();
  321. if (BlockHost.isBlock()) {
  322. UnblockLimit.watchEventListener();
  323. UnblockLimit.proxyKeyEventListener();
  324. UnblockLimit.clearJsOnXXXEvent();
  325. }
  326. BlockHost.updateBlockHostList();
  327. window.addEventListener('load', function () {
  328. if (BlockHost.isBlock()) {
  329. UnblockLimit.clearDomOnXXXEvent();
  330. UnblockLimit.clearCSS();
  331. }
  332. });
  333. if (location.href ===
  334. 'https://rxliuli.com/userjs/src/UnblockWebRestrictions/website/dist/' ||
  335. location.hostname === '127.0.0.1') {
  336. Reflect.set(unsafeWindow, 'configBlockApi', new ConfigBlockApi());
  337. }
  338. }
  339. }
  340. new Application().start();
  341. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址