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

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

310 lines 12.4 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 class ABJ_404_Solution_DatabaseUpgradeMultiSite extends ABJ_404_Solution_DatabaseUpgradeComponent {
8
9 /**
10 * Schedule a background multisite batch operation.
11 *
12 * @param string $optionPrefix e.g. 'abj404_activation' or 'abj404_upgrade'
13 * @param string $hookName e.g. 'abj404_network_activation_background'
14 * @param string $label Human-readable label for log messages, e.g. 'activation'
15 * @param int $alreadyProcessedBlogId Blog ID already processed on this request.
16 * @return void
17 */
18 private function scheduleBackgroundMultisiteBatch(string $optionPrefix, string $hookName, string $label, int $alreadyProcessedBlogId): void {
19 update_site_option($optionPrefix . '_processed_blogs', array($alreadyProcessedBlogId));
20 update_site_option($optionPrefix . '_in_progress', true);
21
22 if (abj_cron_scheduler()->nextScheduled($hookName)) {
23 $this->logger->debugMessage("Background multisite $label already scheduled.");
24 return;
25 }
26
27 $scheduled = abj_cron_scheduler()->scheduleSingle($hookName, 30);
28
29 if ($scheduled === false) {
30 $this->logger->errorMessage("Failed to schedule background multisite $label. Remaining sites will not be processed automatically.");
31 } else {
32 $this->logger->infoMessage("Background multisite $label scheduled successfully.");
33 }
34 }
35
36 /**
37 * Process a batch of multisite sites with the given per-site action.
38 *
39 * @param string $optionPrefix e.g. 'abj404_activation' or 'abj404_upgrade'
40 * @param string $hookName e.g. 'abj404_network_activation_background'
41 * @param string $label Human-readable label for log messages, e.g. 'activation'
42 * @param callable $perSiteAction Called for each site (receives int $siteId).
43 * @return bool True if all sites are done, false if more batches needed.
44 */
45 public function processMultisiteBatch(string $optionPrefix, string $hookName, string $label, callable $perSiteAction): bool {
46 $processedBlogs = get_site_option($optionPrefix . '_processed_blogs', array());
47 if (!is_array($processedBlogs)) {
48 $processedBlogs = array();
49 }
50
51 $allSitesRaw = get_sites(array('fields' => 'ids', 'number' => 0));
52 $allSites = array();
53 if (is_array($allSitesRaw)) {
54 foreach ($allSitesRaw as $siteId) {
55 if (is_numeric($siteId)) {
56 $allSites[] = (int)$siteId;
57 }
58 }
59 }
60 $processedBlogIds = array();
61 foreach ($processedBlogs as $blogId) {
62 if (is_numeric($blogId)) {
63 $processedBlogIds[] = (int)$blogId;
64 }
65 }
66 $remainingSites = array_diff($allSites, $processedBlogIds);
67
68 if (empty($remainingSites)) {
69 delete_site_option($optionPrefix . '_processed_blogs');
70 delete_site_option($optionPrefix . '_in_progress');
71 $this->logger->infoMessage("Background multisite $label complete. All sites processed.");
72 return true;
73 }
74
75 $batchSize = 10;
76 $sitesToProcess = array_slice($remainingSites, 0, $batchSize);
77
78 $this->logger->infoMessage(sprintf(
79 "Processing multisite $label batch: %d sites (of %d remaining)",
80 count($sitesToProcess),
81 count($remainingSites)
82 ));
83
84 foreach ($sitesToProcess as $siteId) {
85 try {
86 switch_to_blog($siteId);
87 $this->logger->debugMessage(sprintf("Processing $label for site ID %d...", $siteId));
88
89 $perSiteAction((int)$siteId);
90
91 $processedBlogs[] = $siteId;
92 update_site_option($optionPrefix . '_processed_blogs', $processedBlogs);
93
94 $this->logger->debugMessage(sprintf("Successfully processed $label for site ID %d", $siteId));
95 } catch (Throwable $e) {
96 $this->logger->errorMessage(sprintf(
97 "Failed to process $label for site ID %d: %s",
98 $siteId,
99 $e->getMessage()
100 ));
101 $processedBlogs[] = $siteId;
102 update_site_option($optionPrefix . '_processed_blogs', $processedBlogs);
103 } finally {
104 restore_current_blog();
105 }
106 }
107
108 $stillRemaining = count($remainingSites) - count($sitesToProcess);
109 if ($stillRemaining > 0) {
110 $this->logger->infoMessage(sprintf(
111 "Batch complete. Rescheduling for %d remaining sites.",
112 $stillRemaining
113 ));
114 abj_cron_scheduler()->scheduleSingle($hookName, 30);
115 return false;
116 } else {
117 delete_site_option($optionPrefix . '_processed_blogs');
118 delete_site_option($optionPrefix . '_in_progress');
119 $this->logger->infoMessage("Background multisite $label complete. All sites processed.");
120 return true;
121 }
122 }
123
124 /**
125 * Schedule a background activation for all network sites except the one that
126 * was just activated synchronously.
127 *
128 * @param int $alreadyProcessedBlogId Blog ID of the site already activated.
129 * @return void
130 */
131 public function scheduleBackgroundMultisiteActivation(int $alreadyProcessedBlogId): void {
132 $this->scheduleBackgroundMultisiteBatch(
133 'abj404_activation',
134 ABJ_404_Solution_CronScheduler::HOOK_NETWORK_ACTIVATION_BACKGROUND,
135 'activation',
136 $alreadyProcessedBlogId
137 );
138 }
139
140 /**
141 * Process multisite activation in batches (called by WP-Cron).
142 *
143 * Processes remaining sites that weren't handled during initial activation.
144 * Processes up to 10 sites per run to avoid timeouts, then reschedules itself
145 * if more sites remain.
146 *
147 * @return bool True if all sites processed, false if more remain
148 */
149 public function processMultisiteActivationBatch(): bool {
150 return $this->processMultisiteBatch(
151 'abj404_activation',
152 ABJ_404_Solution_CronScheduler::HOOK_NETWORK_ACTIVATION_BACKGROUND,
153 'activation',
154 function (int $siteId): void {
155 add_option('abj404_settings', '', '', false);
156
157 $this->upgrades()->bootstrapUpgrade()->runInitialCreateTables();
158 $this->upgrades()->collationDriftUpgrade()->correctCollations();
159 $this->upgrades()->engineNormalizationUpgrade()->updateTableEngineToInnoDB();
160 $this->upgrades()->indexesUpgrade()->createIndexes();
161 $this->upgrades()->canonicalUrlBackfillUpgrade()->backfillRedirectsCanonicalUrl();
162 $this->upgrades()->bootstrapUpgrade()->renameAbj404TablesToLowerCase();
163
164 // Canonical self-heal prologue runs after schema creation so
165 // SelfHealingPrologueReachabilityTest sees per-subsite activation
166 // reach the same recovery primitives as the daily cron.
167 $this->upgrades()->selfHealUpgrade()->runSelfHealPrologue();
168
169 $this->logic->registerCrons();
170
171 abj_service('version_upgrade')->stampDbVersion();
172 }
173 );
174 }
175
176 /**
177 * Schedule a background upgrade for all network sites except the one that
178 * was just upgraded synchronously.
179 *
180 * @param int $alreadyProcessedBlogId Blog ID of the site already upgraded.
181 * @return void
182 */
183 public function scheduleBackgroundMultisiteUpgrade(int $alreadyProcessedBlogId): void {
184 $this->scheduleBackgroundMultisiteBatch(
185 'abj404_upgrade',
186 ABJ_404_Solution_CronScheduler::HOOK_NETWORK_UPGRADE_BACKGROUND,
187 'upgrade',
188 $alreadyProcessedBlogId
189 );
190 }
191
192 /**
193 * Process multisite plugin upgrade in batches (called by WP-Cron).
194 *
195 * Upgrades remaining sites that weren't handled during the initial upgrade.
196 * Processes up to 10 sites per run to avoid timeouts, then reschedules itself
197 * if more sites remain.
198 *
199 * @return bool True if all sites processed, false if more remain.
200 */
201 public function processMultisiteUpgradeBatch(): bool {
202 return $this->processMultisiteBatch(
203 'abj404_upgrade',
204 ABJ_404_Solution_CronScheduler::HOOK_NETWORK_UPGRADE_BACKGROUND,
205 'upgrade',
206 function (int $siteId): void {
207 // Run the full upgrade sequence for this site without going through
208 // createDatabaseTables() — that would re-schedule more background tasks.
209 $this->upgrades()->tableRepairUpgrade()->correctIssuesBefore();
210 $this->upgrades()->bootstrapUpgrade()->runInitialCreateTables();
211 $this->upgrades()->collationDriftUpgrade()->correctCollations();
212 $this->upgrades()->engineNormalizationUpgrade()->updateTableEngineToInnoDB();
213 $this->upgrades()->indexesUpgrade()->createIndexes();
214 $this->upgrades()->canonicalUrlBackfillUpgrade()->backfillRedirectsCanonicalUrl();
215 $this->upgrades()->bootstrapUpgrade()->renameAbj404TablesToLowerCase();
216 $this->upgrades()->tableRepairUpgrade()->correctIssuesAfter();
217
218 // Canonical self-heal prologue closes the per-subsite upgrade
219 // batch so SelfHealingPrologueReachabilityTest can prove the
220 // multisite upgrade path reaches the same recovery primitives
221 // as the daily cron tick.
222 $this->upgrades()->selfHealUpgrade()->runSelfHealPrologue();
223
224 abj_service('version_upgrade')->stampDbVersion();
225 }
226 );
227 }
228
229 /**
230 * Create tables for all sites in a multisite network.
231 *
232 * This function iterates through all sites in the network and creates
233 * the plugin's database tables for each site. This ensures that when
234 * the plugin is network-activated, all sites have the necessary tables.
235 *
236 * @since 3.0.1
237 */
238 /** @return void */
239 public function createTablesForAllSites() {
240 global $wpdb;
241
242 // Get all sites in the network
243 $sites = get_sites(array('fields' => 'ids', 'number' => 0));
244 $totalSites = count($sites);
245 $successCount = 0;
246 $failureCount = 0;
247
248 $this->logger->infoMessage(sprintf(
249 "Starting network-wide table creation for %d sites.",
250 $totalSites
251 ));
252
253 foreach ($sites as $siteId) {
254 try {
255 // Switch to the site
256 switch_to_blog($siteId);
257
258 $currentPrefix = $wpdb->prefix;
259 $this->logger->debugMessage(sprintf(
260 "Creating tables for site ID %d (prefix: %s)...",
261 $siteId,
262 $currentPrefix
263 ));
264
265 // Create tables for this site
266 $this->upgrades()->bootstrapUpgrade()->runInitialCreateTables();
267 $this->upgrades()->collationDriftUpgrade()->correctCollations();
268 $this->upgrades()->engineNormalizationUpgrade()->updateTableEngineToInnoDB();
269 $this->upgrades()->indexesUpgrade()->createIndexes();
270 $this->upgrades()->canonicalUrlBackfillUpgrade()->backfillRedirectsCanonicalUrl();
271
272 $successCount++;
273 $this->logger->debugMessage(sprintf(
274 "Successfully created tables for site ID %d (prefix: %s)",
275 $siteId,
276 $currentPrefix
277 ));
278
279 } catch (Throwable $e) {
280 $failureCount++;
281 $this->logger->errorMessage(sprintf(
282 "Failed to create tables for site ID %d (prefix: %s): %s",
283 $siteId,
284 $wpdb->prefix,
285 $e->getMessage()
286 ));
287 } finally {
288 // Always restore blog context
289 restore_current_blog();
290 }
291 }
292
293 // Log summary
294 $this->logger->infoMessage(sprintf(
295 "Network-wide table creation complete: %d successful, %d failed out of %d total sites.",
296 $successCount,
297 $failureCount,
298 $totalSites
299 ));
300
301 if ($failureCount > 0) {
302 $this->logger->errorMessage(sprintf(
303 "Warning: Table creation failed for %d sites. Check error logs for details.",
304 $failureCount
305 ));
306 }
307 }
308
309 }
310