Don't track me Google

Removes the annoying link-conversion at Google Search/maps/...

// ==UserScript==
// @name Don't track me Google
// @namespace Rob W
// @description Removes the annoying link-conversion at Google Search/maps/...
// @version 4.28
// @icon https://raw.githubusercontent.com/Rob--W/dont-track-me-google/master/icon48.png
// @supportURL https://github.com/Rob--W/dont-track-me-google/issues
// @license MIT
// @run-at document-start
// @match *://*.google.com/*
// @match *://*.google.ad/*
// @match *://*.google.ae/*
// @match *://*.google.com.af/*
// @match *://*.google.com.ag/*
// @match *://*.google.com.ai/*
// @match *://*.google.al/*
// @match *://*.google.am/*
// @match *://*.google.co.ao/*
// @match *://*.google.com.ar/*
// @match *://*.google.as/*
// @match *://*.google.at/*
// @match *://*.google.com.au/*
// @match *://*.google.az/*
// @match *://*.google.ba/*
// @match *://*.google.com.bd/*
// @match *://*.google.be/*
// @match *://*.google.bf/*
// @match *://*.google.bg/*
// @match *://*.google.com.bh/*
// @match *://*.google.bi/*
// @match *://*.google.bj/*
// @match *://*.google.com.bn/*
// @match *://*.google.com.bo/*
// @match *://*.google.com.br/*
// @match *://*.google.bs/*
// @match *://*.google.bt/*
// @match *://*.google.co.bw/*
// @match *://*.google.by/*
// @match *://*.google.com.bz/*
// @match *://*.google.ca/*
// @match *://*.google.cd/*
// @match *://*.google.cf/*
// @match *://*.google.cg/*
// @match *://*.google.ch/*
// @match *://*.google.ci/*
// @match *://*.google.co.ck/*
// @match *://*.google.cl/*
// @match *://*.google.cm/*
// @match *://*.google.cn/*
// @match *://*.google.com.co/*
// @match *://*.google.co.cr/*
// @match *://*.google.com.cu/*
// @match *://*.google.cv/*
// @match *://*.google.com.cy/*
// @match *://*.google.cz/*
// @match *://*.google.de/*
// @match *://*.google.dj/*
// @match *://*.google.dk/*
// @match *://*.google.dm/*
// @match *://*.google.com.do/*
// @match *://*.google.dz/*
// @match *://*.google.com.ec/*
// @match *://*.google.ee/*
// @match *://*.google.com.eg/*
// @match *://*.google.es/*
// @match *://*.google.com.et/*
// @match *://*.google.fi/*
// @match *://*.google.com.fj/*
// @match *://*.google.fm/*
// @match *://*.google.fr/*
// @match *://*.google.ga/*
// @match *://*.google.ge/*
// @match *://*.google.gg/*
// @match *://*.google.com.gh/*
// @match *://*.google.com.gi/*
// @match *://*.google.gl/*
// @match *://*.google.gm/*
// @match *://*.google.gp/*
// @match *://*.google.gr/*
// @match *://*.google.com.gt/*
// @match *://*.google.gy/*
// @match *://*.google.com.hk/*
// @match *://*.google.hn/*
// @match *://*.google.hr/*
// @match *://*.google.ht/*
// @match *://*.google.hu/*
// @match *://*.google.co.id/*
// @match *://*.google.ie/*
// @match *://*.google.co.il/*
// @match *://*.google.im/*
// @match *://*.google.co.in/*
// @match *://*.google.iq/*
// @match *://*.google.is/*
// @match *://*.google.it/*
// @match *://*.google.je/*
// @match *://*.google.com.jm/*
// @match *://*.google.jo/*
// @match *://*.google.co.jp/*
// @match *://*.google.co.ke/*
// @match *://*.google.com.kh/*
// @match *://*.google.ki/*
// @match *://*.google.kg/*
// @match *://*.google.co.kr/*
// @match *://*.google.com.kw/*
// @match *://*.google.kz/*
// @match *://*.google.la/*
// @match *://*.google.com.lb/*
// @match *://*.google.li/*
// @match *://*.google.lk/*
// @match *://*.google.co.ls/*
// @match *://*.google.lt/*
// @match *://*.google.lu/*
// @match *://*.google.lv/*
// @match *://*.google.com.ly/*
// @match *://*.google.co.ma/*
// @match *://*.google.md/*
// @match *://*.google.me/*
// @match *://*.google.mg/*
// @match *://*.google.mk/*
// @match *://*.google.ml/*
// @match *://*.google.com.mm/*
// @match *://*.google.mn/*
// @match *://*.google.ms/*
// @match *://*.google.com.mt/*
// @match *://*.google.mu/*
// @match *://*.google.mv/*
// @match *://*.google.mw/*
// @match *://*.google.com.mx/*
// @match *://*.google.com.my/*
// @match *://*.google.co.mz/*
// @match *://*.google.com.na/*
// @match *://*.google.com.nf/*
// @match *://*.google.com.ng/*
// @match *://*.google.com.ni/*
// @match *://*.google.ne/*
// @match *://*.google.nl/*
// @match *://*.google.no/*
// @match *://*.google.com.np/*
// @match *://*.google.nr/*
// @match *://*.google.nu/*
// @match *://*.google.co.nz/*
// @match *://*.google.com.om/*
// @match *://*.google.com.pa/*
// @match *://*.google.com.pe/*
// @match *://*.google.com.pg/*
// @match *://*.google.com.ph/*
// @match *://*.google.com.pk/*
// @match *://*.google.pl/*
// @match *://*.google.pn/*
// @match *://*.google.com.pr/*
// @match *://*.google.ps/*
// @match *://*.google.pt/*
// @match *://*.google.com.py/*
// @match *://*.google.com.qa/*
// @match *://*.google.ro/*
// @match *://*.google.ru/*
// @match *://*.google.rw/*
// @match *://*.google.com.sa/*
// @match *://*.google.com.sb/*
// @match *://*.google.sc/*
// @match *://*.google.se/*
// @match *://*.google.com.sg/*
// @match *://*.google.sh/*
// @match *://*.google.si/*
// @match *://*.google.sk/*
// @match *://*.google.com.sl/*
// @match *://*.google.sn/*
// @match *://*.google.so/*
// @match *://*.google.sm/*
// @match *://*.google.sr/*
// @match *://*.google.st/*
// @match *://*.google.com.sv/*
// @match *://*.google.td/*
// @match *://*.google.tg/*
// @match *://*.google.co.th/*
// @match *://*.google.com.tj/*
// @match *://*.google.tk/*
// @match *://*.google.tl/*
// @match *://*.google.tm/*
// @match *://*.google.tn/*
// @match *://*.google.to/*
// @match *://*.google.com.tr/*
// @match *://*.google.tt/*
// @match *://*.google.com.tw/*
// @match *://*.google.co.tz/*
// @match *://*.google.com.ua/*
// @match *://*.google.co.ug/*
// @match *://*.google.co.uk/*
// @match *://*.google.com.uy/*
// @match *://*.google.co.uz/*
// @match *://*.google.com.vc/*
// @match *://*.google.co.ve/*
// @match *://*.google.vg/*
// @match *://*.google.co.vi/*
// @match *://*.google.com.vn/*
// @match *://*.google.vu/*
// @match *://*.google.ws/*
// @match *://*.google.rs/*
// @match *://*.google.co.za/*
// @match *://*.google.co.zm/*
// @match *://*.google.co.zw/*
// @match *://*.google.cat/*
// @match *://*.google.ng/*
// ==/UserScript==

