PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
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 4.3.0, at includes/uninstall/UninstallDatabaseMetadataReader.php

222 lines 7.1 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 preg_match('/(?:DEFAULT\s+)?(?:CHARSET|CHARACTER\s+SET)(?:\s*=\s*|\s+)([\w\d]+)/i', $ddl, $charsetMatch);
159 preg_match('/(?:DEFAULT\s+)?COLLATE(?:\s*=\s*|\s+)([\w\d_]+)/i', $ddl, $collationMatch);
160 preg_match('/ENGINE\s*=\s*([\w]+)/i', $ddl, $engineMatch);
161
162 $charset = $charsetMatch[1] ?? null;
163 $collation = $collationMatch[1] ?? null;
164 $engine = $engineMatch[1] ?? 'Unknown';
165
166 if ($charset && !$collation) {
167 $collation = $charset . '_general_ci';
168 }
169
170 if (empty($charset) && empty($collation)) {
171 return null;
172 }
173
174 return array(
175 'charset' => $charset ?: explode('_', $collation)[0],
176 'collation' => $collation,
177 'engine' => $engine
178 );
179 }
180
181 /**
182 * @return array{charset:string,collation:string,engine:string,source:string}
183 */
184 public function getWpdbDefaults(): array {
185 global $wpdb;
186
187 $charset = 'utf8mb4';
188 $collation = 'utf8mb4_unicode_ci';
189
190 if (isset($wpdb->charset) && !empty($wpdb->charset)) {
191 $charset = $wpdb->charset;
192 } elseif (defined('DB_CHARSET') && DB_CHARSET) {
193 $charset = DB_CHARSET;
194 }
195
196 if (isset($wpdb->collate) && !empty($wpdb->collate)) {
197 $collation = $wpdb->collate;
198 }
199
200 return array(
201 'charset' => $charset,
202 'collation' => $collation,
203 'engine' => 'Unknown',
204 'source' => 'wpdb defaults'
205 );
206 }
207
208 /**
209 * @param array<array-key, mixed> $row
210 * @param string $key
211 * @return string|null
212 */
213 private function rowValue(array $row, string $key): ?string {
214 foreach ($row as $rowKey => $value) {
215 if (is_string($rowKey) && strcasecmp($rowKey, $key) === 0) {
216 return is_string($value) ? $value : null;
217 }
218 }
219 return null;
220 }
221 }
222