PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.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 / PluginLogicLifecycle.php

PluginLogicLifecycle.php in 404 Solution 4.2.0, at includes/PluginLogicLifecycle.php

377 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Plugin activation, deactivation, multisite lifecycle, and cron registration.
10 * Standalone class extracted from PluginLogicTrait_Lifecycle.
11 */
12 class ABJ_404_Solution_PluginLogicLifecycle {
13
14 /** Remove cron jobs. @return void */
15 static function doUnregisterCrons(): void {
16 $crons = array(
17 'abj404_cleanupCronAction',
18 'abj404_gsc_fetch_cron',
19 'abj404_gsc_background_refresh',
20 'abj404_rebuildViewDone',
21 'abj404_updatePermalinkCacheAction',
22 'abj404_updateLogsHitsTableAction',
23 'abj404_send_digest',
24 'abj404_rebuild_ngram_cache_hook',
25 'abj404_logsv2_canonical_backfill',
26 'abj404_send_queued_report',
27 'abj404_duplicateCronAction',
28 'removeDuplicatesCron',
29 'deleteOldRedirectsCron',
30 );
31 for ($i = 0; $i < count($crons); $i++) {
32 $cron_name = $crons[$i];
33 $timestamp1 = wp_next_scheduled($cron_name);
34 while ($timestamp1 != false) {
35 wp_unschedule_event($timestamp1, $cron_name);
36 $timestamp1 = wp_next_scheduled($cron_name);
37 }
38
39 $timestamp2 = wp_next_scheduled($cron_name, array(''));
40 while ($timestamp2 != false) {
41 wp_unschedule_event($timestamp2, $cron_name, array(''));
42 $timestamp2 = wp_next_scheduled($cron_name, array(''));
43 }
44
45 wp_clear_scheduled_hook($cron_name);
46 }
47 }
48
49 /**
50 * Create database tables. Register crons. etc.
51 *
52 * @param bool $network_wide Whether this is a network-wide activation
53 * @return void
54 */
55 static function runOnPluginActivation(bool $network_wide = false): void {
56 if (is_multisite() && $network_wide) {
57 $sites = get_sites(array('fields' => 'ids', 'number' => 0));
58
59 update_site_option('abj404_pending_network_activation', $sites);
60 update_site_option('abj404_network_activation_total', count($sites));
61
62 wp_schedule_single_event(time(), 'abj404_network_activation_hook');
63
64 add_action('network_admin_notices', function() {
65 $pendingRaw = get_site_option('abj404_pending_network_activation', array());
66 $pending = is_array($pendingRaw) ? $pendingRaw : array();
67 $totalRaw = get_site_option('abj404_network_activation_total', 0);
68 $total = is_scalar($totalRaw) ? (int)$totalRaw : 0;
69 $completed = $total - count($pending);
70
71 if (!empty($pending)) {
72 echo '<div class="notice notice-info"><p><strong>404 Solution:</strong> Network activation in progress... ' .
73 esc_html((string)$completed) . ' of ' . esc_html((string)$total) . ' sites activated. ' .
74 'This will complete in the background.</p></div>';
75 }
76 });
77 } else {
78 self::activateSingleSite();
79 }
80 }
81
82 /**
83 * Activate plugin for a single site.
84 * @return void
85 */
86 private static function activateSingleSite(): void {
87 $abj404logic = abj_service('plugin_logic');
88 add_option('abj404_settings', '', '', false);
89
90 $upgradesEtc = abj_service('database_upgrades');
91 $upgradesEtc->createDatabaseTables();
92
93 $upgradesEtc->runSelfHealPrologue();
94
95 self::doRegisterCrons();
96
97 $abj404logic->doUpdateDBVersionOption();
98 }
99
100 /**
101 * Background cron handler for network activation.
102 * @return void
103 */
104 static function networkActivationCronHandler(): void {
105 $pendingRaw = get_site_option('abj404_pending_network_activation', array());
106 $pending = is_array($pendingRaw) ? $pendingRaw : array();
107
108 if (empty($pending)) {
109 delete_site_option('abj404_pending_network_activation');
110 delete_site_option('abj404_network_activation_total');
111 return;
112 }
113
114 $blog_id = array_shift($pending);
115 $blog_id_int = is_scalar($blog_id) ? (int)$blog_id : 0;
116
117 try {
118 switch_to_blog($blog_id_int);
119 self::activateSingleSite();
120 restore_current_blog();
121 } catch (Exception $e) {
122 $remaining = max(0, count($pending));
123 $errorLine = '404 Solution: Network activation failed for site ' . $blog_id_int .
124 ': ' . $e->getMessage() . '. Remaining sites=' . $remaining .
125 '. Action: skipping this site, continuing with next.';
126 error_log($errorLine);
127 $logger = abj_service('logging');
128 if ($logger !== null) {
129 $logger->errorMessage($errorLine, $e);
130 }
131 restore_current_blog();
132 }
133
134 update_site_option('abj404_pending_network_activation', $pending);
135
136 if (!empty($pending)) {
137 wp_schedule_single_event(time() + 10, 'abj404_network_activation_hook');
138 } else {
139 delete_site_option('abj404_pending_network_activation');
140 delete_site_option('abj404_network_activation_total');
141 }
142 }
143
144 /**
145 * Handle new blog creation in multisite (WordPress < 5.1).
146 *
147 * @param int $blog_id Blog ID of the new blog
148 * @param int $user_id User ID of the user creating the blog
149 * @param string $domain Domain of the new blog
150 * @param string $path Path of the new blog
151 * @param int $site_id Site ID (network ID)
152 * @param array<string, mixed> $meta Additional meta information
153 * @return void
154 */
155 static function activateNewSite($blog_id, $user_id, $domain, $path, $site_id, $meta): void {
156 if (!function_exists('is_plugin_active_for_network')) {
157 return;
158 }
159 if (is_plugin_active_for_network(plugin_basename(ABJ404_FILE))) {
160 switch_to_blog($blog_id);
161 try {
162 self::activateSingleSite();
163 } catch (\Throwable $e) {
164 $logger = abj_service('logging');
165 if ($logger !== null && method_exists($logger, 'warn')) {
166 $logger->warn(sprintf(
167 '404 Solution: subsite activation failed for blog_id=%d: %s',
168 (int)$blog_id,
169 $e->getMessage()
170 ));
171 }
172 } finally {
173 restore_current_blog();
174 }
175 }
176 }
177
178 /**
179 * Handle new blog creation in multisite (WordPress >= 5.1).
180 *
181 * @param mixed $site The WP_Site object for the new site.
182 * @param array<string, mixed> $args Additional arguments passed to the hook
183 * @return void
184 */
185 static function activateNewSiteModern($site, $args): void {
186 if (!function_exists('is_plugin_active_for_network')) {
187 return;
188 }
189 if (is_plugin_active_for_network(plugin_basename(ABJ404_FILE))) {
190 $siteRef = ABJ_404_Solution_SiteRef::fromWpSite($site);
191 if ($siteRef === null) {
192 return;
193 }
194 $blogId = $siteRef->getBlogId();
195 switch_to_blog($blogId);
196 try {
197 self::activateSingleSite();
198 } catch (\Throwable $e) {
199 $logger = abj_service('logging');
200 if ($logger !== null && method_exists($logger, 'warn')) {
201 $logger->warn(sprintf(
202 '404 Solution: subsite activation failed for blog_id=%d: %s',
203 $blogId,
204 $e->getMessage()
205 ));
206 }
207 } finally {
208 restore_current_blog();
209 }
210 }
211 }
212
213 /**
214 * Handle plugin deactivation for both single-site and multisite.
215 *
216 * @param bool $network_wide Whether this is a network-wide deactivation
217 * @return void
218 */
219 static function runOnPluginDeactivation(bool $network_wide = false): void {
220 if (is_multisite() && $network_wide) {
221 $sites = get_sites(array('fields' => 'ids', 'number' => 0));
222
223 foreach ($sites as $blog_id) {
224 switch_to_blog($blog_id);
225 self::deactivateSingleSite();
226 restore_current_blog();
227 }
228 } else {
229 self::deactivateSingleSite();
230 }
231 }
232
233 /**
234 * Deactivate plugin for a single site.
235 * @return void
236 */
237 private static function deactivateSingleSite(): void {
238 self::doUnregisterCrons();
239 }
240
241 /**
242 * Clean up when a blog is deleted in multisite.
243 *
244 * @global wpdb $wpdb WordPress database object
245 * @param int $blog_id Blog ID being deleted
246 * @param bool $drop Whether to drop the tables
247 * @return void
248 */
249 static function deleteBlogData($blog_id, $drop = false): void {
250 // CRON GUARD: refuse cron context as a structural backstop so
251 // CronReachableDestructiveSqlLintTest can prove the DROP TABLE below
252 // is never reachable from a daily cron tick.
253 if (function_exists('wp_doing_cron') && wp_doing_cron()) {
254 return;
255 }
256
257 if ($drop) {
258 switch_to_blog($blog_id);
259
260 global $wpdb;
261 $dbCore = abj_service('db_core');
262 $prefix = $dbCore->getLowercasePrefix();
263
264 // DAO-bypass-approved: deleteBlogData() runs during multisite blog teardown after switch_to_blog()
265 $tables = $wpdb->get_results(
266 // DAO-bypass-approved: prepare() argument to the get_results above
267 $wpdb->prepare("SHOW TABLES LIKE %s", $wpdb->esc_like($prefix . 'abj404_') . '%'),
268 ARRAY_N
269 );
270 foreach ($tables as $tableRow) {
271 $tblName = is_array($tableRow) && isset($tableRow[0]) ? $tableRow[0] : '';
272 if (preg_match('/^[a-zA-Z0-9_]+$/', $tblName) && strpos($tblName, 'abj404') !== false) {
273 // DAO-bypass-approved: deleteBlogData(), DDL drop during blog teardown
274 $wpdb->query("DROP TABLE IF EXISTS `{$tblName}`");
275 }
276 }
277
278 $plugin_options = array(
279 'abj404_settings',
280 'abj404_db_version',
281 'abj404_migrated_to_relative_paths',
282 'abj404_migration_results',
283 'abj404_ngram_cache_initialized',
284 'abj404_ngram_rebuild_offset',
285 'abj404_ngram_usage_stats',
286 'abj404_installed_time',
287 'abj404_user_feedback',
288 'abj404_uninstall_preferences'
289 );
290
291 foreach ($plugin_options as $option) {
292 delete_option($option);
293 }
294
295 // DAO-bypass-approved: deleteBlogData() runs wp_options cleanup during blog teardown
296 $wpdb->query(
297 // DAO-bypass-approved: prepare() argument to the query above
298 $wpdb->prepare(
299 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
300 $wpdb->esc_like('abj404_sync_') . '%'
301 )
302 );
303
304 $cron_hooks = array(
305 'abj404_cleanupCronAction',
306 'abj404_updateLogsHitsTableAction',
307 'abj404_updatePermalinkCacheAction',
308 'abj404_rebuild_ngram_cache_hook',
309 'abj404_rebuildViewDone',
310 'abj404_gsc_fetch_cron',
311 'abj404_gsc_background_refresh',
312 'abj404_send_digest',
313 'abj404_logsv2_canonical_backfill',
314 'abj404_send_queued_report',
315 );
316
317 foreach ($cron_hooks as $hook) {
318 wp_clear_scheduled_hook($hook);
319 }
320
321 $legacy_hooks = array(
322 'abj404_duplicateCronAction',
323 'abj404_updatePermalinkCache',
324 'abj404_cleanupCron',
325 'removeDuplicatesCron',
326 'deleteOldRedirectsCron',
327 );
328
329 foreach ($legacy_hooks as $hook) {
330 wp_clear_scheduled_hook($hook);
331 }
332
333 restore_current_blog();
334 }
335 }
336
337 /** @return void */
338 static function doRegisterCrons(): void {
339 if (!wp_next_scheduled('abj404_cleanupCronAction')) {
340 $timeForEvent = '0' . random_int(0, 5) . ':' . random_int(10, 59) . ':' . random_int(10, 59);
341 $eventTimestamp = strtotime($timeForEvent);
342 if ($eventTimestamp !== false) {
343 wp_schedule_event($eventTimestamp, 'daily', 'abj404_cleanupCronAction');
344 }
345 }
346
347 if (!wp_next_scheduled('abj404_gsc_fetch_cron')) {
348 $timeForGsc = '0' . random_int(1, 4) . ':' . random_int(10, 59) . ':' . random_int(10, 59);
349 $gscTimestamp = strtotime($timeForGsc);
350 if ($gscTimestamp !== false) {
351 wp_schedule_event($gscTimestamp, 'daily', 'abj404_gsc_fetch_cron');
352 }
353 }
354
355 self::scheduleViewDoneWarmup();
356 }
357
358 /** @return void */
359 private static function scheduleViewDoneWarmup(): void {
360 if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_single_event')) {
361 return;
362 }
363 if (class_exists('ABJ_404_Solution_ServiceContainer')
364 && ABJ_404_Solution_ServiceContainer::safeHas('rebuild_health')) {
365 $rebuildHealth = ABJ_404_Solution_ServiceContainer::safeGet('rebuild_health');
366 if ($rebuildHealth instanceof ABJ_404_Solution_RebuildHealthState
367 && !$rebuildHealth->mayStartExpensiveRebuild()) {
368 return;
369 }
370 }
371 if (wp_next_scheduled('abj404_rebuildViewDone') !== false) {
372 return;
373 }
374 wp_schedule_single_event(time() + 5, 'abj404_rebuildViewDone');
375 }
376 }
377