document.addEventListener('mousedown', handlePointerPress, true);
document.addEventListener('touchstart', handlePointerPress, true);
document.addEventListener('click', handleClick, true);
var scriptCspNonce;
var needsCspNonce = typeof browser !== 'undefined'; // Firefox.
var preferenceObservers = [];
setupAggresiveUglyLinkPreventer();

var forceNoReferrer = true;
var noping = true;
if (typeof chrome == 'object' && chrome.storage) {
    (chrome.storage.sync || chrome.storage.local).get({
        forceNoReferrer: true,
        // From version 4.7 until 4.11, the preference was the literal value of
        // the referrer policy.
        referrerPolicy: 'no-referrer',
        noping: true,
    }, function(items) {
        if (items) {
            // Migration code (to be removed in the future).
            if (items.referrerPolicy === '') {
                // User explicitly allowed referrers to be sent, respect that.
                items.forceNoReferrer = false;
            }
            forceNoReferrer = items.forceNoReferrer;
            noping = items.noping;
            callPreferenceObservers();
        }
    });
    chrome.storage.onChanged.addListener(function(changes) {
        if (changes.forceNoReferrer) {
            forceNoReferrer = changes.forceNoReferrer.newValue;
        }
        if (changes.noping) {
            noping = changes.noping.newValue;
        }
        callPreferenceObservers();
    });
}

