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 / database / DatabaseTableNameResolver.php

DatabaseTableNameResolver.php in 404 Solution 4.3.0, at includes/database/DatabaseTableNameResolver.php

185 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Resolves plugin table names and reads table schema metadata.
9 *
10 * Owns the pure, stateless half of the former DatabaseCore: expanding
11 * {wp_*} placeholders to physical prefixed names, building plugin table
12 * names from suffixes, checking table existence, reading column names and
13 * CREATE TABLE DDL, and producing option-derived SQL value lists.
14 *
15 * This class holds no error-handling state. The two methods that must run
16 * SQL (getCreateTableDDL, setSqlBigSelects) receive a query-runner callable
17 * at construction time, so the resolver depends on a function rather than
18 * on DatabaseCore itself (no cyclic coupling). The callable signature is
19 * the same as DatabaseCore::queryAndGetResults():
20 * function(string $query, array<string,mixed> $options): array<string,mixed>
21 */
22 class ABJ_404_Solution_DatabaseTableNameResolver {
23
24 /** @var ABJ_404_Solution_Functions */
25 private $f;
26
27 /** @var callable(string, array<string,mixed>): array<string,mixed> */
28 private $queryRunner;
29
30 /**
31 * @param ABJ_404_Solution_Functions $functions
32 * @param callable(string, array<string,mixed>): array<string,mixed> $queryRunner
33 * Runs a SQL query through the centralized error-handling pipeline and
34 * returns its result array. Supplied by DatabaseCore as a bound closure
35 * over queryAndGetResults() so this class needs no DatabaseCore reference.
36 */
37 public function __construct($functions, callable $queryRunner) {
38 $this->f = $functions;
39 $this->queryRunner = $queryRunner;
40 }
41
42 /**
43 * Check if a database table exists.
44 *
45 * @param string $tableName Full table name to check (including prefix)
46 * @return bool
47 */
48 public function tableExists($tableName): bool {
49 global $wpdb;
50 if (!isset($wpdb) || !is_object($wpdb) || !is_callable(array($wpdb, 'get_var'))) {
51 return false;
52 }
53 // @utf8-audit: opt-out - tableExists receives system-generated plugin table names from DAO/core callers.
54 // DAO-bypass-approved: metadata table existence probe for system-generated plugin table names.
55 $table = $wpdb->get_var("SHOW TABLES LIKE '" . esc_sql($tableName) . "'");
56 return ($table == $tableName);
57 }
58
59 /**
60 * Get the column names of an actual database table via SHOW COLUMNS.
61 *
62 * @param string $tableName Full table name (including prefix)
63 * @return array<int, string>
64 */
65 public function getTableColumnNames(string $tableName): array {
66 global $wpdb;
67 if (!isset($wpdb) || !is_object($wpdb) || !is_callable(array($wpdb, 'get_results'))) { return []; }
68 // @utf8-audit: opt-out - getTableColumnNames receives system-generated plugin table names only.
69 // DAO-bypass-approved: metadata column probe for system-generated plugin table names.
70 $rows = $wpdb->get_results("SHOW COLUMNS FROM `" . esc_sql($tableName) . "`", ARRAY_A);
71 if (!is_array($rows) || !empty($wpdb->last_error)) { return []; }
72 $columns = [];
73 foreach ($rows as $row) {
74 if (is_array($row) && isset($row['Field']) && is_scalar($row['Field'])) {
75 $columns[] = (string)$row['Field'];
76 }
77 }
78 return $columns;
79 }
80
81 /**
82 * @param string $query
83 * @return string
84 */
85 public function doTableNameReplacements($query): string {
86 global $wpdb;
87
88 $replacements = array();
89 $tables = (isset($wpdb->tables) && is_array($wpdb->tables)) ? $wpdb->tables : array();
90 $prefix = isset($wpdb->prefix) && is_scalar($wpdb->prefix) ? (string)$wpdb->prefix : 'wp_';
91 foreach ($tables as $tableName) {
92 if (!is_scalar($tableName)) { continue; }
93 $tableNameStr = (string)$tableName;
94 $replacements['{wp_' . $tableNameStr . '}'] = $prefix . $tableNameStr;
95 }
96 $replacements['{wp_users}'] = (isset($wpdb->users) && is_scalar($wpdb->users))
97 ? (string)$wpdb->users : ($prefix . 'users');
98 $replacements['{wp_prefix}'] = $prefix;
99 $replacements['{wp_prefix_lower}'] = $this->getLowercasePrefix();
100
101 $wpdbCollate = 'utf8mb4_unicode_ci';
102 if (isset($wpdb->collate) && !empty($wpdb->collate)) {
103 $sanitized = preg_replace('/[^A-Za-z0-9_]/', '', $wpdb->collate);
104 if ($sanitized !== '' && $sanitized !== null) {
105 $wpdbCollate = $sanitized;
106 }
107 }
108 $replacements['{wpdb_collate}'] = $wpdbCollate;
109
110 $query = $this->f->str_replace(array_keys($replacements), array_values($replacements), $query);
111
112 $fpreg = ABJ_404_Solution_FunctionsPreg::getInstance();
113 $query = $fpreg->regexReplace('[{]wp_abj404_(.*?)[}]',
114 $this->getLowercasePrefix() . "abj404_\\1", $query);
115
116 return $query !== null ? $query : '';
117 }
118
119 /** @return string */
120 public function getLowercasePrefix(): string {
121 global $wpdb;
122 return $this->f->strtolower($wpdb->prefix ?? 'wp_');
123 }
124
125 /**
126 * @param string $tableSuffix
127 * @return string
128 */
129 public function getPrefixedTableName($tableSuffix): string {
130 return $this->getLowercasePrefix() . ltrim($tableSuffix, '_');
131 }
132
133 /**
134 * @param string $tableName
135 * @return string
136 */
137 public function getCreateTableDDL($tableName): string {
138 $query = "show create table " . $tableName;
139 $result = ($this->queryRunner)($query, array('log_errors' => false, 'skip_repair' => true));
140 $rows = $result['rows'];
141 if (!is_array($rows) || empty($rows) || !isset($rows[0]) || !is_array($rows[0])) {
142 return '';
143 }
144 $row1 = array_values($rows[0]);
145 $existingTableSQL = $row1[1] ?? '';
146 return is_scalar($existingTableSQL) ? (string)$existingTableSQL : '';
147 }
148
149 /**
150 * @param array<string, mixed> $options
151 * @return string
152 */
153 public function buildPostTypeSqlList(array $options): string {
154 $rptVal = $options['recognized_post_types'] ?? '';
155 $postTypes = $this->f->explodeNewlineOrComma(is_string($rptVal) ? $rptVal : '');
156 $recognizedPostTypes = '';
157 foreach ($postTypes as $postType) {
158 $recognizedPostTypes .= "'" . trim($this->f->strtolower($postType)) . "', ";
159 }
160 return rtrim($recognizedPostTypes, ", ");
161 }
162
163 /**
164 * @param array<string, mixed> $options
165 * @return string
166 */
167 public function buildCategorySqlList(array $options): string {
168 $rcVal = $options['recognized_categories'] ?? '';
169 $categories = $this->f->explodeNewlineOrComma(is_string($rcVal) ? $rcVal : '');
170 $recognizedCategories = '';
171 foreach ($categories as $category) {
172 $recognizedCategories .= "'" . trim($this->f->strtolower($category)) . "', ";
173 }
174 return rtrim($recognizedCategories, ", ");
175 }
176
177 /** @return void */
178 public function setSqlBigSelects(): void {
179 $ignoreErrorsOptions = array('log_errors' => false);
180 ($this->queryRunner)("set session max_join_size = 18446744073709551615",
181 $ignoreErrorsOptions);
182 ($this->queryRunner)("set session sql_big_selects = 1", $ignoreErrorsOptions);
183 }
184 }
185