PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / database / DatabasePrefixDiagnostics.php

DatabasePrefixDiagnostics.php in 404 Solution trunk, at includes/database/DatabasePrefixDiagnostics.php

122 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Prefix mismatch and multisite cross-prefix diagnostics for DB errors.
4 */
5
6 if (!defined('ABSPATH')) {
7 exit;
8 }
9
10 // allow-no-test-found: covered through DatabaseErrorClassifier facade by tests/InfrastructureErrorClassificationTest.php and tests/MultisiteCrossPrefixErrorTest.php
11 class ABJ_404_Solution_DatabasePrefixDiagnostics {
12
13 /** @var ABJ_404_Solution_DatabaseCore */
14 private $core;
15
16 /** @var ABJ_404_Solution_Logging */
17 private $logger;
18
19 /**
20 * @param ABJ_404_Solution_DatabaseCore $core
21 * @param ABJ_404_Solution_Logging $logger
22 */
23 public function __construct(ABJ_404_Solution_DatabaseCore $core, $logger) {
24 $this->core = $core;
25 $this->logger = $logger;
26 }
27
28 /** @return string */
29 public function diagnosePrefixMismatch(): string {
30 global $wpdb;
31 try {
32 $dbName = $wpdb->dbname ?? '';
33 if ($dbName === '') {
34 return '';
35 }
36 // @utf8-audit: opt-out - $dbname comes from $wpdb internals which WP populates
37 // with a system-controlled string (the configured database name), not user input.
38 $dbNameEscaped = esc_sql((string)$dbName);
39 $dbNameStr = is_array($dbNameEscaped) ? '' : $dbNameEscaped;
40 // LIMIT 50 bounds the scan on shared-hosting databases with tens of thousands of tables
41 // (design-audit-2026-06-06 M502 / i308). The diagnostic only needs to enumerate a handful
42 // of '*abj404_redirects' tables across subsite prefixes. 50 is well above any plausible
43 // legitimate count and prevents an unbounded information_schema sweep on the degraded path.
44 // DAO-bypass-approved: read-only information_schema prefix diagnostic when a plugin table is missing.
45 $rows = $wpdb->get_results(
46 "SELECT table_name FROM information_schema.tables "
47 . "WHERE table_schema = '{$dbNameStr}' "
48 . "AND LOWER(table_name) LIKE '%abj404\_redirects' "
49 . "LIMIT 50",
50 ARRAY_A
51 );
52 if (!is_array($rows) || empty($rows)) {
53 return '';
54 }
55 $expectedTable = $this->core->tableNameResolver()->getLowercasePrefix() . 'abj404_redirects';
56 $foundTables = [];
57 foreach ($rows as $row) {
58 if (!is_iterable($row)) {
59 continue;
60 }
61 $name = null;
62 foreach ($row as $key => $value) {
63 if (strtolower((string)$key) === 'table_name') {
64 $name = (string)$value;
65 break;
66 }
67 }
68 if ($name !== null) {
69 $foundTables[] = $name;
70 }
71 }
72 $mismatched = array_filter($foundTables, function ($t) use ($expectedTable) {
73 return strtolower($t) !== strtolower($expectedTable);
74 });
75 if (empty($mismatched)) {
76 return '';
77 }
78 $msg = ', PREFIX MISMATCH DETECTED: $wpdb->prefix is "' . ($wpdb->prefix ?? '')
79 . '" (expected table: ' . $expectedTable . ') but plugin tables exist as: '
80 . implode(', ', $mismatched) . '.';
81 if (function_exists('is_multisite') && is_multisite()) {
82 $msg .= ' This is a multisite installation; the other prefixes likely belong to other subsites (normal).';
83 } else {
84 $msg .= ' Check $table_prefix in wp-config.php.';
85 }
86 return $msg;
87 } catch (Throwable $e) {
88 $this->logger->debugMessage(__METHOD__ . ': prefix diagnostic failed while preserving original DB error.', $e);
89 return '';
90 }
91 }
92
93 /**
94 * Detect whether a missing-table error references a different multisite
95 * subsite's prefix.
96 *
97 * @param string $errorText
98 * @return bool
99 */
100 public function isMultisiteCrossPrefixError(string $errorText): bool {
101 if ($errorText === '' || !function_exists('is_multisite') || !is_multisite()) {
102 return false;
103 }
104
105 global $wpdb;
106 if (!preg_match("/['\x60](?:[^'\x60]+\.)?([^'\x60]*abj404_[^'\x60]+)['\x60]/i", $errorText, $matches)) {
107 return false;
108 }
109 $referencedTable = strtolower($matches[1]);
110
111 $currentPrefix = strtolower($wpdb->prefix ?? 'wp_');
112 $basePrefix = strtolower($wpdb->base_prefix ?? 'wp_');
113
114 if (strpos($referencedTable, $currentPrefix . 'abj404_') === 0) {
115 return false;
116 }
117
118 $pattern = '/^' . preg_quote($basePrefix, '/') . '(\d+)_abj404_/';
119 return preg_match($pattern, $referencedTable) === 1;
120 }
121 }
122