function callImmediatelyAndOnPreferenceUpdate(callback) {
    callback();
    preferenceObservers.push(callback);
}
function callPreferenceObservers() {
    // This method is usually once, and occasionally more than once if the user
    // changes a preference. For simplicity we don't check whether a pref was
    // changed before calling a callback - these are cheap anyway.
    preferenceObservers.forEach(function(callback) {
        callback();
    });
}

function getReferrerPolicy() {
    return forceNoReferrer ? 'origin' : '';
}

function updateReferrerPolicy(a) {
    if (a.referrerPolicy === 'no-referrer') {
        // "no-referrer" is more privacy-friendly than "origin".
        return;
    }
    var referrerPolicy = getReferrerPolicy();
    if (referrerPolicy) {
        a.referrerPolicy = referrerPolicy;
    }
}

function handlePointerPress(e) {
    var a = e.target;
    while (a && !a.href) {
        a = a.parentElement;
    }
    if (!a) {
        return;
    }
    var inlineMousedown = a.getAttribute('onmousedown');
    // return rwt(....); // E.g Google search results.
    // return google.rwt(...); // E.g. sponsored search results
    // return google.arwt(this); // E.g. sponsored search results (dec 2016).
    if (inlineMousedown && /\ba?rwt\(/.test(inlineMousedown)) {
        a.removeAttribute('onmousedown');
        // Just in case:
        a.removeAttribute('ping');
        // In Chrome, removing onmousedown during event dispatch does not
        // prevent the inline listener from running... So we have to cancel
        // event propagation just in case.
        e.stopImmediatePropagation();
    }
    if (noping) {
        a.removeAttribute('ping');
    }
    var realLink = getRealLinkFromGoogleUrl(a);
    if (realLink) {
        a.href = realLink;
        // Sometimes, two fixups are needed, on old mobile user agents:
        // /url?q=https://googleweblight.com/fp?u=... -> ...
        realLink = getRealLinkFromGoogleUrl(a);
        if (realLink) {
            a.href = realLink;
        }
    }
    updateReferrerPolicy(a);

    if (e.eventPhase === Event.CAPTURING_PHASE) {
        // Our event listener runs first, to sanitize the link.
        // But the page may have an event handler that modifies the link again.
        // We can append a listener to the bubbling phase of the (current)
        // event dispatch to fix the link up again, provided that the page did
        // not call stopPropagation() or stopImmediatePropagation().
        var eventOptions = { capture: false, once: true };
        a.addEventListener(e.type, handlePointerPress, eventOptions);
        document.addEventListener(e.type, handlePointerPress, eventOptions);
    }
}

// This is specifically designed for catching clicks in Gmail.
// Gmail binds a click handler to a <div> and cancels the event after opening
// a window with an ugly URL. It uses a blank window + meta refresh in Firefox,
// which is too crazy to patch. So we just make sure that the browser's default
// click handler is activated (=open link in new tab).
// The entry point for this crazy stuff is shown in my comment at
// https://github.com/Rob--W/dont-track-me-google/issues/2
function handleClick(e) {
    if (e.button !== 0) {
        return;
    }
    var a = e.target;
    while (a && !a.href) {
        a = a.parentElement;
    }
    if (!a) {
        return;
    }
    if (a.dataset && a.dataset.url) {
        var realLink = getSanitizedIntentUrl(a.dataset.url);
        if (realLink) {
            a.dataset.url = realLink;
        }
    }
    if (!location.hostname.startsWith('mail.')) {
        // This hack was designed for Gmail, but broke other Google sites:
        // - https://github.com/Rob--W/dont-track-me-google/issues/6
        // - https://github.com/Rob--W/dont-track-me-google/issues/19
        // So let's disable it for every domain except Gmail.
        return;
    }
    // TODO: Consider using a.baseURI instead of location in case Gmail ever
    // starts using <base href>?
    if (a.origin === location.origin) {
        // Same-origin link.
        // E.g. an in-page navigation at Google Docs (#...)
        // or an attachment at Gmail (https://mail.google.com/mail/u/0?ui=2&...)
        return;
    }
    if (a.protocol !== 'http:' &&
        a.protocol !== 'https:' &&
        a.protocol !== 'ftp:') {
        // Be conservative and don't block too much. E.g. Gmail has special
        // handling for mailto:-URLs, and using stopPropagation now would
        // cause mailto:-links to be opened by the platform's default mailto
        // handler instead of Gmail's handler (=open in new window).
        return;
    }
    if (a.target === '_blank') {
        e.stopPropagation();
        updateReferrerPolicy(a);
    }
}

/**
 * @param {URL|HTMLHyperlinkElementUtils} a
 * @returns {String} the real URL if the given link is a Google redirect URL.
 */
function getRealLinkFromGoogleUrl(a) {
    if (a.protocol !== 'https:' && a.protocol !== 'http:') {
        return;
    }
    var url;
    if ((a.hostname === location.hostname || a.hostname === 'www.google.com') &&
        (a.pathname === '/url' || a.pathname === '/local_url' ||
         a.pathname === '/searchurl/rr.html' ||
         a.pathname === '/linkredirect')) {
        // Google Maps / Dito (/local_url?q=<url>)
        // Mobile (/url?q=<url>)
        // Google Meet's chat (/linkredirect?authuser=0&dest=<url>)
        url = /[?&](?:q|url|dest)=((?:https?|ftp)[%:][^&]+)/.exec(a.search);
        if (url) {
            return decodeURIComponent(url[1]);
        }
        // Help pages, e.g. safe browsing (/url?...&q=%2Fsupport%2Fanswer...)
        url = /[?&](?:q|url)=((?:%2[Ff]|\/)[^&]+)/.exec(a.search);
        if (url) {
            return a.origin + decodeURIComponent(url[1]);
        }
        // Redirect pages for Android intents (/searchurl/rr.html#...&url=...)
        // rr.html only supports http(s). So restrict to http(s) only.
        url = /[#&]url=(https?[:%][^&]+)/.exec(a.hash);
        if (url) {
            return decodeURIComponent(url[1]);
        }
    }
    // Google Search with old mobile UA (e.g. Firefox 41).
    if (a.hostname === 'googleweblight.com' && a.pathname === '/fp') {
        url = /[?&]u=((?:https?|ftp)[%:][^&]+)/.exec(a.search);
        if (url) {
            return decodeURIComponent(url[1]);
        }
    }
}

/**
 * @param {string} intentUrl
 * @returns {string|undefined} The sanitized intent:-URL if it was an intent URL
 *   with embedded tracking link.
 */
function getSanitizedIntentUrl(intentUrl) {
    if (!intentUrl.startsWith('intent:')) {
        return;
    }
    // https://developer.chrome.com/multidevice/android/intents#syntax
    var BROWSER_FALLBACK_URL = ';S.browser_fallback_url=';
    var indexStart = intentUrl.indexOf(BROWSER_FALLBACK_URL);
    if (indexStart === -1) {
        return;
    }
    indexStart += BROWSER_FALLBACK_URL.length;
    var indexEnd = intentUrl.indexOf(';', indexStart);
    indexEnd = indexEnd === -1 ? intentUrl.length : indexEnd;

    var url = decodeURIComponent(intentUrl.substring(indexStart, indexEnd));
    var realUrl = getRealLinkFromGoogleUrl(newURL(url));
    if (!realUrl) {
        return;
    }
    return intentUrl.substring(0, indexStart) +
        encodeURIComponent(realUrl) +
        intentUrl.substring(indexEnd);
}

/**
 * Intercept the .href setter in the page so that the page can never change the
 * URL to a tracking URL. Just intercepting mousedown/touchstart is not enough
 * because e.g. on Google Maps, the page rewrites the URL in the contextmenu
 * event at the bubbling event stage and then stops the event propagation. So
 * there is no event-driven way to fix the URL. The DOMAttrModified event could
 * be used, but the event is deprecated, so not a viable long-term solution.
 */
function setupAggresiveUglyLinkPreventer() {
    // This content script runs as document_start, so we can have some assurance
    // that the methods in the page are reliable.
    var s = document.createElement('script');
    if (getScriptCspNonce()) {
        s.setAttribute('nonce', scriptCspNonce);
    } else if (document.readyState !== 'complete' && needsCspNonce) {
        // In Firefox, a page's CSP is enforced for content scripts, so we need
        // to wait for the document to be loaded (we may be at document_start)
        // and find a fitting CSP nonce.
        findScriptCspNonce(setupAggresiveUglyLinkPreventer);
        return;
    }
    s.textContent = '(' + function(getRealLinkFromGoogleUrl) {
        var proto = HTMLAnchorElement.prototype;
        // The link target can be changed in many ways, but let's only consider
        // the .href attribute since it's probably the only used setter.
        var hrefProp = Object.getOwnPropertyDescriptor(proto, 'href');
        var hrefGet = Function.prototype.call.bind(hrefProp.get);
        var hrefSet = Function.prototype.call.bind(hrefProp.set);

        Object.defineProperty(proto, 'href', {
            configurable: true,
            enumerable: true,
            get() {
                return hrefGet(this);
            },
            set(v) {
                hrefSet(this, v);
                try {
                    v = getRealLinkFromGoogleUrl(this);
                    if (v) {
                        hrefSet(this, v);
                    }
                } catch (e) {
                    // Not expected to happen, but don't break the setter if for
                    // some reason the (hostile) page broke the link APIs.
                }
                updateReferrerPolicy(this);
            },
        });
        function replaceAMethod(methodName, methodFunc) {
            // Overwrite the methods without triggering setters, because that
            // may inadvertently overwrite the prototype, as observed in
            // https://github.com/Rob--W/dont-track-me-google/issues/52#issuecomment-1596207655
            Object.defineProperty(proto, methodName, {
                configurable: true,
                // All methods that we are overriding are not part of
                // HTMLAnchorElement.prototype, but inherit.
                enumerable: false,
                writable: true,
                value: methodFunc,
            });
        }

        // proto inherits Element.prototype.setAttribute:
        var setAttribute = Function.prototype.call.bind(proto.setAttribute);
        replaceAMethod('setAttribute', function(name, value) {
            // Attribute names are not case-sensitive, but weird capitalizations
            // are unlikely, so only check all-lowercase and all-uppercase.
            if (name === 'href' || name === 'HREF') {
                this.href = value;
            } else {
                setAttribute(this, name, value);
            }
        });

        // proto inherits EventTarget.prototype.dispatchEvent:
        var aDispatchEvent = Function.prototype.apply.bind(proto.dispatchEvent);
        replaceAMethod('dispatchEvent', function() {
            updateReferrerPolicy(this);
            return aDispatchEvent(this, arguments);
        });

        // proto inherits HTMLElement.prototype.click:
        var aClick = Function.prototype.apply.bind(proto.click);
        replaceAMethod('click', function() {
            updateReferrerPolicy(this);
            return aClick(this, arguments);
        });

        var rpProp = Object.getOwnPropertyDescriptor(proto, 'referrerPolicy');
        var rpGet = Function.prototype.call.bind(rpProp.get);
        var rpSet = Function.prototype.call.bind(rpProp.set);

        var currentScript = document.currentScript;
        var getReferrerPolicy = Object.getOwnPropertyDescriptor(
            HTMLScriptElement.prototype,
            'referrerPolicy'
        ).get.bind(currentScript);

        function updateReferrerPolicy(a) {
            try {
                if (rpGet(a) === 'no-referrer') {
                    // "no-referrer" is more privacy-friendly than "origin".
                    return;
                }
                var referrerPolicy = getReferrerPolicy();
                if (referrerPolicy) {
                    rpSet(a, referrerPolicy);
                }
            } catch (e) {
                // Not expected to happen, but don't break callers if it happens
                // anyway.
            }
        }
        currentScript.dataset.jsEnabled = 1;
    } + ')(' + getRealLinkFromGoogleUrl + ');';
    callImmediatelyAndOnPreferenceUpdate(function forceNoReferrerChanged() {
        // Send the desired referrerPolicy value to the injected script.
        s.referrerPolicy = getReferrerPolicy();
    });
    (document.head || document.documentElement).appendChild(s);
    s.remove();
    if (!s.dataset.jsEnabled) {
        cleanLinksWhenJsIsDisabled();
        if (!needsCspNonce) {
            needsCspNonce = true;
            // This is not Firefox, but the script was blocked. Perhaps a CSP
            // nonce is needed anyway.
            findScriptCspNonce(function() {
                if (scriptCspNonce) {
                    setupAggresiveUglyLinkPreventer();
                }
            });
        }
    } else {
        // Scripts enabled (not blocked by CSP), run other inline scripts.
        blockTrackingBeacons();
        overwriteWindowOpen();

        if (location.hostname === 'docs.google.com') {
            // Google Docs have simple non-JS interfaces where the ugly links
            // are hard-coded in the HTML. Remove them (#51).
            // https://docs.google.com/document/d/.../mobilebasic
            // https://docs.google.com/spreadsheets/d/.../htmlview
            cleanLinksWhenJsIsDisabled();
        }
    }
}

// Block sendBeacon requests with destination /gen_204, because Google
// asynchronously sends beacon requests in response to mouse events on links:
// https://github.com/Rob--W/dont-track-me-google/issues/20
//
// This implementation also blocks other forms of tracking via gen_204 as a side
// effect. That is not fully intentional, but given the lack of obvious ways to
// discern such link-tracking events from others, I will block all of them.
function blockTrackingBeacons() {
    var s = document.createElement('script');
    if (getScriptCspNonce()) {
        s.setAttribute('nonce', scriptCspNonce);
    }
    s.textContent = '(' + function() {
        var navProto = window.Navigator.prototype;
        var navProtoSendBeacon = navProto.sendBeacon;
        if (!navProtoSendBeacon) {
            return;
        }
        var sendBeacon = Function.prototype.apply.bind(navProtoSendBeacon);

        // Blocks the following:
        //   gen_204
        //   /gen_204
        //   https://www.google.com/gen_204
        var isTrackingUrl = RegExp.prototype.test.bind(
            /^(?:(?:https?:\/\/[^\/]+)?\/)?gen_204(?:[?#]|$)/);

        navProto.sendBeacon = function(url, data) {
            if (isTrackingUrl(url) && isNoPingEnabled()) {
                // Lie that the data has been transmitted to avoid fallbacks.
                return true;
            }
            return sendBeacon(this, arguments);
        };

        var currentScript = document.currentScript;
        var getElementId = Object.getOwnPropertyDescriptor(
            Element.prototype,
            'id'
        ).get.bind(currentScript);
        function isNoPingEnabled() {
            try {
                return getElementId() !== '_dtmg_do_not_touch_ping';
            } catch (e) {
                return true;
            }
        }
    } + ')();';
    callImmediatelyAndOnPreferenceUpdate(function nopingChanged() {
        // Send the noping value to the injected script. The "id" property is
        // mirrored and can have an arbitrary (string) value, so we use that:
        s.id = noping ? '' : '_dtmg_do_not_touch_ping';
    });
    (document.head || document.documentElement).appendChild(s);
    s.remove();
}

// Google sometimes uses window.open() to open ugly links.
// https://github.com/Rob--W/dont-track-me-google/issues/18
// https://github.com/Rob--W/dont-track-me-google/issues/41
function overwriteWindowOpen() {
    var s = document.createElement('script');
    if (getScriptCspNonce()) {
        s.setAttribute('nonce', scriptCspNonce);
    }
    s.textContent = '(' + function() {
        var open = window.open;
        window.open = function(url, windowName, windowFeatures) {
            var isBlankUrl = !url || url === "about:blank";
            try {
                if (!isBlankUrl) {
                    var a = document.createElement('a');
                    // Triggers getRealLinkFromGoogleUrl via the href setter in
                    // setupAggresiveUglyLinkPreventer.
                    a.href = url;
                    url = a.href;
                    // The origin check exists to avoid adding "noreferrer" to
                    // same-origin popups. That implies noopener and causes
                    // https://github.com/Rob--W/dont-track-me-google/issues/43
                    // And allow any Google domain to support auth popups:
                    // https://github.com/Rob--W/dont-track-me-google/issues/45
                    // And don't bother editing the list if it already contains
                    // "opener" (it would be disabled by "noreferrer").
                    if (a.referrerPolicy && a.origin !== location.origin &&
                        !/\.google\.([a-z]+)$/.test(a.hostname) &&
                        !/\bopener|noreferrer/.test(windowFeatures)) {
                        if (windowFeatures) {
                            windowFeatures += ',';
                        } else {
                            windowFeatures = '';
                        }
                        windowFeatures += 'noreferrer';
                    }
                }
            } catch (e) {
                // Not expected to happen, but don't break callers if it does.
            }
            var win = open(url, windowName, windowFeatures);
            try {
                if (isBlankUrl && win) {
                    // In Google Docs, sometimes a blank document is opened,
                    // and document.write is used to insert a redirector.
                    // https://github.com/Rob--W/dont-track-me-google/issues/41
                    var doc = win.document;
                    var docWrite = win.Function.prototype.call.bind(doc.write);
                    doc.write = function(markup) {
                        try {
                            markup = fixupDocMarkup(markup);
                        } catch (e) {
                            // Not expected, but don't break callers otherwise.
                        }
                        return docWrite(this, markup);
                    };
                }
            } catch (e) {
                // Not expected to happen, but don't break callers if it does.
            }
            return win;
        };
        function fixupDocMarkup(html) {
            html = html || '';
            html += '';
            return html.replace(
                /<meta [^>]*http-equiv=(["']?)refresh\1[^>]*>/i,
                function(m) {
                    var doc = new DOMParser().parseFromString(m, 'text/html');
                    var meta = doc.querySelector('meta[http-equiv=refresh]');
                    return meta && fixupMetaUrl(meta) || m;
                });
        }
        function fixupMetaUrl(meta) {
            var parts = /^(\d*;\s*url=)(.+)$/i.exec(meta.content);
            if (!parts) {
                return;
            }
            var metaPrefix = parts[1];
            var url = parts[2];
            var a = document.createElement('a');
            // Triggers getRealLinkFromGoogleUrl via the href setter in
            // setupAggresiveUglyLinkPreventer.
            a.href = url;
            url = a.href;
            meta.content = metaPrefix + url;

            var html = meta.outerHTML;
            if (a.referrerPolicy) {
                // Google appears to already append the no-referrer
                // meta tag, but add one just in case it doesn't.
                html = '<meta name="referrer" content="no-referrer">' + html;
            }
            return html;
        }
    } + ')();';
    (document.head || document.documentElement).appendChild(s);
    s.remove();
}

function cleanLinksWhenJsIsDisabled() {
    // When JavaScript is disabled, Google sets the "href" attribute's value to
    // an ugly URL. Although the link is rewritten on click, we still need to
    // rewrite the link even earlier because otherwise the ugly URL is shown in
    // the tooltip upon hover.

    if (document.readyState == 'complete') {
        cleanAllLinks();
        return;
    }

    // When JS is disabled, the links won't change after the document finishes
    // loading. Until the DOM has finished loading, use the mouseover event to
    // beautify links (the DOMContentLoaded may be delayed on slow networks).
    document.addEventListener('mouseover', handleMouseOver);
    document.addEventListener('DOMContentLoaded', function() {
        document.removeEventListener('mouseover', handleMouseOver);
        cleanAllLinks();
    }, {once: true});

    function cleanAllLinks() {
        var as = document.querySelectorAll('a[href]');
        for (var i = 0; i < as.length; ++i) {
            var href = getRealLinkFromGoogleUrl(as[i]);
            if (href) {
                as[i].href = href;
            }
        }
    }

    function handleMouseOver(e) {
        var a = e.target;
        var href = a.href && getRealLinkFromGoogleUrl(a);
        if (href) {
            a.href = href;
        }
    }
}

function getScriptCspNonce() {
    var scripts = document.querySelectorAll('script[nonce]');
    for (var i = 0; i < scripts.length && !scriptCspNonce; ++i) {
        scriptCspNonce = scripts[i].nonce;
    }
    return scriptCspNonce;
}

function findScriptCspNonce(callback) {
    var timer;
    function checkDOM() {
        if (getScriptCspNonce() || document.readyState === 'complete') {
            document.removeEventListener('DOMContentLoaded', checkDOM, true);
            if (timer) {
                clearTimeout(timer);
            }
            callback();
            return;
        }
        timer = setTimeout(checkDOM, 50);
    }
    document.addEventListener('DOMContentLoaded', checkDOM, true);
    checkDOM();
}

function newURL(href) {
    try {
        return new URL(href);
    } catch (e) {
        var a = document.createElement('a');
        a.href = href;
        return a;
    }
}