Plugin Directory

Changeset 3442686


Ignore:
Timestamp:
01/19/2026 05:02:29 PM (8 weeks ago)
Author:
aaron13100
Message:
  • FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
  • FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
Location:
404-solution
Files:
8 edited
6 copied

Legend:

Unmodified
Added
Removed
  • 404-solution/tags/3.1.8/404-solution.php

    r3423944 r3442686  
    88    Author URI:  https://www.ajexperience.com/404-solution/
    99
    10         Version: 3.1.7
     10        Version: 3.1.8
    1111    Requires at least: 5.0
    1212    Requires PHP: 7.4
  • 404-solution/tags/3.1.8/CHANGELOG.md

    r3423944 r3442686  
    11# Changelog #
     2
     3## Version 3.1.8 (Jan 19, 2026) ##
     4* FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
     5* FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
    26
    37## Version 3.1.7 (Dec 19, 2025) ##
  • 404-solution/tags/3.1.8/README.md

    r3423944 r3442686  
    8181## Changelog ##
    8282
     83## Version 3.1.8 (Jan 19, 2026) ##
     84* FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
     85* FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
     86
    8387## Version 3.1.7 (Dec 19, 2025) ##
    8488* FIX: Prevent invalid SQL during missing-index creation by parsing index definitions from the plugin SQL templates and emitting structured `ALTER TABLE ... ADD INDEX ...` statements.
  • 404-solution/tags/3.1.8/includes/DataAccess.php

    r3423122 r3442686  
    27832783     */
    27842784    function getActiveRedirectForURL($url) {
    2785         $redirect = array();
    2786 
    2787         // remove ridiculous non-printable characters
    2788         $url = preg_replace('/[^\x20-\x7E]/', '', $url); // Remove non-printable ASCII characters
     2785        // Strip invalid UTF-8/control bytes but keep valid unicode for multilingual slugs.
     2786        $url = $this->f->sanitizeInvalidUTF8($url);
    27892787
    27902788        // Normalize to relative path before querying (Issue #24)
     
    27982796        }
    27992797        $url = $abj404logic->normalizeToRelativePath($url);
     2798
     2799        $redirect = $this->getActiveRedirectForNormalizedUrl($url);
     2800        if ($redirect['id'] !== 0) {
     2801            return $redirect;
     2802        }
     2803
     2804        if (strpos($url, '%') !== false) {
     2805            $decodedUrl = rawurldecode($url);
     2806            if ($decodedUrl !== $url) {
     2807                $decodedUrl = $this->f->sanitizeInvalidUTF8($decodedUrl);
     2808                $decodedUrl = $abj404logic->normalizeToRelativePath($decodedUrl);
     2809                $redirect = $this->getActiveRedirectForNormalizedUrl($decodedUrl);
     2810            }
     2811        }
     2812
     2813        return $redirect;
     2814    }
     2815
     2816    /** Get the redirect for the URL.
     2817     * @param string $url
     2818     * @return array
     2819     */
     2820    function getExistingRedirectForURL($url) {
     2821        // Strip invalid UTF-8/control bytes but keep valid unicode for multilingual slugs.
     2822        $url = $this->f->sanitizeInvalidUTF8($url);
     2823
     2824        // Normalize to relative path before querying (Issue #24)
     2825        // Fix HIGH #1 (5th review): Abort operation if normalization fails
     2826        // Querying with un-normalized URLs causes lookup failures
     2827        $abj404logic = ABJ_404_Solution_PluginLogic::getInstance();
     2828        if ($abj404logic === null) {
     2829            $abj404logging = ABJ_404_Solution_Logging::getInstance();
     2830            $abj404logging->errorMessage("CRITICAL: PluginLogic singleton not initialized in getExistingRedirectForURL()! Cannot normalize URL, aborting: " . $url);
     2831            return array('id' => 0);  // Return empty result - no redirect found
     2832        }
     2833        $url = $abj404logic->normalizeToRelativePath($url);
     2834
     2835        $redirect = $this->getExistingRedirectForNormalizedUrl($url);
     2836        if ($redirect['id'] !== 0) {
     2837            return $redirect;
     2838        }
     2839
     2840        if (strpos($url, '%') !== false) {
     2841            $decodedUrl = rawurldecode($url);
     2842            if ($decodedUrl !== $url) {
     2843                $decodedUrl = $this->f->sanitizeInvalidUTF8($decodedUrl);
     2844                $decodedUrl = $abj404logic->normalizeToRelativePath($decodedUrl);
     2845                $redirect = $this->getExistingRedirectForNormalizedUrl($decodedUrl);
     2846            }
     2847        }
     2848
     2849        return $redirect;
     2850    }
     2851
     2852    private function getActiveRedirectForNormalizedUrl($url) {
     2853        $redirect = array();
    28002854
    28012855        // we look for two URLs that might match. one with a trailing slash and one without.
     
    28092863            $url2 = $url2 . '/';
    28102864        }
    2811        
     2865
    28122866        // join to the wp_posts table to make sure the post exists.
    28132867        $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getPermalinkFromURL.sql");
     
    28202874
    28212875        if (is_array($rows)) {
    2822             if (empty($rows)) {
    2823                 $redirect['id'] = 0;
    2824                
    2825             } else {
    2826                 foreach ($rows[0] as $key => $value) {
    2827                     $redirect[$key] = $value;
    2828                 }
    2829             }
    2830         }
     2876            if (empty($rows)) {
     2877                $redirect['id'] = 0;
     2878            } else {
     2879                foreach ($rows[0] as $key => $value) {
     2880                    $redirect[$key] = $value;
     2881                }
     2882            }
     2883        }
     2884
     2885        if (!isset($redirect['id'])) {
     2886            $redirect['id'] = 0;
     2887        }
     2888
    28312889        return $redirect;
    28322890    }
    28332891
    2834     /** Get the redirect for the URL.
    2835      * @param string $url
    2836      * @return array
    2837      */
    2838     function getExistingRedirectForURL($url) {
     2892    private function getExistingRedirectForNormalizedUrl($url) {
    28392893        $redirect = array();
    28402894
    2841         // remove ridiculous non-printable characters
    2842         $url = preg_replace('/[^\x20-\x7E]/', '', $url); // Remove non-printable ASCII characters
    2843 
    2844         // Normalize to relative path before querying (Issue #24)
    2845         // Fix HIGH #1 (5th review): Abort operation if normalization fails
    2846         // Querying with un-normalized URLs causes lookup failures
    2847         $abj404logic = ABJ_404_Solution_PluginLogic::getInstance();
    2848         if ($abj404logic === null) {
    2849             $abj404logging = ABJ_404_Solution_Logging::getInstance();
    2850             $abj404logging->errorMessage("CRITICAL: PluginLogic singleton not initialized in getExistingRedirectForURL()! Cannot normalize URL, aborting: " . $url);
    2851             return array('id' => 0);  // Return empty result - no redirect found
    2852         }
    2853         $url = $abj404logic->normalizeToRelativePath($url);
    2854 
    28552895        // a disabled value of '1' means in the trash.
    2856         $query = $this->prepare_query_wp('select * from {wp_abj404_redirects} where BINARY url = BINARY {url} ' . 
     2896        $query = $this->prepare_query_wp('select * from {wp_abj404_redirects} where BINARY url = BINARY {url} ' .
    28572897            " and disabled = 0 ", array("url" => $url));
    28582898        $results = $this->queryAndGetResults($query);
     
    28602900
    28612901        if (is_array($rows)) {
    2862             if (empty($rows)) {
    2863                 $redirect['id'] = 0;
    2864                
    2865             } else {
    2866                 foreach ($rows[0] as $key => $value) {
    2867                     $redirect[$key] = $value;
    2868                 }
    2869             }
    2870         }
     2902            if (empty($rows)) {
     2903                $redirect['id'] = 0;
     2904            } else {
     2905                foreach ($rows[0] as $key => $value) {
     2906                    $redirect[$key] = $value;
     2907                }
     2908            }
     2909        }
     2910
     2911        if (!isset($redirect['id'])) {
     2912            $redirect['id'] = 0;
     2913        }
     2914
    28712915        return $redirect;
    28722916    }
  • 404-solution/tags/3.1.8/includes/PluginLogic.php

    r3421367 r3442686  
    351351
    352352        return $relativePath;
     353    }
     354
     355    /**
     356     * Translate a redirect destination URL to the current language when possible.
     357     *
     358     * @param string $location Full URL or path to redirect to.
     359     * @param string $requestedURL Original requested path/URL that triggered the 404.
     360     * @return string URL to use for redirect.
     361     */
     362    function maybeTranslateRedirectUrl($location, $requestedURL = '') {
     363        if (!is_string($location) || $location === '') {
     364            return $location;
     365        }
     366
     367        $translated = $this->translatePressRedirectUrl($location, $requestedURL);
     368        if ($translated !== null && $translated !== '') {
     369            $location = $translated;
     370        }
     371
     372        // Allow other multilingual plugins/themes to override redirect destinations.
     373        return apply_filters('abj404_translate_redirect_url', $location, $requestedURL);
     374    }
     375
     376    private function translatePressRedirectUrl($location, $requestedURL) {
     377        if (!$this->translatePressIntegrationAvailable()) {
     378            return null;
     379        }
     380
     381        if (!$this->isLocalUrl($location)) {
     382            return null;
     383        }
     384
     385        $language = $this->getTranslatePressLanguageFromRequest($requestedURL);
     386        if ($language === '') {
     387            return null;
     388        }
     389
     390        $translated = $this->translatePressTranslateUrl($location, $language);
     391        if (!is_string($translated) || $translated === '' || $translated === $location) {
     392            return null;
     393        }
     394
     395        if (!$this->isLocalUrl($translated)) {
     396            return null;
     397        }
     398
     399        return $translated;
     400    }
     401
     402    private function translatePressIntegrationAvailable() {
     403        return function_exists('trp_get_language_from_url') ||
     404            function_exists('trp_get_current_language') ||
     405            function_exists('trp_get_url_for_language') ||
     406            function_exists('trp_translate_url') ||
     407            has_filter('trp_translate_url');
     408    }
     409
     410    private function translatePressTranslateUrl($url, $language) {
     411        if (function_exists('trp_get_url_for_language')) {
     412            return trp_get_url_for_language($language, $url);
     413        }
     414
     415        if (function_exists('trp_translate_url')) {
     416            return trp_translate_url($url, $language);
     417        }
     418
     419        return apply_filters('trp_translate_url', $url, $language);
     420    }
     421
     422    private function getTranslatePressLanguageFromRequest($requestedURL) {
     423        $fullRequestedUrl = $this->buildFullUrlFromRequest($requestedURL);
     424
     425        if (function_exists('trp_get_language_from_url')) {
     426            $language = trp_get_language_from_url($fullRequestedUrl);
     427            if (is_string($language) && $language !== '') {
     428                return $language;
     429            }
     430        }
     431
     432        if (function_exists('trp_get_current_language')) {
     433            $language = trp_get_current_language();
     434            if (is_string($language) && $language !== '') {
     435                return $language;
     436            }
     437        }
     438
     439        return '';
     440    }
     441
     442    private function buildFullUrlFromRequest($requestedURL) {
     443        $path = $requestedURL;
     444        if ($path === '' || $path === null) {
     445            $userRequest = ABJ_404_Solution_UserRequest::getInstance();
     446            $path = $userRequest->getPathWithSortedQueryString();
     447        }
     448
     449        if ($path === '' || $path === null) {
     450            return home_url('/');
     451        }
     452
     453        if (preg_match('#^https?://#i', $path)) {
     454            return $path;
     455        }
     456
     457        return home_url($path);
     458    }
     459
     460    private function isLocalUrl($url) {
     461        if (!is_string($url) || $url === '') {
     462            return false;
     463        }
     464
     465        $parsedUrl = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);
     466        if (!is_array($parsedUrl) || !isset($parsedUrl['host'])) {
     467            // Relative URLs are treated as local.
     468            return true;
     469        }
     470
     471        $siteUrl = home_url();
     472        $parsedSite = function_exists('wp_parse_url') ? wp_parse_url($siteUrl) : parse_url($siteUrl);
     473        $siteHost = is_array($parsedSite) && isset($parsedSite['host']) ? strtolower($parsedSite['host']) : '';
     474
     475        return $siteHost !== '' && strtolower($parsedUrl['host']) === $siteHost;
    353476    }
    354477    /** Forward to a real page for queries like ?p=10
     
    20612184        }
    20622185
    2063         $manualURL = isset($_POST['manual_redirect_url']) ? $_POST['manual_redirect_url'] : '';
     2186        $manualURL = isset($_POST['manual_redirect_url']) ? wp_unslash($_POST['manual_redirect_url']) : '';
     2187        $manualURL = $this->f->sanitizeInvalidUTF8($manualURL);
     2188        $manualURL = sanitize_text_field($manualURL);
     2189        $manualURL = trim($manualURL);
    20642190        if ($this->f->substr($manualURL, 0, 1) != "/") {
    20652191            $message .= __('Error: URL must start with /', '404-solution') . "<BR/>";
     
    20842210            $code = isset($_POST['code']) && !empty($_POST['code']) ? $_POST['code'] : ABJ404_STATUS_MANUAL;
    20852211
    2086             $this->dao->setupRedirect(esc_url($_POST['manual_redirect_url']), $statusType,
     2212            $this->dao->setupRedirect($manualURL, $statusType,
    20872213                    $typeAndDest['type'], $typeAndDest['dest'],
    20882214                    sanitize_text_field($code), 0);
     
    27462872     */
    27472873    function forceRedirect($location, $status = 302, $type = -1, $requestedURL = '', $isCustom404 = false) {
     2874        // Translate redirect destination for multilingual sites (TranslatePress, etc.)
     2875        $location = $this->maybeTranslateRedirectUrl($location, $requestedURL);
    27482876
    27492877        $commentPartAndQueryPart = $this->getCommentPartAndQueryPartOfRequest();
  • 404-solution/tags/3.1.8/readme.txt

    r3423944 r3442686  
    66Requires PHP: 7.4
    77Tested up to: 6.9
    8 Stable tag: 3.1.7
     8Stable tag: 3.1.8
    99License: GPL-3.0-or-later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    237237
    238238== Changelog ==
     239
     240= Version 3.1.8 (Jan 19, 2026) =
     241* FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
     242* FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
    239243
    240244= Version 3.1.7 (Dec 19, 2025) =
  • 404-solution/trunk/404-solution.php

    r3423944 r3442686  
    88    Author URI:  https://www.ajexperience.com/404-solution/
    99
    10         Version: 3.1.7
     10        Version: 3.1.8
    1111    Requires at least: 5.0
    1212    Requires PHP: 7.4
  • 404-solution/trunk/CHANGELOG.md

    r3423944 r3442686  
    11# Changelog #
     2
     3## Version 3.1.8 (Jan 19, 2026) ##
     4* FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
     5* FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
    26
    37## Version 3.1.7 (Dec 19, 2025) ##
  • 404-solution/trunk/README.md

    r3423944 r3442686  
    8181## Changelog ##
    8282
     83## Version 3.1.8 (Jan 19, 2026) ##
     84* FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
     85* FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
     86
    8387## Version 3.1.7 (Dec 19, 2025) ##
    8488* FIX: Prevent invalid SQL during missing-index creation by parsing index definitions from the plugin SQL templates and emitting structured `ALTER TABLE ... ADD INDEX ...` statements.
  • 404-solution/trunk/includes/DataAccess.php

    r3423122 r3442686  
    27832783     */
    27842784    function getActiveRedirectForURL($url) {
    2785         $redirect = array();
    2786 
    2787         // remove ridiculous non-printable characters
    2788         $url = preg_replace('/[^\x20-\x7E]/', '', $url); // Remove non-printable ASCII characters
     2785        // Strip invalid UTF-8/control bytes but keep valid unicode for multilingual slugs.
     2786        $url = $this->f->sanitizeInvalidUTF8($url);
    27892787
    27902788        // Normalize to relative path before querying (Issue #24)
     
    27982796        }
    27992797        $url = $abj404logic->normalizeToRelativePath($url);
     2798
     2799        $redirect = $this->getActiveRedirectForNormalizedUrl($url);
     2800        if ($redirect['id'] !== 0) {
     2801            return $redirect;
     2802        }
     2803
     2804        if (strpos($url, '%') !== false) {
     2805            $decodedUrl = rawurldecode($url);
     2806            if ($decodedUrl !== $url) {
     2807                $decodedUrl = $this->f->sanitizeInvalidUTF8($decodedUrl);
     2808                $decodedUrl = $abj404logic->normalizeToRelativePath($decodedUrl);
     2809                $redirect = $this->getActiveRedirectForNormalizedUrl($decodedUrl);
     2810            }
     2811        }
     2812
     2813        return $redirect;
     2814    }
     2815
     2816    /** Get the redirect for the URL.
     2817     * @param string $url
     2818     * @return array
     2819     */
     2820    function getExistingRedirectForURL($url) {
     2821        // Strip invalid UTF-8/control bytes but keep valid unicode for multilingual slugs.
     2822        $url = $this->f->sanitizeInvalidUTF8($url);
     2823
     2824        // Normalize to relative path before querying (Issue #24)
     2825        // Fix HIGH #1 (5th review): Abort operation if normalization fails
     2826        // Querying with un-normalized URLs causes lookup failures
     2827        $abj404logic = ABJ_404_Solution_PluginLogic::getInstance();
     2828        if ($abj404logic === null) {
     2829            $abj404logging = ABJ_404_Solution_Logging::getInstance();
     2830            $abj404logging->errorMessage("CRITICAL: PluginLogic singleton not initialized in getExistingRedirectForURL()! Cannot normalize URL, aborting: " . $url);
     2831            return array('id' => 0);  // Return empty result - no redirect found
     2832        }
     2833        $url = $abj404logic->normalizeToRelativePath($url);
     2834
     2835        $redirect = $this->getExistingRedirectForNormalizedUrl($url);
     2836        if ($redirect['id'] !== 0) {
     2837            return $redirect;
     2838        }
     2839
     2840        if (strpos($url, '%') !== false) {
     2841            $decodedUrl = rawurldecode($url);
     2842            if ($decodedUrl !== $url) {
     2843                $decodedUrl = $this->f->sanitizeInvalidUTF8($decodedUrl);
     2844                $decodedUrl = $abj404logic->normalizeToRelativePath($decodedUrl);
     2845                $redirect = $this->getExistingRedirectForNormalizedUrl($decodedUrl);
     2846            }
     2847        }
     2848
     2849        return $redirect;
     2850    }
     2851
     2852    private function getActiveRedirectForNormalizedUrl($url) {
     2853        $redirect = array();
    28002854
    28012855        // we look for two URLs that might match. one with a trailing slash and one without.
     
    28092863            $url2 = $url2 . '/';
    28102864        }
    2811        
     2865
    28122866        // join to the wp_posts table to make sure the post exists.
    28132867        $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getPermalinkFromURL.sql");
     
    28202874
    28212875        if (is_array($rows)) {
    2822             if (empty($rows)) {
    2823                 $redirect['id'] = 0;
    2824                
    2825             } else {
    2826                 foreach ($rows[0] as $key => $value) {
    2827                     $redirect[$key] = $value;
    2828                 }
    2829             }
    2830         }
     2876            if (empty($rows)) {
     2877                $redirect['id'] = 0;
     2878            } else {
     2879                foreach ($rows[0] as $key => $value) {
     2880                    $redirect[$key] = $value;
     2881                }
     2882            }
     2883        }
     2884
     2885        if (!isset($redirect['id'])) {
     2886            $redirect['id'] = 0;
     2887        }
     2888
    28312889        return $redirect;
    28322890    }
    28332891
    2834     /** Get the redirect for the URL.
    2835      * @param string $url
    2836      * @return array
    2837      */
    2838     function getExistingRedirectForURL($url) {
     2892    private function getExistingRedirectForNormalizedUrl($url) {
    28392893        $redirect = array();
    28402894
    2841         // remove ridiculous non-printable characters
    2842         $url = preg_replace('/[^\x20-\x7E]/', '', $url); // Remove non-printable ASCII characters
    2843 
    2844         // Normalize to relative path before querying (Issue #24)
    2845         // Fix HIGH #1 (5th review): Abort operation if normalization fails
    2846         // Querying with un-normalized URLs causes lookup failures
    2847         $abj404logic = ABJ_404_Solution_PluginLogic::getInstance();
    2848         if ($abj404logic === null) {
    2849             $abj404logging = ABJ_404_Solution_Logging::getInstance();
    2850             $abj404logging->errorMessage("CRITICAL: PluginLogic singleton not initialized in getExistingRedirectForURL()! Cannot normalize URL, aborting: " . $url);
    2851             return array('id' => 0);  // Return empty result - no redirect found
    2852         }
    2853         $url = $abj404logic->normalizeToRelativePath($url);
    2854 
    28552895        // a disabled value of '1' means in the trash.
    2856         $query = $this->prepare_query_wp('select * from {wp_abj404_redirects} where BINARY url = BINARY {url} ' . 
     2896        $query = $this->prepare_query_wp('select * from {wp_abj404_redirects} where BINARY url = BINARY {url} ' .
    28572897            " and disabled = 0 ", array("url" => $url));
    28582898        $results = $this->queryAndGetResults($query);
     
    28602900
    28612901        if (is_array($rows)) {
    2862             if (empty($rows)) {
    2863                 $redirect['id'] = 0;
    2864                
    2865             } else {
    2866                 foreach ($rows[0] as $key => $value) {
    2867                     $redirect[$key] = $value;
    2868                 }
    2869             }
    2870         }
     2902            if (empty($rows)) {
     2903                $redirect['id'] = 0;
     2904            } else {
     2905                foreach ($rows[0] as $key => $value) {
     2906                    $redirect[$key] = $value;
     2907                }
     2908            }
     2909        }
     2910
     2911        if (!isset($redirect['id'])) {
     2912            $redirect['id'] = 0;
     2913        }
     2914
    28712915        return $redirect;
    28722916    }
  • 404-solution/trunk/includes/PluginLogic.php

    r3421367 r3442686  
    351351
    352352        return $relativePath;
     353    }
     354
     355    /**
     356     * Translate a redirect destination URL to the current language when possible.
     357     *
     358     * @param string $location Full URL or path to redirect to.
     359     * @param string $requestedURL Original requested path/URL that triggered the 404.
     360     * @return string URL to use for redirect.
     361     */
     362    function maybeTranslateRedirectUrl($location, $requestedURL = '') {
     363        if (!is_string($location) || $location === '') {
     364            return $location;
     365        }
     366
     367        $translated = $this->translatePressRedirectUrl($location, $requestedURL);
     368        if ($translated !== null && $translated !== '') {
     369            $location = $translated;
     370        }
     371
     372        // Allow other multilingual plugins/themes to override redirect destinations.
     373        return apply_filters('abj404_translate_redirect_url', $location, $requestedURL);
     374    }
     375
     376    private function translatePressRedirectUrl($location, $requestedURL) {
     377        if (!$this->translatePressIntegrationAvailable()) {
     378            return null;
     379        }
     380
     381        if (!$this->isLocalUrl($location)) {
     382            return null;
     383        }
     384
     385        $language = $this->getTranslatePressLanguageFromRequest($requestedURL);
     386        if ($language === '') {
     387            return null;
     388        }
     389
     390        $translated = $this->translatePressTranslateUrl($location, $language);
     391        if (!is_string($translated) || $translated === '' || $translated === $location) {
     392            return null;
     393        }
     394
     395        if (!$this->isLocalUrl($translated)) {
     396            return null;
     397        }
     398
     399        return $translated;
     400    }
     401
     402    private function translatePressIntegrationAvailable() {
     403        return function_exists('trp_get_language_from_url') ||
     404            function_exists('trp_get_current_language') ||
     405            function_exists('trp_get_url_for_language') ||
     406            function_exists('trp_translate_url') ||
     407            has_filter('trp_translate_url');
     408    }
     409
     410    private function translatePressTranslateUrl($url, $language) {
     411        if (function_exists('trp_get_url_for_language')) {
     412            return trp_get_url_for_language($language, $url);
     413        }
     414
     415        if (function_exists('trp_translate_url')) {
     416            return trp_translate_url($url, $language);
     417        }
     418
     419        return apply_filters('trp_translate_url', $url, $language);
     420    }
     421
     422    private function getTranslatePressLanguageFromRequest($requestedURL) {
     423        $fullRequestedUrl = $this->buildFullUrlFromRequest($requestedURL);
     424
     425        if (function_exists('trp_get_language_from_url')) {
     426            $language = trp_get_language_from_url($fullRequestedUrl);
     427            if (is_string($language) && $language !== '') {
     428                return $language;
     429            }
     430        }
     431
     432        if (function_exists('trp_get_current_language')) {
     433            $language = trp_get_current_language();
     434            if (is_string($language) && $language !== '') {
     435                return $language;
     436            }
     437        }
     438
     439        return '';
     440    }
     441
     442    private function buildFullUrlFromRequest($requestedURL) {
     443        $path = $requestedURL;
     444        if ($path === '' || $path === null) {
     445            $userRequest = ABJ_404_Solution_UserRequest::getInstance();
     446            $path = $userRequest->getPathWithSortedQueryString();
     447        }
     448
     449        if ($path === '' || $path === null) {
     450            return home_url('/');
     451        }
     452
     453        if (preg_match('#^https?://#i', $path)) {
     454            return $path;
     455        }
     456
     457        return home_url($path);
     458    }
     459
     460    private function isLocalUrl($url) {
     461        if (!is_string($url) || $url === '') {
     462            return false;
     463        }
     464
     465        $parsedUrl = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);
     466        if (!is_array($parsedUrl) || !isset($parsedUrl['host'])) {
     467            // Relative URLs are treated as local.
     468            return true;
     469        }
     470
     471        $siteUrl = home_url();
     472        $parsedSite = function_exists('wp_parse_url') ? wp_parse_url($siteUrl) : parse_url($siteUrl);
     473        $siteHost = is_array($parsedSite) && isset($parsedSite['host']) ? strtolower($parsedSite['host']) : '';
     474
     475        return $siteHost !== '' && strtolower($parsedUrl['host']) === $siteHost;
    353476    }
    354477    /** Forward to a real page for queries like ?p=10
     
    20612184        }
    20622185
    2063         $manualURL = isset($_POST['manual_redirect_url']) ? $_POST['manual_redirect_url'] : '';
     2186        $manualURL = isset($_POST['manual_redirect_url']) ? wp_unslash($_POST['manual_redirect_url']) : '';
     2187        $manualURL = $this->f->sanitizeInvalidUTF8($manualURL);
     2188        $manualURL = sanitize_text_field($manualURL);
     2189        $manualURL = trim($manualURL);
    20642190        if ($this->f->substr($manualURL, 0, 1) != "/") {
    20652191            $message .= __('Error: URL must start with /', '404-solution') . "<BR/>";
     
    20842210            $code = isset($_POST['code']) && !empty($_POST['code']) ? $_POST['code'] : ABJ404_STATUS_MANUAL;
    20852211
    2086             $this->dao->setupRedirect(esc_url($_POST['manual_redirect_url']), $statusType,
     2212            $this->dao->setupRedirect($manualURL, $statusType,
    20872213                    $typeAndDest['type'], $typeAndDest['dest'],
    20882214                    sanitize_text_field($code), 0);
     
    27462872     */
    27472873    function forceRedirect($location, $status = 302, $type = -1, $requestedURL = '', $isCustom404 = false) {
     2874        // Translate redirect destination for multilingual sites (TranslatePress, etc.)
     2875        $location = $this->maybeTranslateRedirectUrl($location, $requestedURL);
    27482876
    27492877        $commentPartAndQueryPart = $this->getCommentPartAndQueryPartOfRequest();
  • 404-solution/trunk/readme.txt

    r3423944 r3442686  
    66Requires PHP: 7.4
    77Tested up to: 6.9
    8 Stable tag: 3.1.7
     8Stable tag: 3.1.8
    99License: GPL-3.0-or-later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    237237
    238238== Changelog ==
     239
     240= Version 3.1.8 (Jan 19, 2026) =
     241* FIX: Preserve Unicode slugs during redirect lookups and allow manual redirect source paths with non-ASCII characters.
     242* FIX: TranslatePress-aware redirect translation for localized paths with a filter hook for other multilingual plugins.
    239243
    240244= Version 3.1.7 (Dec 19, 2025) =
Note: See TracChangeset for help on using the changeset viewer.