Plugin Directory

Changeset 3401938


Ignore:
Timestamp:
11/24/2025 02:47:33 PM (4 months ago)
Author:
aaron13100
Message:
  • FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
  • FIX: Resolve "Table doesnt exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
Location:
404-solution
Files:
32 edited
1 copied

Legend:

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

    r3401351 r3401938  
    88    Author URI:  https://www.ajexperience.com/404-solution/
    99
    10     Version: 3.0.3
     10    Version: 3.0.4
    1111    Requires at least: 5.0
    1212    Requires PHP: 7.4
  • 404-solution/tags/3.0.4/CHANGELOG.md

    r3401351 r3401938  
    11# Changelog #
    22
     3## Version 3.0.4 (Nov 24, 2025) ##
     4* FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
     5* FIX: Resolve "Table doesn't exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
     6
    37## Version 3.0.3 (Nov 23, 2025) ##
    4 * Improved: GDPR compliance in log files (just in case). 
     8* Improved: GDPR compliance in log files (just in case).
    59* Improved: Some missing translation keys.
    610* Improved: Deactivation feedback.
  • 404-solution/tags/3.0.4/README.md

    r3401351 r3401938  
    8181## Changelog ##
    8282
     83## Version 3.0.4 (Nov 24, 2025) ##
     84* FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
     85* FIX: Resolve "Table doesn't exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
     86
    8387## Version 3.0.3 (Nov 23, 2025) ##
    8488* Improved: GDPR compliance in log files (just in case).
  • 404-solution/tags/3.0.4/includes/DataAccess.php

    r3400707 r3401938  
    229229        $replacements['{wp_users}'] = $wpdb->users;
    230230        $replacements['{wp_prefix}'] = $wpdb->prefix;
    231         $replacements['{wp_prefix_lower}'] = $this->f->strtolower($wpdb->prefix);
     231        $replacements['{wp_prefix_lower}'] = $this->getLowercasePrefix();
    232232       
    233233        // wp database table replacements
     
    238238        $fpreg = ABJ_404_Solution_FunctionsPreg::getInstance();
    239239        $query = $fpreg->regexReplace('[{]wp_abj404_(.*?)[}]',
    240             strtolower($wpdb->prefix) . "abj404_\\1", $query);
     240            $this->getLowercasePrefix() . "abj404_\\1", $query);
    241241       
    242242        return $query;
     243    }
     244
     245    /**
     246     * Get the normalized (lowercase) prefix used for all plugin tables.
     247     * This avoids case-sensitive MySQL filesystems from treating mixed-case
     248     * prefixes as distinct tables.
     249     *
     250     * @return string
     251     */
     252    public function getLowercasePrefix() {
     253        global $wpdb;
     254        return $this->f->strtolower($wpdb->prefix);
     255    }
     256
     257    /**
     258     * Build a fully-qualified plugin table name using the normalized prefix.
     259     *
     260     * @param string $tableSuffix Table name without the WordPress prefix.
     261     * @return string
     262     */
     263    public function getPrefixedTableName($tableSuffix) {
     264        return $this->getLowercasePrefix() . ltrim($tableSuffix, '_');
    243265    }
    244266   
     
    645667    function storeSpellingPermalinksToCache($requestedURLRaw, $returnValue) {
    646668        $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/insertSpellingCache.sql");
    647         $query = $this->f->str_replace('{url}', esc_sql($requestedURLRaw), $query);
     669
     670        // Sanitize invalid UTF-8 sequences before storing to database
     671        // This prevents "Could not perform query because it contains invalid data" errors
     672        // when URLs contain invalid UTF-8 byte sequences (e.g., %c1%1c from scanner probes)
     673        $cleanURL = $this->f->sanitizeInvalidUTF8($requestedURLRaw);
     674
     675        $query = $this->f->str_replace('{url}', esc_sql($cleanURL), $query);
    648676        $query = $this->f->str_replace('{matchdata}', esc_sql(json_encode($returnValue)), $query);
    649677
     
    12831311        // Fetch all logs data in a single batch query
    12841312        $placeholders = implode(',', array_fill(0, count($urls), '%s'));
     1313        $logsTable = $this->getPrefixedTableName('abj404_logsv2');
    12851314        $query = $wpdb->prepare(
    12861315            "SELECT requested_url,
     
    12881317                    MAX(timestamp) AS last_used,
    12891318                    COUNT(requested_url) AS logshits
    1290              FROM {$wpdb->prefix}abj404_logsv2
     1319             FROM {$logsTable}
    12911320             WHERE requested_url IN ($placeholders)
    12921321             GROUP BY requested_url",
  • 404-solution/tags/3.0.4/includes/DatabaseUpgradesEtc.php

    r3400945 r3401938  
    999999
    10001000        $startTime = microtime(true);
    1001         $redirectsTable = $wpdb->prefix . 'abj404_redirects';
     1001        $redirectsTable = $this->dao->getPrefixedTableName('abj404_redirects');
    10021002
    10031003        $abj404logging->infoMessage("Migrating redirects table to relative paths...");
     
    13581358
    13591359                // Count pages for THIS site only
    1360                 $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1360                $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    13611361                $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    13621362
     
    14571457                // SINGLE SITE: Use original simple logic
    14581458                $offset = $this->getNetworkAwareOption('abj404_ngram_rebuild_offset', 0);
    1459                 $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1459                $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    14601460                $totalPages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    14611461
     
    16021602
    16031603        try {
    1604             $ngramTable = $wpdb->prefix . 'abj404_ngram_cache';
    1605             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1604            $ngramTable = $this->dao->getPrefixedTableName('abj404_ngram_cache');
     1605            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    16061606
    16071607            // Check if cache is already populated (unless force rebuild)
     
    17181718
    17191719        try {
    1720             $ngramTable = $wpdb->prefix . 'abj404_ngram_cache';
    1721             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1720            $ngramTable = $this->dao->getPrefixedTableName('abj404_ngram_cache');
     1721            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    17221722
    17231723            $stats = ['posts_added' => 0, 'posts_failed' => 0, 'categories_added' => 0, 'categories_failed' => 0];
     
    18381838        global $wpdb;
    18391839
    1840         $ngramTable = $wpdb->prefix . 'abj404_ngram_cache';
    1841         $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1840        $ngramTable = $this->dao->getPrefixedTableName('abj404_ngram_cache');
     1841        $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    18421842
    18431843        $this->logger->debugMessage("Checking for orphaned ngram entries...");
     
    19941994
    19951995        $missingTables = [];
     1996        $normalizedPrefix = $this->dao->getLowercasePrefix();
    19961997
    19971998        // Check each required table
    19981999        foreach ($requiredTables as $tableName) {
    1999             $fullTableName = $wpdb->prefix . $tableName;
     2000            $fullTableName = $this->dao->getPrefixedTableName($tableName);
    20002001            $tableExists = $wpdb->get_var("SHOW TABLES LIKE '{$fullTableName}'");
    20012002
     
    20082009        if (!empty($missingTables)) {
    20092010            $this->logger->infoMessage(sprintf(
    2010                 "Site %d (prefix: %s) is missing %d table(s): %s. Running repair...",
     2011                "Site %d (prefix: %s, normalized: %s) is missing %d table(s): %s. Running repair...",
    20112012                get_current_blog_id(),
    20122013                $wpdb->prefix,
     2014                $normalizedPrefix,
    20132015                count($missingTables),
    20142016                implode(', ', $missingTables)
     
    23342336        if (!$this->isNetworkActivated()) {
    23352337            // Single site: count only current site's pages
    2336             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     2338            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    23372339            return (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    23382340        }
     
    23442346        foreach ($sites as $blog_id) {
    23452347            switch_to_blog($blog_id);
    2346             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     2348            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    23472349            $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    23482350            $totalPages += $sitePages;
  • 404-solution/tags/3.0.4/includes/Functions.php

    r3399802 r3401938  
    232232   
    233233    abstract function regexReplace($pattern, $replacement, $string);
    234    
     234
     235    abstract function sanitizeInvalidUTF8($string);
     236
    235237    /**  Used with array_filter()
    236238     * @param string $value
  • 404-solution/tags/3.0.4/includes/Loader.php

    r3401351 r3401938  
    1111    basename(dirname(ABJ404_FILE)) . '/' . basename(ABJ404_FILE));
    1212
    13 define( 'ABJ404_VERSION', '3.0.3' );
     13define( 'ABJ404_VERSION', '3.0.4' );
    1414define( 'URL_TRACKING_SUFFIX', '?utm_source=404SolutionPlugin&utm_medium=WordPress');
    1515define( 'ABJ404_HOME_URL', 'https://www.ajexperience.com/404-solution/' . URL_TRACKING_SUFFIX);
  • 404-solution/tags/3.0.4/includes/Logging.php

    r3401351 r3401938  
    292292        $bodyLines[] = "MySQL version: " . $wpdb->db_version();
    293293        $bodyLines[] = "Site URL: " . get_site_url();
     294        $bodyLines[] = "Multisite: " . (is_multisite() ? 'yes' : 'no');
     295        if (is_multisite() && function_exists('is_plugin_active_for_network')) {
     296            $bodyLines[] = "Network activated: " . (is_plugin_active_for_network(plugin_basename(ABJ404_FILE)) ? 'yes' : 'no');
     297        }
    294298        $bodyLines[] = "WP_MEMORY_LIMIT: " . WP_MEMORY_LIMIT;
    295299        $bodyLines[] = "Extensions: " . implode(", ", get_loaded_extensions());
  • 404-solution/tags/3.0.4/includes/NGramFilter.php

    r3400707 r3401938  
    193193        $ngramCount = count($ngrams['bi']) + count($ngrams['tri']);
    194194
    195         $table = $wpdb->prefix . 'abj404_ngram_cache';
     195        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    196196
    197197        // Use REPLACE to handle updates (REPLACE = DELETE + INSERT)
     
    217217                $wpdb->last_error,
    218218                $table,
    219                 $wpdb->prefix,
     219                $this->dao->getLowercasePrefix(),
    220220                $wpdb->dbname
    221221            );
     
    243243        global $wpdb;
    244244
    245         $table = $wpdb->prefix . 'abj404_ngram_cache';
     245        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    246246        $query = $wpdb->prepare(
    247247            "SELECT ngrams FROM {$table} WHERE id = %d AND type = %s",
     
    271271        global $wpdb;
    272272
    273         $table = $wpdb->prefix . 'abj404_ngram_cache';
     273        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    274274
    275275        // Check cache size first - abort if too large
     
    318318        global $wpdb;
    319319
    320         $table = $wpdb->prefix . 'abj404_ngram_cache';
     320        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    321321
    322322        // Database-side filtering by ngram_count range
     
    359359        global $wpdb;
    360360
    361         $table = $wpdb->prefix . 'abj404_ngram_cache';
     361        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    362362        $result = $wpdb->delete($table, ['id' => $pageId, 'type' => $type], ['%d', '%s']);
    363363
     
    380380
    381381        global $wpdb;
    382         $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     382        $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    383383
    384384        // Prepare IN clause for page IDs
     
    445445        global $wpdb;
    446446
    447         $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     447        $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    448448
    449449        // Get a batch of pages from permalink cache
     
    542542
    543543        // Check cache size to determine strategy
    544         $table = $wpdb->prefix . 'abj404_ngram_cache';
     544        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    545545        $totalCount = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
    546546
     
    645645        global $wpdb;
    646646
    647         $table = $wpdb->prefix . 'abj404_ngram_cache';
     647        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    648648        $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
    649649
     
    659659        global $wpdb;
    660660
    661         $table = $wpdb->prefix . 'abj404_ngram_cache';
     661        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    662662
    663663        $stats = [
  • 404-solution/tags/3.0.4/includes/PluginLogic.php

    r3400707 r3401938  
    680680                            ' log rows were migrated to the new table structre.');
    681681                    // log the rows inserted/migrated.
    682                     $wpdb->query('drop table ' . $this->f->strtolower($wpdb->prefix) . 'abj404_logs');
     682                    $wpdb->query('drop table ' . $this->dao->getLowercasePrefix() . 'abj404_logs');
    683683                }
    684684            }
     
    10041004
    10051005            global $wpdb;
     1006            $dao = ABJ_404_Solution_DataAccess::getInstance();
     1007            $prefix = $dao->getLowercasePrefix();
    10061008
    10071009            // Remove ALL custom database tables
    10081010            // Core tables
    1009             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_redirects");
    1010             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_logsv2");
    1011             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_lookup");
     1011            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_redirects");
     1012            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_logsv2");
     1013            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_lookup");
    10121014
    10131015            // Cache tables
    1014             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_permalink_cache");
    1015             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_ngram_cache");
    1016             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_spelling_cache");
     1016            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_permalink_cache");
     1017            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_ngram_cache");
     1018            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_spelling_cache");
    10171019
    10181020            // Temporary tables
    1019             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_logs_hits_temp");
     1021            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_logs_hits_temp");
    10201022
    10211023            // Remove ALL plugin options
  • 404-solution/tags/3.0.4/includes/UninstallModal.php

    r3401351 r3401938  
    477477    private static function getRedirectCount() {
    478478        global $wpdb;
    479         $table_name = $wpdb->prefix . 'abj404_redirects';
     479        $dao = ABJ_404_Solution_DataAccess::getInstance();
     480        $table_name = $dao->getPrefixedTableName('abj404_redirects');
    480481
    481482        // Check if table exists
    482         $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
     483        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name;
    483484
    484485        if (!$table_exists) {
  • 404-solution/tags/3.0.4/includes/Uninstaller.php

    r3401351 r3401938  
    7373     */
    7474    private static function deleteTables($wpdb, $preferences) {
    75         $prefix = $wpdb->prefix;
     75        $dao = ABJ_404_Solution_DataAccess::getInstance();
     76        $prefix = $dao->getLowercasePrefix();
    7677
    7778        // Delete redirect table if user chose to
  • 404-solution/tags/3.0.4/includes/php/FunctionsMBString.php

    r3399802 r3401938  
    4949        return mb_ereg_replace($pattern, $replacement, $string);
    5050    }
    51    
     51
     52    /**
     53     * Sanitize invalid UTF-8 byte sequences from a string.
     54     *
     55     * This method removes or replaces invalid UTF-8 byte sequences that would cause
     56     * database errors like "Could not perform query because it contains invalid data".
     57     *
     58     * Uses mb_convert_encoding() to strip invalid UTF-8 bytes by converting from UTF-8 to UTF-8,
     59     * which automatically removes any invalid sequences.
     60     *
     61     * @param string|null $string The string to sanitize
     62     * @return string The sanitized string with only valid UTF-8 characters
     63     */
     64    function sanitizeInvalidUTF8($string) {
     65        // Handle null and empty cases
     66        if ($string === null || $string === '') {
     67            return '';
     68        }
     69
     70        // Convert to string if not already
     71        if (!is_string($string)) {
     72            $string = strval($string);
     73        }
     74
     75        // Use mb_convert_encoding to strip invalid UTF-8 bytes
     76        // Converting from UTF-8 to UTF-8 removes invalid sequences
     77        $sanitized = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
     78
     79        // Additional safety: remove null bytes and control characters that might cause issues
     80        // Keep only valid UTF-8 characters, removing C0 control characters except whitespace
     81        $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $sanitized);
     82
     83        return $sanitized;
     84    }
     85
    5286}
    5387
  • 404-solution/tags/3.0.4/includes/php/FunctionsPreg.php

    r3190969 r3401938  
    9898    }
    9999
     100    /**
     101     * Sanitize invalid UTF-8 byte sequences from a string.
     102     *
     103     * This is the fallback implementation for systems without mbstring extension.
     104     * It uses preg_replace with the 'u' modifier to remove invalid UTF-8 sequences.
     105     *
     106     * The approach:
     107     * 1. Use iconv if available (faster and more reliable)
     108     * 2. Fall back to preg_replace to remove non-UTF-8 bytes
     109     * 3. Remove control characters that cause database issues
     110     *
     111     * @param string|null $string The string to sanitize
     112     * @return string The sanitized string with only valid UTF-8 characters
     113     */
     114    function sanitizeInvalidUTF8($string) {
     115        // Handle null and empty cases
     116        if ($string === null || $string === '') {
     117            return '';
     118        }
     119
     120        // Convert to string if not already
     121        if (!is_string($string)) {
     122            $string = strval($string);
     123        }
     124
     125        // Try iconv first (if available, it's very efficient)
     126        if (function_exists('iconv')) {
     127            // iconv with //IGNORE will skip invalid UTF-8 sequences
     128            $sanitized = iconv('UTF-8', 'UTF-8//IGNORE', $string);
     129
     130            // iconv returns false on error, fall through to preg approach
     131            if ($sanitized !== false) {
     132                // Remove null bytes and problematic control characters
     133                $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $sanitized);
     134                return $sanitized;
     135            }
     136        }
     137
     138        // Fallback: use preg_replace with 'u' modifier to validate UTF-8
     139        // The //u modifier forces UTF-8 mode - invalid sequences cause match failure
     140        // By replacing '' with '', we essentially validate and keep only valid UTF-8
     141        $sanitized = @preg_replace('//u', '', $string);
     142
     143        // If preg_replace failed (invalid UTF-8), use byte-by-byte filtering
     144        if ($sanitized === null) {
     145            // Filter out invalid UTF-8 lead bytes:
     146            // - C0, C1 (overlong 2-byte sequences)
     147            // - F5-FF (invalid lead bytes beyond UTF-8 range)
     148            // Keep valid ranges: C2-DF (2-byte), E0-EF (3-byte), F0-F4 (4-byte)
     149            $sanitized = preg_replace('/[\xC0\xC1\xF5-\xFF][\x80-\xBF]*/', '', $string);
     150
     151            // Remove incomplete sequences (continuation bytes without lead byte)
     152            $sanitized = preg_replace('/[\x80-\xBF]+/', '', $sanitized);
     153
     154            // Verify the result is now valid UTF-8 by attempting a UTF-8 match
     155            if (@preg_match('//u', $sanitized) === false) {
     156                // Still invalid - fall back to ASCII-only (safe but lossy)
     157                $sanitized = preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string);
     158            }
     159        }
     160
     161        // Remove null bytes and other problematic control characters
     162        $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $sanitized);
     163
     164        return $sanitized;
     165    }
     166
    100167}
    101168
  • 404-solution/tags/3.0.4/readme.txt

    r3401351 r3401938  
    66Requires PHP: 7.4
    77Tested up to: 6.8
    8 Stable tag: 3.0.3
     8Stable tag: 3.0.4
    99License: GPL-3.0-or-later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    240240== Changelog ==
    241241
     242## Version 3.0.4 (Nov 24, 2025) ##
     243* FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
     244* FIX: Resolve "Table doesn't exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
     245
    242246= Version 3.0.3 (Nov 23, 2025) =
    243247* Improved: GDPR compliance in log files (just in case).
  • 404-solution/tags/3.0.4/uninstall.php

    r3401351 r3401938  
    2828// Define version constant for use in Uninstaller
    2929// This matches the version defined in includes/Loader.php
    30 define('ABJ404_VERSION', '3.0.3');
     30define('ABJ404_VERSION', '3.0.4');
    3131
    3232// Load the Uninstaller class
  • 404-solution/trunk/404-solution.php

    r3401351 r3401938  
    88    Author URI:  https://www.ajexperience.com/404-solution/
    99
    10     Version: 3.0.3
     10    Version: 3.0.4
    1111    Requires at least: 5.0
    1212    Requires PHP: 7.4
  • 404-solution/trunk/CHANGELOG.md

    r3401351 r3401938  
    11# Changelog #
    22
     3## Version 3.0.4 (Nov 24, 2025) ##
     4* FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
     5* FIX: Resolve "Table doesn't exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
     6
    37## Version 3.0.3 (Nov 23, 2025) ##
    4 * Improved: GDPR compliance in log files (just in case). 
     8* Improved: GDPR compliance in log files (just in case).
    59* Improved: Some missing translation keys.
    610* Improved: Deactivation feedback.
  • 404-solution/trunk/README.md

    r3401351 r3401938  
    8181## Changelog ##
    8282
     83## Version 3.0.4 (Nov 24, 2025) ##
     84* FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
     85* FIX: Resolve "Table doesn't exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
     86
    8387## Version 3.0.3 (Nov 23, 2025) ##
    8488* Improved: GDPR compliance in log files (just in case).
  • 404-solution/trunk/includes/DataAccess.php

    r3400707 r3401938  
    229229        $replacements['{wp_users}'] = $wpdb->users;
    230230        $replacements['{wp_prefix}'] = $wpdb->prefix;
    231         $replacements['{wp_prefix_lower}'] = $this->f->strtolower($wpdb->prefix);
     231        $replacements['{wp_prefix_lower}'] = $this->getLowercasePrefix();
    232232       
    233233        // wp database table replacements
     
    238238        $fpreg = ABJ_404_Solution_FunctionsPreg::getInstance();
    239239        $query = $fpreg->regexReplace('[{]wp_abj404_(.*?)[}]',
    240             strtolower($wpdb->prefix) . "abj404_\\1", $query);
     240            $this->getLowercasePrefix() . "abj404_\\1", $query);
    241241       
    242242        return $query;
     243    }
     244
     245    /**
     246     * Get the normalized (lowercase) prefix used for all plugin tables.
     247     * This avoids case-sensitive MySQL filesystems from treating mixed-case
     248     * prefixes as distinct tables.
     249     *
     250     * @return string
     251     */
     252    public function getLowercasePrefix() {
     253        global $wpdb;
     254        return $this->f->strtolower($wpdb->prefix);
     255    }
     256
     257    /**
     258     * Build a fully-qualified plugin table name using the normalized prefix.
     259     *
     260     * @param string $tableSuffix Table name without the WordPress prefix.
     261     * @return string
     262     */
     263    public function getPrefixedTableName($tableSuffix) {
     264        return $this->getLowercasePrefix() . ltrim($tableSuffix, '_');
    243265    }
    244266   
     
    645667    function storeSpellingPermalinksToCache($requestedURLRaw, $returnValue) {
    646668        $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/insertSpellingCache.sql");
    647         $query = $this->f->str_replace('{url}', esc_sql($requestedURLRaw), $query);
     669
     670        // Sanitize invalid UTF-8 sequences before storing to database
     671        // This prevents "Could not perform query because it contains invalid data" errors
     672        // when URLs contain invalid UTF-8 byte sequences (e.g., %c1%1c from scanner probes)
     673        $cleanURL = $this->f->sanitizeInvalidUTF8($requestedURLRaw);
     674
     675        $query = $this->f->str_replace('{url}', esc_sql($cleanURL), $query);
    648676        $query = $this->f->str_replace('{matchdata}', esc_sql(json_encode($returnValue)), $query);
    649677
     
    12831311        // Fetch all logs data in a single batch query
    12841312        $placeholders = implode(',', array_fill(0, count($urls), '%s'));
     1313        $logsTable = $this->getPrefixedTableName('abj404_logsv2');
    12851314        $query = $wpdb->prepare(
    12861315            "SELECT requested_url,
     
    12881317                    MAX(timestamp) AS last_used,
    12891318                    COUNT(requested_url) AS logshits
    1290              FROM {$wpdb->prefix}abj404_logsv2
     1319             FROM {$logsTable}
    12911320             WHERE requested_url IN ($placeholders)
    12921321             GROUP BY requested_url",
  • 404-solution/trunk/includes/DatabaseUpgradesEtc.php

    r3400945 r3401938  
    999999
    10001000        $startTime = microtime(true);
    1001         $redirectsTable = $wpdb->prefix . 'abj404_redirects';
     1001        $redirectsTable = $this->dao->getPrefixedTableName('abj404_redirects');
    10021002
    10031003        $abj404logging->infoMessage("Migrating redirects table to relative paths...");
     
    13581358
    13591359                // Count pages for THIS site only
    1360                 $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1360                $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    13611361                $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    13621362
     
    14571457                // SINGLE SITE: Use original simple logic
    14581458                $offset = $this->getNetworkAwareOption('abj404_ngram_rebuild_offset', 0);
    1459                 $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1459                $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    14601460                $totalPages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    14611461
     
    16021602
    16031603        try {
    1604             $ngramTable = $wpdb->prefix . 'abj404_ngram_cache';
    1605             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1604            $ngramTable = $this->dao->getPrefixedTableName('abj404_ngram_cache');
     1605            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    16061606
    16071607            // Check if cache is already populated (unless force rebuild)
     
    17181718
    17191719        try {
    1720             $ngramTable = $wpdb->prefix . 'abj404_ngram_cache';
    1721             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1720            $ngramTable = $this->dao->getPrefixedTableName('abj404_ngram_cache');
     1721            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    17221722
    17231723            $stats = ['posts_added' => 0, 'posts_failed' => 0, 'categories_added' => 0, 'categories_failed' => 0];
     
    18381838        global $wpdb;
    18391839
    1840         $ngramTable = $wpdb->prefix . 'abj404_ngram_cache';
    1841         $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     1840        $ngramTable = $this->dao->getPrefixedTableName('abj404_ngram_cache');
     1841        $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    18421842
    18431843        $this->logger->debugMessage("Checking for orphaned ngram entries...");
     
    19941994
    19951995        $missingTables = [];
     1996        $normalizedPrefix = $this->dao->getLowercasePrefix();
    19961997
    19971998        // Check each required table
    19981999        foreach ($requiredTables as $tableName) {
    1999             $fullTableName = $wpdb->prefix . $tableName;
     2000            $fullTableName = $this->dao->getPrefixedTableName($tableName);
    20002001            $tableExists = $wpdb->get_var("SHOW TABLES LIKE '{$fullTableName}'");
    20012002
     
    20082009        if (!empty($missingTables)) {
    20092010            $this->logger->infoMessage(sprintf(
    2010                 "Site %d (prefix: %s) is missing %d table(s): %s. Running repair...",
     2011                "Site %d (prefix: %s, normalized: %s) is missing %d table(s): %s. Running repair...",
    20112012                get_current_blog_id(),
    20122013                $wpdb->prefix,
     2014                $normalizedPrefix,
    20132015                count($missingTables),
    20142016                implode(', ', $missingTables)
     
    23342336        if (!$this->isNetworkActivated()) {
    23352337            // Single site: count only current site's pages
    2336             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     2338            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    23372339            return (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    23382340        }
     
    23442346        foreach ($sites as $blog_id) {
    23452347            switch_to_blog($blog_id);
    2346             $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     2348            $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    23472349            $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}");
    23482350            $totalPages += $sitePages;
  • 404-solution/trunk/includes/Functions.php

    r3399802 r3401938  
    232232   
    233233    abstract function regexReplace($pattern, $replacement, $string);
    234    
     234
     235    abstract function sanitizeInvalidUTF8($string);
     236
    235237    /**  Used with array_filter()
    236238     * @param string $value
  • 404-solution/trunk/includes/Loader.php

    r3401351 r3401938  
    1111    basename(dirname(ABJ404_FILE)) . '/' . basename(ABJ404_FILE));
    1212
    13 define( 'ABJ404_VERSION', '3.0.3' );
     13define( 'ABJ404_VERSION', '3.0.4' );
    1414define( 'URL_TRACKING_SUFFIX', '?utm_source=404SolutionPlugin&utm_medium=WordPress');
    1515define( 'ABJ404_HOME_URL', 'https://www.ajexperience.com/404-solution/' . URL_TRACKING_SUFFIX);
  • 404-solution/trunk/includes/Logging.php

    r3401351 r3401938  
    292292        $bodyLines[] = "MySQL version: " . $wpdb->db_version();
    293293        $bodyLines[] = "Site URL: " . get_site_url();
     294        $bodyLines[] = "Multisite: " . (is_multisite() ? 'yes' : 'no');
     295        if (is_multisite() && function_exists('is_plugin_active_for_network')) {
     296            $bodyLines[] = "Network activated: " . (is_plugin_active_for_network(plugin_basename(ABJ404_FILE)) ? 'yes' : 'no');
     297        }
    294298        $bodyLines[] = "WP_MEMORY_LIMIT: " . WP_MEMORY_LIMIT;
    295299        $bodyLines[] = "Extensions: " . implode(", ", get_loaded_extensions());
  • 404-solution/trunk/includes/NGramFilter.php

    r3400707 r3401938  
    193193        $ngramCount = count($ngrams['bi']) + count($ngrams['tri']);
    194194
    195         $table = $wpdb->prefix . 'abj404_ngram_cache';
     195        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    196196
    197197        // Use REPLACE to handle updates (REPLACE = DELETE + INSERT)
     
    217217                $wpdb->last_error,
    218218                $table,
    219                 $wpdb->prefix,
     219                $this->dao->getLowercasePrefix(),
    220220                $wpdb->dbname
    221221            );
     
    243243        global $wpdb;
    244244
    245         $table = $wpdb->prefix . 'abj404_ngram_cache';
     245        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    246246        $query = $wpdb->prepare(
    247247            "SELECT ngrams FROM {$table} WHERE id = %d AND type = %s",
     
    271271        global $wpdb;
    272272
    273         $table = $wpdb->prefix . 'abj404_ngram_cache';
     273        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    274274
    275275        // Check cache size first - abort if too large
     
    318318        global $wpdb;
    319319
    320         $table = $wpdb->prefix . 'abj404_ngram_cache';
     320        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    321321
    322322        // Database-side filtering by ngram_count range
     
    359359        global $wpdb;
    360360
    361         $table = $wpdb->prefix . 'abj404_ngram_cache';
     361        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    362362        $result = $wpdb->delete($table, ['id' => $pageId, 'type' => $type], ['%d', '%s']);
    363363
     
    380380
    381381        global $wpdb;
    382         $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     382        $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    383383
    384384        // Prepare IN clause for page IDs
     
    445445        global $wpdb;
    446446
    447         $permalinkCacheTable = $wpdb->prefix . 'abj404_permalink_cache';
     447        $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache');
    448448
    449449        // Get a batch of pages from permalink cache
     
    542542
    543543        // Check cache size to determine strategy
    544         $table = $wpdb->prefix . 'abj404_ngram_cache';
     544        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    545545        $totalCount = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
    546546
     
    645645        global $wpdb;
    646646
    647         $table = $wpdb->prefix . 'abj404_ngram_cache';
     647        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    648648        $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
    649649
     
    659659        global $wpdb;
    660660
    661         $table = $wpdb->prefix . 'abj404_ngram_cache';
     661        $table = $this->dao->getPrefixedTableName('abj404_ngram_cache');
    662662
    663663        $stats = [
  • 404-solution/trunk/includes/PluginLogic.php

    r3400707 r3401938  
    680680                            ' log rows were migrated to the new table structre.');
    681681                    // log the rows inserted/migrated.
    682                     $wpdb->query('drop table ' . $this->f->strtolower($wpdb->prefix) . 'abj404_logs');
     682                    $wpdb->query('drop table ' . $this->dao->getLowercasePrefix() . 'abj404_logs');
    683683                }
    684684            }
     
    10041004
    10051005            global $wpdb;
     1006            $dao = ABJ_404_Solution_DataAccess::getInstance();
     1007            $prefix = $dao->getLowercasePrefix();
    10061008
    10071009            // Remove ALL custom database tables
    10081010            // Core tables
    1009             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_redirects");
    1010             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_logsv2");
    1011             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_lookup");
     1011            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_redirects");
     1012            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_logsv2");
     1013            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_lookup");
    10121014
    10131015            // Cache tables
    1014             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_permalink_cache");
    1015             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_ngram_cache");
    1016             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_spelling_cache");
     1016            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_permalink_cache");
     1017            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_ngram_cache");
     1018            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_spelling_cache");
    10171019
    10181020            // Temporary tables
    1019             $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}abj404_logs_hits_temp");
     1021            $wpdb->query("DROP TABLE IF EXISTS {$prefix}abj404_logs_hits_temp");
    10201022
    10211023            // Remove ALL plugin options
  • 404-solution/trunk/includes/UninstallModal.php

    r3401351 r3401938  
    477477    private static function getRedirectCount() {
    478478        global $wpdb;
    479         $table_name = $wpdb->prefix . 'abj404_redirects';
     479        $dao = ABJ_404_Solution_DataAccess::getInstance();
     480        $table_name = $dao->getPrefixedTableName('abj404_redirects');
    480481
    481482        // Check if table exists
    482         $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
     483        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name;
    483484
    484485        if (!$table_exists) {
  • 404-solution/trunk/includes/Uninstaller.php

    r3401351 r3401938  
    7373     */
    7474    private static function deleteTables($wpdb, $preferences) {
    75         $prefix = $wpdb->prefix;
     75        $dao = ABJ_404_Solution_DataAccess::getInstance();
     76        $prefix = $dao->getLowercasePrefix();
    7677
    7778        // Delete redirect table if user chose to
  • 404-solution/trunk/includes/php/FunctionsMBString.php

    r3399802 r3401938  
    4949        return mb_ereg_replace($pattern, $replacement, $string);
    5050    }
    51    
     51
     52    /**
     53     * Sanitize invalid UTF-8 byte sequences from a string.
     54     *
     55     * This method removes or replaces invalid UTF-8 byte sequences that would cause
     56     * database errors like "Could not perform query because it contains invalid data".
     57     *
     58     * Uses mb_convert_encoding() to strip invalid UTF-8 bytes by converting from UTF-8 to UTF-8,
     59     * which automatically removes any invalid sequences.
     60     *
     61     * @param string|null $string The string to sanitize
     62     * @return string The sanitized string with only valid UTF-8 characters
     63     */
     64    function sanitizeInvalidUTF8($string) {
     65        // Handle null and empty cases
     66        if ($string === null || $string === '') {
     67            return '';
     68        }
     69
     70        // Convert to string if not already
     71        if (!is_string($string)) {
     72            $string = strval($string);
     73        }
     74
     75        // Use mb_convert_encoding to strip invalid UTF-8 bytes
     76        // Converting from UTF-8 to UTF-8 removes invalid sequences
     77        $sanitized = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
     78
     79        // Additional safety: remove null bytes and control characters that might cause issues
     80        // Keep only valid UTF-8 characters, removing C0 control characters except whitespace
     81        $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $sanitized);
     82
     83        return $sanitized;
     84    }
     85
    5286}
    5387
  • 404-solution/trunk/includes/php/FunctionsPreg.php

    r3190969 r3401938  
    9898    }
    9999
     100    /**
     101     * Sanitize invalid UTF-8 byte sequences from a string.
     102     *
     103     * This is the fallback implementation for systems without mbstring extension.
     104     * It uses preg_replace with the 'u' modifier to remove invalid UTF-8 sequences.
     105     *
     106     * The approach:
     107     * 1. Use iconv if available (faster and more reliable)
     108     * 2. Fall back to preg_replace to remove non-UTF-8 bytes
     109     * 3. Remove control characters that cause database issues
     110     *
     111     * @param string|null $string The string to sanitize
     112     * @return string The sanitized string with only valid UTF-8 characters
     113     */
     114    function sanitizeInvalidUTF8($string) {
     115        // Handle null and empty cases
     116        if ($string === null || $string === '') {
     117            return '';
     118        }
     119
     120        // Convert to string if not already
     121        if (!is_string($string)) {
     122            $string = strval($string);
     123        }
     124
     125        // Try iconv first (if available, it's very efficient)
     126        if (function_exists('iconv')) {
     127            // iconv with //IGNORE will skip invalid UTF-8 sequences
     128            $sanitized = iconv('UTF-8', 'UTF-8//IGNORE', $string);
     129
     130            // iconv returns false on error, fall through to preg approach
     131            if ($sanitized !== false) {
     132                // Remove null bytes and problematic control characters
     133                $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $sanitized);
     134                return $sanitized;
     135            }
     136        }
     137
     138        // Fallback: use preg_replace with 'u' modifier to validate UTF-8
     139        // The //u modifier forces UTF-8 mode - invalid sequences cause match failure
     140        // By replacing '' with '', we essentially validate and keep only valid UTF-8
     141        $sanitized = @preg_replace('//u', '', $string);
     142
     143        // If preg_replace failed (invalid UTF-8), use byte-by-byte filtering
     144        if ($sanitized === null) {
     145            // Filter out invalid UTF-8 lead bytes:
     146            // - C0, C1 (overlong 2-byte sequences)
     147            // - F5-FF (invalid lead bytes beyond UTF-8 range)
     148            // Keep valid ranges: C2-DF (2-byte), E0-EF (3-byte), F0-F4 (4-byte)
     149            $sanitized = preg_replace('/[\xC0\xC1\xF5-\xFF][\x80-\xBF]*/', '', $string);
     150
     151            // Remove incomplete sequences (continuation bytes without lead byte)
     152            $sanitized = preg_replace('/[\x80-\xBF]+/', '', $sanitized);
     153
     154            // Verify the result is now valid UTF-8 by attempting a UTF-8 match
     155            if (@preg_match('//u', $sanitized) === false) {
     156                // Still invalid - fall back to ASCII-only (safe but lossy)
     157                $sanitized = preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string);
     158            }
     159        }
     160
     161        // Remove null bytes and other problematic control characters
     162        $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $sanitized);
     163
     164        return $sanitized;
     165    }
     166
    100167}
    101168
  • 404-solution/trunk/readme.txt

    r3401351 r3401938  
    66Requires PHP: 7.4
    77Tested up to: 6.8
    8 Stable tag: 3.0.3
     8Stable tag: 3.0.4
    99License: GPL-3.0-or-later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    240240== Changelog ==
    241241
     242## Version 3.0.4 (Nov 24, 2025) ##
     243* FIX: Resolve SQL error "Could not perform query because it contains invalid data" caused by invalid UTF-8 byte sequences in URLs. Added sanitization to strip invalid UTF-8 characters before database storage.
     244* FIX: Resolve "Table doesn't exist" errors on case-sensitive MySQL installations (lower_case_table_names=0) with mixed-case WordPress prefixes. All plugin table references now use normalized lowercase prefixes to match table creation behavior.
     245
    242246= Version 3.0.3 (Nov 23, 2025) =
    243247* Improved: GDPR compliance in log files (just in case).
  • 404-solution/trunk/uninstall.php

    r3401351 r3401938  
    2828// Define version constant for use in Uninstaller
    2929// This matches the version defined in includes/Loader.php
    30 define('ABJ404_VERSION', '3.0.3');
     30define('ABJ404_VERSION', '3.0.4');
    3131
    3232// Load the Uninstaller class
Note: See TracChangeset for help on using the changeset viewer.