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 / core / PluginLogicLifecycle.php

PluginLogicLifecycle.php in 404 Solution trunk, at includes/core/PluginLogicLifecycle.php

326 lines 11.6 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 abj_cron_scheduler()->clearRegisteredHooks();
17 }
18
19 /**
20 * Create database tables. Register crons. etc.
21 *
22 * @param bool $network_wide Whether this is a network-wide activation
23 * @return void
24 */
25 static function runOnPluginActivation(bool $network_wide = false): void {
26 if (is_multisite() && $network_wide) {
27 $sites = get_sites(array('fields' => 'ids', 'number' => 0));
28
29 update_site_option('abj404_pending_network_activation', $sites);
30 update_site_option('abj404_network_activation_total', count($sites));
31
32 abj_cron_scheduler()->scheduleSingle(
33 ABJ_404_Solution_CronScheduler::HOOK_NETWORK_ACTIVATION
34 );
35
36 add_action('network_admin_notices', function() {
37 $pendingRaw = get_site_option('abj404_pending_network_activation', array());
38 $pending = is_array($pendingRaw) ? $pendingRaw : array();
39 $totalRaw = get_site_option('abj404_network_activation_total', 0);
40 $total = is_scalar($totalRaw) ? (int)$totalRaw : 0;
41 $completed = $total - count($pending);
42
43 if (!empty($pending)) {
44 $template = ABJ_404_Solution_FileSystemService::readFileContents(
45 dirname(__DIR__) . '/html/networkActivationNotice.html',
46 false
47 );
48 echo str_replace(
49 array('{completed}', '{total}'),
50 array(esc_html((string)$completed), esc_html((string)$total)),
51 $template
52 );
53 }
54 });
55 } else {
56 self::activateSingleSite();
57 }
58 }
59
60 /**
61 * Activate plugin for a single site.
62 * @return void
63 */
64 private static function activateSingleSite(): void {
65 add_option('abj404_settings', '', '', false);
66
67 $upgradesEtc = abj_service('database_upgrades');
68 $upgradesEtc->components()->bootstrapUpgrade()->createDatabaseTables();
69
70 $upgradesEtc->components()->selfHealUpgrade()->runSelfHealPrologue();
71
72 self::doRegisterCrons();
73
74 abj_service('version_upgrade')->stampDbVersion();
75 }
76
77 /**
78 * Background cron handler for network activation.
79 * @return void
80 */
81 static function networkActivationCronHandler(): void {
82 $pendingRaw = get_site_option('abj404_pending_network_activation', array());
83 $pending = is_array($pendingRaw) ? $pendingRaw : array();
84
85 if (empty($pending)) {
86 delete_site_option('abj404_pending_network_activation');
87 delete_site_option('abj404_network_activation_total');
88 return;
89 }
90
91 $blog_id = array_shift($pending);
92 $blog_id_int = is_scalar($blog_id) ? (int)$blog_id : 0;
93
94 try {
95 switch_to_blog($blog_id_int);
96 self::activateSingleSite();
97 restore_current_blog();
98 } catch (Exception $e) {
99 $remaining = max(0, count($pending));
100 $errorLine = '404 Solution: Network activation failed for site ' . $blog_id_int .
101 ': ' . $e->getMessage() . '. Remaining sites=' . $remaining .
102 '. Action: skipping this site, continuing with next.';
103 $logger = abj_service('logging');
104 if ($logger !== null) {
105 $logger->errorMessage($errorLine, $e);
106 }
107 restore_current_blog();
108 }
109
110 update_site_option('abj404_pending_network_activation', $pending);
111
112 if (!empty($pending)) {
113 abj_cron_scheduler()->scheduleSingle(
114 ABJ_404_Solution_CronScheduler::HOOK_NETWORK_ACTIVATION,
115 10
116 );
117 } else {
118 delete_site_option('abj404_pending_network_activation');
119 delete_site_option('abj404_network_activation_total');
120 }
121 }
122
123 /**
124 * Handle new blog creation in multisite (WordPress < 5.1).
125 *
126 * @param int $blog_id Blog ID of the new blog
127 * @param int $user_id User ID of the user creating the blog
128 * @param string $domain Domain of the new blog
129 * @param string $path Path of the new blog
130 * @param int $site_id Site ID (network ID)
131 * @param array<string, mixed> $meta Additional meta information
132 * @return void
133 */
134 static function activateNewSite($blog_id, $user_id, $domain, $path, $site_id, $meta): void {
135 if (!function_exists('is_plugin_active_for_network')) {
136 return;
137 }
138 if (is_plugin_active_for_network(plugin_basename(ABJ404_FILE))) {
139 switch_to_blog($blog_id);
140 try {
141 self::activateSingleSite();
142 } catch (\Throwable $e) {
143 $logger = abj_service('logging');
144 if ($logger !== null && method_exists($logger, 'warn')) {
145 $logger->warn(sprintf(
146 '404 Solution: subsite activation failed for blog_id=%d: %s',
147 (int)$blog_id,
148 $e->getMessage()
149 ));
150 }
151 } finally {
152 restore_current_blog();
153 }
154 }
155 }
156
157 /**
158 * Handle new blog creation in multisite (WordPress >= 5.1).
159 *
160 * @param mixed $site The WP_Site object for the new site.
161 * @param array<string, mixed> $args Additional arguments passed to the hook
162 * @return void
163 */
164 static function activateNewSiteModern($site, $args): void {
165 if (!function_exists('is_plugin_active_for_network')) {
166 return;
167 }
168 if (is_plugin_active_for_network(plugin_basename(ABJ404_FILE))) {
169 $siteRef = ABJ_404_Solution_SiteRef::fromWpSite($site);
170 if ($siteRef === null) {
171 return;
172 }
173 $blogId = $siteRef->getBlogId();
174 switch_to_blog($blogId);
175 try {
176 self::activateSingleSite();
177 } catch (\Throwable $e) {
178 $logger = abj_service('logging');
179 if ($logger !== null && method_exists($logger, 'warn')) {
180 $logger->warn(sprintf(
181 '404 Solution: subsite activation failed for blog_id=%d: %s',
182 $blogId,
183 $e->getMessage()
184 ));
185 }
186 } finally {
187 restore_current_blog();
188 }
189 }
190 }
191
192 /**
193 * Handle plugin deactivation for both single-site and multisite.
194 *
195 * @param bool $network_wide Whether this is a network-wide deactivation
196 * @return void
197 */
198 static function runOnPluginDeactivation(bool $network_wide = false): void {
199 if (is_multisite() && $network_wide) {
200 $sites = get_sites(array('fields' => 'ids', 'number' => 0));
201
202 foreach ($sites as $blog_id) {
203 switch_to_blog($blog_id);
204 self::deactivateSingleSite();
205 restore_current_blog();
206 }
207 } else {
208 self::deactivateSingleSite();
209 }
210 }
211
212 /**
213 * Deactivate plugin for a single site.
214 * @return void
215 */
216 private static function deactivateSingleSite(): void {
217 self::doUnregisterCrons();
218 }
219
220 /**
221 * Clean up when a blog is deleted in multisite.
222 *
223 * @global wpdb $wpdb WordPress database object
224 * @param int $blog_id Blog ID being deleted
225 * @param bool $drop Whether to drop the tables
226 * @return void
227 */
228 static function deleteBlogData($blog_id, $drop = false): void {
229 // CRON GUARD: refuse cron context as a structural backstop so
230 // CronReachableDestructiveSqlLintTest can prove the DROP TABLE below
231 // is never reachable from a daily cron tick.
232 if (function_exists('wp_doing_cron') && wp_doing_cron()) {
233 return;
234 }
235
236 if ($drop) {
237 switch_to_blog($blog_id);
238
239 global $wpdb;
240 $dbCore = abj_service('db_core');
241 $prefix = $dbCore->tableNameResolver()->getLowercasePrefix();
242
243 // DAO-bypass-approved: deleteBlogData() runs during multisite blog teardown after switch_to_blog()
244 $tables = $wpdb->get_results(
245 // DAO-bypass-approved: prepare() argument to the get_results above
246 $wpdb->prepare("SHOW TABLES LIKE %s", $wpdb->esc_like($prefix . 'abj404_') . '%'),
247 ARRAY_N
248 );
249 foreach ($tables as $tableRow) {
250 $tblName = is_array($tableRow) && isset($tableRow[0]) ? $tableRow[0] : '';
251 if (preg_match('/^[a-zA-Z0-9_]+$/', $tblName) && strpos($tblName, 'abj404') !== false) {
252 // DAO-bypass-approved: deleteBlogData(), DDL drop during blog teardown
253 $wpdb->query("DROP TABLE IF EXISTS `{$tblName}`");
254 }
255 }
256
257 $plugin_options = array(
258 'abj404_settings',
259 'abj404_db_version',
260 'abj404_migrated_to_relative_paths',
261 'abj404_migration_results',
262 'abj404_ngram_cache_initialized',
263 'abj404_ngram_rebuild_offset',
264 'abj404_ngram_usage_stats',
265 'abj404_installed_time',
266 'abj404_user_feedback',
267 'abj404_uninstall_preferences'
268 );
269
270 foreach ($plugin_options as $option) {
271 delete_option($option);
272 }
273
274 // DAO-bypass-approved: deleteBlogData() runs wp_options cleanup during blog teardown
275 $wpdb->query(
276 // DAO-bypass-approved: prepare() argument to the query above
277 $wpdb->prepare(
278 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
279 $wpdb->esc_like('abj404_sync_') . '%'
280 )
281 );
282
283 $cron_hooks = array(
284 'abj404_cleanupCronAction',
285 'abj404_updateLogsHitsTableAction',
286 'abj404_updatePermalinkCacheAction',
287 'abj404_rebuild_ngram_cache_hook',
288 'abj404_rebuildViewDone',
289 'abj404_gsc_fetch_cron',
290 'abj404_gsc_background_refresh',
291 'abj404_send_digest',
292 'abj404_logsv2_canonical_backfill',
293 'abj404_redirects_denorm_backfill',
294 'abj404_redirects_sort_key_backfill',
295 'abj404_send_queued_report',
296 'abj404_repair_collations',
297 );
298
299 foreach ($cron_hooks as $hook) {
300 abj_cron_scheduler()->clearHook($hook);
301 }
302
303 $legacy_hooks = array(
304 'abj404_duplicateCronAction',
305 'abj404_updatePermalinkCache',
306 'abj404_cleanupCron',
307 'removeDuplicatesCron',
308 'deleteOldRedirectsCron',
309 );
310
311 foreach ($legacy_hooks as $hook) {
312 abj_cron_scheduler()->clearHook($hook);
313 }
314
315 restore_current_blog();
316 }
317 }
318
319 /** @return void */
320 static function doRegisterCrons(): void {
321 $scheduler = abj_cron_scheduler();
322 $scheduler->scheduleDailyInWindowIfMissing(ABJ_404_Solution_CronScheduler::HOOK_CLEANUP, 0, 5);
323 $scheduler->scheduleDailyInWindowIfMissing(ABJ_404_Solution_CronScheduler::HOOK_GSC_FETCH, 1, 4);
324 }
325 }
326