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 / DatabaseErrorTableInspector.php

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

150 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Table-name parsing and metadata probes for database error handling.
4 */
5
6 if (!defined('ABSPATH')) {
7 exit;
8 }
9
10 // allow-no-test-found: covered through DatabaseErrorClassifier facade by tests/MissingTableErrorDoubleReportingTest.php and tests/DataAccessRetrySemanticsTest.php
11 class ABJ_404_Solution_DatabaseErrorTableInspector {
12
13 /** @var ABJ_404_Solution_Logging */
14 private $logger;
15
16 /** @var (callable(string): bool)|null */
17 private $confirmedTableAbsenceProbe;
18
19 /**
20 * @param ABJ_404_Solution_Logging $logger
21 * @param (callable(string): bool)|null $confirmedTableAbsenceProbe
22 * Returns true only when the database positively confirms absence.
23 */
24 public function __construct($logger, $confirmedTableAbsenceProbe = null) {
25 $this->logger = $logger;
26 $this->confirmedTableAbsenceProbe = is_callable($confirmedTableAbsenceProbe)
27 ? $confirmedTableAbsenceProbe : null;
28 }
29
30 /**
31 * Extract a table name from a MySQL "table is full" error message.
32 *
33 * @param string $errorText
34 * @return string|null
35 */
36 public function extractTableNameFromFullError(string $errorText): ?string {
37 if (preg_match("/table '([^']+)' is full/i", $errorText, $m)) {
38 return $m[1];
39 }
40 return null;
41 }
42
43 /**
44 * Extract the table name from a MySQL "doesn't exist" error message.
45 *
46 * @param string $errorText
47 * @return string
48 */
49 public function extractMissingTableNameFromError(string $errorText): string {
50 if ($errorText === '') {
51 return '';
52 }
53 if (!preg_match("/Table '([^']+)' doesn't exist/i", $errorText, $matches)) {
54 return '';
55 }
56 $fullName = $matches[1];
57 $dotPos = strrpos($fullName, '.');
58 return $dotPos !== false ? substr($fullName, $dotPos + 1) : $fullName;
59 }
60
61 /**
62 * Whether WordPress recognizes the missing table and the database also
63 * positively confirms that WordPress's authoritative name is absent.
64 *
65 * A failed query against a generated prefix remains a plugin ERROR when
66 * WordPress's own table exists: that means our name resolution drifted.
67 * An inconclusive SHOW TABLES probe also remains an ERROR because failure
68 * to prove presence is not evidence of absence.
69 *
70 * @param string $errorText
71 * @return bool
72 */
73 public function isConfirmedMissingWordPressTableError(string $errorText): bool {
74 if ($this->confirmedTableAbsenceProbe === null) {
75 return false;
76 }
77
78 $missingTable = $this->extractMissingTableNameFromError($errorText);
79 if ($missingTable === '') {
80 return false;
81 }
82
83 global $wpdb;
84 if (!isset($wpdb) || !is_object($wpdb)) {
85 return false;
86 }
87
88 $tableSuffixes = isset($wpdb->tables) && is_array($wpdb->tables)
89 ? $wpdb->tables : array();
90 $tableSuffixes[] = 'users';
91 $prefix = isset($wpdb->prefix) && is_scalar($wpdb->prefix)
92 ? (string)$wpdb->prefix : '';
93
94 foreach (array_unique($tableSuffixes, SORT_REGULAR) as $tableSuffix) {
95 if (!is_scalar($tableSuffix) || (string)$tableSuffix === '') {
96 continue;
97 }
98 $property = (string)$tableSuffix;
99 if (!isset($wpdb->{$property}) || !is_scalar($wpdb->{$property})) {
100 continue;
101 }
102 $wordpressTable = (string)$wpdb->{$property};
103 if ($wordpressTable === '') {
104 continue;
105 }
106 $generatedTable = $prefix . $property;
107 if ($missingTable !== $generatedTable && $missingTable !== $wordpressTable) {
108 continue;
109 }
110 return (bool)call_user_func($this->confirmedTableAbsenceProbe, $wordpressTable);
111 }
112
113 return false;
114 }
115
116 /**
117 * Check if a given table uses the InnoDB storage engine.
118 *
119 * @param string $tableName
120 * @return bool
121 */
122 public function isInnoDBTable(string $tableName): bool {
123 global $wpdb;
124 /** @var wpdb $wpdb */
125 if (!is_object($wpdb) || !method_exists($wpdb, 'get_var') || !method_exists($wpdb, 'prepare')) {
126 return false;
127 }
128 if (defined('DB_NAME')) {
129 $dbName = (string)DB_NAME;
130 } else {
131 static $warnedNoDbName = false;
132 if (!$warnedNoDbName) {
133 $warnedNoDbName = true;
134 $this->logger->warn(__METHOD__ . ': DB_NAME undefined; using empty schema in InnoDB probe');
135 }
136 $dbName = '';
137 }
138 // DAO-bypass-approved: read-only information_schema engine probe for classifying table-full DB errors.
139 $engine = $wpdb->get_var(
140 // DAO-bypass-approved: prepare call for read-only information_schema engine probe placeholders.
141 $wpdb->prepare(
142 "SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
143 $dbName,
144 $tableName
145 )
146 );
147 return is_string($engine) && strtolower($engine) === 'innodb';
148 }
149 }
150