// ==UserScript==
// @name 解除网页限制
// @description 破解禁止复制/剪切/粘贴/选择/右键菜单的网站
// @namespace http://github.com/rxliuli/userjs
// @version 2.4.2
// @author rxliuli
// @include *
// @require https://cdn.jsdelivr.net/npm/rx-util@1.9.2/dist/index.min.js
// @connect userjs.rxliuli.com
// @run-at document-start
// @license MIT
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// ==/UserScript==
(function (rxUtil) {
'use strict';
//region 公共的函数
const LastUpdateKey = 'LastUpdate';
const LastValueKey = 'LastValue';
/**
* 在固定时间周期内只执行函数一次
* @param {Function} fn 执行的函数
* @param {Number} time 时间周期
* @returns {Function} 包装后的函数
*/
function onceOfCycle(fn, time) {
const get = window.GM_getValue.bind(window);
const set = window.GM_setValue.bind(window);
return new Proxy(fn, {
apply(_, _this, args) {
const now = Date.now();
const last = get(LastUpdateKey);
if (
![null, undefined, 'null', 'undefined'].includes(last ) &&
now - (last ) < time
) {
return rxUtil.safeExec(() => JSON.parse(get(LastValueKey)), 1)
}
return rxUtil.compatibleAsync(Reflect.apply(_, _this, args), (res) => {
set(LastUpdateKey, now);
set(LastValueKey, JSON.stringify(res));
return res
})
},
})
}
//endregion
/**
* 解除限制
*/
class UnblockLimit {
static __initStatic() {this.eventTypes = [
'copy',
'cut',
'paste',
'select',
'selectstart',
'contextmenu',
'dragstart',
'mousedown',
];}
static __initStatic2() {this.keyEventTypes = [
'keydown',
'keypress',
'keyup',
];}
/**
* 监听 event 的添加
* 注:必须及早运行
*/
static watchEventListener() {
const documentAddEventListener = document.addEventListener;
const eventTargetAddEventListener = EventTarget.prototype.addEventListener;
const proxyHandler = {
apply(
target,
thisArg,
[type, listener, useCapture]
,
) {
const $addEventListener =
target instanceof Document
? documentAddEventListener
: eventTargetAddEventListener;
// 在这里阻止会更合适一点
if (UnblockLimit.eventTypes.includes(type )) {
console.log('拦截 addEventListener: ', type, this);
return
}
Reflect.apply($addEventListener, thisArg, [type, listener, useCapture]);
},
};
document.addEventListener = new Proxy(
documentAddEventListener,
proxyHandler,
);
EventTarget.prototype.addEventListener = new Proxy(
eventTargetAddEventListener,
proxyHandler,
);
}
// 代理网页添加的键盘快捷键,阻止自定义 C-C/C-V/C-X 这三个快捷键
static proxyKeyEventListener() {
const documentAddEventListener = document.addEventListener;
const eventTargetAddEventListener = EventTarget.prototype.addEventListener;
const keyProxyHandler = {
apply(
target,
thisArg,
argArray,
) {
const ev = argArray[0];
const proxyKey = ['c', 'x', 'v', 'a'];
const proxyAssistKey = ['Control', 'Alt'];
if (
(ev.ctrlKey && proxyKey.includes(ev.key)) ||
proxyAssistKey.includes(ev.key)
) {
console.log('已阻止: ', ev.ctrlKey, ev.altKey, ev.key);
return
}
if (ev.altKey) {
return
}
Reflect.apply(target, thisArg, argArray);
},
};
const proxyHandler = {
apply(
target,
thisArg,
[type, listener, useCapture]
,
) {
const $addEventListener =
target instanceof Document
? documentAddEventListener
: eventTargetAddEventListener;
Reflect.apply($addEventListener, thisArg, [
type,
UnblockLimit.keyEventTypes.includes(type )
? new Proxy(listener , keyProxyHandler)
: listener,
useCapture,
]);
},
};
document.addEventListener = new Proxy(
documentAddEventListener,
proxyHandler,
);
EventTarget.prototype.addEventListener = new Proxy(
eventTargetAddEventListener,
proxyHandler,
);
}
// 清理使用 onXXX 添加到事件
static clearJsOnXXXEvent() {
const emptyFunc = () => {};
function modifyPrototype(
prototype,
type,
) {
Object.defineProperty(prototype, `on${type}`, {
get() {
return emptyFunc
},
set() {
return true
},
});
}
UnblockLimit.eventTypes.forEach((type) => {
modifyPrototype(HTMLElement.prototype, type);
modifyPrototype(document, type);
});
}
// 清理或还原DOM节点的onXXX 属性
static clearDomOnXXXEvent() {
function _innerClear() {
UnblockLimit.eventTypes.forEach((type) => {
document
.querySelectorAll(`[on${type}]`)
.forEach((el) => el.setAttribute(`on${type}`, 'return true'));
});
}
setInterval(_innerClear, 3000);
}
// 清理掉网页添加的全局防止复制/选择的 CSS
static clearCSS() {
GM_addStyle(
`html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video, html body * {
-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;
}
`,
);
}
} UnblockLimit.__initStatic(); UnblockLimit.__initStatic2();
/**
* 屏蔽配置项类型
*/
//更新屏蔽列表
class BlockHost {
static fetchHostList() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://userjs.rxliuli.com/blockList.json',
headers: {
'Cache-Control': 'no-cache',
},
onload(res) {
const list = JSON.parse(res.responseText);
resolve(list);
console.info('更新配置成功: ', list);
},
onerror(e) {
reject(e);
console.error('更新配置失败: ', e);
},
});
})
}
static updateHostList(hostList) {
hostList
.filter((config) => GM_getValue(JSON.stringify(config)) === undefined)
.forEach((domain) => {
console.log('更新了屏蔽域名: ', domain);
GM_setValue(JSON.stringify(domain), true);
});
}
// 更新支持的网站列表
static __initStatic3() {this.updateBlockHostList = onceOfCycle(async () => {
BlockHost.updateHostList(await BlockHost.fetchHostList());
}, 1000 * 60 * 60 * 24);}
static findKey() {
return GM_listValues()
.filter((config) => GM_getValue(config))
.find((configStr) => {
const config = rxUtil.safeExec(() => JSON.parse(configStr) , {
type: 'domain',
url: configStr,
});
return this.match(new URL(location.href), config)
})
}
static match(
href,
config
,
) {
if (typeof config === 'string') {
return href.host.includes(config)
} else {
const { type, url } = config;
switch (type) {
case 'domain':
return href.host === url
case 'link':
return href.href === url
case 'linkPrefix':
return href.href.startsWith(url)
case 'regex':
return new RegExp(url).test(href.href)
}
}
}
} BlockHost.__initStatic3();
//注册菜单
class MenuHandler {
static register() {
const findKey = BlockHost.findKey();
const key =
findKey ||
JSON.stringify({
type: 'domain',
url: location.host,
});
console.log('key: ', key);
GM_registerMenuCommand(findKey ? '恢复默认' : '解除限制', () => {
GM_setValue(key, !GM_getValue(key));
console.log('isBlock: ', key, GM_getValue(key));
location.reload();
});
}
}
/**
* 屏蔽列表配置 API,用以在指定 API 进行高级配置
*/
class ConfigBlockApi {
listKey() {
return GM_listValues().filter(
(key) => ![LastUpdateKey, LastValueKey].includes(key),
)
}
list() {
return this.listKey().map((config) => ({
...rxUtil.safeExec(() => JSON.parse(config ), {
type: 'domain',
url: config,
} ),
enable: GM_getValue(config),
key: config,
}))
}
switch(key) {
console.log('ConfigBlockApi.switch: ', key);
GM_setValue(key, !GM_getValue(key));
}
remove(key) {
console.log('ConfigBlockApi.remove: ', key);
GM_deleteValue(key);
}
add(config) {
console.log('ConfigBlockApi.add: ', config);
GM_setValue(JSON.stringify(config), true);
}
clear() {
const delKeyList = this.listKey();
console.log('ConfigBlockApi.clear: ', delKeyList);
delKeyList.forEach(GM_deleteValue);
}
async update() {
await BlockHost.updateHostList(await BlockHost.fetchHostList());
}
}
//启动类
class Application {
start() {
MenuHandler.register();
if (BlockHost.findKey()) {
UnblockLimit.watchEventListener();
UnblockLimit.proxyKeyEventListener();
UnblockLimit.clearJsOnXXXEvent();
}
BlockHost.updateBlockHostList();
window.addEventListener('load', function () {
if (BlockHost.findKey()) {
UnblockLimit.clearDomOnXXXEvent();
UnblockLimit.clearCSS();
}
});
if (
location.href.startsWith('https://userjs.rxliuli.com/') ||
location.hostname === '127.0.0.1'
) {
Reflect.set(
unsafeWindow,
'com.rxliuli.UnblockWebRestrictions.configBlockApi',
new ConfigBlockApi(),
);
}
}
}
new Application().start();
}(rx));