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 / upgrades / DatabaseTableLowercaseRenamer.php

DatabaseTableLowercaseRenamer.php in 404 Solution trunk, at includes/database/upgrades/DatabaseTableLowercaseRenamer.php

186 lines 8.3 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 * Lowercase-rename maintenance operation for plugin tables.
9 *
10 * Owns making all plugin table names lowercase (for hosts running with
11 * lower_case_table_names=0), and detecting/adopting orphaned plugin tables
12 * left under old prefixes (from site migrations or the rename bug in
13 * v2.35.16 through v3.x). This is a prefix-based, tenant-isolation-aware
14 * flow: on multisite, a sibling subsite owns and lowercases its own tables,
15 * so a table under a DIFFERENT active blog's prefix is left alone; a
16 * genuinely orphaned prefix (no live blog) is renamed and handed to the
17 * orphan-adoption collaborator to recover.
18 *
19 * Extracted from the table-bootstrap orchestrator (DatabaseUpgradeBootstrap)
20 * because table-bootstrap orchestration and the lowercase-rename maintenance
21 * operation are distinct concerns: this one runs unconditionally on every
22 * createDatabaseTables() pass, has its own MySQL-variable-driven skip path,
23 * and reaches into information_schema directly rather than the plugin's own
24 * tables.
25 *
26 * Collaborator of ABJ_404_Solution_DatabaseUpgradeBootstrap, which constructs
27 * it fresh on every call (never cached) with the upgrade coordinator, the
28 * current DB core / logger, and a pre-computed active-blog-prefix set, so it
29 * always observes whichever dependencies are current at call time (dbCore /
30 * logger can be swapped at runtime via replaceDatabaseUpgradeDependencies()
31 * on the owning component).
32 */
33 class ABJ_404_Solution_DatabaseTableLowercaseRenamer {
34
35 /** @var ABJ_404_Solution_DatabaseUpgradeCoordinator */
36 private $coordinator;
37
38 /** @var ABJ_404_Solution_DatabaseCore */
39 private $dbCore;
40
41 /** @var ABJ_404_Solution_Logging */
42 private $logger;
43
44 /** @var array<int, string> */
45 private $activeBlogPrefixesLowercase;
46
47 /**
48 * @param ABJ_404_Solution_DatabaseUpgradeCoordinator $coordinator
49 * @param ABJ_404_Solution_DatabaseCore $dbCore Deliberately untyped at the
50 * PHP level (matching ABJ_404_Solution_DatabaseUpgradeComponent::$dbCore):
51 * this collaborator needs methods from both DatabaseCoreInterface
52 * (tableNameResolver) and DatabaseQueryInterface (queryAndGetResults),
53 * and PHP 7.4 (the plugin's floor version) has no intersection types.
54 * Test doubles (e.g. Abj404NullDatabaseCore) implement those interfaces
55 * without extending the concrete class, so a native type hint here would
56 * break them.
57 * @param ABJ_404_Solution_Logging $logger
58 * @param array<int, string> $activeBlogPrefixesLowercase Lowercased table
59 * prefixes of every active site on the network (or just the current
60 * prefix on single-site), computed once by the caller before
61 * construction since it's a pure read with no per-call state.
62 */
63 public function __construct(ABJ_404_Solution_DatabaseUpgradeCoordinator $coordinator,
64 $dbCore, ABJ_404_Solution_Logging $logger,
65 array $activeBlogPrefixesLowercase) {
66 $this->coordinator = $coordinator;
67 $this->dbCore = $dbCore;
68 $this->logger = $logger;
69 $this->activeBlogPrefixesLowercase = $activeBlogPrefixesLowercase;
70 }
71
72 /** Makes all plugin table names lowercase, in case someone thought it was funny to use
73 * the lower_case_table_names=0 setting. Also detects and adopts orphaned plugin tables
74 * under old prefixes (from site migrations or the rename bug in v2.35.16 through v3.x).
75 * @return void
76 */
77 public function rename() {
78 global $wpdb;
79
80 // On case-insensitive MySQL (lower_case_table_names >= 1), table names
81 // are already treated as lowercase internally. Renaming is pointless and
82 // can cause issues on some hosting setups.
83 // DAO-bypass-approved: Schema-bootstrap inside renameAbj404TablesToLowerCase(). Runs before plugin DAO is fully wired during DB upgrades.
84 $lctnResult = $wpdb->get_row("SHOW VARIABLES LIKE 'lower_case_table_names'", ARRAY_A);
85 if (is_array($lctnResult)) {
86 $lctnValue = null;
87 foreach ($lctnResult as $key => $value) {
88 if (strtolower((string)$key) === 'value') {
89 $lctnValue = $value;
90 break;
91 }
92 }
93 if (is_scalar($lctnValue) && (int)$lctnValue >= 1) {
94 // MySQL already handles table names case-insensitively.
95 // Still run adoption check in case of prefix mismatch.
96 $this->coordinator->orphanAdoptionUpgrade()->adoptOrphanedTables();
97 return;
98 }
99 }
100
101 // Fetch all tables containing "abj404", case-insensitive
102 $dbNameRaw = $wpdb->dbname ?? '';
103 if ($dbNameRaw === '') {
104 $this->logger->warn("Could not determine database name for lowercase rename.");
105 return;
106 }
107 $dbNameEscaped = esc_sql($dbNameRaw);
108 $dbName = is_array($dbNameEscaped) ? '' : $dbNameEscaped;
109 $query = "SELECT table_name
110 FROM information_schema.tables
111 WHERE table_schema = '{$dbName}'
112 AND LOWER(table_name) LIKE '%abj404%'";
113 $results = $this->dbCore->queryAndGetResults($query);
114
115 if (!is_array($results['rows'])) {
116 $this->logger->warn("Could not query information_schema tables for lowercase rename.");
117 return;
118 }
119
120 // Tenant isolation: a sibling subsite owns and lowercases its own
121 // tables. Never rename tables that belong to a DIFFERENT active
122 // multisite blog (same whole-schema-scan class as the orphan-adoption
123 // finding). A migrated old prefix with no live blog is absent from the
124 // active-blog set and is still renamed so adoption can recover it.
125 $currentPrefix = $this->dbCore->tableNameResolver()->getLowercasePrefix();
126 $activeBlogPrefixes = $this->activeBlogPrefixesLowercase;
127
128 foreach ($results['rows'] as $row) {
129 if (!is_array($row)) {
130 continue;
131 }
132 // Case-insensitive key lookup: MySQL drivers return information_schema
133 // column names in varying cases (table_name, TABLE_NAME, Table_Name).
134 $tableName = null;
135 foreach ($row as $key => $value) {
136 if (strtolower((string)$key) === 'table_name' && is_scalar($value)) {
137 $tableName = (string)$value;
138 break;
139 }
140 }
141
142 if (!empty($tableName)) {
143 $lowercaseName = strtolower($tableName);
144
145 // Check if the table name is already lowercase, skip if it is
146 if ($tableName !== $lowercaseName) {
147 $tablePrefix = $this->prefixOfAbj404Table($lowercaseName);
148 if ($tablePrefix !== null && $tablePrefix !== $currentPrefix
149 && in_array($tablePrefix, $activeBlogPrefixes, true)) {
150 // Belongs to a different active subsite. Leave it alone.
151 continue;
152 }
153 // Rename the table to lowercase
154 $renameQuery = "RENAME TABLE `{$tableName}` TO `{$lowercaseName}`";
155 $this->dbCore->queryAndGetResults($renameQuery,
156 ['ignore_errors' => ["already exists"]]);
157 $this->logger->infoMessage("Renamed table {$tableName} to {$lowercaseName}\n");
158 }
159 } else {
160 $this->logger->warn("I didn't find a table name in the results of this row: " .
161 print_r($row, true));
162 }
163 }
164
165 // After renaming, check for orphaned tables under old prefixes.
166 $this->coordinator->orphanAdoptionUpgrade()->adoptOrphanedTables();
167 }
168
169 /**
170 * Extract the table prefix from a lowercase plugin table name: everything
171 * before the first "abj404" segment (e.g. "wp_2_abj404_redirects" -> "wp_2_").
172 * Returns null when the name has no abj404 segment.
173 *
174 * @param string $lowercaseTableName
175 * @return string|null
176 */
177 private function prefixOfAbj404Table(string $lowercaseTableName): ?string {
178 $pos = strpos($lowercaseTableName, 'abj404');
179 if ($pos === false) {
180 return null;
181 }
182 $prefix = substr($lowercaseTableName, 0, $pos);
183 return is_string($prefix) ? $prefix : null;
184 }
185 }
186