- // ==UserScript==
- // @name Webpage mask
- // @name:zh-CN 网页遮罩层
- // @name:en Webpage mask
- // @namespace https://gf.qytechs.cn/users/667968-pyudng
- // @version 0.4.1
- // @description A customizable mask layer above any webpage. You can use it as a privacy mask, a screensaver, a nightmode filter... and so on.
- // @description:zh-CN 在网页上方添加一个可以自定义的遮罩层。可以用来遮挡隐私内容,或者用作屏保,又或是用来设置护眼模式... 等等等等
- // @description:en A customizable mask layer above any webpage. You can use it as a privacy mask, a screensaver, a nightmode filter... and so on.
- // @author PY-DNG
- // @license MIT
- // @match http*://*/*
- // @require https://update.gf.qytechs.cn/scripts/456034/1532680/Basic%20Functions%20%28For%20userscripts%29.js
- // @grant GM_registerMenuCommand
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addElement
- // @run-at document-start
- // ==/UserScript==
-
- /* eslint-disable no-multi-spaces */
- /* eslint-disable no-return-assign */
-
- /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask FunctionLoader loadFuncs require isLoaded */
-
- /* Important note: this script is for convenience, but is NOT a security tool.
- ANYONE with basic web programming knowledge CAN EASYILY UNLOCK/UNCENSOR/REMOVE MASK
- without permission/password AND EVEN YOU CANNOT KNOW IT */
-
- (function __MAIN__() {
- 'use strict';
-
- const CONST = {
- TextAllLang: {
- DEFAULT: 'en',
- 'zh-CN': {
- CompatAlert: '用户脚本 [网页遮罩层] 提示:\n(本提示仅展示一次)本脚本推荐使用最新版Tampermonkey运行,如果使用旧版Tampermonkey或其他脚本管理器可能导致兼容性问题,请注意。',
- Mask: '开启',
- Unmask: '关闭',
- EnableAutomask: '为此网站开启自动遮罩',
- DisableAutomask: '关闭此网站的自动遮罩',
- SetIDLETime: '设置自动遮罩触发时间',
- PromptIDLETime: '每当 N 秒无操作后,将为网站自动开启遮罩\n您希望 N 为:',
- TamperorilyDisable: '暂时禁用遮罩层',
- TamperorilyDisabled: '已暂时禁用遮罩层:当前网页在下一次刷新前,都不会展示遮罩层',
- CustomUserstyle: '自定义遮罩层样式',
- PromptUserstyle: '您可以在此彻底地自定义遮罩层\n如果您不确定怎么写或者不小心写错了,留空并点击确定即可重置为默认值\n\n格式:\ncss:CSS值 - 设定自定义CSS样式\nimg:网址 - 在遮罩层上全屏显示网址对应的图片\njs:代码 - 执行自定义js代码,您可以使用js:debugger测试运行环境、调试您的代码',
- IDLETimeInvalid: '您的输入不正确:只能输入大于等于零的整数或小数'
- },
- 'en': {
- CompatAlert: '(This is a one-time alert)\nFrom userscript [Privacy mask]:\nThis userscript is designed for latest versions of Tampermonkey, working with old versions or other script manager may encounter bugs.',
- Mask: 'Show mask',
- Unmask: 'Hide mask',
- EnableAutomask: 'Enable auto-mask for this site',
- DisableAutomask: 'Disable auto-mask for this site',
- SetIDLETime: 'Configure auto-mask time',
- PromptIDLETime: 'Mask will be shown after the webpage has been idle for N second(s).\n You can set that N here:',
- TamperorilyDisable: 'Tamperorily disable mask',
- TamperorilyDisabled: 'Mask tamperorily disabled: mask will not be shown in current webpage before refreshing the webpage',
- CustomUserstyle: 'Custom auto-mask style',
- PromptUserstyle: 'You can custom the content and style of the mask here\nIf you\'re not sure how to compose styles, leave it blank to set back to default.\n\nStyle format:\ncss:CSS - Apply custom css stylesheet\nimg:url - Display custom image by fullscreen\njs:code - Execute custom javascript when mask created. You can use "js:debugger" to test your code an the environment.',
- IDLETimeInvalid: 'Invalid input: positive numbers only'
- }
- },
- Style: {
- BuiltinStyle: '#mask {position: fixed; top: 0; left: 0; right: 100vw; bottom: 100vh; width: 100vw; height: 100vh; border: 0; margin: 0; padding: 0; background: transparent; z-index: 2147483647; display: none} #mask.show {display: block;}',
- DefaultUserstyle: 'css:#mask {backdrop-filter: blur(30px);}'
- }
- };
-
- // Init language
- const i18n = Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT;
- CONST.Text = CONST.TextAllLang[i18n];
-
- loadFuncs([{
- id: 'mask',
- desc: 'Core: create mask DOM, provide basic mask api',
- detectDom: 'body',
- dependencies: 'utils',
- func: () => {
- const utils = require('utils');
- const return_obj = new EventTarget();
-
- // Make mask
- const mask_container = $$CrE({
- tagName: 'div',
- styles: { all: 'initial' }
- });
- const mask = $$CrE({
- tagName: 'div',
- props: { id: 'mask' }
- });
- const shadow = mask_container.attachShadow({ mode: unsafeWindow.isPY_DNG ? 'open' : 'closed' });
- shadow.appendChild(mask);
- document.body.after(mask_container);
-
- // Styles
- const style = addStyle(shadow, CONST.Style.BuiltinStyle, 'mask-builtin-style');
- applyUserstyle();
-
- ['mouseup', 'keyup', 'dragenter'].forEach(evtname => utils.$AEL(unsafeWindow, evtname, e => hide()));
-
- const return_props = {
- mask_container, element: mask, shadow, style, show, hide,
- get showing() { return showing(); },
- set showing(v) { v ? show() : hide() },
- get userstyle() { return getUserstyle() },
- set userstyle(v) { return setUserstyle(v) }
- };
- utils.copyPropDescs(return_props, return_obj);
- return return_obj;
-
- function show() {
- const defaultNotPrevented = return_obj.dispatchEvent(new Event('show', { cancelable: true }));
- defaultNotPrevented && mask.classList.add('show');
- }
-
- function hide() {
- const defaultNotPrevented = return_obj.dispatchEvent(new Event('hide', { cancelable: true }));
- defaultNotPrevented && mask.classList.remove('show');
- }
-
- function showing() {
- return mask.classList.contains('show');
- }
-
- function getUserstyle() {
- return GM_getValue('userstyle', CONST.Style.DefaultUserstyle);
- }
-
- function setUserstyle(val) {
- const defaultNotPrevented = return_obj.dispatchEvent(new Event('restyle', { cancelable: true }));
- if (defaultNotPrevented) {
- applyUserstyle(val);
- GM_setValue('userstyle', val);
- }
- }
-
- function applyUserstyle(val) {
- if (!val) { val = getUserstyle() }
- if (!val.includes(':')) { Err('mask.applyUserStyle: type not found') }
- const type = val.substring(0, val.indexOf(':')).toLowerCase();
- const value = val.substring(val.indexOf(':') + 1).trim();
- switch (type) {
- case 'css':
- GM_addElement(shadow, 'style', { textContent: value, style: 'user' });
- utils.$AEL(return_obj, 'restyle', e => $(shadow, 'style[style="user"]').remove(), { once: true });
- break;
- case 'js':
- case 'javascript':
- utils.exec(value, { require, CONST });
- break;
- case 'img':
- case 'image': {
- addImage(value);
- break;
-
- function addImage(src, remaining_retry=3) {
- const img = GM_addElement(mask, 'img', { src: value, style: 'width: 100vw; height: 100vh; border: 0; padding: 0; margin: 0;' }) ?? $(mask, 'img');
- const controller = new AbortController();
- utils.$AEL(img, 'error', e => {
- if (remaining_retry-- > 0) {
- DoLog(LogLevel.Warning, `Mask image load error, retrying...\n(remaining ${remaining_retry} retries)`);
- controller.abort();
- img.remove();
- addImage(src, remaining_retry);
- } else {
- DoLog(LogLevel.Error, `Mask image load error (after maximum retries)\nTry reloading the page or changing an image\nImage url: ${src}`);
- }
- }, { once: true });
- utils.$AEL(return_obj, 'restyle', e => img.remove(), {
- once: true,
- signal: controller.signal
- });
- }
- }
- default:
- Error(`mask.applyUserStyle: Unknown type: ${type}`);
- }
- }
- }
- }, {
- id: 'control',
- desc: 'Provide mask control ui to user',
- dependencies: ['utils', 'mask'],
- func: () => {
- const utils = require('utils');
- const mask = require('mask');
-
- // Switch menu builder
- const buildMenu = (text_getter, callback, id=null) => GM_registerMenuCommand(text_getter(), callback, id !== null ? { id } : {});
-
- // Enable/Disable switch
- const show_text_getter = () => CONST.Text[mask.showing ? 'Unmask' : 'Mask'];
- const show_menu_onclick = e => mask.showing = !mask.showing;
- const buildShowMenu = (id = null) => buildMenu(show_text_getter, show_menu_onclick, id);
- const id = buildShowMenu();
- utils.$AEL(mask, 'show', e => setTimeout(() => buildShowMenu(id)));
- utils.$AEL(mask, 'hide', e => setTimeout(() => buildShowMenu(id)));
-
- // Tamperorily disable
- GM_registerMenuCommand(CONST.Text.TamperorilyDisable, e => {
- mask.hide();
- utils.$AEL(mask, 'show', e => e.preventDefault());
- DoLog(LogLevel.Success, CONST.Text.TamperorilyDisabled);
- setTimeout(() => alert(CONST.Text.TamperorilyDisabled));
- });
-
- // Custom user style
- GM_registerMenuCommand(CONST.Text.CustomUserstyle, e => {
- let style = prompt(CONST.Text.PromptUserstyle, mask.userstyle);
- if (style === null) { return; }
- if (style === '') { style = CONST.Style.DefaultUserstyle }
- // Here should add an style valid check
- mask.userstyle = style;
- });
-
- return { id };
- }
- }, {
- id: 'automask',
- desc: 'extension: auto-mask after certain idle time',
- detectDom: 'body',
- dependencies: ['mask', 'utils'],
- func: () => {
- const utils = require('utils');
- const mask = require('mask');
- const id = GM_registerMenuCommand(
- isAutomaskEnabled() ? CONST.Text.DisableAutomask : CONST.Text.EnableAutomask,
- function onClick(e) {
- isAutomaskEnabled() ? disable() : enable();
- GM_registerMenuCommand(
- isAutomaskEnabled() ? CONST.Text.DisableAutomask : CONST.Text.EnableAutomask,
- onClick, { id }
- );
- isAutomaskEnabled() && check_idle();
- }
- );
- GM_registerMenuCommand(CONST.Text.SetIDLETime, e => {
- const config = getConfig();
- const time = prompt(CONST.Text.PromptIDLETime, config.idle_time);
- if (time === null) { return; }
- if (!/^(\d+\.)?\d+$/.test(time)) { alert(CONST.Text.IDLETimeInvalid); return; }
- config.idle_time = +time;
- setConfig(config);
- });
-
- // Auto-mask when idle
- let last_refreshed = Date.now();
- const cancel_idle = () => {
- // Iframe events won't bubble into parent window, so manually tell top window to also cancel_idle
- const in_iframe = unsafeWindow !== unsafeWindow.top;
- in_iframe && utils.postMessage(unsafeWindow.top, 'iframe_cancel_idle');
- // Refresh time record
- last_refreshed = Date.now();
- };
- ['mousemove', 'mousedown', 'mouseup', 'wheel', 'keydown', 'keyup'].forEach(evt_name =>
- utils.$AEL(unsafeWindow, evt_name, e => cancel_idle(), { capture: true }));
- utils.recieveMessage('iframe_cancel_idle', e => cancel_idle());
- const check_idle = () => {
- const config = getConfig();
- const time_left = config.idle_time * 1000 - (Date.now() - last_refreshed);
- if (time_left <= 0) {
- isAutomaskEnabled() && !mask.showing && mask.show();
- utils.$AEL(mask, 'hide', e => {
- cancel_idle();
- check_idle();
- }, { once: true });
- } else {
- setTimeout(check_idle, time_left);
- }
- }
- check_idle();
-
- return {
- id, enable, disable,
- get enabled() { return isAutomaskEnabled(); }
- };
-
- function getConfig() {
- return GM_getValue('automask', {
- sites: [],
- idle_time: 30
- });
- }
-
- function setConfig(val) {
- return GM_setValue('automask', val);
- }
-
- function isAutomaskEnabled() {
- return getConfig().sites.includes(location.host);
- }
-
- function enable() {
- if (isAutomaskEnabled()) { return; }
- const config = getConfig();
- config.sites.push(location.host);
- setConfig(config);
- }
-
- function disable() {
- if (!isAutomaskEnabled()) { return; }
- const config = getConfig();
- config.sites.splice(config.sites.indexOf(location.host), 1);
- setConfig(config);
- }
- }
- }, {
- id: 'utils',
- desc: 'helper functions',
- func: () => {
- function GM_hasVersion(version) {
- return hasVersion(GM_info?.version || '0', version);
-
- function hasVersion(ver1, ver2) {
- return compareVersions(ver1.toString(), ver2.toString()) >= 0;
-
- // https://gf.qytechs.cn/app/javascript/versioncheck.js
- // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
- function compareVersions(a, b) {
- if (a == b) {
- return 0;
- }
- let aParts = a.split('.');
- let bParts = b.split('.');
- for (let i = 0; i < aParts.length; i++) {
- let result = compareVersionPart(aParts[i], bParts[i]);
- if (result != 0) {
- return result;
- }
- }
- // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
- if (bParts.length > aParts.length) {
- return -1;
- }
- return 0;
- }
-
- function compareVersionPart(partA, partB) {
- let partAParts = parseVersionPart(partA);
- let partBParts = parseVersionPart(partB);
- for (let i = 0; i < partAParts.length; i++) {
- // "A string-part that exists is always less than a string-part that doesn't exist"
- if (partAParts[i].length > 0 && partBParts[i].length == 0) {
- return -1;
- }
- if (partAParts[i].length == 0 && partBParts[i].length > 0) {
- return 1;
- }
- if (partAParts[i] > partBParts[i]) {
- return 1;
- }
- if (partAParts[i] < partBParts[i]) {
- return -1;
- }
- }
- return 0;
- }
-
- // It goes number, string, number, string. If it doesn't exist, then
- // 0 for numbers, empty string for strings.
- function parseVersionPart(part) {
- if (!part) {
- return [0, "", 0, ""];
- }
- let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
- return [
- partParts[1] ? parseInt(partParts[1]) : 0,
- partParts[2],
- partParts[3] ? parseInt(partParts[3]) : 0,
- partParts[4]
- ];
- }
- }
- }
-
- function copyPropDescs(from, to) {
- Object.defineProperties(to, Object.getOwnPropertyDescriptors(from));
- }
-
- function randint(min, max) {
- return Math.random() * (max - min) + min;
- }
-
- function randstr(len) {
- const letters = [...Array(26).keys()].map( i => String.fromCharCode('a'.charCodeAt(0) + i) );
- let str = '';
- for (let i = 0; i < len; i++) {
- str += letters.at(randint(0, 25));
- }
- return str;
- }
-
- /**
- * execute js code in a global function closure and try to bypass CSP rules
- * { name: 'John', number: 123456 } will be executed by (function(name, number) { code }) ('John', 123456);
- *
- * @param {string} code
- * @param {Object} args
- */
- function exec(code, args) {
- // Parse args
- const arg_names = Object.keys(args);
- const arg_vals = arg_names.map(name => args[name]);
- // Construct middle code and middle obj
- const id = randstr(16);
- const middle_obj = unsafeWindow[id] = { id, arg_vals, url: null };
- const middle_code_parts = {
- single_instance: [
- '// Run code only once',
- `if (!window.hasOwnProperty('${id}')) { return; }`
- ].join('\n'),
- cleaner: [
- '// Do some cleaning first',
- `const middle_obj = window.${id};`,
- `delete window.${id};`,
- `URL.revokeObjectURL(middle_obj.url);`,
- `document.querySelector('#${id}')?.remove();`
- ].join('\n'),
- executer: [
- '// Execute user code',
- `(function(${arg_names.join(', ')}, middle_obj) {`,
- code,
- `}).call(null, ...middle_obj.arg_vals, undefined);`
- ].join('\n'),
- }
- const middle_code = `(function() {\n${Object.values(middle_code_parts).join('\n\n')}\n}) ();`;
- const blob = new Blob([middle_code], { type: 'application/javascript' });
- const url = middle_obj.url = URL.createObjectURL(blob);
- // Create and execute <script>
- GM_addElement(document.head, 'script', { src: url, id });
- GM_addElement(document.head, 'script', { textContent: middle_code, id });
- }
-
- /**
- * Some website (like icourse163.com, dolmods.com, etc.) hooked addEventListener,\
- * so calling target.addEventListener has no effect.
- * this function get a "pure" addEventListener from new iframe, and make use of it
- */
- function $AEL(target, ...args) {
- if (!$AEL.id_prop) {
- $AEL.id_prop = randstr(16);
- $AEL.id_val = randstr(16);
- GM_addElement(document.body, 'iframe', {
- srcdoc: '<html></html>',
- style: [
- 'border: 0',
- 'padding: 0',
- 'margin: 0',
- 'width: 0',
- 'height: 0',
- 'display: block',
- 'visibility: visible'
- ].concat('').join(' !important; '),
- [$AEL.id_prop]: $AEL.id_val,
- });
- }
- try {
- const ifr = $(`[${$AEL.id_prop}=${$AEL.id_val}]`);
- const AEL = ifr.contentWindow.EventTarget.prototype.addEventListener;
- return AEL.call(target, ...args);
- } catch (e) {
- if (!$(`[${$AEL.id_prop}=${$AEL.id_val}]`)) {
- DoLog(LogLevel.Warning, 'GM_addElement is not working properly: added iframe not found\nUsing normal addEventListener instead');
- } else {
- DoLog(LogLevel.Warning, 'Unknown error occured\nUsing normal addEventListener instead')
- }
- return window.EventTarget.prototype.addEventListener.call(target, ...args);
- }
- }
-
- const [postMessage, recieveMessage] = (function() {
- // Check and init security key
- let securityKey = GM_getValue('Message-Security-Key');
- if (!securityKey) {
- securityKey = { prop: randstr(8), value: randstr(16) };
- GM_setValue('Message-Security-Key', securityKey);
- }
-
- /**
- * post a message to target window using window.postMessage
- * name, data will be formed as an object in format of { name, data, securityKeyProp: securityKeyVal }
- * securityKey will be randomly generated with first initialization
- * and saved with GM_setValue('Message-Security-Key', { prop, value })
- *
- * @param {string} name - type of this message
- * @param {*} [data] - data of this message
- */
- function postMessage(targetWindow, name, data=null) {
- const securityKeyProp = securityKey.prop;
- const securityKeyVal = securityKey.value;
- targetWindow.postMessage({ name, data, [securityKey.prop]: securityKey.value }, '*');
- }
-
- /**
- * recieve message posted by postMessage
- * @param {string} name - which kind of message you want to recieve
- * @param {function} [callback] - if provided, return undefined and call this callback function each time a message
- recieved; if not, returns a Promise that will be fulfilled with next message for once
- * @returns {(Promise<number>|undefined)}
- */
- function recieveMessage(name, callback=null) {
- const win = typeof unsafeWindow === 'object' ? unsafeWindow : window;
- let resolve;
- $AEL(win, 'message', function listener(e) {
- // Check security key first
- if (!(e?.data?.[securityKey.prop] === securityKey.value)) { return; }
- if (e?.data?.name === name) {
- if (callback) {
- callback(e);
- } else {
- resolve(e);
- }
- }
- });
- if (!callback) {
- return new Promise((res, rej) => { resolve = res; });
- }
- }
-
- return [postMessage, recieveMessage];
- }) ();
-
- return { GM_hasVersion, copyPropDescs, randint, randstr, exec, $AEL, postMessage, recieveMessage };
- }
- }, {
- desc: 'compatibility alert',
- dependencies: 'utils',
- func: () => {
- const utils = require('utils');
- if (!GM_getValue('compat-alert') && (GM_info.scriptHandler !== 'Tampermonkey' || !utils.GM_hasVersion('5.0'))) {
- alert(CONST.Text.CompatAlert);
- GM_setValue('compat-alert', true);
- }
- }
- }]);
- })();