URL Replacer/Redirector

Redirect specific sites by replacing part of the URL.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         URL Replacer/Redirector
// @namespace    https://github.com/theborg3of5/Userscripts/
// @version      2.0
// @description  Redirect specific sites by replacing part of the URL.
// @author       Gavin Borg
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// @match        https://greasyfork.org/en/scripts/403100-url-replacer-redirector
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM.getValue
// @grant        GM.setValue
// ==/UserScript==

// Grant GM_*value for legacy Greasemonkey, GM.*value for Greasemonkey 4+

// Settings conversion should be removed after a while, including:
//  - convertOldStyleSettings()
//  - Grant for GM_deleteValue

// Our configuration instance - this loads/saves settings and handles the config popup.
const Config = new GM_config();

(async function ()
{
    'use strict';

    // Find the site that we matched
    const startURL = window.location.href;
    const currentSite = getUserSiteForURL(startURL);
    
    // Add a menu item to the menu to launch the config
    GM_registerMenuCommand('Configure redirect settings', () => Config.open());
    
    // Set up and load config
    let configSettings = buildConfigSettings(currentSite);
    await initConfigAsync(configSettings); // await because we need to read from the resulting (async-loaded) values

    // Convert old-style settings if we find them.
    await convertOldStyleSettings(configSettings);
    
    // Get replacement settings for the current URL
    const replaceSettings = getSettingsForSite(currentSite);
    if (!replaceSettings)
    {
        return;
    }

    // Build new URL
    const newURL = transformURL(startURL, replaceSettings);
    
    // Redirect to the new URL
    if (startURL === newURL)
    { 
        logToConsole("Current URL is same as redirection target: " + newURL);
        return
    }
    window.location.replace(newURL);
})();

// Get the site (entry from user includes/matches) that matches the current URL.
function getUserSiteForURL(startURL)
{
    for (const site of getUserSites())
    {
        // Use a RegExp so we check case-insensitively
        let siteRegex = "";
        if (site.startsWith("/"))
        {
            siteRegex = new RegExp(site.slice(1, -1), "i"); // If the site starts with a /, treat it as a regex (but remove the leading/trailing /)
        }
        else
        {
            siteRegex = new RegExp(site.replace(/\*/g, "[^ ]*"), "i"); // Otherwise replace * wildcards with regex-style [^ ]* wildcards
        }

        if (siteRegex.test(startURL)) {
            return site; // First match always wins
        }
    }
}

// We support both includes and matches, but only the user-overridden ones of each.
function getUserSites()
{ 
    return GM_info.script.options.override.use_matches.concat(GM_info.script.options.override.use_includes);
}

// Perform the replacements specified by the given settings.
function transformURL(startURL, siteSettings)
{
    const { prefix, suffix, targetStrings, replacementStrings } = siteSettings;

    // Transform the URL
    let newURL = startURL;
    for (let i = 0; i < targetStrings.length; i++)
    {
        let toReplace = prefix + targetStrings[i] + suffix;
        const replaceWith = prefix + replacementStrings[i] + suffix;

        // Use a RegEx to allow case-insensitive matching
        toReplace = new RegExp(escapeRegex(toReplace), "i"); // Escape any regex characters - we don't support actual regex matching.

        newURL = newURL.replace(toReplace, replaceWith);
    }

    return newURL;
}

// From https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711#3561711
function escapeRegex(string)
{
    return string.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&');
}

// Log a message to the console with a prefix so we know it's from this script.
function logToConsole(message)
{ 
    console.log("URL Replacer/Redirector: " + message);
}

//#region Config handling
// Build the settings object for GM_config.init()
function buildConfigSettings(currentSite)
{
    // Build fields for each site
    const fields = buildSiteFields(currentSite);

    const styles = `
        /* Float the target strings fields to the left so that they can line up with their corresponding replacements */
        div[id*=${fieldTargetStrings("")}] {
            float: left;
        }

        /* We use one section sub-header on the current site to call it out. We're overriding the
            default settings from the framework (which include the ID), so !important is needed for
            most of these properties. */
        .section_desc {
            float: right !important;
            background: #00FF00 !important;
            color: black !important;
            width: fit-content !important;
            font-weight: bold !important;
            padding: 4px !important;
            margin: 0px auto !important;
            border-top: none !important;
            border-radius: 0px 0px 10px 10px !important;
        }";
    `.replaceAll("\n", ""); // This format is nicer to read but the newlines cause issues in the config framework, so remove them

    return {
        id: "URLReplacerRedirectorConfig",
        title: "URL Replacer/Redirector Config",
        fields: fields,
        css: styles,
    };
}

