// ==UserScript== // @name 去除链接重定向 // @author Meriel // @description 能原地解析的链接绝不在后台访问,去除重定向的过程快速且高效,平均时间在0.02ms~0.05ms之间。几乎没有任何在后台访问网页获取去重链接的操作,一切都在原地进行,对速度精益求精。去除网页内链接的重定向,具有高准确性和高稳定性,以及相比同类插件更低的时间占用。 // @version 2.1.3 // @namespace Violentmonkey Scripts // @update 2024-07-10 // @grant GM.xmlHttpRequest // @match *://www.baidu.com/* // @match *://tieba.baidu.com/* // @match *://v.baidu.com/* // @match *://xueshu.baidu.com/* // @include *://www.google* // @match *://www.google.com/* // @match *://docs.google.com/* // @match *://mail.google.com/* // @match *://play.google.com/* // @match *://www.youtube.com/* // @match *://encrypted.google.com/* // @match *://www.so.com/* // @match *://www.zhihu.com/* // @match *://daily.zhihu.com/* // @match *://zhuanlan.zhihu.com/* // @match *://weibo.com/* // @match *://twitter.com/* // @match *://www.sogou.com/* // @match *://juejin.im/* // @match *://juejin.cn/* // @match *://mail.qq.com/* // @match *://addons.mozilla.org/* // @match *://www.jianshu.com/* // @match *://www.douban.com/* // @match *://getpocket.com/* // @match *://51.ruyo.net/* // @match *://steamcommunity.com/* // @match *://blog.csdn.net/* // @match *://*.blog.csdn.net/* // @match *://*.oschina.net/* // @match *://app.yinxiang.com/* // @match *://www.logonews.cn/* // @match *://afdian.net/* // @match *://blog.51cto.com/* // @match *://xie.infoq.cn/* // @match *://gitee.com/* // @match *://sspai.com/* // @match *://*.bing.com/* // @connect * // @supportURL https://github.com/MerielVaren/remove-link-redirects/issues/new/choose // @homepage https://github.com/MerielVaren/remove-link-redirects // @run-at document-start // @namespace https://greasyfork.org/zh-CN/users/876245-meriel-varen // @license MIT // @downloadURL none // ==/UserScript== (() => { class App { constructor() { this.registeredProviders = []; this.mutationObserver = new MutationObserver((mutations) => { mutations.forEach(this.handleMutation.bind(this)); }); } /** * 处理变动 * @param mutation * @returns * */ handleMutation(mutation) { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node instanceof HTMLAnchorElement) { this.handleNode(node); } }); } } /** * 处理节点 * @param node * @returns */ handleNode(node) { for (const provider of this.registeredProviders) { if (this.isMatchProvider(node, provider)) { provider.resolve(node); break; } } } /** * A 标签是否匹配服务提供者 * @param element * @param provider */ isMatchProvider(element, provider) { if (element.getAttribute(Marker.RedirectStatusDone)) { return false; } if (provider.linkTest instanceof RegExp && !provider.linkTest.test(element.href)) { return false; } if (typeof provider.linkTest === "function" && !provider.linkTest(element)) { return false; } if (provider.linkTest instanceof Boolean) { return provider.linkTest; } return true; } /** * 当页面准备就绪时,进行初始化动作 */ async pageOnReady() { for (const provider of this.registeredProviders) { if (provider.onInit) { await provider.onInit(); } } } /** * 注册服务提供者 * @param providers */ registerProvider(providers) { for (const provider of providers) { if (provider.urlTest === false) { continue; } if (provider.urlTest instanceof RegExp && !provider.urlTest.test(location.hostname)) { continue; } if (typeof provider.urlTest === "function" && !provider.urlTest() === false) { continue; } this.registeredProviders.push(provider); } return this; } /** * 启动应用 */ bootstrap() { addEventListener("DOMContentLoaded", this.pageOnReady.bind(this)); this.mutationObserver.observe(document.documentElement, { childList: true, subtree: true, }); } } const Marker = { RedirectStatusDone: "redirect-status-done", }; /** * 根据url上的路径匹配,去除重定向 * @param {HTMLAnchorElement} element * @param {RegExp} tester * @returns {boolean} */ function matchLinkFromUrl(element, tester) { const match = tester.exec(element.href); if (!match || !match[1]) return ""; try { return decodeURIComponent(match[1]); } catch { return /https?:\/\//.test(match[1]) ? match[1] : ""; } } /** * 重试异步操作 * @param {() => Promise} operation * @param {number} maxRetries * @param {number} currentRetry */ async function retryAsyncOperation(operation, maxRetries, currentRetry = 0) { try { // 尝试执行操作 return await operation(); } catch (err) { if (currentRetry < maxRetries) { // 如果当前重试次数小于最大重试次数,等待一段时间后重试 await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒 return retryAsyncOperation(operation, maxRetries, currentRetry + 1); } // 如果重试次数用尽,抛出错误 throw err; } } /** * 去除重定向 * @param element A标签元素 * @param realUrl 真实的地址 * @param options */ function removeLinkRedirect(element, realUrl, options = {}) { if (options.force || (realUrl && element.href !== realUrl)) { element.setAttribute("redirect-status-done", element.href); element.href = realUrl; } } /** * 监听URL变化 */ function monitorUrlChange() { function urlChange(event) { const destinationUrl = event?.destination?.url || ""; if (destinationUrl.startsWith("about:blank")) return; const href = destinationUrl || location.href; if (href !== location.href) { location.href = href; } } unsafeWindow?.navigation?.addEventListener("navigate", urlChange); unsafeWindow.addEventListener("replaceState", urlChange); unsafeWindow.addEventListener("pushState", urlChange); unsafeWindow.addEventListener("popState", urlChange); unsafeWindow.addEventListener("hashchange", urlChange); } const providers = [ { name: "如有乐享", urlTest: /51\.ruyo\.net/, linkTest: /\/[^\?]*\?u=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("u")); }, }, { name: "Mozilla", urlTest: /addons\.mozilla\.org/, linkTest: /outgoing\.prod\.mozaws\.net\/v\d\/\w+\/(.*)/, resolve: function (element) { removeLinkRedirect(element, matchLinkFromUrl(element, this.linkTest)); }, }, { name: "爱发电", urlTest: /afdian\.net/, linkTest: /afdian\.net\/link\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, { name: "印象笔记", urlTest: /app\.yinxiang\.com/, linkTest: /^http:\/\//, resolve: function (element) { if (element.hasAttribute("data-mce-href")) { if (!element.onclick) { removeLinkRedirect(element, element.href, { force: true }); element.onclick = (e) => { // 阻止事件冒泡, 因为上层元素绑定的click事件会重定向 if (e.stopPropagation) { e.stopPropagation(); } element.setAttribute("target", "_blank"); window.top ? window.top.open(element.href) : window.open(element.href); }; } } // 分享页面 else if (/^https:\/\/app\.yinxiang\.com\/OutboundRedirect\.action\?dest=/.test(element.href)) { removeLinkRedirect(element, new URL(element.href).searchParams.get("dest")); } }, onInit: async function () { const handler = (e) => { const dom = e.target; const tagName = dom.tagName.toUpperCase(); switch (tagName) { case "A": { this.resolve(dom); break; } case "IFRAME": { if (dom.hasAttribute("redirect-link-removed")) { return; } dom.setAttribute("redirect-link-removed", "1"); dom.contentWindow.document.addEventListener("mouseover", handler); break; } } }; document.addEventListener("mouseover", handler); }, }, { name: "Bing", urlTest: /bing\.com/, linkTest: /.+\.bing\.com\/ck\/a\?.*&u=a1(.*)&ntb=1/, textDecoder: new TextDecoder("utf-8"), resolve: function (element) { removeLinkRedirect( element, this.textDecoder.decode( Uint8Array.from( Array.from( atob( element.href .split("&u=a1")[1] .split("&ntb=1")[0] .replace(/[-_]/g, (e) => ("-" === e ? "+" : "/")) .replace(/[^A-Za-z0-9\\+\\/]/g, ""), ), ).map((e) => e.charCodeAt(0)), ), ), ); }, }, { name: "51CTO博客", urlTest: /blog\.51cto\.com/, linkTest: true, resolve: function (element) { this.container = document.querySelector(".article-detail"); if (this.container?.contains(element)) { if (!element.onclick && element.href) { element.onclick = function removeLinkRedirectOnClickFn(e) { e.stopPropagation(); e.preventDefault(); e.stopImmediatePropagation(); const $a = document.createElement("a"); $a.href = element.href; $a.target = element.target; $a.click(); }; } } }, }, { name: "CSDN", urlTest: /blog\.csdn\.net/, linkTest: /^https?:\/\//, resolve: function (element) { this.container = document.querySelector("#content_views"); if (this.container?.contains(element)) { if (!element.onclick && element.origin !== window.location.origin) { removeLinkRedirect(element, element.href, { force: true }); element.onclick = (e) => { // 阻止事件冒泡, 因为上层元素绑定的click事件会重定向 if (e.stopPropagation) { e.stopPropagation(); } element.setAttribute("target", "_blank"); }; } } }, }, { name: "知乎日报", urlTest: /daily\.zhihu\.com/, linkTest: /zhihu\.com\/\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, { name: "Google Docs", urlTest: /docs\.google\.com/, linkTest: /www\.google\.com\/url\?q=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("q")); }, }, { name: "Pocket", urlTest: /getpocket\.com/, linkTest: /getpocket\.com\/redirect\?url=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("url")); }, }, { name: "Gitee", urlTest: /gitee\.com/, linkTest: /gitee\.com\/link\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, { name: "InfoQ", urlTest: /infoq\.cn/, linkTest: /infoq\.cn\/link\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, { name: "掘金", urlTest: /juejin\.(im|cn)/, linkTest: /link\.juejin\.(im|cn)\/\?target=(.*)/, resolve: function (element) { const finalURL = new URL(element.href).searchParams.get("target"); removeLinkRedirect(element, finalURL); if (this.linkTest.test(element.title)) { element.title = finalURL; } }, }, { name: "QQ邮箱", urlTest: /mail\.qq\.com/, linkTest: true, resolve: function (element) { this.container = document.querySelector("#contentDiv"); if (this.container?.contains(element)) { if (element.onclick) { element.onclick = (e) => { // 阻止事件冒泡, 因为上层元素绑定的click事件会重定向 if (e.stopPropagation) { e.stopPropagation(); } }; } } }, }, { name: "OS China", urlTest: /oschina\.net/, linkTest: /oschina\.net\/action\/GoToLink\?url=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("url")); }, }, { name: "Google Play", urlTest: /play\.google\.com/, linkTest: function (element) { if (/google\.com\/url\?q=(.*)/.test(element.href)) { return true; } else if (/^\/store\/apps\/details/.test(location.pathname)) { return true; } return false; }, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("q")); // 移除开发者栏目下的重定向 const eles = [].slice.call(document.querySelectorAll("a.hrTbp")); for (const ele of eles) { if (!ele.href || ele.getAttribute(Marker.RedirectStatusDone)) { continue; } ele.setAttribute(Marker.RedirectStatusDone, ele.href); ele.setAttribute("target", "_blank"); ele.addEventListener( "click", (event) => { event.stopPropagation(); }, true, ); } }, }, { name: "少数派", urlTest: /sspai\.com/, linkTest: /sspai\.com\/link\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, { name: "Steam Community", urlTest: /steamcommunity\.com/, linkTest: /steamcommunity\.com\/linkfilter\/\?url=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("url")); }, }, { name: "百度贴吧", urlTest: /tieba\.baidu\.com/, linkTest: /jump\d*\.bdimg\.com/, resolve: function (element) { if (!this.test.test(element.href)) { return; } let url = ""; const text = element.innerText || element.textContent || ""; try { if (/https?:\/\//.test(text)) { url = decodeURIComponent(text); } } catch (e) { url = /https?:\/\//.test(text) ? text : ""; } if (url) { removeLinkRedirect(element, url); } }, }, { name: "Twitter", urlTest: /twitter\.com/, linkTest: /t\.co\/\w+/, resolve: function (element) { if (!this.linkTest.test(element.href)) { return; } if (/https?:\/\//.test(element.title)) { const url = decodeURIComponent(element.title); removeLinkRedirect(element, url); return; } const innerText = element.innerText.replace(/…$/, ""); if (/https?:\/\//.test(innerText)) { removeLinkRedirect(element, innerText); return; } }, }, { name: "百度视频", urlTest: /v\.baidu\.com/, linkTest: /v\.baidu\.com\/link\?url=/, resolve: function (element) { GM.xmlHttpRequest({ method: "GET", url: element.href, anonymous: true, onload: (res) => { if (res.finalUrl) { removeLinkRedirect(element, res.finalUrl); } }, onerror: (err) => { console.error(err); }, }); }, }, { name: "微博", urlTest: /\.weibo\.com/, linkTest: /t\.cn\/\w+/, resolve: function (element) { if (!(this.linkTest.test(element.href) && /^https?:\/\//.test(element.title))) { return; } const url = decodeURIComponent(element.title); if (url) { element.href = url; removeLinkRedirect(element, url); } }, }, { name: "百度搜索", urlTest: /www\.baidu\.com/, linkTest: /www\.baidu\.com\/link\?url=/, unresolvable: ["nourl.ubs.baidu.com", "lightapp.baidu.com"], handleOneElement: (element) => { retryAsyncOperation(async () => { const res = await GM.xmlHttpRequest({ method: "GET", url: element.href, anonymous: true, }); if (res.finalUrl) { removeLinkRedirect(element, res.finalUrl); } }, 3); }, resolve: async function (element) { const url = element.closest(".cos-row") ? null : element.closest(".c-container")?.getAttribute("mu"); if (url && url !== "null" && url !== "undefined" && !this.unresolvable.some((u) => url.includes(u))) { removeLinkRedirect(element, url); } else { this.handleOneElement(element); } }, onInit: async function () { monitorUrlChange(); }, }, { name: "豆瓣", urlTest: /douban\.com/, linkTest: /douban\.com\/link2\/?\?url=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("url")); }, }, { name: "Google搜索", urlTest: /\w+\.google\./, linkTest: true, resolve: function (element) { const traceProperties = ["ping", "data-jsarwt", "data-usg", "data-ved"]; // 移除追踪 for (const property of traceProperties) { if (element.getAttribute(property)) { element.removeAttribute(property); } } // 移除多余的事件 if (element.getAttribute("onmousedown")) { element.removeAttribute("onmousedown"); } // 尝试去除重定向 if (element.getAttribute("data-href")) { const realUrl = element.getAttribute("data-href"); removeLinkRedirect(element, realUrl); } const url = new URL(element.href); if (url.searchParams.get("url")) { removeLinkRedirect(element, url.searchParams.get("url")); } }, }, { name: "简书", urlTest: /www\.jianshu\.com/, linkTest: function (element) { const isLink1 = /links\.jianshu\.com\/go/.test(element.href); const isLink2 = /link\.jianshu\.com(\/)?\?t=/.test(element.href); const isLink3 = /jianshu\.com\/go-wild\/?\?(.*)url=/.test(element.href); if (isLink1 || isLink2 || isLink3) { return true; } return false; }, resolve: function (element) { const search = new URL(element.href).searchParams; removeLinkRedirect(element, search.get("to") || search.get("t") || search.get("url")); }, }, { name: "标志情报局", urlTest: /www\.logonews\.cn/, linkTest: /link\.logonews\.cn\/\?url=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("url")); }, }, { name: "360搜索", urlTest: /www\.so\.com/, linkTest: /so\.com\/link\?(.*)/, resolve: function (element) { const url = element.getAttribute("data-mdurl") || element.getAttribute("e-landurl"); if (url) { removeLinkRedirect(element, url); } // remove track element.removeAttribute("e_href"); element.removeAttribute("data-res"); }, }, { name: "搜狗搜索", urlTest: /www\.sogou\.com/, linkTest: /www\.sogou\.com\/link\?url=/, resolve: function (element) { // 从这个a往上找到的第一个class=vrwrap的元素 const vrwrap = element.closest(".vrwrap"); // 往子孙找到的第一个class包含r-sech的元素 const rSech = vrwrap.querySelector(".r-sech"); const url = rSech.getAttribute("data-url"); removeLinkRedirect(element, url); }, }, { name: "Youtube", urlTest: /www\.youtube\.com/, linkTest: /www\.youtube\.com\/redirect\?.{1,}/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("q")); }, }, { name: "知乎", urlTest: /www\.zhihu\.com/, linkTest: /zhihu\.com\/\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, { name: "百度学术", urlTest: /xueshu\.baidu\.com/, linkTest: /xueshu\.baidu\.com\/s?\?(.*)/, resolve: function (element) { const realHref = element.getAttribute("data-link") || element.getAttribute("data-url"); if (realHref) { removeLinkRedirect(element, decodeURIComponent(realHref)); } }, }, { name: "知乎专栏", urlTest: /zhuanlan\.zhihu\.com/, linkTest: /link\.zhihu\.com\/\?target=(.*)/, resolve: function (element) { removeLinkRedirect(element, new URL(element.href).searchParams.get("target")); }, }, ]; const app = new App(); app.registerProvider(providers).bootstrap(); })();