// ==UserScript== // @name pURLfy for Tampermonkey // @name:zh-CN pURLfy for Tampermonkey // @namespace http://tampermonkey.net/ // @version 0.2.6 // @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_getResourceText // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect * // @require https://update.greasyfork.org/scripts/492078/1376834/pURLfy.js // @resource rules-cn https://cdn.jsdelivr.net/gh/PRO-2684/pURLfy-rules@core-0.3.x/cn.min.json // @resource rules-alternative https://cdn.jsdelivr.net/gh/PRO-2684/pURLfy-rules@core-0.3.x/alternative.min.json // @license gpl-3.0 // @downloadURL none // ==/UserScript== (function () { const tag1 = "purlfy-purifying"; const tag2 = "purlfy-purified"; const eventName = "purlfy-purify-done"; const log = console.log.bind(console, "[pURLfy for Tampermonkey]"); const window = unsafeWindow; const initStatistics = { url: 0, param: 0, decoded: 0, redirected: 0, visited: 0, char: 0 }; const initRulesCfg = { "cn": true, "alternative": false }; // Initialize pURLfy core const purifier = new Purlfy({ fetchEnabled: true, lambdaEnabled: true, fetch: async function (url, options) { // Adapted from https://github.com/AlttiRi/gm_fetch function normalize(gm_response) { const headers = new Headers(); for (const line of gm_response.responseHeaders.trim().split("\n")) { const [key, ...values] = line.split(": "); headers.append(key, values.join(": ")); } const r = new Response(gm_response.response, { status: gm_response.status, statusText: gm_response.statusText, headers: headers, url: gm_response.finalUrl || url }); Object.defineProperty(r, "url", { value: gm_response.finalUrl || url }); return r; } return new Promise((resolve, reject) => { // Fetch with GM_xmlhttpRequest GM_xmlhttpRequest({ url: url, method: "GET", responseType: "arraybuffer", onload: function (gm_response) { resolve(normalize(gm_response)); }, onerror: function (error) { reject(error); }, ...options }); }); } }); // Import rules const rulesCfg = GM_getValue("rules", { ...initRulesCfg }); for (const key in initRulesCfg) { const enabled = rulesCfg[key] ?? initRulesCfg[key]; rulesCfg[key] = enabled; if (enabled) { log(`Importing rules: ${key}`); const rules = JSON.parse(GM_getResourceText(`rules-${key}`)); purifier.importRules(rules); } } GM_setValue("rules", rulesCfg); // Statistics listener purifier.addEventListener("statisticschange", e => { log("Statistics increment:", e.detail); const statistics = GM_getValue("statistics", { ...initStatistics }); for (const [key, increment] of Object.entries(e.detail)) { statistics[key] = (statistics[key] ?? 0) + increment; } GM_setValue("statistics", statistics); log("Statistics updated to:", statistics); }); // Hooks const hooks = new Map(); class Hook { // Dummy class for hooks name; constructor(name) { // Register a hook this.name = name; hooks.set(name, this); } 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("location.href"); locationHook.enable = async function () { // Intercept location.href const original = location.href; const purified = (await purifier.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 function cloneAndStop(e, stop=true) { // Clone an event and stop the original const newEvt = new e.constructor(e.type, e); stop && e.preventDefault(); stop && e.stopImmediatePropagation(); return newEvt; } async function mouseHandler(e) { // Intercept mouse events const ele = e.target.tagName === "A" ? e.target : e.target.closest("a"); if (ele && !ele.hasAttribute(tag2) && ele.href) { 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 = cloneAndStop(e); this.toast(`Intercepted: "${href}"`); const purified = await purifier.purify(href); ele.href = purified.url; this.toast(`Processed: "${ele.href}"`); ele.toggleAttribute(tag2, true); ele.removeAttribute(tag1); ele.dispatchEvent(newEvt); ele.dispatchEvent(new Event(eventName, { bubbles: false, cancelable: true })); if (ele.textContent === href) ele.textContent = purified.url; // Update the text content } else { // Someone else has intercepted 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 }); } }); // Hook form submit async function submitHandler(e) { const form = e.submitter.form; if (!form || 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 form.toggleAttribute(tag1, true); this.toast(`Intercepted: "${url.href}"`); const purified = await purifier.purify(url.href); form.action = purified.url; this.toast(`Processed: "${form.action}"`); form.toggleAttribute(tag2, true); form.removeAttribute(tag1); form.dispatchEvent(new Event(eventName, { bubbles: false, cancelable: true })); } } 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("window.open"); openHook.original = window.open.bind(window); openHook.patched = async function (url, target, features) { // Intercept window.open this.toast(`Intercepted: "${url}"`); const purified = await purifier.purify(url); return this.original(purified.url, target, features); }.bind(openHook); openHook.enable = async function () { window.open = this.patched; } openHook.disable = async function () { window.open = this.original; } // Is there more hooks to add? // Enable hooks const promises = []; const hooksCfg = GM_getValue("hooks", {}); // Load hook configs for (const [name, hook] of hooks) { let enabled = hooksCfg[name]; if (enabled === undefined) { enabled = true; hooksCfg[name] = enabled; } enabled && promises.push(hook.enable().then(() => { log(`Hook "${name}" enabled.`); })); } GM_setValue("hooks", hooksCfg); // Save hook configs Promise.all(promises).then(() => { log(`[core ${Purlfy.version}] Initialized successfully! 🎉`); }); // 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; purifier.purify(url).then(result => { GM_setClipboard(result.url); alert(`Original: ${trim(url)}\nResult (copied): ${trim(result.url)}\nMatched rule: ${result.rule}`); }); }; GM_registerMenuCommand("Purify URL", showPurify); // Statistics function showStatistics() { const statistics = GM_getValue("statistics", { ...initStatistics }); const text = Object.entries(statistics).map(([key, value]) => `${key}: ${value}`).join(", ") + `\npURLfy core version: ${Purlfy.version}`; const r = confirm(text + "\nDo you want to reset the statistics?"); if (!r) return; GM_setValue("statistics", initStatistics); log("Statistics reset."); }; GM_registerMenuCommand("Show Statistics", showStatistics); })();