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

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

260 lines 9.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 * Shared dependency carrier for DatabaseUpgradesEtc delegates.
9 */
10 abstract class ABJ_404_Solution_DatabaseUpgradeComponent {
11
12 /** @var ABJ_404_Solution_DatabaseUpgradeCoordinator */
13 private $owner;
14
15 /** @var ABJ_404_Solution_DataAccess */
16 protected $dao;
17
18 /** @var ABJ_404_Solution_DatabaseCore */
19 protected $dbCore;
20
21 /** @var ABJ_404_Solution_ContentRepositoryInterface */
22 protected $contentRepo;
23
24 /** @var ABJ_404_Solution_ViewReadServiceInterface */
25 protected $viewRead;
26
27 /** @var ABJ_404_Solution_LogsRepositoryInterface */
28 protected $logsRepo;
29
30 /** @var ABJ_404_Solution_PluginUpdateMetadataRepository */
31 protected $pluginUpdateRepo;
32
33 /** @var ABJ_404_Solution_Logging */
34 protected $logger;
35
36 /** @var ABJ_404_Solution_Functions */
37 protected $f;
38
39 /** @var ABJ_404_Solution_PermalinkCache */
40 protected $permalinkCache;
41
42 /** @var ABJ_404_Solution_SynchronizationUtils */
43 protected $syncUtils;
44
45 /** @var ABJ_404_Solution_PluginLogicInterface */
46 protected $logic;
47
48 /** @var ABJ_404_Solution_NGramFilter */
49 protected $ngramFilter;
50
51 /** @var mixed */
52 protected $ngramExtractor;
53
54 /** @var mixed */
55 protected $ngramCacheRepository;
56
57 /** @var mixed */
58 protected $ngramCoveragePolicy;
59
60 /** @var mixed */
61 protected $ngramRebuilder;
62
63 /** @var ABJ_404_Solution_CronScheduler|null Optional injected cron scheduler; null falls back to abj_cron_scheduler(). */
64 protected $cronScheduler;
65
66 /**
67 * @param array<string, mixed> $deps
68 */
69 public function __construct(ABJ_404_Solution_DatabaseUpgradeCoordinator $owner, array $deps) {
70 $this->owner = $owner;
71 $this->replaceDatabaseUpgradeDependencies($deps);
72 }
73
74 /**
75 * @param array<string, mixed> $deps
76 * @return void
77 */
78 public function replaceDatabaseUpgradeDependencies(array $deps) {
79 foreach ($deps as $name => $value) {
80 $this->$name = $value;
81 }
82 }
83
84 protected function upgrades(): ABJ_404_Solution_DatabaseUpgradeCoordinator {
85 return $this->owner;
86 }
87
88 /**
89 * Case-insensitive "does this column exist" probe. Delegates to the
90 * canonical-url backfill component's implementation so the SHOW COLUMNS
91 * probe has a single source of truth. Components that own the real probe
92 * (the canonical-url backfill) override this; every other component inherits
93 * this shared delegator.
94 *
95 * @param string $tableName Fully-qualified table name.
96 * @param string $columnName Column to look for.
97 * @return bool
98 */
99 public function columnExists(string $tableName, string $columnName): ?bool {
100 return $this->upgrades()->canonicalUrlBackfillUpgrade()->columnExists($tableName, $columnName);
101 }
102
103 /**
104 * Whether a failed schema statement failed only because the schema already
105 * reflects the change it asked for.
106 *
107 * Every "ensure it exists" helper in this package is a SHOW COLUMNS or
108 * SHOW INDEX followed by an ALTER, and MySQL has no way to make those two
109 * one statement. On a plugin update that arrives on several concurrent
110 * requests at once, more than one of them decides the same column or index
111 * is missing and issues the same ALTER; all but one are then told the work
112 * is already done. That is the state the helper wanted, so the answer is to
113 * stop -- not to retry a statement whose only obstacle is its own goal
114 * having been met.
115 *
116 * Lives here rather than in each helper so the several fallback ladders ask
117 * the classifier one way. The classification itself belongs to
118 * {@see ABJ_404_Solution_DatabaseSchemaErrorTaxonomy::isRedundantSchemaChangeError()},
119 * which is also what stops the shared DAO reporter mailing these to the
120 * developer.
121 *
122 * @param string $lastError What the engine said, or '' when it said nothing.
123 * @return bool
124 */
125 protected function schemaChangeWasAlreadyApplied(string $lastError): bool {
126 if ($lastError === '') {
127 return false;
128 }
129 return $this->dbCore->errorClassifier()->taxonomy()->schema()
130 ->isRedundantSchemaChangeError($lastError);
131 }
132
133 /**
134 * Read a resumable id cursor from a WordPress option, clamped to a
135 * non-negative int. Shared by the chunked backfill drains so the cursor I/O
136 * has one definition.
137 *
138 * @param string $option
139 * @return int
140 */
141 protected function readCursorOption(string $option): int {
142 if ($option === '' || !function_exists('get_option')) {
143 return 0;
144 }
145 $raw = get_option($option, 0);
146 return max(0, is_scalar($raw) ? (int)$raw : 0);
147 }
148
149 /**
150 * Persist a resumable id cursor to a WordPress option (autoload=false,
151 * non-negative). Shared by the chunked backfill drains.
152 *
153 * @param string $option
154 * @param int $cursor
155 * @return void
156 */
157 protected function writeCursorOption(string $option, int $cursor): void {
158 if ($option === '' || !function_exists('update_option')) {
159 return;
160 }
161 update_option($option, (string)max(0, $cursor), false);
162 }
163
164 /** @return string|null */
165 protected function getUpgradeRuntimeId() {
166 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::getRuntimeId();
167 }
168
169 protected function getCanonicalUrlBackfillChunkSize(): int {
170 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::CANONICAL_URL_BACKFILL_CHUNK_SIZE;
171 }
172
173 protected function getCanonicalUrlBackfillTimeBudgetSec(): float {
174 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::CANONICAL_URL_BACKFILL_TIME_BUDGET_SEC;
175 }
176
177 protected function getLogsv2CanonicalUrlBackfillTimeBudgetSec(): float {
178 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::LOGSV2_CANONICAL_URL_BACKFILL_TIME_BUDGET_SEC;
179 }
180
181 protected function getLogsv2CanonicalUrlBackfillCompleteOption(): string {
182 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::LOGSV2_CANONICAL_URL_BACKFILL_COMPLETE_OPTION;
183 }
184
185 protected function getRedirectsCanonicalUrlBackfillCompleteOption(): string {
186 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::REDIRECTS_CANONICAL_URL_BACKFILL_COMPLETE_OPTION;
187 }
188
189 protected function getRedirectsDenormBackfillChunkSize(): int {
190 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::REDIRECTS_DENORM_BACKFILL_CHUNK_SIZE;
191 }
192
193 protected function getRedirectsDenormBackfillTimeBudgetSec(): float {
194 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::REDIRECTS_DENORM_BACKFILL_TIME_BUDGET_SEC;
195 }
196
197 protected function getRedirectsDenormReconcileChunkSize(): int {
198 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::REDIRECTS_DENORM_RECONCILE_CHUNK_SIZE;
199 }
200
201 protected function getRedirectsDenormReconcileTimeBudgetSec(): float {
202 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::REDIRECTS_DENORM_RECONCILE_TIME_BUDGET_SEC;
203 }
204
205 protected function getRedirectsDenormReconcileCursorOption(): string {
206 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::REDIRECTS_DENORM_RECONCILE_CURSOR_OPTION;
207 }
208
209 /** @return array<int, string> */
210 protected function getPluginTableSuffixes(): array {
211 return ABJ_404_Solution_DatabaseUpgradeRuntimeState::getPluginTableSuffixes();
212 }
213
214 /**
215 * Lowercased table prefixes of every active site on a multisite network,
216 * including the current site. On single-site this is just the current
217 * prefix.
218 *
219 * This is the authoritative tenant-isolation boundary for the cross-prefix
220 * maintenance paths (orphan-table adoption and the mixed-case lowercase
221 * rename). A live blog's tables are never an "orphan" to adopt or a table
222 * the current site should rename: a sibling subsite lowercases and owns its
223 * own tables. A genuinely orphaned old prefix (left by a prefix migration)
224 * has NO corresponding live blog, so it is absent from this set and remains
225 * eligible.
226 *
227 * When the WordPress multisite APIs are unavailable the multisite branch is
228 * skipped and only the current prefix is returned; callers must treat that
229 * as "could not enumerate siblings" and fall back to their content
230 * heuristic, never as "no siblings exist".
231 *
232 * @return array<int, string> Distinct lowercase prefixes, e.g. ['wp_', 'wp_2_'].
233 */
234 protected function getActiveBlogPrefixesLowercase(): array {
235 global $wpdb;
236 $prefixes = array();
237
238 if (is_object($wpdb) && isset($wpdb->prefix) && is_string($wpdb->prefix) && $wpdb->prefix !== '') {
239 $prefixes[] = strtolower($wpdb->prefix);
240 }
241
242 if (function_exists('is_multisite') && is_multisite()
243 && function_exists('get_sites')
244 && is_object($wpdb) && is_callable(array($wpdb, 'get_blog_prefix'))) {
245 $siteIds = get_sites(array('number' => 0, 'fields' => 'ids'));
246 if (is_array($siteIds)) {
247 foreach ($siteIds as $siteId) {
248 $prefix = $wpdb->get_blog_prefix((int)$siteId);
249 if (is_string($prefix) && $prefix !== '') {
250 $prefixes[] = strtolower($prefix);
251 }
252 }
253 }
254 }
255
256 return array_values(array_unique($prefixes));
257 }
258
259 }
260