Skip to content

Instantly share code, notes, and snippets.

@LawrenceHwang
Created May 22, 2026 14:42
Show Gist options
  • Select an option

  • Save LawrenceHwang/9ee7401d2c4e32857cd623b86fface6f to your computer and use it in GitHub Desktop.

Select an option

Save LawrenceHwang/9ee7401d2c4e32857cd623b86fface6f to your computer and use it in GitHub Desktop.
UserScript to clean up tracking, unwanted parameters. To be used with extension such as TamperMonkey
// ==UserScript==
// @name URL Clean
// @namespace https://github.com/
// @version 0.1.7
// @description Cleans safe tracking parameters and known redirect wrappers on an allowlisted set of video, shopping, search, and social sites.
// @match https://*.youtube.com/*
// @match https://youtu.be/*
// @match https://*.twitter.com/*
// @match https://x.com/*
// @match https://*.facebook.com/*
// @match https://*.instagram.com/*
// @match https://*.threads.net/*
// @match https://*.linkedin.com/*
// @match https://*.reddit.com/*
// @match https://*.tiktok.com/*
// @match https://*.snapchat.com/*
// @match https://*.messenger.com/*
// @match https://t.me/*
// @match https://telegram.me/*
// @match https://*.whatsapp.com/*
// @match https://*.pinterest.com/*
// @match https://*.tumblr.com/*
// @match https://vk.com/*
// @match https://news.ycombinator.com/*
// @match https://*.bing.com/*
// @match https://*.msn.com/*
// @match https://google.com/search*
// @match https://www.google.com/search*
// @match https://amazon.com/*
// @match https://*.amazon.com/*
// @match https://amazon.ca/*
// @match https://*.amazon.ca/*
// @match https://amazon.co.uk/*
// @match https://*.amazon.co.uk/*
// @match https://amazon.de/*
// @match https://*.amazon.de/*
// @match https://amazon.fr/*
// @match https://*.amazon.fr/*
// @match https://amazon.es/*
// @match https://*.amazon.es/*
// @match https://amazon.it/*
// @match https://*.amazon.it/*
// @match https://amazon.co.jp/*
// @match https://*.amazon.co.jp/*
// @match https://amazon.com.au/*
// @match https://*.amazon.com.au/*
// @match https://amazon.in/*
// @match https://*.amazon.in/*
// @match https://amazon.com.br/*
// @match https://*.amazon.com.br/*
// @match https://amazon.com.mx/*
// @match https://*.amazon.com.mx/*
// @match https://amazon.nl/*
// @match https://*.amazon.nl/*
// @match https://amazon.se/*
// @match https://*.amazon.se/*
// @match https://amazon.pl/*
// @match https://*.amazon.pl/*
// @match https://amazon.sg/*
// @match https://*.amazon.sg/*
// @match https://amazon.ae/*
// @match https://*.amazon.ae/*
// @match https://amazon.sa/*
// @match https://*.amazon.sa/*
// @match https://amazon.eg/*
// @match https://*.amazon.eg/*
// @match https://amazon.com.tr/*
// @match https://*.amazon.com.tr/*
// @match https://amazon.be/*
// @match https://*.amazon.be/*
// @match https://ebay.com/*
// @match https://*.ebay.com/*
// @match https://ebay.ca/*
// @match https://*.ebay.ca/*
// @match https://ebay.co.uk/*
// @match https://*.ebay.co.uk/*
// @match https://ebay.de/*
// @match https://*.ebay.de/*
// @match https://ebay.fr/*
// @match https://*.ebay.fr/*
// @match https://ebay.es/*
// @match https://*.ebay.es/*
// @match https://ebay.it/*
// @match https://*.ebay.it/*
// @match https://ebay.com.au/*
// @match https://*.ebay.com.au/*
// @run-at document-start
// @grant none
// @noframes
// ==/UserScript==
(() => {
"use strict";
// @grant none is intentional: the injected bridge relies on patching the same page-facing browser APIs that the userscript sees.
const DEBUG = false;
const DIRECT_URL_PATTERN = /^(https?:\/\/|mailto:)/i;
const INPUT_TYPES_WITH_SELECTION = new Set(["text", "search", "url", "email", "tel", "password"]);
const SHAREISH_PATTERN = /\b(share|copy(?:[-_\s]?link|[-_\s]?url)?|clipboard)\b/i;
const SHARE_DATA_ATTRIBUTES = [
"data-url",
"data-href",
"data-share-url",
"data-permalink",
"data-link",
"data-copy-link",
"data-copy-url",
];
const MAX_SANITIZE_PASSES = 6;
// Fixed bridge names are intentional for now; nonce hardening can be added later if real page-script interference shows up.
const SANITIZE_BRIDGE_EVENT = "url-cleaner:sanitize-text";
const SANITIZE_BRIDGE_INPUT_ATTRIBUTE = "data-url-cleaner-sanitize-input";
const SANITIZE_BRIDGE_OUTPUT_ATTRIBUTE = "data-url-cleaner-sanitize-output";
const SANITIZE_BRIDGE_CONTEXT_ATTRIBUTE = "data-url-cleaner-sanitize-context";
const SANITIZE_BRIDGE_INSTALLED_ATTRIBUTE = "data-url-cleaner-sanitize-bridge-installed";
const SANITIZE_BRIDGE_STATE_USERSCRIPT = "userscript";
const SANITIZE_BRIDGE_STATE_PAGE = "page";
const SHARE_CLEANUP_PASS_DELAYS = [150, 500];
const GLOBAL_EXACT_PARAMS = new Set([
"gclid",
"dclid",
"gbraid",
"wbraid",
"msclkid",
"yclid",
"fbclid",
"mc_cid",
"mc_eid",
"mkt_tok",
"_hsenc",
"_hsmi",
"vero_id",
"vero_conv",
"oly_anon_id",
"oly_enc_id",
"li_fat_id",
"ttclid",
"twclid",
"rb_clickid",
"s_cid",
"epik",
"_gl",
"gad_source",
"gad_campaignid",
"gad_adid",
"igshid",
"mibextid",
"wickedid",
"ef_id",
]);
const GLOBAL_PARAM_PATTERNS = [
/^utm_[a-z0-9_]+$/i,
/^mtm_[a-z0-9_]+$/i,
/^ga_[a-z0-9_]+$/i,
/^hsa_[a-z0-9_]+$/i,
];
const PRESERVE_PARAM_PATTERNS = [
/(^|_)(state|nonce|csrf|xsrf|session|signature|sig)(_|$)/i,
/(^|_)(code|token)(_|$)/i,
];
const SITE_RULES = [
{
hostPattern: /(^|\.)youtube\.com$/i,
exactParams: new Set(["feature", "si", "pp", "app"]),
},
{
hostPattern: /(^|\.)youtu\.be$/i,
exactParams: new Set(["feature", "si", "pp", "app"]),
},
{
hostPattern: /(^|\.)twitter\.com$/i,
exactParams: new Set(["ref_src", "s", "t", "cn"]),
},
{
hostPattern: /(^|\.)x\.com$/i,
exactParams: new Set(["ref_src", "s", "t", "cn"]),
},
{
hostPattern: /(^|\.)instagram\.com$/i,
exactParams: new Set(["igshid", "igsh"]),
},
{
hostPattern: /(^|\.)tiktok\.com$/i,
exactParams: new Set(["u_code", "preview_pb", "_d", "_t", "_r", "timestamp", "user_id", "share_app_name", "share_iid", "source"]),
},
{
hostPattern: /(^|\.)snapchat\.com$/i,
exactParams: new Set(["sc_referrer", "sc_referrer_domain", "sc_ua"]),
},
{
hostPattern: /(^|\.)amazon\./i,
exactParams: new Set(["tag", "linkcode", "ascsubtag", "camp", "creative", "creativeasin"]),
paramPatterns: [/^ref_?$/i, /^pd_rd_/i, /^pf_rd_/i],
pathSanitizer: (pathname) => pathname.replace(/\/ref(?:=[^/?#]*)?(?=\/|$)/i, ""),
},
{
hostPattern: /(^|\.)ebay\./i,
exactParams: new Set(["_trkparms", "_trksid", "mkcid", "mkrid", "siteid", "campid", "customid", "toolid"]),
},
{
hostPattern: /(^|\.)linkedin\.com$/i,
exactParams: new Set(["trk", "trackingid", "lipi", "midtoken", "eid", "rcm"]),
},
{
hostPattern: /(^|\.)bing\.com$/i,
exactParams: new Set(["ocid", "cvid"]),
},
{
hostPattern: /(^|\.)msn\.com$/i,
exactParams: new Set(["ocid", "cvid"]),
},
{
hostPattern: /(^|\.)google\./i,
pathPattern: /^\/search$/i,
exactParams: new Set(["source", "sourceid", "ved", "ei", "sa", "oq", "aqs", "sei", "sclient", "uact", "biw", "bih", "iflsig", "sca_esv", "sca_upv"]),
paramPatterns: [/^gs_/i, /^gws_/i],
},
];
const REDIRECT_RULES = [
{
hostPattern: /(^|\.)youtube\.com$/i,
pathPattern: /^\/redirect$/i,
paramNames: ["q"],
},
{
hostPattern: /(^|\.)google\./i,
pathPattern: /^\/url$/i,
paramNames: ["q", "url"],
},
{
hostPattern: /(^|\.)facebook\.com$/i,
pathPattern: /^\/l\.php$/i,
paramNames: ["u"],
},
{
hostPattern: /(^|\.)messenger\.com$/i,
pathPattern: /^\/l\.php$/i,
paramNames: ["u"],
},
{
hostPattern: /^out\.reddit\.com$/i,
paramNames: ["url"],
},
{
hostPattern: /^t\.umblr\.com$/i,
pathPattern: /^\/redirect$/i,
paramNames: ["z"],
},
{
hostPattern: /^vk\.com$/i,
pathPattern: /^\/away\.php$/i,
paramNames: ["to"],
},
];
const SHARE_SERVICES = [
{
hostPattern: /(^|\.)twitter\.com$/i,
pathPattern: /^\/intent\/tweet$/i,
urlParams: ["url"],
textParams: ["text"],
},
{
hostPattern: /(^|\.)x\.com$/i,
pathPattern: /^\/intent\/tweet$/i,
urlParams: ["url"],
textParams: ["text"],
},
{
hostPattern: /(^|\.)facebook\.com$/i,
pathPattern: /^\/(?:sharer(?:\.php)?|dialog\/share)$/i,
urlParams: ["u", "href"],
},
{
hostPattern: /(^|\.)linkedin\.com$/i,
pathPattern: /^\/(?:sharing\/share-offsite|shareArticle)\/?$/i,
urlParams: ["url"],
textParams: ["summary"],
},
{
hostPattern: /(^|\.)reddit\.com$/i,
pathPattern: /^\/submit$/i,
urlParams: ["url"],
textParams: ["title", "text"],
},
{
hostPattern: /(^|\.)t\.me$/i,
pathPattern: /^\/share\/url$/i,
urlParams: ["url"],
textParams: ["text"],
},
{
hostPattern: /(^|\.)telegram\.me$/i,
pathPattern: /^\/share\/url$/i,
urlParams: ["url"],
textParams: ["text"],
},
{
hostPattern: /(^|\.)whatsapp\.com$/i,
pathPattern: /^\/send$/i,
textParams: ["text"],
},
{
hostPattern: /(^|\.)api\.whatsapp\.com$/i,
pathPattern: /^\/send$/i,
textParams: ["text"],
},
{
hostPattern: /(^|\.)pinterest\./i,
pathPattern: /^\/pin\/create\/button\/?$/i,
urlParams: ["url"],
textParams: ["description"],
},
{
hostPattern: /(^|\.)tumblr\.com$/i,
pathPattern: /^\/widgets\/share\/tool$/i,
urlParams: ["canonicalUrl", "url"],
textParams: ["caption", "content"],
},
{
hostPattern: /(^|\.)vk\.com$/i,
pathPattern: /^\/share\.php$/i,
urlParams: ["url"],
textParams: ["title", "comment"],
},
{
hostPattern: /(^|\.)news\.ycombinator\.com$/i,
pathPattern: /^\/submitlink$/i,
urlParams: ["u"],
textParams: ["t"],
},
];
const nativeHistoryPushState = typeof history.pushState === "function" ? history.pushState.bind(history) : null;
const nativeHistoryReplaceState = typeof history.replaceState === "function" ? history.replaceState.bind(history) : null;
const nativeDataTransferSetData = typeof DataTransfer !== "undefined" && DataTransfer.prototype && typeof DataTransfer.prototype.setData === "function"
? DataTransfer.prototype.setData
: null;
let isApplyingInternalHistoryCleanup = false;
let shareCleanupObserver = null;
let shareCleanupAnimationFrameId = null;
let shareCleanupTimeoutIds = [];
let shareCleanupPending = false;
function logDebug(message, details) {
if (!DEBUG) {
return;
}
console.debug("[url-cleaner]", message, details || "");
}
function pushUnique(list, value) {
if (value && !list.includes(value)) {
list.push(value);
}
}
function tryPatchMethod(target, methodName, createReplacement) {
if (!target || typeof target[methodName] !== "function") {
return false;
}
const originalMethod = target[methodName].bind(target);
const replacement = createReplacement(originalMethod);
try {
target[methodName] = replacement;
return target[methodName] === replacement;
} catch {
try {
Object.defineProperty(target, methodName, {
configurable: true,
writable: true,
value: replacement,
});
return true;
} catch {
return false;
}
}
}
function getElementDescriptorText(element) {
if (!(element instanceof Element)) {
return "";
}
const textParts = [
element.getAttribute("aria-label"),
element.getAttribute("title"),
element.getAttribute("id"),
element.getAttribute("class"),
element.getAttribute("data-action"),
element.getAttribute("data-testid"),
element.getAttribute("role"),
];
if (element instanceof HTMLButtonElement || element instanceof HTMLAnchorElement) {
const textContent = (element.textContent || "").trim().replace(/\s+/g, " ");
if (textContent && textContent.length <= 80) {
textParts.push(textContent);
}
}
return textParts.filter(Boolean).join(" ");
}
function isShareishElement(element) {
return SHAREISH_PATTERN.test(getElementDescriptorText(element));
}
function isPreservedParam(paramName) {
return PRESERVE_PARAM_PATTERNS.some((pattern) => pattern.test(paramName));
}
function isGlobalTrackingParam(paramName) {
if (GLOBAL_EXACT_PARAMS.has(paramName)) {
return true;
}
return GLOBAL_PARAM_PATTERNS.some((pattern) => pattern.test(paramName));
}
function isGoogleSearchPage() {
return /(^|\.)google\.com$/i.test(location.hostname) && /^\/search$/i.test(location.pathname);
}
function getMatchingSiteRules(url) {
const host = url.hostname;
const path = url.pathname;
return SITE_RULES.filter((rule) => {
if (!rule.hostPattern.test(host)) {
return false;
}
if (rule.pathPattern && !rule.pathPattern.test(path)) {
return false;
}
return true;
});
}
function getMatchingRedirectRules(url) {
return REDIRECT_RULES.filter((rule) => {
if (!rule.hostPattern.test(url.hostname)) {
return false;
}
if (rule.pathPattern && !rule.pathPattern.test(url.pathname)) {
return false;
}
return true;
});
}
function isSiteTrackingParam(paramName, rules) {
for (const rule of rules) {
if (rule.exactParams && rule.exactParams.has(paramName)) {
return true;
}
if (rule.paramPatterns && rule.paramPatterns.some((pattern) => pattern.test(paramName))) {
return true;
}
}
return false;
}
function sanitizeHash(hash) {
if (!hash || hash === "#") {
return { hash, removed: [] };
}
const rawHash = hash.slice(1);
if (!rawHash || rawHash.includes("/") || rawHash.startsWith("!")) {
return { hash, removed: [] };
}
const isQueryStyle = rawHash.startsWith("?") || rawHash.includes("=");
if (!isQueryStyle) {
return { hash, removed: [] };
}
const hashParams = new URLSearchParams(rawHash.startsWith("?") ? rawHash.slice(1) : rawHash);
const removed = [];
for (const key of new Set(hashParams.keys())) {
const lowerKey = key.toLowerCase();
if (isPreservedParam(lowerKey)) {
continue;
}
if (!isGlobalTrackingParam(lowerKey)) {
continue;
}
hashParams.delete(key);
removed.push(key);
}
if (!removed.length) {
return { hash, removed };
}
const remaining = hashParams.toString();
if (!remaining) {
return { hash: "", removed };
}
const prefix = rawHash.startsWith("?") ? "#?" : "#";
return { hash: `${prefix}${remaining}`, removed };
}
function decodeUrlCandidate(value) {
let candidate = value.trim();
for (let pass = 0; pass < MAX_SANITIZE_PASSES; pass += 1) {
let decoded;
try {
decoded = decodeURIComponent(candidate);
} catch {
break;
}
if (decoded === candidate) {
break;
}
candidate = decoded;
}
return candidate;
}
function extractRedirectTarget(url) {
const redirectRules = getMatchingRedirectRules(url);
for (const rule of redirectRules) {
for (const paramName of rule.paramNames || []) {
const rawValue = url.searchParams.get(paramName);
if (!rawValue) {
continue;
}
const candidate = decodeUrlCandidate(rawValue);
let targetUrl;
try {
targetUrl = new URL(candidate, url.origin);
} catch {
continue;
}
if (!/^https?:$/i.test(targetUrl.protocol)) {
continue;
}
const targetUrlString = targetUrl.toString();
if (targetUrlString === url.toString()) {
continue;
}
return {
changed: true,
url: targetUrlString,
removed: [`redirect:${paramName}`],
requiresNavigation: true,
};
}
}
return null;
}
function sanitizeUrlPass(rawUrl, context) {
if (typeof rawUrl !== "string") {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const trimmed = rawUrl.trim();
if (!trimmed || !DIRECT_URL_PATTERN.test(trimmed)) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
let url;
try {
url = new URL(trimmed);
} catch {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
if (!/^https?:$/i.test(url.protocol)) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const redirectResult = extractRedirectTarget(url);
if (redirectResult) {
logDebug("Unwrapped redirect", {
context,
original: rawUrl,
cleaned: redirectResult.url,
removed: redirectResult.removed,
});
return redirectResult;
}
const rules = getMatchingSiteRules(url);
const removed = [];
const searchKeys = Array.from(new Set(url.searchParams.keys()));
for (const key of searchKeys) {
const lowerKey = key.toLowerCase();
if (isPreservedParam(lowerKey)) {
continue;
}
if (!isGlobalTrackingParam(lowerKey) && !isSiteTrackingParam(lowerKey, rules)) {
continue;
}
url.searchParams.delete(key);
pushUnique(removed, key);
}
const originalPathname = url.pathname;
let pathname = originalPathname.replace(/;jsessionid=[^/?#;]*/i, "");
for (const rule of rules) {
if (typeof rule.pathSanitizer === "function") {
const sanitizedPathname = rule.pathSanitizer(pathname);
if (typeof sanitizedPathname === "string") {
pathname = sanitizedPathname || "/";
}
}
}
if (pathname !== originalPathname) {
url.pathname = pathname;
pushUnique(removed, "pathname");
}
const nextHash = sanitizeHash(url.hash);
if (nextHash.hash !== url.hash) {
url.hash = nextHash.hash;
for (const removedHashKey of nextHash.removed) {
pushUnique(removed, `hash:${removedHashKey}`);
}
}
if (!removed.length) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const cleanedUrl = url.toString();
logDebug("Sanitized URL pass", {
context,
original: rawUrl,
cleaned: cleanedUrl,
removed,
});
return {
changed: cleanedUrl !== rawUrl,
url: cleanedUrl,
removed,
requiresNavigation: false,
};
}
function sanitizeUrlString(rawUrl, context) {
if (typeof rawUrl !== "string") {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
let currentUrl = rawUrl.trim();
if (!currentUrl || !DIRECT_URL_PATTERN.test(currentUrl)) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const removed = [];
let requiresNavigation = false;
for (let pass = 0; pass < MAX_SANITIZE_PASSES; pass += 1) {
const passResult = sanitizeUrlPass(currentUrl, `${context || "url"}:pass-${pass + 1}`);
if (!passResult.changed || passResult.url === currentUrl) {
break;
}
currentUrl = passResult.url;
requiresNavigation = requiresNavigation || passResult.requiresNavigation;
for (const removedEntry of passResult.removed) {
pushUnique(removed, removedEntry);
}
}
if (!removed.length || currentUrl === rawUrl) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
logDebug("Sanitized URL", {
context,
original: rawUrl,
cleaned: currentUrl,
removed,
requiresNavigation,
});
return {
changed: true,
url: currentUrl,
removed,
requiresNavigation,
};
}
function toPreferredRelativeUrl(rawUrl, cleanedUrl, baseUrl) {
if (typeof rawUrl !== "string" || !baseUrl) {
return null;
}
let base;
let resolvedUrl;
try {
base = new URL(baseUrl);
resolvedUrl = cleanedUrl instanceof URL ? cleanedUrl : new URL(String(cleanedUrl), base);
} catch {
return null;
}
if (resolvedUrl.origin !== base.origin) {
return null;
}
const trimmedRawUrl = rawUrl.trim();
if (!trimmedRawUrl || DIRECT_URL_PATTERN.test(trimmedRawUrl)) {
return null;
}
if (trimmedRawUrl.startsWith("#") && resolvedUrl.pathname === base.pathname && resolvedUrl.search === base.search) {
return resolvedUrl.hash || "#";
}
if (trimmedRawUrl.startsWith("?") && resolvedUrl.pathname === base.pathname) {
return `${resolvedUrl.search}${resolvedUrl.hash}` || base.pathname;
}
return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
}
function sanitizeResolvableUrl(rawUrl, baseUrl, context) {
if (typeof rawUrl !== "string" && !(rawUrl instanceof URL)) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const rawText = String(rawUrl).trim();
if (!rawText) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
if (DIRECT_URL_PATTERN.test(rawText)) {
return sanitizeUrlString(rawText, context);
}
if (!baseUrl) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
let resolvedUrl;
try {
resolvedUrl = new URL(rawText, baseUrl).toString();
} catch {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const result = sanitizeUrlString(resolvedUrl, context);
if (!result.changed) {
return { changed: false, url: rawUrl, removed: [], requiresNavigation: false };
}
const relativeUrl = toPreferredRelativeUrl(rawText, result.url, baseUrl);
if (!relativeUrl) {
return result;
}
return {
...result,
url: relativeUrl,
};
}
function createEmbeddedUrlPattern() {
return /https?:\/\/[^\s<>"'`\])]+/gi;
}
function replaceEmbeddedUrls(text, context) {
if (typeof text !== "string" || !text) {
return { changed: false, text, removed: [] };
}
let changed = false;
const removed = [];
const replacedText = text.replace(createEmbeddedUrlPattern(), (match) => {
const result = sanitizeUrlString(match, context);
if (!result.changed) {
return match;
}
changed = true;
removed.push(...result.removed);
return result.url;
});
return {
changed,
text: replacedText,
removed,
};
}
function getShareService(url) {
if (/^mailto:$/i.test(url.protocol)) {
return {
urlParams: [],
textParams: ["body"],
};
}
return SHARE_SERVICES.find((service) => {
if (!service.hostPattern.test(url.hostname)) {
return false;
}
if (service.pathPattern && !service.pathPattern.test(url.pathname)) {
return false;
}
return true;
}) || null;
}
function sanitizeShareLauncherUrl(rawUrl, context) {
if (typeof rawUrl !== "string") {
return { changed: false, url: rawUrl, removed: [] };
}
let url;
try {
url = new URL(rawUrl, location.href);
} catch {
return { changed: false, url: rawUrl, removed: [] };
}
const shareService = getShareService(url);
if (!shareService) {
return { changed: false, url: rawUrl, removed: [] };
}
const removed = [];
const urlParams = shareService.urlParams || [];
const textParams = shareService.textParams || [];
for (const paramName of urlParams) {
const value = url.searchParams.get(paramName);
if (!value) {
continue;
}
const result = sanitizeUrlString(value, `${context || "share"}:${paramName}`);
if (!result.changed) {
continue;
}
url.searchParams.set(paramName, result.url);
removed.push(`${paramName}:${result.removed.join(",")}`);
}
for (const paramName of textParams) {
const value = url.searchParams.get(paramName);
if (!value) {
continue;
}
const result = replaceEmbeddedUrls(value, `${context || "share"}:${paramName}`);
if (!result.changed) {
continue;
}
url.searchParams.set(paramName, result.text);
removed.push(`${paramName}:embedded-url`);
}
if (!removed.length) {
return { changed: false, url: rawUrl, removed: [] };
}
const cleanedShareUrl = url.toString();
logDebug("Sanitized share launcher", {
context,
original: rawUrl,
cleaned: cleanedShareUrl,
removed,
});
return {
changed: cleanedShareUrl !== rawUrl,
url: cleanedShareUrl,
removed,
};
}
function cleanCurrentPageUrl(context) {
if (!nativeHistoryReplaceState || isApplyingInternalHistoryCleanup) {
return false;
}
const result = sanitizeUrlString(location.href, context);
if (!result.changed || result.url === location.href) {
return false;
}
if (result.requiresNavigation) {
window.location.replace(result.url);
return true;
}
try {
isApplyingInternalHistoryCleanup = true;
nativeHistoryReplaceState(history.state, "", result.url);
logDebug("Sanitized current page URL", {
context,
original: location.href,
cleaned: result.url,
removed: result.removed,
});
return true;
} catch {
return false;
} finally {
isApplyingInternalHistoryCleanup = false;
}
}
function sanitizeReadonlyUrlField(field, context) {
if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) {
return false;
}
const isReadonlyLike = field.readOnly || field.disabled || field.getAttribute("aria-readonly") === "true";
if (!isReadonlyLike) {
return false;
}
const value = field.value.trim();
if (!DIRECT_URL_PATTERN.test(value)) {
return false;
}
const result = sanitizeUrlString(value, context);
if (!result.changed) {
return false;
}
field.value = result.url;
if (field.hasAttribute("value")) {
field.setAttribute("value", result.url);
}
logDebug("Sanitized readonly URL field", {
context,
original: value,
cleaned: result.url,
removed: result.removed,
});
return true;
}
function getDeepActiveElement(root) {
let activeElement = root && "activeElement" in root ? root.activeElement : null;
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) {
activeElement = activeElement.shadowRoot.activeElement;
}
return activeElement;
}
function clearQueuedShareCleanupPasses() {
if (shareCleanupAnimationFrameId !== null) {
window.cancelAnimationFrame(shareCleanupAnimationFrameId);
shareCleanupAnimationFrameId = null;
}
for (const timeoutId of shareCleanupTimeoutIds) {
window.clearTimeout(timeoutId);
}
shareCleanupTimeoutIds = [];
}
function stopShareCleanupObserver() {
if (!shareCleanupObserver) {
return;
}
shareCleanupObserver.disconnect();
shareCleanupObserver = null;
}
function runShareCleanup(root) {
shareCleanupPending = false;
return sanitizeShareNodes(root);
}
function scheduleDeferredShareCleanup() {
const runIfNeeded = () => {
if (!shareCleanupPending) {
return;
}
shareCleanupPending = runShareCleanup(document);
};
shareCleanupAnimationFrameId = window.requestAnimationFrame(() => {
shareCleanupAnimationFrameId = null;
runIfNeeded();
});
shareCleanupTimeoutIds = SHARE_CLEANUP_PASS_DELAYS.map((delay, index) => window.setTimeout(() => {
runIfNeeded();
if (index === SHARE_CLEANUP_PASS_DELAYS.length - 1) {
stopShareCleanupObserver();
}
}, delay));
}
function queueShareCleanupPasses() {
clearQueuedShareCleanupPasses();
stopShareCleanupObserver();
shareCleanupPending = runShareCleanup(document);
shareCleanupObserver = new MutationObserver((mutations) => {
if (mutations.some((mutation) => mutation.type === "childList" && (mutation.addedNodes.length || mutation.removedNodes.length))) {
shareCleanupPending = true;
}
});
shareCleanupObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
scheduleDeferredShareCleanup();
}
function stripAnchorTrackingAttributes(anchor) {
if (!(anchor instanceof HTMLAnchorElement)) {
return false;
}
let changed = false;
if (anchor.hasAttribute("ping")) {
anchor.removeAttribute("ping");
changed = true;
}
if (isGoogleSearchPage() && anchor.hasAttribute("data-cthref")) {
anchor.removeAttribute("data-cthref");
changed = true;
}
return changed;
}
function sanitizeAnchorHref(anchor, context) {
if (!(anchor instanceof HTMLAnchorElement)) {
return false;
}
const href = anchor.getAttribute("href");
if (!href) {
return false;
}
const result = sanitizeResolvableUrl(href, location.href, context);
if (!result.changed) {
return false;
}
anchor.setAttribute("href", result.url);
return true;
}
function isTextInput(element) {
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
return false;
}
if (element instanceof HTMLTextAreaElement) {
return true;
}
return INPUT_TYPES_WITH_SELECTION.has((element.type || "text").toLowerCase());
}
function getSelectionText() {
const activeElement = getDeepActiveElement(document);
if (isTextInput(activeElement)) {
const start = activeElement.selectionStart;
const end = activeElement.selectionEnd;
if (typeof start === "number" && typeof end === "number" && end > start) {
return activeElement.value.slice(start, end).trim();
}
}
const selection = document.getSelection();
if (!selection) {
return "";
}
return selection.toString().trim();
}
function sanitizeClipboardPayload(text, context) {
if (typeof text !== "string") {
return { changed: false, text, removed: [] };
}
const trimmed = text.trim();
if (DIRECT_URL_PATTERN.test(trimmed)) {
const result = sanitizeUrlString(trimmed, context);
if (result.changed) {
return {
changed: true,
text: result.url,
removed: result.removed,
};
}
}
const embeddedResult = replaceEmbeddedUrls(text, context);
if (!embeddedResult.changed) {
return { changed: false, text, removed: [] };
}
return {
changed: true,
text: embeddedResult.text,
removed: embeddedResult.removed,
};
}
function isTextualClipboardFormat(format) {
const normalizedFormat = String(format || "").toLowerCase();
return normalizedFormat === "text" || normalizedFormat.startsWith("text/");
}
function sanitizeClipboardDataValue(format, value, context) {
if (typeof value !== "string") {
return { changed: false, value, removed: [] };
}
const normalizedFormat = String(format || "").toLowerCase();
if (normalizedFormat === "text/uri-list") {
let changed = false;
const removed = [];
const nextValue = value
.split(/\r?\n/)
.map((line) => {
if (!line || line.startsWith("#")) {
return line;
}
const result = sanitizeClipboardPayload(line, `${context}:uri-list`);
if (!result.changed) {
return line;
}
changed = true;
removed.push(...result.removed);
return result.text;
})
.join("\n");
if (!changed) {
return { changed: false, value, removed: [] };
}
return {
changed: true,
value: nextValue,
removed,
};
}
if (!isTextualClipboardFormat(normalizedFormat)) {
return { changed: false, value, removed: [] };
}
const result = sanitizeClipboardPayload(value, context);
if (!result.changed) {
return { changed: false, value, removed: [] };
}
return {
changed: true,
value: result.text,
removed: result.removed,
};
}
function handleCopy(event) {
if (!event.clipboardData || event.defaultPrevented) {
return;
}
const selectedText = getSelectionText();
if (!selectedText) {
return;
}
const result = sanitizeClipboardPayload(selectedText, "copy-event");
if (!result.changed) {
return;
}
const setClipboardData = nativeDataTransferSetData
? (format, data) => nativeDataTransferSetData.call(event.clipboardData, format, data)
: (format, data) => event.clipboardData.setData(format, data);
setClipboardData("text/plain", result.text);
if (/^https?:\/\//i.test(result.text)) {
setClipboardData("text/uri-list", result.text);
}
event.preventDefault();
logDebug("Sanitized copied URL", {
original: selectedText,
cleaned: result.text,
removed: result.removed,
});
}
function patchClipboardWriteText() {
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
return;
}
// Keep a direct fallback in the userscript context because some sites block the injected page-world bridge with CSP.
tryPatchMethod(navigator.clipboard, "writeText", (originalWriteText) => (text) => {
const result = sanitizeClipboardPayload(text, "clipboard-writeText");
return originalWriteText(result.changed ? result.text : text);
});
}
function patchDataTransferSetData() {
if (typeof DataTransfer === "undefined" || !DataTransfer.prototype || typeof DataTransfer.prototype.setData !== "function") {
return;
}
const originalSetData = DataTransfer.prototype.setData;
const replacement = function (format, data) {
const result = sanitizeClipboardDataValue(format, data, `clipboard.setData:${format || "unknown"}`);
return originalSetData.call(this, format, result.changed ? result.value : data);
};
try {
DataTransfer.prototype.setData = replacement;
} catch {
try {
Object.defineProperty(DataTransfer.prototype, "setData", {
configurable: true,
writable: true,
value: replacement,
});
} catch {
return;
}
}
}
function installPageClipboardSanitizer() {
const bridgeRoot = document.documentElement;
const bridgeState = bridgeRoot ? bridgeRoot.getAttribute(SANITIZE_BRIDGE_INSTALLED_ATTRIBUTE) : null;
if (!bridgeRoot || bridgeState === SANITIZE_BRIDGE_STATE_USERSCRIPT || bridgeState === SANITIZE_BRIDGE_STATE_PAGE) {
return;
}
bridgeRoot.setAttribute(SANITIZE_BRIDGE_INSTALLED_ATTRIBUTE, SANITIZE_BRIDGE_STATE_USERSCRIPT);
document.addEventListener(
SANITIZE_BRIDGE_EVENT,
() => {
const currentBridgeRoot = document.documentElement;
if (!currentBridgeRoot) {
return;
}
const encodedInput = currentBridgeRoot.getAttribute(SANITIZE_BRIDGE_INPUT_ATTRIBUTE);
if (typeof encodedInput !== "string") {
return;
}
const context = currentBridgeRoot.getAttribute(SANITIZE_BRIDGE_CONTEXT_ATTRIBUTE) || "page-clipboard";
let inputText;
try {
inputText = decodeURIComponent(encodedInput);
} catch {
inputText = encodedInput;
}
const result = sanitizeClipboardPayload(inputText, context);
const outputText = result.changed ? result.text : inputText;
currentBridgeRoot.setAttribute(SANITIZE_BRIDGE_OUTPUT_ATTRIBUTE, encodeURIComponent(outputText));
},
true
);
// The injected page-world patch keeps its own tiny helpers because this source runs outside the userscript closure.
injectPageScript(`(() => {
"use strict";
const eventName = ${JSON.stringify(SANITIZE_BRIDGE_EVENT)};
const inputAttribute = ${JSON.stringify(SANITIZE_BRIDGE_INPUT_ATTRIBUTE)};
const outputAttribute = ${JSON.stringify(SANITIZE_BRIDGE_OUTPUT_ATTRIBUTE)};
const contextAttribute = ${JSON.stringify(SANITIZE_BRIDGE_CONTEXT_ATTRIBUTE)};
const markerAttribute = ${JSON.stringify(SANITIZE_BRIDGE_INSTALLED_ATTRIBUTE)};
const markerPageState = ${JSON.stringify(SANITIZE_BRIDGE_STATE_PAGE)};
const bridgeRoot = document.documentElement;
if (!bridgeRoot || bridgeRoot.getAttribute(markerAttribute) === markerPageState) {
return;
}
bridgeRoot.setAttribute(markerAttribute, markerPageState);
const requestSanitizedText = (text, context) => {
if (typeof text !== "string") {
return text;
}
const activeBridgeRoot = document.documentElement;
if (!activeBridgeRoot) {
return text;
}
activeBridgeRoot.setAttribute(inputAttribute, encodeURIComponent(text));
activeBridgeRoot.setAttribute(contextAttribute, context || "page-clipboard");
activeBridgeRoot.removeAttribute(outputAttribute);
document.dispatchEvent(new CustomEvent(eventName));
const encodedOutput = activeBridgeRoot.getAttribute(outputAttribute);
activeBridgeRoot.removeAttribute(inputAttribute);
activeBridgeRoot.removeAttribute(outputAttribute);
activeBridgeRoot.removeAttribute(contextAttribute);
if (typeof encodedOutput !== "string") {
return text;
}
try {
return decodeURIComponent(encodedOutput);
} catch {
return text;
}
};
const tryPatchMethod = (target, methodName, createReplacement) => {
if (!target || typeof target[methodName] !== "function") {
return false;
}
const originalMethod = target[methodName].bind(target);
const replacement = createReplacement(originalMethod);
try {
target[methodName] = replacement;
return target[methodName] === replacement;
} catch {
try {
Object.defineProperty(target, methodName, {
configurable: true,
writable: true,
value: replacement,
});
return true;
} catch {
return false;
}
}
};
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
tryPatchMethod(navigator.clipboard, "writeText", (originalWriteText) => (text) => {
const sanitizedText = requestSanitizedText(text, "page-clipboard.writeText");
return originalWriteText(sanitizedText);
});
}
const isTextualClipboardFormat = (format) => {
const normalizedFormat = String(format || "").toLowerCase();
return normalizedFormat === "text" || normalizedFormat.startsWith("text/");
};
if (navigator.clipboard && typeof navigator.clipboard.write === "function" && typeof ClipboardItem === "function") {
tryPatchMethod(navigator.clipboard, "write", (originalWrite) => async (items) => {
if (!Array.isArray(items)) {
return originalWrite(items);
}
const nextItems = await Promise.all(items.map(async (item) => {
if (!item || typeof item.getType !== "function" || !Array.isArray(item.types) || !item.types.some((type) => isTextualClipboardFormat(type))) {
return item;
}
const itemPayload = {};
let changed = false;
for (const type of item.types) {
let blob = await item.getType(type);
if (isTextualClipboardFormat(type) && blob && typeof blob.text === "function") {
const originalText = await blob.text();
const sanitizedText = requestSanitizedText(originalText, "page-clipboard.write:" + type);
if (sanitizedText !== originalText) {
blob = new Blob([sanitizedText], { type });
changed = true;
}
}
itemPayload[type] = blob;
}
return changed ? new ClipboardItem(itemPayload) : item;
}));
return originalWrite(nextItems);
});
}
})();`);
}
function patchNavigatorShare() {
if (typeof navigator.share !== "function") {
return;
}
tryPatchMethod(navigator, "share", (originalShare) => (shareData) => {
if (!shareData || typeof shareData !== "object") {
return originalShare(shareData);
}
const nextShareData = { ...shareData };
if (typeof nextShareData.url === "string") {
const result = sanitizeUrlString(nextShareData.url, "navigator.share:url");
if (result.changed) {
nextShareData.url = result.url;
}
}
if (typeof nextShareData.text === "string") {
const result = replaceEmbeddedUrls(nextShareData.text, "navigator.share:text");
if (result.changed) {
nextShareData.text = result.text;
}
}
return originalShare(nextShareData);
});
}
function patchWindowOpen() {
if (typeof window.open !== "function") {
return;
}
tryPatchMethod(window, "open", (originalOpen) => (url, target, features) => {
if (typeof url !== "string") {
return originalOpen(url, target, features);
}
const trackingResult = sanitizeResolvableUrl(url, location.href, "window.open:tracking");
const sanitizedUrl = trackingResult.changed ? trackingResult.url : url;
const shareResult = sanitizeShareLauncherUrl(sanitizedUrl, "window.open:share");
return originalOpen(shareResult.changed ? shareResult.url : sanitizedUrl, target, features);
});
}
function patchHistoryState() {
if (nativeHistoryPushState) {
tryPatchMethod(history, "pushState", (originalPushState) => (state, title, url) => {
if (isApplyingInternalHistoryCleanup || typeof url === "undefined" || url === null) {
return originalPushState(state, title, url);
}
const result = sanitizeResolvableUrl(url, location.href, "history.pushState");
return originalPushState(state, title, result.changed ? result.url : url);
});
}
if (nativeHistoryReplaceState) {
tryPatchMethod(history, "replaceState", (originalReplaceState) => (state, title, url) => {
if (isApplyingInternalHistoryCleanup || typeof url === "undefined" || url === null) {
return originalReplaceState(state, title, url);
}
const result = sanitizeResolvableUrl(url, location.href, "history.replaceState");
return originalReplaceState(state, title, result.changed ? result.url : url);
});
}
}
function sanitizeShareAttributes(element) {
if (!(element instanceof Element)) {
return false;
}
if (!isShareishElement(element)) {
return false;
}
let changed = false;
for (const attributeName of SHARE_DATA_ATTRIBUTES) {
const value = element.getAttribute(attributeName);
if (!value) {
continue;
}
const result = sanitizeUrlString(value, `${attributeName}`);
if (!result.changed) {
continue;
}
element.setAttribute(attributeName, result.url);
changed = true;
}
return changed;
}
function sanitizeShareAnchor(anchor) {
if (!(anchor instanceof HTMLAnchorElement)) {
return false;
}
const href = anchor.getAttribute("href");
if (!href) {
return false;
}
const result = sanitizeShareLauncherUrl(href, "anchor-href:share");
if (!result.changed) {
return false;
}
anchor.setAttribute("href", result.url);
return true;
}
function sanitizeInteractiveAnchor(anchor) {
let changed = false;
changed = stripAnchorTrackingAttributes(anchor) || changed;
changed = sanitizeAnchorHref(anchor, "anchor-href:tracking") || changed;
changed = sanitizeShareAnchor(anchor) || changed;
return changed;
}
function sanitizeShareNodes(root) {
if (!(root instanceof Element || root instanceof Document)) {
return false;
}
let changed = false;
const anchors = root.querySelectorAll ? root.querySelectorAll("a[href]") : [];
for (const anchor of anchors) {
changed = sanitizeInteractiveAnchor(anchor) || changed;
}
const attributeSelector = SHARE_DATA_ATTRIBUTES.map((name) => `[${name}]`).join(",");
const candidates = root.querySelectorAll ? root.querySelectorAll(attributeSelector) : [];
for (const candidate of candidates) {
changed = sanitizeShareAttributes(candidate) || changed;
}
const readonlyFieldSelector = [
"input[readonly]",
"input[disabled]",
"input[aria-readonly='true']",
"textarea[readonly]",
"textarea[disabled]",
"textarea[aria-readonly='true']",
].join(",");
const readonlyFields = [];
if (root instanceof Element && root.matches(readonlyFieldSelector)) {
readonlyFields.push(root);
}
if (root.querySelectorAll) {
readonlyFields.push(...root.querySelectorAll(readonlyFieldSelector));
}
for (const field of readonlyFields) {
changed = sanitizeReadonlyUrlField(field, "share-field") || changed;
}
return changed;
}
function injectPageScript(source) {
const script = document.createElement("script");
script.textContent = source;
const parent = document.documentElement || document.head || document.body;
if (!parent) {
return;
}
parent.appendChild(script);
script.remove();
}
function installGoogleAntiRewrite() {
if (!isGoogleSearchPage()) {
return;
}
injectPageScript(`(() => {
"use strict";
const allowClick = () => true;
try {
const descriptor = Object.getOwnPropertyDescriptor(window, "rwt");
if (!descriptor || descriptor.configurable) {
Object.defineProperty(window, "rwt", {
configurable: false,
enumerable: false,
writable: false,
value: allowClick,
});
}
} catch (error) {
console.debug("[url-cleaner] Failed to neutralize Google rwt", error);
}
})();`);
}
function handlePotentialAnchorInteraction(event) {
if (!(event.target instanceof Element)) {
return;
}
const anchor = event.target.closest("a[href]");
if (!anchor) {
return;
}
sanitizeInteractiveAnchor(anchor);
}
function handlePotentialShareInteraction(event) {
if (!(event.target instanceof Element)) {
return;
}
const shareishControl = event.target.closest("button, a, [role='button'], [aria-label], [title], [data-action], [data-testid]");
if (shareishControl && isShareishElement(shareishControl)) {
queueShareCleanupPasses();
}
const attributeSelector = SHARE_DATA_ATTRIBUTES.map((name) => `[${name}]`).join(",");
const shareTarget = event.target.closest(attributeSelector);
if (shareTarget) {
sanitizeShareAttributes(shareTarget);
}
}
function startDomShareCleanup() {
document.addEventListener(
"DOMContentLoaded",
() => {
sanitizeShareNodes(document);
},
{ once: true }
);
}
document.addEventListener("copy", handleCopy, true);
document.addEventListener("mouseover", handlePotentialAnchorInteraction, true);
document.addEventListener("focusin", handlePotentialAnchorInteraction, true);
document.addEventListener("pointerdown", handlePotentialShareInteraction, true);
document.addEventListener("click", handlePotentialShareInteraction, true);
window.addEventListener("popstate", () => {
cleanCurrentPageUrl("popstate");
}, true);
window.addEventListener("hashchange", () => {
cleanCurrentPageUrl("hashchange");
}, true);
patchClipboardWriteText();
patchDataTransferSetData();
installPageClipboardSanitizer();
patchNavigatorShare();
patchWindowOpen();
patchHistoryState();
installGoogleAntiRewrite();
cleanCurrentPageUrl("startup");
startDomShareCleanup();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment