PluginProbe ʕ •ᴥ•ʔ
Advanced Database Cleaner – Optimize & Clean Database to Speed Up Site Performance / 4.2.0
Advanced Database Cleaner – Optimize & Clean Database to Speed Up Site Performance v4.2.0
4.2.0 trunk 1.0.0 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.5 1.3.6 1.3.7 2.0.0 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.1.0 4.1.1
advanced-database-cleaner / includes / models / class-adbc-cron-jobs.php
advanced-database-cleaner / includes / models Last commit date
class-adbc-common-model.php 7 months ago class-adbc-cron-jobs.php 2 months ago class-adbc-database.php 5 months ago class-adbc-options.php 4 months ago class-adbc-post-types.php 3 months ago class-adbc-posts-meta.php 4 months ago class-adbc-sites.php 7 months ago class-adbc-tables.php 3 months ago class-adbc-transients.php 4 months ago class-adbc-users-meta.php 4 months ago
class-adbc-cron-jobs.php
669 lines
1 <?php
2
3 // Exit if accessed directly
4 if ( ! defined( 'ABSPATH' ) )
5 exit;
6
7 /**
8 * ADBC cron jobs class.
9 *
10 * This class provides cron jobs functions.
11 */
12 class ADBC_Cron_Jobs {
13
14 /**
15 * Get total cron jobs.
16 *
17 * @return int Total cron jobs.
18 */
19 public static function get_total_cron_jobs_count() {
20
21 $sites = ADBC_Sites::instance()->get_sites_list();
22
23 $total_tasks = 0;
24
25 foreach ( $sites as $site ) {
26
27 ADBC_Sites::instance()->switch_to_blog_id( $site['id'] );
28
29 $cron_jobs = self::get_cron_array();
30 foreach ( $cron_jobs as $timestamp => $tasks ) {
31 $total_tasks += count( $tasks );
32 }
33
34 ADBC_Sites::instance()->restore_blog();
35
36 }
37
38 return $total_tasks;
39
40 }
41
42 /**
43 * Get cron jobs names for all the sites.
44 *
45 * @return array Associative cron jobs names.
46 */
47 public static function get_cron_jobs_names() {
48
49 $all_cron_jobs = [];
50
51 $sites = ADBC_Sites::instance()->get_sites_list();
52
53 foreach ( $sites as $site ) {
54
55 ADBC_Sites::instance()->switch_to_blog_id( $site['id'] );
56
57 $site_cron_jobs = self::get_cron_array();
58
59 foreach ( $site_cron_jobs as $timestamp => $tasks ) {
60 foreach ( $tasks as $hook => $events ) {
61 $all_cron_jobs[ $hook ] = true;
62 }
63 }
64
65 ADBC_Sites::instance()->restore_blog();
66
67 }
68
69 return $all_cron_jobs;
70
71 }
72
73 /**
74 * Get the core cron array.
75 *
76 * This function retrieves the core cron array for the current site.
77 *
78 * @return array The core cron array.
79 */
80 public static function get_cron_array() {
81
82 // Get the core cron array.
83 $core_cron = _get_cron_array();
84
85 if ( ! is_array( $core_cron ) )
86 return [];
87
88 // Return the core cron array.
89 return $core_cron;
90
91 }
92
93 /**
94 * Get the cron jobs list for the endpoint.
95 *
96 * @param array $filters Output of sanitize_filters().
97 *
98 * @return WP_REST_Response The list of cron jobs.
99 */
100 public static function get_cron_jobs_list( $filters ) {
101
102 // Prepare variables
103 $cron_jobs_list = [];
104 $total_cron_jobs = 0;
105
106 $scan_counter = new ADBC_Scan_Counter();
107
108 $startRecord = ( $filters['current_page'] - 1 ) * $filters['items_per_page'];
109 $endRecord = $startRecord + $filters['items_per_page'];
110 $currentRecord = 0;
111
112 $limit = ADBC_Settings::instance()->get_setting( 'database_rows_batch' );
113 $offset = 0;
114
115 do { // Loop through all cron jobs in batches of $limit to avoid memory issues
116
117 $cron_jobs = self::get_cron_jobs_list_batch( $filters, $limit, $offset );
118
119 $fetched_count = count( $cron_jobs );
120
121 if ( ADBC_VERSION_TYPE === 'PREMIUM' )
122 ADBC_Scan_Results::instance()->load_scan_results_to_items_rows( $cron_jobs, 'cron_jobs' );
123 else
124 ADBC_Common_Model::load_scan_results_to_items_for_free_version( $cron_jobs );
125
126 ADBC_Hardcoded_Items::instance()->load_hardcoded_scan_results_to_items_rows( $cron_jobs, 'cron_jobs' ); // Load hardcoded items to the cron jobs rows
127
128 foreach ( $cron_jobs as $index => $cron_job ) {
129
130 $scan_counter->refresh_categorization_count( $cron_job->belongs_to );
131
132 if ( ! ADBC_Common_Model::is_item_satisfies_belongs_to( $filters, $cron_job->belongs_to ) )
133 continue;
134
135 $total_cron_jobs++; // Count cron jobs that satisfy all filters and belongs_to
136
137 // Only process the current batch if it's within the desired page range
138 if ( $currentRecord >= $startRecord && $currentRecord < $endRecord ) {
139
140 $cron_jobs_list[] = [
141 // This id is used to identify the cron job in the frontend and take actions on it
142 'composite_id' => [
143 'items_type' => 'cron_jobs',
144 'site_id' => (int) $cron_job->site_id,
145 'timestamp' => (int) $cron_job->timestamp,
146 'name' => $cron_job->name,
147 'args' => $cron_job->args
148 ],
149 'name' => $cron_job->name, // Used in the known addons modal & "show value modal". To be generic and work for all items types.
150 'hook_name' => $cron_job->name,
151 'args' => $cron_job->args,
152 'timestamp' => $cron_job->timestamp,
153 'frequency' => $cron_job->frequency,
154 'frequency_display' => $cron_job->frequency_display,
155 'interval' => $cron_job->interval,
156 'site_id' => $cron_job->site_id,
157 'has_action' => $cron_job->has_action ? 'yes' : 'no',
158 'action' => $cron_job->action,
159 'action_file' => $cron_job->action_file,
160 'belongs_to' => $cron_job->belongs_to,
161 'known_plugins' => $cron_job->known_plugins,
162 'known_themes' => $cron_job->known_themes,
163 ];
164 }
165
166 $currentRecord++;
167 }
168
169 $offset += $limit;
170
171 } while ( $fetched_count == $limit ); // Continue if the last batch was full
172
173 // Loop over the $cron_jobs_list and $scan_counter add the plugins/themes names from the dictionary if they are empty
174 // This is because load_scan_results_to_rows() only loads the names of the plugins/themes that are currently installed
175 if ( ADBC_VERSION_TYPE === 'PREMIUM' )
176 ADBC_Dictionary::add_missing_addons_names_from_dictionary( $cron_jobs_list, $scan_counter, 'cron_jobs' );
177
178 // Calculate total number of pages to verify that the current page sent by the user is within the range
179 $total_real_pages = max( 1, ceil( $total_cron_jobs / $filters['items_per_page'] ) );
180
181 return ADBC_Rest::success( "", [
182 'items' => $cron_jobs_list,
183 'total_items' => $total_cron_jobs,
184 'real_current_page' => min( $filters['current_page'], $total_real_pages ),
185 'categorization_count' => $scan_counter->get_categorization_count(),
186 'plugins_count' => $scan_counter->get_plugins_count(),
187 'themes_count' => $scan_counter->get_themes_count(),
188 ] );
189 }
190
191 /**
192 * Get the cron jobs list that satisfy the UI filters.
193 *
194 * @param array $filters Output of sanitize_filters().
195 * @param int $limit Limit for the number of rows to return.
196 * @param int $offset Offset for the number of rows to return.
197 *
198 * @return array List of cron jobs that satisfy the filters.
199 */
200 private static function get_cron_jobs_list_batch( $filters, $limit, $offset ) {
201
202 $sites_list = ADBC_Sites::instance()->get_sites_list( $filters['site_id'] );
203 $all_cron_jobs = [];
204
205 foreach ( $sites_list as $site ) {
206
207 ADBC_Sites::instance()->switch_to_blog_id( $site['id'] );
208
209 $cron_jobs = self::get_cron_array();
210 $schedules = wp_get_schedules();
211
212 foreach ( $cron_jobs as $timestamp => $tasks ) {
213 foreach ( $tasks as $hook => $events ) {
214 foreach ( $events as $event_key => $event ) {
215
216 // Determine human-readable frequency
217 if ( empty( $event['schedule'] ) ) {
218 $frequency_display = __( 'Once', 'advanced-database-cleaner' );
219 } else {
220 $key = $event['schedule'];
221 if ( isset( $schedules[ $key ]['display'] ) && $schedules[ $key ]['display'] ) {
222 $frequency_display = $schedules[ $key ]['display'];
223 } else {
224 // Fallback: prettify custom schedule key
225 $frequency_display = ucwords( str_replace( '_', ' ', (string) $key ) );
226 }
227 }
228
229 // Create a cron job object
230 $cron_job = (object) [
231 'name' => $hook,
232 'hook_name' => $hook,
233 'args' => $event['args'],
234 'timestamp' => $timestamp,
235 'frequency' => ! empty( $event['schedule'] ) ? $event['schedule'] : 'once',
236 'frequency_display' => $frequency_display,
237 'interval' => ! empty( $event['interval'] ) ? $event['interval'] : 'N/A',
238 'site_id' => $site['id'],
239 'has_action' => has_action( $hook ) !== false,
240 'action' => self::get_hook_action_info( $hook )['action'],
241 'action_file' => self::get_hook_action_info( $hook )['action_file'],
242 ];
243
244 // Apply filters
245 if ( ! self::cron_job_satisfies_filters( $cron_job, $filters ) ) {
246 continue;
247 }
248
249 $all_cron_jobs[] = $cron_job;
250 }
251 }
252 }
253
254 ADBC_Sites::instance()->restore_blog();
255 }
256
257 // Apply sorting
258 $all_cron_jobs = self::sort_cron_jobs( $all_cron_jobs, $filters );
259
260 // Apply pagination
261 return array_slice( $all_cron_jobs, $offset, $limit );
262 }
263
264 /**
265 * Check if a cron job satisfies the UI filters.
266 *
267 * @param object $cron_job The cron job object.
268 * @param array $filters Output of sanitize_filters().
269 *
270 * @return bool True if the cron job satisfies the filters.
271 */
272 private static function cron_job_satisfies_filters( $cron_job, $filters ) {
273
274 // Search filter
275 if ( ! empty( $filters['search_for'] ) && ! empty( $filters['search_in'] ) ) {
276
277 $needle = strtolower( $filters['search_for'] );
278
279 switch ( $filters['search_in'] ) {
280 case 'name':
281 if ( strpos( strtolower( $cron_job->name ), $needle ) === false ) {
282 return false;
283 }
284 break;
285
286 case 'value':
287
288 foreach ( $cron_job->args as $arg ) {
289 if ( strpos( strtolower( $arg ), $needle ) !== false ) {
290 break 2;
291 }
292 }
293
294 return false;
295
296 case 'all':
297 if ( strpos( strtolower( $cron_job->name ), $needle ) !== false ) {
298 break;
299 }
300 foreach ( $cron_job->args as $arg ) {
301 if ( strpos( strtolower( $arg ), $needle ) !== false ) {
302 break 2;
303 }
304 }
305 return false;
306 }
307 }
308
309 // frequency filter
310 if ( $filters['frequency'] !== 'all' ) {
311 if ( $cron_job->frequency !== $filters['frequency'] ) {
312 return false;
313 }
314 }
315
316 // interval filter
317 if ( $filters['interval'] !== 'all' ) {
318 if ( $cron_job->interval !== $filters['interval'] ) {
319 return false;
320 }
321 }
322
323 // has_action filter
324 if ( isset( $filters['has_action'] ) && $filters['has_action'] !== 'all' ) {
325 $expected = $filters['has_action'] === 'yes';
326 if ( (bool) $cron_job->has_action !== $expected ) {
327 return false;
328 }
329 }
330
331 return true;
332
333 }
334
335 /**
336 * Get the action label and source file for the first callback registered on a hook.
337 *
338 * @param string $hook Hook name.
339 * @return array { action: string, action_file: string }
340 */
341 private static function get_hook_action_info( $hook ) {
342
343 global $wp_filter;
344
345 $result = [ 'action' => '', 'action_file' => '' ];
346
347 if ( empty( $hook ) || ! isset( $wp_filter[ $hook ] ) )
348 return $result;
349
350 $hook_obj = $wp_filter[ $hook ];
351 if ( ! is_object( $hook_obj ) || empty( $hook_obj->callbacks ) )
352 return $result;
353
354 // Find the first registered callback
355 $callback = null;
356 foreach ( $hook_obj->callbacks as $priority => $callbacks ) {
357 if ( empty( $callbacks ) )
358 continue;
359 foreach ( $callbacks as $cb ) {
360 if ( isset( $cb['function'] ) ) {
361 $callback = $cb['function'];
362 break 2;
363 }
364 }
365 }
366
367 if ( $callback === null )
368 return $result;
369
370 $result['action'] = self::callback_to_string( $callback );
371 $result['action_file'] = self::get_callback_file_info( $callback );
372
373 return $result;
374
375 }
376
377 /**
378 * Convert a PHP callback to a readable string.
379 *
380 * @param mixed $callback Callback.
381 * @return string
382 */
383 private static function callback_to_string( $callback ) {
384
385 if ( is_string( $callback ) )
386 return $callback . '()';
387
388 if ( is_array( $callback ) && count( $callback ) === 2 ) {
389 list( $obj_or_class, $method ) = $callback;
390 if ( is_object( $obj_or_class ) )
391 return get_class( $obj_or_class ) . '->' . (string) $method . '()';
392 return (string) $obj_or_class . '::' . (string) $method . '()';
393 }
394
395 if ( $callback instanceof \Closure )
396 return __( 'Anonymous function', 'advanced-database-cleaner' );
397
398 if ( is_object( $callback ) && method_exists( $callback, '__invoke' ) )
399 return get_class( $callback ) . '->__invoke()';
400
401 return __( 'Unknown action', 'advanced-database-cleaner' );
402
403 }
404
405 /**
406 * Get the file:line info for a callback.
407 *
408 * @param mixed $callback Callback.
409 * @return string File:line info.
410 */
411 private static function get_callback_file_info( $callback ) {
412
413 try {
414
415 if ( is_array( $callback ) && count( $callback ) === 2 ) {
416 $class = is_object( $callback[0] ) ? get_class( $callback[0] ) : (string) $callback[0];
417 $method = (string) $callback[1];
418 $ref = PHP_VERSION_ID >= 80400
419 ? \ReflectionMethod::createFromMethodName( $class . '::' . $method )
420 : new \ReflectionMethod( $class, $method );
421 } elseif ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
422 $ref = PHP_VERSION_ID >= 80400
423 ? \ReflectionMethod::createFromMethodName( $callback )
424 : new \ReflectionMethod( ...explode( '::', $callback, 2 ) );
425 } elseif ( $callback instanceof \Closure ) {
426 $ref = new \ReflectionFunction( $callback );
427 } elseif ( is_object( $callback ) ) {
428 $ref = new \ReflectionMethod( $callback, '__invoke' );
429 } else {
430 $ref = new \ReflectionFunction( $callback );
431 }
432
433 $file = $ref->getFileName();
434 if ( ! empty( $file ) )
435 return $file . ':' . $ref->getStartLine();
436
437 } catch (\Throwable $e) {
438 return '';
439 }
440
441 }
442
443 /**
444 * Sort cron jobs based on filters.
445 *
446 * @param array $cron_jobs Array of cron job objects.
447 * @param array $filters Output of sanitize_filters().
448 *
449 * @return array Sorted array of cron job objects.
450 */
451 private static function sort_cron_jobs( $cron_jobs, $filters ) {
452
453 $sort_col = $filters['sort_by'] ?? '';
454 $sort_dir = strtoupper( $filters['sort_order'] ?? 'ASC' );
455
456 $allowed_columns = [ 'hook_name', 'timestamp', 'frequency_display', 'site_id', 'interval' ];
457
458 if ( ! in_array( $sort_col, $allowed_columns ) ) {
459 return $cron_jobs;
460 }
461
462 usort( $cron_jobs, function ($a, $b) use ($sort_col, $sort_dir) {
463
464 $val_a = $a->$sort_col;
465 $val_b = $b->$sort_col;
466
467 // Handle different data types
468 if ( is_numeric( $val_a ) && is_numeric( $val_b ) ) {
469 $result = $val_a <=> $val_b;
470 } else {
471 $result = strcasecmp( (string) $val_a, (string) $val_b );
472 }
473
474 return $sort_dir === 'DESC' ? -$result : $result;
475 } );
476
477 return $cron_jobs;
478 }
479
480 /**
481 * Count the total number of cron jobs that are not scanned.
482 *
483 * @return int Total not scanned cron jobs.
484 */
485 public static function count_total_not_scanned_cron_jobs() {
486
487 $total_not_scanned = 0;
488
489 $sites_list = ADBC_Sites::instance()->get_sites_list();
490
491 foreach ( $sites_list as $site ) {
492
493 $cron_jobs_names = self::get_site_cron_jobs_names( $site['id'] );
494 $not_scanned_count = 0;
495
496 if ( ADBC_VERSION_TYPE === 'PREMIUM' )
497 $not_scanned_count = ADBC_Scan_Utils::count_not_scanned_items_in_list( "cron_jobs", $cron_jobs_names );
498 else
499 $not_scanned_count = ADBC_Common_Model::count_not_scanned_items_in_list_for_free( "cron_jobs", $cron_jobs_names );
500
501 $total_not_scanned += $not_scanned_count;
502
503 }
504
505 return $total_not_scanned;
506
507 }
508
509 /**
510 * Count the total number of cron jobs that have no registered action (no callbacks hooked to their hook name).
511 *
512 * @return int Total cron jobs without action handlers.
513 */
514 public static function count_total_cron_jobs_with_no_action() {
515
516 $total_no_action = 0;
517
518 $sites_list = ADBC_Sites::instance()->get_sites_list();
519
520 foreach ( $sites_list as $site ) {
521 ADBC_Sites::instance()->switch_to_blog_id( $site['id'] );
522 $cron_jobs = self::get_cron_array();
523 foreach ( $cron_jobs as $timestamp => $tasks ) {
524 foreach ( $tasks as $hook => $events ) {
525 if ( has_action( $hook ) === false ) {
526 // Count each scheduled event for this hook
527 $total_no_action += count( $events );
528 }
529 }
530 }
531 ADBC_Sites::instance()->restore_blog();
532 }
533
534 return $total_no_action;
535 }
536
537 /**
538 * Get cron jobs names for a site.
539 *
540 * @param int $site_id Site ID.
541 *
542 * @return array Cron jobs names.
543 */
544 public static function get_site_cron_jobs_names( $site_id ) {
545
546 $all_cron_jobs_names = [];
547
548 ADBC_Sites::instance()->switch_to_blog_id( $site_id );
549
550 $cron_jobs = self::get_cron_array();
551
552 foreach ( $cron_jobs as $timestamp => $tasks ) {
553 foreach ( $tasks as $hook => $events ) {
554 foreach ( $events as $event_key => $event ) {
555 $all_cron_jobs_names[] = $hook;
556 }
557 }
558 }
559
560 ADBC_Sites::instance()->restore_blog();
561
562 return $all_cron_jobs_names;
563
564 }
565
566 /**
567 * Get cron jobs for a site.
568 *
569 * @param int $site_id Site ID.
570 *
571 * @return array Cron jobs objects.
572 */
573 public static function get_site_cron_jobs( $site_id ) {
574
575 $cron_jobs = [];
576
577 ADBC_Sites::instance()->switch_to_blog_id( $site_id );
578
579 $all_cron_jobs = self::get_cron_array();
580
581 foreach ( $all_cron_jobs as $timestamp => $tasks ) {
582 foreach ( $tasks as $hook => $events ) {
583 foreach ( $events as $event_key => $event ) {
584 // Create a cron job object
585 $cron_job = (object) [
586 'name' => $hook,
587 'args' => $event['args'],
588 'timestamp' => $timestamp,
589 ];
590
591 $cron_jobs[] = $cron_job;
592 }
593 }
594 }
595
596 ADBC_Sites::instance()->restore_blog();
597
598 return $cron_jobs;
599
600 }
601
602 /**
603 * Delete grouped cron jobs. Cron jobs are grouped by site ID as key.
604 *
605 * @param array $grouped_selected Grouped selected cron jobs to delete.
606 *
607 * @return array An array of cron job names that were not processed (not deleted).
608 */
609 public static function delete_cron_jobs( $grouped_selected ) {
610
611 $not_processed = [];
612
613 foreach ( $grouped_selected as $site_id => $group ) {
614
615 ADBC_Sites::instance()->switch_to_blog_id( $site_id );
616
617 foreach ( $group as $selected ) {
618
619 // Try to unschedule the cron job
620 $success = wp_unschedule_event( $selected['timestamp'], $selected['name'], $selected['args'] );
621
622 if ( ! $success ) {
623 $not_processed[] = $selected['name'];
624 }
625 }
626
627 ADBC_Sites::instance()->restore_blog();
628 }
629
630 return $not_processed;
631 }
632
633 /**
634 * Get cron job hook names that still exist anywhere across the network from a provided list.
635 *
636 * @param array $hooks List of cron hook names to check for existence.
637 *
638 * @return array Existing hook names found across all sites.
639 */
640 public static function get_cron_jobs_names_that_exists_from_list( $hooks ) {
641
642 if ( empty( $hooks ) || ! is_array( $hooks ) )
643 return [];
644
645 $existing_hooks = [];
646 $sites = ADBC_Sites::instance()->get_sites_list();
647
648 foreach ( $sites as $site ) {
649 ADBC_Sites::instance()->switch_to_blog_id( $site['id'] );
650 $cron = self::get_cron_array();
651 foreach ( $cron as $timestamp => $tasks ) {
652 foreach ( $tasks as $hook => $events ) {
653 $existing_hooks[ $hook ] = true;
654 }
655 }
656 ADBC_Sites::instance()->restore_blog();
657 }
658
659 $result = [];
660 foreach ( $hooks as $hook ) {
661 if ( isset( $existing_hooks[ $hook ] ) ) {
662 $result[] = $hook;
663 }
664 }
665
666 return $result;
667 }
668
669 }