open the link directly

点击链接直接跳转

// ==UserScript==
// @name         open the link directly
// @namespace    http://tampermonkey.net/
// @version      0.1.11
// @description  点击链接直接跳转
// @author       nediiii
// @match        *://*.csdn.net/*
// @match        *://*.gitee.com/*
// @match        *://*.uisdc.com/*
// @match        *://*.uiiiuiii.com/*
// @match        *://*.logonews.cn/*
// @match        *://*.afdian.net/*
// @match        *://*.tianyancha.com/*
// @match        *://*.oschina.net/*
// @match        *://*.pixiv.net/*
// @match        *://*.jianshu.com/*
// @match        *://*.juejin.cn/*
// @match        *://*.weibo.cn/*
// @match        *://*.weibo.com/*
// @match        *://*.yuque.com/*
// @match        *://*.segmentfault.com/*
// @match        *://*.zhihu.com/*
// @match        *://*.bookmarkearth.com/*
// @match        *://*.leetcode-cn.com/*
// @match        *://*.huaban.com/*
// @match        *://*.t.cn/*
// @match        *://*.weibo.cn/*
// @match        *://*.leetcode.cn/*
// @run-at       document-start
// @license      GPLv3 License
// @icon         https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    // 保存原始方法
    unsafeWindow.__otld_open = unsafeWindow.open;
    // 重写 open 方法
    var myopen = function (url, name, features) {
        console.log({ url }, { name }, { features });
        // debugger;
        return unsafeWindow.__otld_open(url, name, features);
    }
    // 屏蔽 JS 中对原生函数 native 属性的检测
    var _myopen = myopen.bind(null);
    _myopen.toString = unsafeWindow.__otld_open.toString;
    Object.defineProperty(unsafeWindow, 'open', {
        value: _myopen
    });

    const getValidURL = (url) => {
        try {
            let u = new URL(url);
            return u;
        } catch (error) {
            return getValidURLWithBase(url);
        }
    }

    const getValidURLWithBase = (url) => {
        try {
            let u = new URL(url, getCurrentURLBase());
            return u;
        } catch (error) {
            return null;
        }
    }

    const isHttpProtocol = (url) => {
        return url.protocol == 'http:' || url.protocol == 'https:';
    }

    const getCurrentURLBase = () => {
        return window.location.origin;
    }

    const urlReg = /\bhttps?:\/\/\S+/gi;

    const weiboResolver = async (href) => {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: href,
                onload: function (response) {
                    console.log({ response });
                    if (response.status != 200) {
                        reject(href);
                        return;
                    }

                    // weibo跳转会区分目标网址是否备案
                    // 未备案的, 中转
                    // 已备案的, 直跳
                    let realURI = href;
                    if (response.finalUrl === href) {
                        // 未备案, 中转网址
                        // 网址在html里
                        let doc = new DOMParser().parseFromString(response.responseText, "text/html");
                        let node = doc.querySelector('body > div > div:nth-child(2)');
                        let str = node.innerText;
                        console.log({ doc });
                        console.log({ node });
                        console.log({ str });

                        let extractURL = str.match(urlReg);
                        console.log({ extractURL });
                        if (extractURL) {
                            realURI = extractURL[0];
                        }
                    }
                    else {
                        // 已备案
                        // 网址在finalUrl里
                        let extractURL = response.finalUrl.match(urlReg);
                        if (extractURL) {
                            realURI = extractURL[0];
                        }
                    }

                    resolve(realURI)
                },
                onerror: function (error) {
                    console.log({ error });
                    reject(href)
                }
            });
        });
    }

    const segfaultResolver = async (href) => {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: href,
                onload: function (response) {
                    console.log({ response });
                    if (response.status == 200) {
                        let doc = new DOMParser().parseFromString(response.responseText, "text/html");
                        let node = doc.querySelector('body > p')
                        let str = node.innerText;
                        console.log({ doc });
                        console.log({ node });
                        console.log({ str });

                        let realURI = href;
                        let extractURL = str.match(urlReg);
                        console.log({ extractURL });
                        if (extractURL) {
                            realURI = extractURL[0];
                        }
                        resolve(realURI)
                    } else {
                        reject(href)
                    }
                },
                onerror: function (error) {
                    console.log({ error });
                    reject(href)
                }
            });
        });
    }

    const bmeResolver = async (href) => {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: href,
                onload: function (response) {
                    console.log({ response });
                    if (response.status == 200) {
                        let doc = new DOMParser().parseFromString(response.responseText, "text/html");
                        let node = doc.querySelector('body > div.row.box > div.col-lg-6.jump-box > div > div.content > p.link');
                        //querySelector('body > div > div > div').attributes['data-url'].value
                        let str = doc.querySelector('body > div > div > div').attributes['data-url'].value;
                        console.log({ doc });
                        console.log({ node });
                        console.log({ str });

                        let realURI = href;
                        let extractURL = str.match(urlReg);
                        console.log({ extractURL });
                        if (extractURL) {
                            realURI = extractURL[0];
                        }
                        resolve(realURI)
                    } else {
                        reject(href)
                    }
                },
                onerror: function (error) {
                    console.log({ error });
                    reject(href)
                }
            });
        });
    }

    const huabanResolver = async (href) => {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: href,
                onload: function (response) {
                    console.log({ response });
                    if (response.status == 200) {
                        let doc = new DOMParser().parseFromString(response.responseText, "text/html");
                        let node = doc.querySelector('body > script');
                        let str = node.innerText;
                        const obj = JSON.parse(str);
                        console.log({ doc });
                        console.log({ node });
                        console.log({ str });
                        console.log({ obj });

                        str = obj.props.pageProps.data.link;

                        let realURI = href;
                        let extractURL = str.match(urlReg);
                        console.log({ extractURL });
                        if (extractURL) {
                            realURI = extractURL[0];
                        }
                        resolve(realURI)
                    } else {
                        reject(href)
                    }
                },
                onerror: function (error) {
                    console.log({ error });
                    reject(href)
                }
            });
        });
    }

    const patter_match = {

        // 注意这里的pattern需要去看对应网站dom里的a标签的实际herf值, console也会打印日志, 可以自己添加正则来增加网站支持

        // https://zhuanlan.zhihu.com/p/23333042
        // https://link.zhihu.com/?target=https%3A//getkap.co/
        // https://link.zhihu.com/?target=https%3A//greasyfork.org/en/scripts/5029-yet-another-%25E8%2587%25AA%25E5%258F%25A4cb%25E5%2587%25BA%25E8%25AF%2584%25E8%25AE%25BA-sharing-plugin
        zhihu: { pattern: /https?:\/\/link\.zhihu\.com\/?\?target=(.+)$/ },

        // https://www.jianshu.com/p/a6a63a0c6e53
        // https://links.jianshu.com/go?to=https%3A%2F%2Fnpm.taobao.org%2Fmirrors%2Felectron
        jianshu2: { pattern: /https?:\/\/links\.jianshu\.com\/go\?to=(.+)$/ },

        // https://www.jianshu.com/p/a3af2fb4d99e
        // https://link.jianshu.com/?t=http://www.ihie.org/john-kansky
        // https://www.jianshu.com/go-wild?ac=2&url=http%3A%2F%2Fwww.ihie.org%2Fjohn-kansky
        jianshu3: { pattern: /https?:\/\/link\.jianshu\.com\/\?t=(.+)$/ },
        jianshu4: { pattern: /https?:\/\/www\.jianshu\.com\/go-wild\?ac=2&url=(.+)$/ },

        // href="https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.aliyun.com%2Fgroup%2Falisoftwaretech%2F"
        juejin: { pattern: /https?:\/\/link\.juejin\.cn\/?\?target=(.+)$/ },

        // https://gitee.com/meetqy/acss-dnd
        // href="https://gitee.com/link?target=https%3A%2F%2Fcuyang.me%2Facss-dnd%2F"
        // https://blog.gitee.com/2022/01/20/lowcodetools/
        // https://link.juejin.cn/?target=https%3A%2F%2Flink.zhihu.com%2F%3Ftarget%3Dhttps%253A%2F%2Fgitee.com%2Fnocobase%2Fnocobase
        gitee: { pattern: /https?:\/\/gitee\.com\/link\?target=(.+)$/ },

        // https://www.uisdc.com/build-b-end-grid-system
        // https://link.uisdc.com/?redirect=https%3A%2F%2Fuxdesign.cc%2Fresponsive-grids-and-how-to-actually-use-them-970de4c16e01
        uisdc: { pattern: /https?:\/\/link\.uisdc\.com\/?\?redirect=(.+)$/ },

        // https://www.logonews.cn/apple-sues-sex-topic-blog-for-logo-infringement.html
        // https://link.logonews.cn/?url=http://aasd.k12.wi.us/district
        logonews: { pattern: /https?:\/\/link\.logonews\.cn\/?\?url=(.+)$/ },

        // https://afdian.net/@AdventCirno
        // https://afdian.net/link?target=https%3A%2F%2Fwww.patreon.com%2Fuser%3Fu%3D6139561
        afdian: { pattern: /https?:\/\/afdian\.net\/link\?target=(.+)$/ },

        // https://www.pixiv.net/users/25237
        // href="/jump.php?url=https%3A%2F%2Ftwitter.com%2Fomiya_io"
        // href="/jump.php?https%3A%2F%2Finstagram.com%2Fsnatti89%2F"
        pixiv: { pattern: /https?:\/\/www\.pixiv\.net\/jump\.php\?(?:url=)?(.+)$/ },

        // https://my.oschina.net/androiddevs/blog/5496556
        // https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fmp.weixin.qq.com%2Fmp%2Fappmsgalbum%3F__biz%3DMzk0NDIwMTExNw%3D%3D%26action%3Dgetalbum%26album_id%3D1879128471667326981%23wechat_redirect
        // href="https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fmp.weixin.qq.com%2Fmp%2Fappmsgalbum%3F__biz%3DMzk0NDIwMTExNw%3D%3D%26action%3Dgetalbum%26album_id%3D1879128471667326981%23wechat_redirect"
        oschina: { pattern: /https?:\/\/www\.oschina\.net\/action\/GoToLink\?url=(.+)$/ },


        // https://uiiiuiii.com/software/491152.html
        // https://link.uiiiuiii.com/?redirect=https%3A%2F%2Fwww.behance.net%2Fgallery%2F117044741%2FHARSHITA-FREE-SIGNATURE-FONT%3Ftracking_source%3Dfor_you_feed_recommended
        uiiiuiii: { pattern: /https?:\/\/link\.uiiiuiii\.com\/?\?redirect=(.+)$/ },

        // https://leetcode-cn.com/circle/discuss/mL0gxC/
        // https://leetcode-cn.com/link/?target=http%3A%2F%2Fwww.bytedance.com
        leetcodecn: { pattern: /https?:\/\/leetcode-cn\.com\/link\/\?target=(.+)$/ },

        // https://leetcode.cn/link/?target=http%3A%2F%2Fwww.bytedance.com
        leetcodecn2: { pattern: /https?:\/\/leetcode\.cn\/link\/\?target=(.+)$/ },

        // https://www.tianyancha.com/company/28723141
        // href="https://ss.knet.cn/verifyseal.dll?sn=e18042711010873571xsuv000000&pa=111332"
        // https://www.tianyancha.com/security?target=https%3A%2F%2Fss.knet.cn%2Fverifyseal.dll%3Fsn%3De18042711010873571xsuv000000%26pa%3D111332
        tianyancha: { pattern: /https?:\/\/www\.tianyancha\.com\/security\?target=(.+)$/ },

        // https://huaban.com/pins/5095905933
        // https://huaban.com/go?pin_id=4614749616
        // https://huaban.com/go?pin_id=4614750040&url=item.taobao.com
        huaban: { pattern: /https?:\/\/huaban\.com\/go\?pin_id=(.+)$/, resolver: huabanResolver },

        // https://weibo.cn/sinaurl?u=https%3A%2F%2Fwww.freebsd.org%2F
        // https://weibo.cn/sinaurl?toasturl=https%3A%2F%2Ftime.geekbang.org%2F
        // https://weibo.cn/sinaurl?luicode=10000011&lfid=230259&u=http%3A%2F%2Ft.cn%2FA6qHeVlf
        // https://weibo.cn/sinaurl?f=w&u=http%3A%2F%2Ft.cn%2FA66XY2gI&ep=LlAsNz3HD%2C1683963007%2CLlpkandl6%2C7276218544
        // href="https://weibo.cn/sinaurl?f=w&u=http%3A%2F%2Ft.cn%2FA66XY2gI&ep=LlAsNz3HD%2C1683963007%2CLlpkandl6%2C7276218544"
        weibo: { pattern: /https?:\/\/weibo\.cn\/sinaurl\?(?:toasturl|u|f=w&u|ep)=(.+)$/, resolver: weiboResolver },

        // http://t.cn/A66926Pm  未备案的, 跳转到中转网址,  response.finalUrl仍然还是http://t.cn/A66926Pm 目标网址出现在response.responseText里
        // http://t.cn/A669K964  已备案的, 直接跳转到目标网址, 出现在response.finalUrl里
        weibo2: { pattern: /(https?:\/\/t\.cn\/.+)$/, resolver: weiboResolver },


        // segmentfault对链接进行加密处理, 不知道如何decode, 所以只能写一个函数去单独处理
        // https://link.segmentfault.com/?enc=LZyRulLABKpXOHl2vbA%2F4w%3D%3D.MWhFMvjhyBk1ReIRoGxyxa0VxGtg%2Foyk0DMtfzZTJoKbsgoJFtGCPHe8%2BZ1HbRdcvNsGaVfll9oGQXLsZCHK7w%3D%3D
        // https://segmentfault.com/a/1190000017434150
        segfault: { pattern: /https?:\/\/link\.segmentfault\.com\/?\?enc=(.+)$/, resolver: segfaultResolver },

        // https://www.bookmarkearth.com/detail/097c687c98974691b2174bc1e85103d4
        // https://show.bookmarkearth.com/view/801
        // https://www.bookmarkearth.com/view/801
        bookmarkearth: { pattern: /(https?:\/\/show\.bookmarkearth\.com\/view\/.+)$/, resolver: bmeResolver },
        bookmarkearth2: { pattern: /(https?:\/\/www\.bookmarkearth\.com\/view\/.+)$/, resolver: bmeResolver },

        // 以下网站a标签的herf未修改, 推测是js做的弹窗, 所以不需要匹配, 也匹配不出来
        // csdn https://link.csdn.net/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FRegExp
        // 语雀 https://www.yuque.com/r/goto?url=https%3A%2F%2Fwww.canva.cn%2F

        // https://blog.csdn.net/weixin_41010294/article/details/85289852
        csdn: { host: 'csdn.net' },
        // https://link.csdn.net/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FRegExp
        csdn2: { pattern: /https?:\/\/link\.csdn\.net\/\?target=(.+)$/ },

        // https://www.yuque.com/yuque/gpvawt/fuu6h3
        yuque: { host: 'yuque.com' },
    }

    const matchHostResolver = (url, host) => {
        // 1. 需要当前网站与host匹配
        if (!window.location.host.includes(host)) {
            return false;
        }
        // 2. url为外链
        if (url.host.includes(host)) {
            return false;
        }

        return true;
    }

    const getMatchPattern = (url) => {
        for (let i in patter_match) {
            // console.log("url:", url, "patter_match[i]:", patter_match[i])
            if (patter_match[i].hasOwnProperty('host') && matchHostResolver(url, patter_match[i].host)) {
                return patter_match[i];
            }
            if (patter_match[i].hasOwnProperty('pattern') && url.href.match(patter_match[i].pattern)) {
                return patter_match[i];
            }
        }
        return null;
    }

    const resolveRealURI = async (href) => {
        // TODO 兼容套娃链接, 如 https://link.juejin.cn/?target=https%3A%2F%2Flink.zhihu.com%2F%3Ftarget%3Dhttps%253A%2F%2Fgitee.com%2Fnocobase%2Fnocobase
        const fallbackURI = href;

        for (let i in patter_match) {
            const matcher = href.match(patter_match[i].pattern);
            if (!matcher) {
                continue;
            }
            console.log({ matcher });

            if (patter_match[i].hasOwnProperty('resolver')) {
                // complex customize resolver
                console.log(patter_match[i].resolver)
                let realURI = await patter_match[i].resolver(href);
                return realURI;
            }

            const encodeURI = matcher[1];
            // simple reg resolver
            return decodeURIComponent(encodeURI);
        }
        return fallbackURI;
    }

    const patternResolve = async (patter_match, href) => {

        if (patter_match.hasOwnProperty('resolver')) {
            // complex customize resolver
            console.log(patter_match.resolver)
            let realURI = await patter_match.resolver(href);
            return realURI;
        }

        if (patter_match.hasOwnProperty('pattern')) {
            const matcher = href.match(patter_match.pattern);
            if (!matcher) { return href; }
            const encodeURI = matcher[1];
            return decodeURIComponent(encodeURI);
        }

        return href;
    }

    const getAnchorElement = (e) => {
        let target = e.target;
        while (target) {
            if (target.tagName.toLowerCase() === 'a' && target.hasAttribute('href')) {
                return target;
            }
            target = target.parentElement;
        }
        return null;
    }

    /// 主体函数
    /// link: 原始链接
    /// e: 事件对象 
    /// handleEventAfterMatch: 匹配到规则后的事件处理
    /// handleRealURI: 处理真实链接
    /// errCallback: 错误回调
    const otld = (link, e, handleEventAfterMatch, handleRealURI, errCallback) => {
        let url = getValidURL(link);
        if (url === null || !isHttpProtocol(url)) {
            return;
        }

        console.log({ url });

        const matchPattern = getMatchPattern(url);
        console.log({ matchPattern });
        if (!matchPattern) {
            console.log("不匹配处理规则, 如有误, 请反馈给我, 非常感谢");
            return;
        }

        handleEventAfterMatch && handleEventAfterMatch(e);

        const target = (() => {
            // csdn 打开新标签页兼容处理
            if (matchPattern == patter_match.csdn) {
                return '_blank';
            }

            if (!e || !getAnchorElement(e)) {
                return '_self';
            }

            let anchor = getAnchorElement(e);
            if (anchor.hasAttribute('target')) {
                return anchor.getAttribute('target');
            }
            return '_self';
        })();

        patternResolve(matchPattern, url.href).then((realURI) => {
            console.log({ realURI });
            handleRealURI(realURI, target);
        }).catch(() => { errCallback && errCallback(target); });
    }


    document.addEventListener('click', (e) => {
        console.log({ e })

        // 找到a标签
        let anchor = getAnchorElement(e);
        if (!anchor) {
            return;
        }

        console.log({ anchor })
        let href = anchor.getAttribute('href');
        if (!href) {
            return;
        }

        const stopEvent = (e) => {
            if (!e) {
                return;
            }

            e.preventDefault();
            e.stopPropagation();
        }

        otld(href, e, stopEvent, (realURI, target) => { window.open(realURI, target) }, (target) => { window.open(href, target); });

    }, { capture: true })

    /// 处理手动输入或者鼠标拖动的链接
    otld(window.location.href, null, null, (realURI, target) => { window.location.replace(realURI); }, null);

})();