🏠 Home 

คุณต้องเข้าสู่ระบบหรือลงทะเบียนก่อนดำเนินการต่อ

pURLfy for Tampermonkey

The ultimate URL purifier - for Tampermonkey


Installer dette script?
// ==UserScript==
// @name         pURLfy for Tampermonkey
// @name:zh-CN   pURLfy for Tampermonkey
// @namespace    http://tampermonkey.net/
// @version      0.5.5
// @description  The ultimate URL purifier - for Tampermonkey
// @description:zh-cn 终极 URL 净化器 - Tampermonkey 版本
// @icon         https://github.com/PRO-2684/pURLfy/raw/main/images/logo.svg
// @author       PRO
// @match        *://*/*
// @run-at       document-start
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @grant        GM_getResourceText
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      *
// @require      https://cdn.jsdelivr.net/npm/@trim21/[email protected]
// @require      https://update.greasyfork.org/scripts/492078/1499254/pURLfy.js
// @require      https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2
// @resource     rules-tracking https://cdn.jsdelivr.net/gh/PRO-2684/[email protected]/tracking.min.json
// @resource     rules-outgoing https://cdn.jsdelivr.net/gh/PRO-2684/[email protected]/outgoing.min.json
// @resource     rules-shortener https://cdn.jsdelivr.net/gh/PRO-2684/[email protected]/shortener.min.json
// @resource     rules-alternative https://cdn.jsdelivr.net/gh/PRO-2684/[email protected]/alternative.min.json
// @resource     rules-other https://cdn.jsdelivr.net/gh/PRO-2684/[email protected]/other.min.json
// @license      gpl-3.0
// ==/UserScript==
(function () {
const tag1 = "purlfy-purifying";
const tag2 = "purlfy-purified";
const eventName = "purlfy-purify-done";
const window = unsafeWindow;
const configDesc = {
$default: {
autoClose: false
},
rules: {
name: "📖 Rules Settings",
title: "Enable or disable rules",
type: "folder",
items: {
tracking: {
name: "Tracking",
title: "Rules for purifying tracking links",
type: "bool",
value: true,
},
outgoing: {
name: "Outgoing",
title: "Rules for purifying outgoing links",
type: "bool",
value: true,
},
shortener: {
name: "Shortener",
title: "Rules for restoring shortened links",
type: "bool",
value: true,
},
alternative: {
name: "Alternative",
title: "Redirects you from some websites to their better alternatives",
type: "bool",
value: false,
},
other: {
name: "Other",
title: "Rules for purifying other types of links",
type: "bool",
value: false,
},
removeTextFragment: {
name: "Remove Text Fragment",
title: "Remove Text Fragments from URL",
type: "bool",
value: false,
},
},
},
hooks: {
name: "🪝 Hooks Settings",
title: "Enable or disable hooks",
type: "folder",
items: {
locationHref: {
name: "location.href",
title: "Check location.href",
type: "bool",
value: true,
},
click: {
name: "click",
title: "Intercept `click` events",
type: "bool",
value: true,
},
mousedown: {
name: "mousedown",
title: "Intercept `mousedown` events",
type: "bool",
value: true,
},
auxclick: {
name: "auxclick",
title: "Intercept `auxclick` events",
type: "bool",
value: true,
},
touchstart: {
name: "touchstart",
title: "Intercept `touchstart` events",
type: "bool",
value: true,
},
windowOpen: {
name: "window.open",
title: "Hook `window.open` calls",
type: "bool",
value: true,
},
pushState: {
name: "pushState",
title: "Hook `history.pushState` calls",
type: "bool",
value: false,
},
replaceState: {
name: "replaceState",
title: "Hook `history.replaceState` calls",
type: "bool",
value: false,
},
bing: {
name: "Bing",
title: "Site-specific hook for Bing",
type: "bool",
value: true,
},
},
},
statistics: {
name: "📊 Statistics",
title: "Show statistics",
type: "folder",
items: {
$default: {
input: (prop, orig) => confirm(`Reset "${prop}"?`) ? 0 : orig,
processor: "same",
formatter: "normal",
},
url: {
name: "URL",
title: "Number of links purified",
value: 0,
},
param: {
name: "Parameter",
title: "Number of parameters removed",
value: 0,
},
decoded: {
name: "Decoded",
title: "Number of URLs decoded (`param` mode)",
value: 0,
},
redirected: {
name: "Redirected",
title: "Number of URLs redirected (`redirect` mode)",
value: 0,
},
visited: {
name: "Visited",
title: "Number of URLs visited (`visit` mode)",
value: 0,
},
char: {
name: "Character",
title: "Number of characters deleted",
value: 0,
},
},
},
advanced: {
name: "⚙️ Advanced options",
title: "Advanced options",
type: "folder",
items: {
purify: {
name: "Purify URL",
title: "Manually purify a URL",
type: "action",
},
senseless: {
name: "Senseless Mode",
title: "Enable senseless mode",
type: "bool",
value: true,
},
disableBeacon: {
name: "Disable Beacon",
title: "Overwrite `navigator.sendBeacon` to a no-op function",
type: "bool",
value: false,
},
debug: {
name: "Debug Mode",
title: "Enable debug mode",
type: "bool",
value: false,
}
},
},
};
const config = new GM_config(configDesc);
function log(...args) {
if (config.get("advanced.debug")) console.log("[pURLfy for Tampermonkey]", ...args);
}
// Initialize pURLfy core
const purifier = new Purlfy({
fetchEnabled: true,
lambdaEnabled: true,
fetch: GM_fetch,
log: config.get("advanced.debug") ? undefined : () => { },
});
async function purify(url) {
if (config.get("rules.removeTextFragment")) { // Remove Text Fragment
const index = url.indexOf("#:~:");
if (index !== -1) url = url.slice(0, index);
}
return purifier.purify(url);
}
// Import rules
for (const key of config.list("rules")) {
const enabled = config.get(`rules.${key}`);
if (enabled) {
log(`Importing rules: ${key}`);
const rules = JSON.parse(GM_getResourceText(`rules-${key}`));
purifier.importRules(rules);
}
}
// Senseless mode
const senseless = config.get("advanced.senseless");
log(`Senseless mode is ${senseless ? "enabled" : "disabled"}.`);
// Statistics listener
purifier.addEventListener("statisticschange", e => {
log("Statistics increment:", e.detail);
for (const [key, increment] of Object.entries(e.detail)) {
config.set(`statistics.${key}`, config.get(`statistics.${key}`) + increment);
}
});
// Hooks
const hooks = [];
class Hook { // Dummy class for hooks
name;
enabled;
constructor(name) { // Register a hook
this.name = name;
// hooks.set(name, this);
hooks.push(this);
this.enabled = config.get(`hooks.${name}`);
}
toast(content) { // Indicate that a URL has been intercepted
log(`Hook "${this.name}": ${content}`);
}
async enable() { // Enable the hook
throw new Error("Over-ride me!");
}
async disable() { // Disable the hook
throw new Error("Over-ride me!");
}
}
// Check location.href (not really a hook, actually)
const locationHook = new Hook("locationHref");
locationHook.enable = async function () { // Intercept location.href
const original = location.href;
const purified = (await purify(original)).url;
if (original !== purified) {
window.stop(); // Stop loading
this.toast(`Redirect: "${original}" -> "${purified}"`);
location.replace(purified);
}
}.bind(locationHook);
locationHook.disable = async function () { } // Do nothing
// Mouse-related hooks
const tagNames = new Set(["A", "AREA"]);
function cloneAndStop(e) { // Clone an event and stop the original
const newEvt = new e.constructor(e.type, e);
e.preventDefault();
e.stopImmediatePropagation();
return newEvt;
}
async function mouseHandler(e) { // Intercept mouse events
const ele = e.composedPath().find(ele => tagNames.has(ele.tagName));
if (ele && !ele.hasAttribute(tag2) && ele.href && !ele.getAttribute("href").startsWith("#")) {
ele.removeAttribute("ping"); // Remove `ping` attribute
const href = ele.href;
if (!href.startsWith("https://") && !href.startsWith("http://")) return; // Ignore non-HTTP(S) URLs
if (!ele.hasAttribute(tag1)) { // The first to intercept
ele.toggleAttribute(tag1, true);
const newEvt = senseless ? null : cloneAndStop(e);
this.toast(`Intercepted: "${href}"`);
const purified = await purify(href);
if (purified.url !== href) {
ele.href = purified.url;
// if (ele.innerHTML === href) ele.innerHTML = purified.url; // Update the text
if (ele.childNodes?.length === 1
&& ele.firstChild.nodeType === Node.TEXT_NODE
&& ele.firstChild.textContent === href) { // Update the text
ele.firstChild.textContent = purified.url;
}
this.toast(`Processed: "${ele.href}"`);
} else {
this.toast(`Same: "${ele.href}"`);
}
ele.toggleAttribute(tag2, true);
ele.removeAttribute(tag1);
senseless || ele.dispatchEvent(newEvt);
ele.dispatchEvent(new Event(eventName, { bubbles: false, cancelable: true }));
} else { // Someone else has intercepted
if (!senseless) {
const newEvt = cloneAndStop(e);
this.toast(`Waiting: "${ele.href}"`);
ele.addEventListener(eventName, function () {
log(`Waited: "${ele.href}"`);
ele.dispatchEvent(newEvt);
}, { once: true });
}
}
}
}
["click", "mousedown", "auxclick"].forEach((name) => {
const hook = new Hook(name);
hook.handler = mouseHandler.bind(hook);
hook.enable = async function () {
document.addEventListener(name, this.handler, { capture: true });
}
hook.disable = async function () {
document.removeEventListener(name, this.handler, { capture: true });
}
});
// Listen to `touchstart` event
async function touchstartHandler(e) { // Always "senseless"
const ele = e.composedPath().find(ele => tagNames.has(ele.tagName));
if (ele && !ele.hasAttribute(tag1) && !ele.hasAttribute(tag2) && ele.href && !ele.getAttribute("href").startsWith("#")) {
ele.removeAttribute("ping"); // Remove `ping` attribute
const href = ele.href;
if (!href.startsWith("https://") && !href.startsWith("http://")) return; // Ignore non-HTTP(S) URLs
ele.toggleAttribute(tag1, true);
this.toast(`Intercepted: "${href}"`);
const purified = await purify(href);
if (purified.url !== href) {
ele.href = purified.url;
if (ele.innerHTML === href) ele.innerHTML = purified.url; // Update the text
this.toast(`Processed: "${ele.href}"`);
} else {
this.toast(`Same: "${ele.href}"`);
}
ele.toggleAttribute(tag2, true);
ele.removeAttribute(tag1);
ele.dispatchEvent(new Event(eventName, { bubbles: false, cancelable: true }));
}
}
const touchstartHook = new Hook("touchstart");
touchstartHook.handler = touchstartHandler.bind(touchstartHook);
touchstartHook.enable = async function () {
document.addEventListener("touchstart", this.handler, { capture: true });
}
touchstartHook.disable = async function () {
document.removeEventListener("touchstart", this.handler, { capture: true });
}
// Hook form submit
// function submitHandler(e) { // Always "senseless"
//     let submitter = e.submitter;
//     const form = submitter.form;
//     if (!form || form.method !== "get" || form.hasAttribute(tag2)) return;
//     const url = new URL(form.action, location.href);
//     if (url.protocol !== "http:" && url.protocol !== "https:") return; // Ignore non-HTTP(S) URLs
//     if (!form.hasAttribute(tag1)) { // The first to intercept
//         e.preventDefault();
//         e.stopImmediatePropagation();
//         form.toggleAttribute(tag1, true);
//         for (const input of form.elements) {
//             url.searchParams.set(input.name, input.value);
//         }
//         this.toast(`Intercepted: "${url.href}"`);
//         purify(url.href).then(r###lt => {
//             this.toast(`Processed: "${r###lt.url}"`);
//             const purified = new URL(r###lt.url);
//             if (purified.href !== url.href) {
//                 form.action = purified.origin + purified.pathname;
//                 for (const input of form.elements) {
//                     if (input.name) {
//                         if (purified.searchParams.has(input.name)) {
//                             input.value = purified.searchParams.get(input.name);
//                             purified.searchParams.delete(input.name);
//                             input.toggleAttribute("disabled", false);
//                         } else {
//                             input.value = "";
//                             input.toggleAttribute("disabled", true);
//                             if (submitter === input) submitter = undefined;
//                         }
//                     }
//                 }
//                 for (const [key, value] of purified.searchParams) {
//                     const input = document.createElement("input");
//                     input.type = "hidden";
//                     input.name = key;
//                     input.value = value;
//                     form.appendChild(input);
//                 }
//             } else {
//                 this.toast(`Same: "${form.action}"`);
//             }
//             form.toggleAttribute(tag2, true);
//             form.removeAttribute(tag1);
//             form.dispatchEvent(new Event(eventName, { bubbles: false, cancelable: true }));
//             form.requestSubmit(submitter);
//         });
//     }
// }
// const submitHook = new Hook("submit");
// submitHook.handler = submitHandler.bind(submitHook);
// submitHook.enable = async function () {
//     document.addEventListener("submit", this.handler, { capture: true });
// }
// submitHook.disable = async function () {
//     document.removeEventListener("submit", this.handler, { capture: true });
// }
// Intercept window.open
const openHook = new Hook("windowOpen");
openHook.original = window.open.bind(window);
openHook.patched = function (url, target, features) { // Intercept window.open
url = url?.toString() ?? "about:blank";
if (url && url !== "about:blank" && (url.startsWith("http://") || url.startsWith("https://"))) {
this.toast(`Intercepted: "${url}"`);
purify(url).then(purified => {
this.toast(`Processed: "${purified.url}"`);
this.original(purified.url, target, features);
});
return true; // Ideally, return a window object; however, it's impossible to do so
} else {
return this.original(url, target, features);
}
}.bind(openHook);
openHook.enable = async function () {
window.open = this.patched;
}
openHook.disable = async function () {
window.open = this.original;
}
function patch(orig) { // Patch history functions
function patched(...args) {
const url = args[2];
if (url && (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//") || url.startsWith("/") || url.startsWith("?"))) {
this.toast(`Intercepted: "${url}"`);
const resolved = new URL(url, location.href).href;
purify(resolved).then(purified => {
this.toast(`Processed: "${purified.url}"`);
args[2] = purified.url;
orig.apply(history, args);
});
} else {
orig.apply(history, args);
}
}
return patched;
}
const pushStateHook = new Hook("pushState");
pushStateHook.original = history.pushState;
pushStateHook.patched = patch(pushStateHook.original).bind(pushStateHook);
pushStateHook.enable = async function () {
history.pushState = pushStateHook.patched;
}
pushStateHook.disable = async function () {
history.pushState = pushStateHook.original;
}
const replaceStateHook = new Hook("replaceState");
replaceStateHook.original = history.replaceState;
replaceStateHook.patched = patch(replaceStateHook.original).bind(replaceStateHook);
replaceStateHook.enable = async function () {
history.replaceState = replaceStateHook.patched;
}
replaceStateHook.disable = async function () {
history.replaceState = replaceStateHook.original;
}
// Site-specific hooks
switch (location.hostname) {
case "www.bing.com":
case "cn.bing.com": { // Bing
// Hook `addEventListener`
const bingHook = new Hook("bing");
bingHook.blacklist = { "A": new Set(["mouseenter", "mouseleave", "mousedown"]), "P": new Set(["mouseover", "mouseout", "click"]) }
bingHook.original = HTMLElement.prototype.addEventListener;
bingHook.patched = function (type, listener, options) {
if (bingHook.blacklist[this.tagName] && bingHook.blacklist[this.tagName].has(type)) { // Block events
return;
}
return bingHook.original.call(this, type, listener, options);
};
bingHook.enable = async function () {
HTMLElement.prototype.addEventListener = bingHook.patched;
}
bingHook.disable = async function () {
HTMLElement.prototype.addEventListener = bingHook.original;
}
break;
}
default: {
break;
}
}
// Is there more hooks to add?
// Enable hooks
const promises = [];
for (const hook of hooks) {
hook.enabled && promises.push(hook.enable().then(() => {
log(`Hook "${hook.name}" enabled.`);
}));
}
Promise.all(promises).then(() => {
log(`[core ${Purlfy.version}] Initialized successfully! 🎉`);
});
// advanced.disableBeacon
if (config.get("advanced.disableBeacon")) {
Object.defineProperty(navigator, "sendBeacon", {
value: (...args) => {
log("Blocked `navigator.sendBeacon`:", ...args);
return false;
},
writable: false,
configurable: false,
});
}
// Manual purify
function trim(url) { // Leave at most 100 characters
return url.length > 100 ? url.slice(0, 100) + "..." : url;
}
function showPurify() {
const url = prompt("Enter the URL to purify:", location.href);
if (!url) return;
purify(url).then(r###lt => {
GM_setClipboard(r###lt.url);
alert(`Original: ${trim(url)}\nR###lt (copied): ${trim(r###lt.url)}\nMatched rule: ${r###lt.rule}`);
});
};
config.addEventListener("get", (e) => {
if (e.detail.prop === "advanced.purify") {
showPurify();
}
});
})();