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 / uninstall / UninstallDatabaseMetadataReader.php

UninstallDatabaseMetadataReader.php in 404 Solution trunk, at includes/uninstall/UninstallDatabaseMetadataReader.php

231 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // allow-no-test-found: covered by tests/UninstallDiagnosticsEntryPointTest.php public uninstall modal/email entry points
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Reads per-table metadata for uninstall diagnostics. Each table probe uses
10 * the production fallback chain: information_schema, SHOW TABLE STATUS,
11 * SHOW CREATE TABLE, then wpdb defaults.
12 */
13 class ABJ_404_Solution_UninstallDatabaseMetadataReader {
14
15 /**
16 * @param string $tableName Table name to look up.
17 * @return array{charset?:string|null,collation?:string|null,engine?:string,error?:string,source?:string}
18 */
19 public function getTableInfo(string $tableName): array {
20 $result = $this->tryInformationSchema($tableName);
21 if ($result !== null && !isset($result['error'])) {
22 return $result;
23 }
24
25 $result = $this->tryShowTableStatus($tableName);
26 if ($result !== null && !isset($result['error'])) {
27 return $result;
28 }
29
30 $result = $this->tryShowCreateTable($tableName);
31 if ($result !== null && !isset($result['error'])) {
32 return $result;
33 }
34
35 return $this->getWpdbDefaults();
36 }
37
38 /**
39 * @param string $tableName Table name to look up.
40 * @return array{charset?:string|null,collation?:string|null,engine?:string,error?:string}|null
41 */
42 public function tryInformationSchema(string $tableName) {
43 /** @var \wpdb $wpdb */
44 global $wpdb;
45
46 if (!method_exists($wpdb, 'get_row')) {
47 return array('error' => 'wpdb methods unavailable');
48 }
49
50 // DAO-bypass-approved: Diagnostic table charset/collation metadata probe.
51 $query = $wpdb->prepare(
52 "SELECT TABLE_COLLATION, ENGINE, " .
53 "SUBSTRING_INDEX(TABLE_COLLATION, '_', 1) as TABLE_CHARSET " .
54 "FROM information_schema.tables " .
55 "WHERE TABLE_NAME = %s AND TABLE_SCHEMA = DATABASE()",
56 $tableName
57 );
58
59 // DAO-bypass-approved: Diagnostic information_schema.tables probe
60 $result = $wpdb->get_row($query, ARRAY_A);
61
62 if (!empty($wpdb->last_error)) {
63 if (stripos($wpdb->last_error, 'denied') !== false ||
64 stripos($wpdb->last_error, 'permission') !== false) {
65 return array('error' => 'permission denied');
66 }
67 return array('error' => 'query error');
68 }
69
70 if (empty($result) || !is_array($result)) {
71 return null;
72 }
73
74 $collation = $this->rowValue($result, 'TABLE_COLLATION');
75 $engine = $this->rowValue($result, 'ENGINE') ?: 'Unknown';
76 $charset = $this->rowValue($result, 'TABLE_CHARSET');
77
78 if (empty($charset) && !empty($collation)) {
79 $charset = explode('_', $collation)[0];
80 }
81
82 if (empty($collation)) {
83 return array('error' => 'no collation data');
84 }
85
86 return array(
87 'charset' => $charset,
88 'collation' => $collation,
89 'engine' => $engine
90 );
91 }
92
93 /**
94 * @param string $tableName Table name to look up.
95 * @return array{charset?:string|null,collation?:string|null,engine?:string,error?:string}|null
96 */
97 public function tryShowTableStatus(string $tableName) {
98 /** @var \wpdb $wpdb */
99 global $wpdb;
100
101 if (!method_exists($wpdb, 'get_row')) {
102 return array('error' => 'wpdb methods unavailable');
103 }
104
105 // DAO-bypass-approved: Diagnostic fallback metadata probe needs SHOW TABLE STATUS directly.
106 $result = $wpdb->get_row(
107 // DAO-bypass-approved: prepare() is part of diagnostic SHOW TABLE STATUS metadata probing.
108 $wpdb->prepare("SHOW TABLE STATUS LIKE %s", $tableName),
109 ARRAY_A
110 );
111
112 if (!empty($wpdb->last_error)) {
113 return array('error' => 'SHOW TABLE STATUS failed');
114 }
115
116 if (empty($result) || !is_array($result)) {
117 return null;
118 }
119
120 $collation = $this->rowValue($result, 'Collation');
121 $engine = $this->rowValue($result, 'Engine') ?: 'Unknown';
122 $charset = (is_string($collation) && $collation !== '') ? explode('_', $collation)[0] : null;
123
124 if (empty($collation)) {
125 return null;
126 }
127
128 return array(
129 'charset' => $charset,
130 'collation' => $collation,
131 'engine' => $engine
132 );
133 }
134
135 /**
136 * @param string $tableName Table name to look up.
137 * @return array{charset?:string|null,collation?:string|null,engine?:string,error?:string}|null
138 */
139 public function tryShowCreateTable(string $tableName) {
140 /** @var \wpdb $wpdb */
141 global $wpdb;
142
143 if (!is_object($wpdb) || !method_exists($wpdb, 'get_row')) {
144 return null;
145 }
146
147 // @utf8-audit: opt-out - $tableName is built from $wpdb->prefix plus
148 // 'abj404_*' constants by the uninstall flow; never user input.
149 // DAO-bypass-approved: Diagnostic last-resort SHOW CREATE TABLE charset parse
150 $result = $wpdb->get_row("SHOW CREATE TABLE `" . esc_sql($tableName) . "`", ARRAY_N);
151
152 if (empty($result[1])) {
153 return null;
154 }
155
156 $ddl = is_string($result[1]) ? $result[1] : '';
157
158 // Charset, collation and engine are all TABLE OPTIONS, which sit after
159 // the closing paren of the body. Column definitions come first in real
160 // engine output and a column may carry its own `CHARACTER SET x COLLATE
161 // y`, so a pattern run over the whole statement reports the first
162 // COLUMN's charset as the table's. This tier is the last one the
163 // uninstall diagnostics try, and what it returns is sent to the
164 // developer as the site's actual schema, so a confident wrong answer
165 // here sends a charset investigation after a fiction.
166 $tableDefault = ABJ_404_Solution_CreateTableOptionsParser::tableCharsetAndCollation($ddl);
167 if ($tableDefault === null) {
168 return null;
169 }
170
171 $charset = $tableDefault['charset'];
172 $collation = $tableDefault['collation'];
173 $engine = ABJ_404_Solution_CreateTableOptionsParser::tableEngine($ddl) ?? 'Unknown';
174
175 if ($charset && !$collation) {
176 $collation = $charset . '_general_ci';
177 }
178
179 if (empty($charset) && empty($collation)) {
180 return null;
181 }
182
183 return array(
184 'charset' => $charset ?: explode('_', $collation)[0],
185 'collation' => $collation,
186 'engine' => $engine
187 );
188 }
189
190 /**
191 * @return array{charset:string,collation:string,engine:string,source:string}
192 */
193 public function getWpdbDefaults(): array {
194 global $wpdb;
195
196 $charset = 'utf8mb4';
197 $collation = 'utf8mb4_unicode_ci';
198
199 if (isset($wpdb->charset) && !empty($wpdb->charset)) {
200 $charset = $wpdb->charset;
201 } elseif (defined('DB_CHARSET') && DB_CHARSET) {
202 $charset = DB_CHARSET;
203 }
204
205 if (isset($wpdb->collate) && !empty($wpdb->collate)) {
206 $collation = $wpdb->collate;
207 }
208
209 return array(
210 'charset' => $charset,
211 'collation' => $collation,
212 'engine' => 'Unknown',
213 'source' => 'wpdb defaults'
214 );
215 }
216
217 /**
218 * @param array<array-key, mixed> $row
219 * @param string $key
220 * @return string|null
221 */
222 private function rowValue(array $row, string $key): ?string {
223 foreach ($row as $rowKey => $value) {
224 if (is_string($rowKey) && strcasecmp($rowKey, $key) === 0) {
225 return is_string($value) ? $value : null;
226 }
227 }
228 return null;
229 }
230 }
231