Changeset 3401938
- Timestamp:
- 11/24/2025 02:47:33 PM (4 months ago)
- Location:
- 404-solution
- Files:
-
- 32 edited
- 1 copied
-
tags/3.0.4 (copied) (copied from 404-solution/trunk)
-
tags/3.0.4/404-solution.php (modified) (1 diff)
-
tags/3.0.4/CHANGELOG.md (modified) (1 diff)
-
tags/3.0.4/README.md (modified) (1 diff)
-
tags/3.0.4/includes/DataAccess.php (modified) (5 diffs)
-
tags/3.0.4/includes/DatabaseUpgradesEtc.php (modified) (10 diffs)
-
tags/3.0.4/includes/Functions.php (modified) (1 diff)
-
tags/3.0.4/includes/Loader.php (modified) (1 diff)
-
tags/3.0.4/includes/Logging.php (modified) (1 diff)
-
tags/3.0.4/includes/NGramFilter.php (modified) (11 diffs)
-
tags/3.0.4/includes/PluginLogic.php (modified) (2 diffs)
-
tags/3.0.4/includes/UninstallModal.php (modified) (1 diff)
-
tags/3.0.4/includes/Uninstaller.php (modified) (1 diff)
-
tags/3.0.4/includes/php/FunctionsMBString.php (modified) (1 diff)
-
tags/3.0.4/includes/php/FunctionsPreg.php (modified) (1 diff)
-
tags/3.0.4/readme.txt (modified) (2 diffs)
-
tags/3.0.4/uninstall.php (modified) (1 diff)
-
trunk/404-solution.php (modified) (1 diff)
-
trunk/CHANGELOG.md (modified) (1 diff)
-
trunk/README.md (modified) (1 diff)
-
trunk/includes/DataAccess.php (modified) (5 diffs)
-
trunk/includes/DatabaseUpgradesEtc.php (modified) (10 diffs)
-
trunk/includes/Functions.php (modified) (1 diff)
-
trunk/includes/Loader.php (modified) (1 diff)
-
trunk/includes/Logging.php (modified) (1 diff)
-
trunk/includes/NGramFilter.php (modified) (11 diffs)
-
trunk/includes/PluginLogic.php (modified) (2 diffs)
-
trunk/includes/UninstallModal.php (modified) (1 diff)
-
trunk/includes/Uninstaller.php (modified) (1 diff)
-
trunk/includes/php/FunctionsMBString.php (modified) (1 diff)
-
trunk/includes/php/FunctionsPreg.php (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
404-solution/tags/3.0.4/404-solution.php
r3401351 r3401938 8 8 Author URI: https://www.ajexperience.com/404-solution/ 9 9 10 Version: 3.0. 310 Version: 3.0.4 11 11 Requires at least: 5.0 12 12 Requires PHP: 7.4 -
404-solution/tags/3.0.4/CHANGELOG.md
r3401351 r3401938 1 1 # Changelog # 2 2 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 3 7 ## 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). 5 9 * Improved: Some missing translation keys. 6 10 * Improved: Deactivation feedback. -
404-solution/tags/3.0.4/README.md
r3401351 r3401938 81 81 ## Changelog ## 82 82 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 83 87 ## Version 3.0.3 (Nov 23, 2025) ## 84 88 * Improved: GDPR compliance in log files (just in case). -
404-solution/tags/3.0.4/includes/DataAccess.php
r3400707 r3401938 229 229 $replacements['{wp_users}'] = $wpdb->users; 230 230 $replacements['{wp_prefix}'] = $wpdb->prefix; 231 $replacements['{wp_prefix_lower}'] = $this-> f->strtolower($wpdb->prefix);231 $replacements['{wp_prefix_lower}'] = $this->getLowercasePrefix(); 232 232 233 233 // wp database table replacements … … 238 238 $fpreg = ABJ_404_Solution_FunctionsPreg::getInstance(); 239 239 $query = $fpreg->regexReplace('[{]wp_abj404_(.*?)[}]', 240 strtolower($wpdb->prefix) . "abj404_\\1", $query);240 $this->getLowercasePrefix() . "abj404_\\1", $query); 241 241 242 242 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, '_'); 243 265 } 244 266 … … 645 667 function storeSpellingPermalinksToCache($requestedURLRaw, $returnValue) { 646 668 $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); 648 676 $query = $this->f->str_replace('{matchdata}', esc_sql(json_encode($returnValue)), $query); 649 677 … … 1283 1311 // Fetch all logs data in a single batch query 1284 1312 $placeholders = implode(',', array_fill(0, count($urls), '%s')); 1313 $logsTable = $this->getPrefixedTableName('abj404_logsv2'); 1285 1314 $query = $wpdb->prepare( 1286 1315 "SELECT requested_url, … … 1288 1317 MAX(timestamp) AS last_used, 1289 1318 COUNT(requested_url) AS logshits 1290 FROM {$ wpdb->prefix}abj404_logsv21319 FROM {$logsTable} 1291 1320 WHERE requested_url IN ($placeholders) 1292 1321 GROUP BY requested_url", -
404-solution/tags/3.0.4/includes/DatabaseUpgradesEtc.php
r3400945 r3401938 999 999 1000 1000 $startTime = microtime(true); 1001 $redirectsTable = $ wpdb->prefix . 'abj404_redirects';1001 $redirectsTable = $this->dao->getPrefixedTableName('abj404_redirects'); 1002 1002 1003 1003 $abj404logging->infoMessage("Migrating redirects table to relative paths..."); … … 1358 1358 1359 1359 // Count pages for THIS site only 1360 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';1360 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 1361 1361 $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 1362 1362 … … 1457 1457 // SINGLE SITE: Use original simple logic 1458 1458 $offset = $this->getNetworkAwareOption('abj404_ngram_rebuild_offset', 0); 1459 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';1459 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 1460 1460 $totalPages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 1461 1461 … … 1602 1602 1603 1603 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'); 1606 1606 1607 1607 // Check if cache is already populated (unless force rebuild) … … 1718 1718 1719 1719 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'); 1722 1722 1723 1723 $stats = ['posts_added' => 0, 'posts_failed' => 0, 'categories_added' => 0, 'categories_failed' => 0]; … … 1838 1838 global $wpdb; 1839 1839 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'); 1842 1842 1843 1843 $this->logger->debugMessage("Checking for orphaned ngram entries..."); … … 1994 1994 1995 1995 $missingTables = []; 1996 $normalizedPrefix = $this->dao->getLowercasePrefix(); 1996 1997 1997 1998 // Check each required table 1998 1999 foreach ($requiredTables as $tableName) { 1999 $fullTableName = $ wpdb->prefix . $tableName;2000 $fullTableName = $this->dao->getPrefixedTableName($tableName); 2000 2001 $tableExists = $wpdb->get_var("SHOW TABLES LIKE '{$fullTableName}'"); 2001 2002 … … 2008 2009 if (!empty($missingTables)) { 2009 2010 $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...", 2011 2012 get_current_blog_id(), 2012 2013 $wpdb->prefix, 2014 $normalizedPrefix, 2013 2015 count($missingTables), 2014 2016 implode(', ', $missingTables) … … 2334 2336 if (!$this->isNetworkActivated()) { 2335 2337 // Single site: count only current site's pages 2336 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';2338 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 2337 2339 return (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 2338 2340 } … … 2344 2346 foreach ($sites as $blog_id) { 2345 2347 switch_to_blog($blog_id); 2346 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';2348 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 2347 2349 $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 2348 2350 $totalPages += $sitePages; -
404-solution/tags/3.0.4/includes/Functions.php
r3399802 r3401938 232 232 233 233 abstract function regexReplace($pattern, $replacement, $string); 234 234 235 abstract function sanitizeInvalidUTF8($string); 236 235 237 /** Used with array_filter() 236 238 * @param string $value -
404-solution/tags/3.0.4/includes/Loader.php
r3401351 r3401938 11 11 basename(dirname(ABJ404_FILE)) . '/' . basename(ABJ404_FILE)); 12 12 13 define( 'ABJ404_VERSION', '3.0. 3' );13 define( 'ABJ404_VERSION', '3.0.4' ); 14 14 define( 'URL_TRACKING_SUFFIX', '?utm_source=404SolutionPlugin&utm_medium=WordPress'); 15 15 define( 'ABJ404_HOME_URL', 'https://www.ajexperience.com/404-solution/' . URL_TRACKING_SUFFIX); -
404-solution/tags/3.0.4/includes/Logging.php
r3401351 r3401938 292 292 $bodyLines[] = "MySQL version: " . $wpdb->db_version(); 293 293 $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 } 294 298 $bodyLines[] = "WP_MEMORY_LIMIT: " . WP_MEMORY_LIMIT; 295 299 $bodyLines[] = "Extensions: " . implode(", ", get_loaded_extensions()); -
404-solution/tags/3.0.4/includes/NGramFilter.php
r3400707 r3401938 193 193 $ngramCount = count($ngrams['bi']) + count($ngrams['tri']); 194 194 195 $table = $ wpdb->prefix . 'abj404_ngram_cache';195 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 196 196 197 197 // Use REPLACE to handle updates (REPLACE = DELETE + INSERT) … … 217 217 $wpdb->last_error, 218 218 $table, 219 $ wpdb->prefix,219 $this->dao->getLowercasePrefix(), 220 220 $wpdb->dbname 221 221 ); … … 243 243 global $wpdb; 244 244 245 $table = $ wpdb->prefix . 'abj404_ngram_cache';245 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 246 246 $query = $wpdb->prepare( 247 247 "SELECT ngrams FROM {$table} WHERE id = %d AND type = %s", … … 271 271 global $wpdb; 272 272 273 $table = $ wpdb->prefix . 'abj404_ngram_cache';273 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 274 274 275 275 // Check cache size first - abort if too large … … 318 318 global $wpdb; 319 319 320 $table = $ wpdb->prefix . 'abj404_ngram_cache';320 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 321 321 322 322 // Database-side filtering by ngram_count range … … 359 359 global $wpdb; 360 360 361 $table = $ wpdb->prefix . 'abj404_ngram_cache';361 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 362 362 $result = $wpdb->delete($table, ['id' => $pageId, 'type' => $type], ['%d', '%s']); 363 363 … … 380 380 381 381 global $wpdb; 382 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';382 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 383 383 384 384 // Prepare IN clause for page IDs … … 445 445 global $wpdb; 446 446 447 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';447 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 448 448 449 449 // Get a batch of pages from permalink cache … … 542 542 543 543 // Check cache size to determine strategy 544 $table = $ wpdb->prefix . 'abj404_ngram_cache';544 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 545 545 $totalCount = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); 546 546 … … 645 645 global $wpdb; 646 646 647 $table = $ wpdb->prefix . 'abj404_ngram_cache';647 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 648 648 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); 649 649 … … 659 659 global $wpdb; 660 660 661 $table = $ wpdb->prefix . 'abj404_ngram_cache';661 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 662 662 663 663 $stats = [ -
404-solution/tags/3.0.4/includes/PluginLogic.php
r3400707 r3401938 680 680 ' log rows were migrated to the new table structre.'); 681 681 // 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'); 683 683 } 684 684 } … … 1004 1004 1005 1005 global $wpdb; 1006 $dao = ABJ_404_Solution_DataAccess::getInstance(); 1007 $prefix = $dao->getLowercasePrefix(); 1006 1008 1007 1009 // Remove ALL custom database tables 1008 1010 // 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"); 1012 1014 1013 1015 // 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"); 1017 1019 1018 1020 // 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"); 1020 1022 1021 1023 // Remove ALL plugin options -
404-solution/tags/3.0.4/includes/UninstallModal.php
r3401351 r3401938 477 477 private static function getRedirectCount() { 478 478 global $wpdb; 479 $table_name = $wpdb->prefix . 'abj404_redirects'; 479 $dao = ABJ_404_Solution_DataAccess::getInstance(); 480 $table_name = $dao->getPrefixedTableName('abj404_redirects'); 480 481 481 482 // 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; 483 484 484 485 if (!$table_exists) { -
404-solution/tags/3.0.4/includes/Uninstaller.php
r3401351 r3401938 73 73 */ 74 74 private static function deleteTables($wpdb, $preferences) { 75 $prefix = $wpdb->prefix; 75 $dao = ABJ_404_Solution_DataAccess::getInstance(); 76 $prefix = $dao->getLowercasePrefix(); 76 77 77 78 // Delete redirect table if user chose to -
404-solution/tags/3.0.4/includes/php/FunctionsMBString.php
r3399802 r3401938 49 49 return mb_ereg_replace($pattern, $replacement, $string); 50 50 } 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 52 86 } 53 87 -
404-solution/tags/3.0.4/includes/php/FunctionsPreg.php
r3190969 r3401938 98 98 } 99 99 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 100 167 } 101 168 -
404-solution/tags/3.0.4/readme.txt
r3401351 r3401938 6 6 Requires PHP: 7.4 7 7 Tested up to: 6.8 8 Stable tag: 3.0. 38 Stable tag: 3.0.4 9 9 License: GPL-3.0-or-later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 240 240 == Changelog == 241 241 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 242 246 = Version 3.0.3 (Nov 23, 2025) = 243 247 * Improved: GDPR compliance in log files (just in case). -
404-solution/tags/3.0.4/uninstall.php
r3401351 r3401938 28 28 // Define version constant for use in Uninstaller 29 29 // This matches the version defined in includes/Loader.php 30 define('ABJ404_VERSION', '3.0. 3');30 define('ABJ404_VERSION', '3.0.4'); 31 31 32 32 // Load the Uninstaller class -
404-solution/trunk/404-solution.php
r3401351 r3401938 8 8 Author URI: https://www.ajexperience.com/404-solution/ 9 9 10 Version: 3.0. 310 Version: 3.0.4 11 11 Requires at least: 5.0 12 12 Requires PHP: 7.4 -
404-solution/trunk/CHANGELOG.md
r3401351 r3401938 1 1 # Changelog # 2 2 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 3 7 ## 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). 5 9 * Improved: Some missing translation keys. 6 10 * Improved: Deactivation feedback. -
404-solution/trunk/README.md
r3401351 r3401938 81 81 ## Changelog ## 82 82 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 83 87 ## Version 3.0.3 (Nov 23, 2025) ## 84 88 * Improved: GDPR compliance in log files (just in case). -
404-solution/trunk/includes/DataAccess.php
r3400707 r3401938 229 229 $replacements['{wp_users}'] = $wpdb->users; 230 230 $replacements['{wp_prefix}'] = $wpdb->prefix; 231 $replacements['{wp_prefix_lower}'] = $this-> f->strtolower($wpdb->prefix);231 $replacements['{wp_prefix_lower}'] = $this->getLowercasePrefix(); 232 232 233 233 // wp database table replacements … … 238 238 $fpreg = ABJ_404_Solution_FunctionsPreg::getInstance(); 239 239 $query = $fpreg->regexReplace('[{]wp_abj404_(.*?)[}]', 240 strtolower($wpdb->prefix) . "abj404_\\1", $query);240 $this->getLowercasePrefix() . "abj404_\\1", $query); 241 241 242 242 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, '_'); 243 265 } 244 266 … … 645 667 function storeSpellingPermalinksToCache($requestedURLRaw, $returnValue) { 646 668 $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); 648 676 $query = $this->f->str_replace('{matchdata}', esc_sql(json_encode($returnValue)), $query); 649 677 … … 1283 1311 // Fetch all logs data in a single batch query 1284 1312 $placeholders = implode(',', array_fill(0, count($urls), '%s')); 1313 $logsTable = $this->getPrefixedTableName('abj404_logsv2'); 1285 1314 $query = $wpdb->prepare( 1286 1315 "SELECT requested_url, … … 1288 1317 MAX(timestamp) AS last_used, 1289 1318 COUNT(requested_url) AS logshits 1290 FROM {$ wpdb->prefix}abj404_logsv21319 FROM {$logsTable} 1291 1320 WHERE requested_url IN ($placeholders) 1292 1321 GROUP BY requested_url", -
404-solution/trunk/includes/DatabaseUpgradesEtc.php
r3400945 r3401938 999 999 1000 1000 $startTime = microtime(true); 1001 $redirectsTable = $ wpdb->prefix . 'abj404_redirects';1001 $redirectsTable = $this->dao->getPrefixedTableName('abj404_redirects'); 1002 1002 1003 1003 $abj404logging->infoMessage("Migrating redirects table to relative paths..."); … … 1358 1358 1359 1359 // Count pages for THIS site only 1360 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';1360 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 1361 1361 $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 1362 1362 … … 1457 1457 // SINGLE SITE: Use original simple logic 1458 1458 $offset = $this->getNetworkAwareOption('abj404_ngram_rebuild_offset', 0); 1459 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';1459 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 1460 1460 $totalPages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 1461 1461 … … 1602 1602 1603 1603 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'); 1606 1606 1607 1607 // Check if cache is already populated (unless force rebuild) … … 1718 1718 1719 1719 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'); 1722 1722 1723 1723 $stats = ['posts_added' => 0, 'posts_failed' => 0, 'categories_added' => 0, 'categories_failed' => 0]; … … 1838 1838 global $wpdb; 1839 1839 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'); 1842 1842 1843 1843 $this->logger->debugMessage("Checking for orphaned ngram entries..."); … … 1994 1994 1995 1995 $missingTables = []; 1996 $normalizedPrefix = $this->dao->getLowercasePrefix(); 1996 1997 1997 1998 // Check each required table 1998 1999 foreach ($requiredTables as $tableName) { 1999 $fullTableName = $ wpdb->prefix . $tableName;2000 $fullTableName = $this->dao->getPrefixedTableName($tableName); 2000 2001 $tableExists = $wpdb->get_var("SHOW TABLES LIKE '{$fullTableName}'"); 2001 2002 … … 2008 2009 if (!empty($missingTables)) { 2009 2010 $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...", 2011 2012 get_current_blog_id(), 2012 2013 $wpdb->prefix, 2014 $normalizedPrefix, 2013 2015 count($missingTables), 2014 2016 implode(', ', $missingTables) … … 2334 2336 if (!$this->isNetworkActivated()) { 2335 2337 // Single site: count only current site's pages 2336 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';2338 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 2337 2339 return (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 2338 2340 } … … 2344 2346 foreach ($sites as $blog_id) { 2345 2347 switch_to_blog($blog_id); 2346 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';2348 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 2347 2349 $sitePages = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$permalinkCacheTable}"); 2348 2350 $totalPages += $sitePages; -
404-solution/trunk/includes/Functions.php
r3399802 r3401938 232 232 233 233 abstract function regexReplace($pattern, $replacement, $string); 234 234 235 abstract function sanitizeInvalidUTF8($string); 236 235 237 /** Used with array_filter() 236 238 * @param string $value -
404-solution/trunk/includes/Loader.php
r3401351 r3401938 11 11 basename(dirname(ABJ404_FILE)) . '/' . basename(ABJ404_FILE)); 12 12 13 define( 'ABJ404_VERSION', '3.0. 3' );13 define( 'ABJ404_VERSION', '3.0.4' ); 14 14 define( 'URL_TRACKING_SUFFIX', '?utm_source=404SolutionPlugin&utm_medium=WordPress'); 15 15 define( 'ABJ404_HOME_URL', 'https://www.ajexperience.com/404-solution/' . URL_TRACKING_SUFFIX); -
404-solution/trunk/includes/Logging.php
r3401351 r3401938 292 292 $bodyLines[] = "MySQL version: " . $wpdb->db_version(); 293 293 $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 } 294 298 $bodyLines[] = "WP_MEMORY_LIMIT: " . WP_MEMORY_LIMIT; 295 299 $bodyLines[] = "Extensions: " . implode(", ", get_loaded_extensions()); -
404-solution/trunk/includes/NGramFilter.php
r3400707 r3401938 193 193 $ngramCount = count($ngrams['bi']) + count($ngrams['tri']); 194 194 195 $table = $ wpdb->prefix . 'abj404_ngram_cache';195 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 196 196 197 197 // Use REPLACE to handle updates (REPLACE = DELETE + INSERT) … … 217 217 $wpdb->last_error, 218 218 $table, 219 $ wpdb->prefix,219 $this->dao->getLowercasePrefix(), 220 220 $wpdb->dbname 221 221 ); … … 243 243 global $wpdb; 244 244 245 $table = $ wpdb->prefix . 'abj404_ngram_cache';245 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 246 246 $query = $wpdb->prepare( 247 247 "SELECT ngrams FROM {$table} WHERE id = %d AND type = %s", … … 271 271 global $wpdb; 272 272 273 $table = $ wpdb->prefix . 'abj404_ngram_cache';273 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 274 274 275 275 // Check cache size first - abort if too large … … 318 318 global $wpdb; 319 319 320 $table = $ wpdb->prefix . 'abj404_ngram_cache';320 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 321 321 322 322 // Database-side filtering by ngram_count range … … 359 359 global $wpdb; 360 360 361 $table = $ wpdb->prefix . 'abj404_ngram_cache';361 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 362 362 $result = $wpdb->delete($table, ['id' => $pageId, 'type' => $type], ['%d', '%s']); 363 363 … … 380 380 381 381 global $wpdb; 382 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';382 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 383 383 384 384 // Prepare IN clause for page IDs … … 445 445 global $wpdb; 446 446 447 $permalinkCacheTable = $ wpdb->prefix . 'abj404_permalink_cache';447 $permalinkCacheTable = $this->dao->getPrefixedTableName('abj404_permalink_cache'); 448 448 449 449 // Get a batch of pages from permalink cache … … 542 542 543 543 // Check cache size to determine strategy 544 $table = $ wpdb->prefix . 'abj404_ngram_cache';544 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 545 545 $totalCount = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); 546 546 … … 645 645 global $wpdb; 646 646 647 $table = $ wpdb->prefix . 'abj404_ngram_cache';647 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 648 648 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); 649 649 … … 659 659 global $wpdb; 660 660 661 $table = $ wpdb->prefix . 'abj404_ngram_cache';661 $table = $this->dao->getPrefixedTableName('abj404_ngram_cache'); 662 662 663 663 $stats = [ -
404-solution/trunk/includes/PluginLogic.php
r3400707 r3401938 680 680 ' log rows were migrated to the new table structre.'); 681 681 // 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'); 683 683 } 684 684 } … … 1004 1004 1005 1005 global $wpdb; 1006 $dao = ABJ_404_Solution_DataAccess::getInstance(); 1007 $prefix = $dao->getLowercasePrefix(); 1006 1008 1007 1009 // Remove ALL custom database tables 1008 1010 // 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"); 1012 1014 1013 1015 // 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"); 1017 1019 1018 1020 // 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"); 1020 1022 1021 1023 // Remove ALL plugin options -
404-solution/trunk/includes/UninstallModal.php
r3401351 r3401938 477 477 private static function getRedirectCount() { 478 478 global $wpdb; 479 $table_name = $wpdb->prefix . 'abj404_redirects'; 479 $dao = ABJ_404_Solution_DataAccess::getInstance(); 480 $table_name = $dao->getPrefixedTableName('abj404_redirects'); 480 481 481 482 // 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; 483 484 484 485 if (!$table_exists) { -
404-solution/trunk/includes/Uninstaller.php
r3401351 r3401938 73 73 */ 74 74 private static function deleteTables($wpdb, $preferences) { 75 $prefix = $wpdb->prefix; 75 $dao = ABJ_404_Solution_DataAccess::getInstance(); 76 $prefix = $dao->getLowercasePrefix(); 76 77 77 78 // Delete redirect table if user chose to -
404-solution/trunk/includes/php/FunctionsMBString.php
r3399802 r3401938 49 49 return mb_ereg_replace($pattern, $replacement, $string); 50 50 } 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 52 86 } 53 87 -
404-solution/trunk/includes/php/FunctionsPreg.php
r3190969 r3401938 98 98 } 99 99 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 100 167 } 101 168 -
404-solution/trunk/readme.txt
r3401351 r3401938 6 6 Requires PHP: 7.4 7 7 Tested up to: 6.8 8 Stable tag: 3.0. 38 Stable tag: 3.0.4 9 9 License: GPL-3.0-or-later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 240 240 == Changelog == 241 241 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 242 246 = Version 3.0.3 (Nov 23, 2025) = 243 247 * Improved: GDPR compliance in log files (just in case). -
404-solution/trunk/uninstall.php
r3401351 r3401938 28 28 // Define version constant for use in Uninstaller 29 29 // This matches the version defined in includes/Loader.php 30 define('ABJ404_VERSION', '3.0. 3');30 define('ABJ404_VERSION', '3.0.4'); 31 31 32 32 // Load the Uninstaller class
Note: See TracChangeset
for help on using the changeset viewer.