// Build the specific fields in the config
function buildSiteFields(currentSite)
{
    let fields = {};
    for (const site of getUserSites())
    {
        // Section headers are the site URL as the user entered them
        const sectionName = [site];
        if (currentSite === site)
        {
            sectionName.push("This site"); // If this is the matched site, add a subheader to call it out
        }

        fields[fieldPrefix(site)] = {
            section: sectionName, // Section definition just goes on the first field inside
            type: "text",
            label: "Prefix",
            labelPos: "left",
            size: 75,
            title: "This string (if set) must appear directly before the target string in the URL.",
        }
        fields[fieldSuffix(site)] = {
            type: "text",
            label: "Suffix",
            labelPos: "left",
            size: 75,
            title: "This string (if set) must appear directly after the target string in the URL.",
        }
        fields[fieldTargetStrings(site)] = {
            type: "textarea",
            label: "Targets",
            labelPos: "above",
            title: "Enter one target per line. Each target will be replaced by its corresponding replacement.",
        }
        fields[fieldReplacementStrings(site)] = {
            type: "textarea",
            label: "Replacements",
            labelPos: "above",
            title: "Enter one replacement per line. Each replacement with replace its corresponding target.",
        }
        fields[fieldClearSite(site)] = {
            type: "button",
            label: "Clear redirects for this site",
            title: "Clear all fields for this site, removing all redirection.",
            save: false, // Don't save this field, it's just a button
            click: function (siteToClear)
            {
                return () => {
                    Config.set(fieldPrefix(siteToClear), "");
                    Config.set(fieldSuffix(siteToClear), "");
                    Config.set(fieldTargetStrings(siteToClear), "");
                    Config.set(fieldReplacementStrings(siteToClear), "");
                }
            }(site), // Immediately invoke this wrapper with the current site so the inner function can capture it
        }
    }

    return fields;
}

// This is just a Promise wrapper for GM_config.init that allows us to await initialization.
async function initConfigAsync(settings)
{
    return new Promise((resolve) =>
    {
        // Have the init event (which should fire once config is done loading) resolve the promise
        settings["events"] = {init: resolve};

        Config.init(settings);
    });
}

// Get the settings for the given site.
function getSettingsForSite(site)
{
    if (!site)
    {
        console.log("No matching site found for URL");
        return null;
    }

    // Retrieve and return the settings
    return {
        prefix:             Config.get(fieldPrefix(site)),
        suffix:             Config.get(fieldSuffix(site)),
        targetStrings:      Config.get(fieldTargetStrings(site)).split("\n"),
        replacementStrings: Config.get(fieldReplacementStrings(site)).split("\n"),
    }
}

//#region Field name "constants" based on their corresponding sites
// These are also the keys used with [GM_]Config.get/set.
function fieldPrefix(site)
{
    return "Prefix_" + site;
}
function fieldSuffix(site)
{
    return "Suffix_" + site;
}
function fieldTargetStrings(site)
{
    return "TargetString_" + site;
}
function fieldReplacementStrings(site)
{
    return "ReplacementString_" + site;
}
function fieldClearSite(site)
{
    return "ClearSite_" + site;
}
//#endregion Field name "constants" based on their corresponding sites
//#endregion Config handling

// Convert settings from the old style (simple GM_setValue/GM_getValue storage, 1 config for all
//  sites) to the new style (GM_config, one set of settings per site).
async function convertOldStyleSettings(gmConfigSettings)
{
    // Check the only really required setting (for the script to do anything)
    const replaceAry = GM_getValue("replaceTheseStrings");
    if (!replaceAry)
    {
        return; // No old settings to convert, done.
    }
    logToConsole("Old-style settings found");

    // Safety check: if we ALSO have new-style settings, leave it alone.
    if (GM_getValue("URLReplacerRedirectorConfig"))
    {
        logToConsole("New-style settings already exist, not converting old settings.");
        return;
    }
    
    const replacePrefix = GM_getValue("replacePrefix");
    const replaceSuffix = GM_getValue("replaceSuffix");
    
    // Old style: 1 config for ALL sites
    // New style: 1 config PER site
    // So, the conversion is just to copy the config onto each site.
    logToConsole("Starting settings conversion...");
    for (const site of getUserSites())
    {
        // Split replaceAry into targets (keys) and replacements (values)
        let targetsAry = [];
        let replacementsAry = [];
        for (const target in replaceAry)
        {
            targetsAry.push(target);
            replacementsAry.push(replaceAry[target]);
        }

        Config.set(fieldPrefix(site), replacePrefix);
        Config.set(fieldSuffix(site), replaceSuffix);
        Config.set(fieldTargetStrings(site), targetsAry.join("\n"));
        Config.set(fieldReplacementStrings(site), replacementsAry.join("\n"));
    }

    // Save new settings
    Config.save();
    logToConsole("New-style settings saved.");
    
    // Remove the old-style settings so we don't do this again each time.
    GM_deleteValue("replaceTheseStrings");
    GM_deleteValue("replacePrefix");
    GM_deleteValue("replaceSuffix");
    logToConsole("Old-style settings removed.");
    
    logToConsole("Conversion complete.");
}