WP Crontrol - Version 1.8.2

Version Description

Download this release

Release Info

Developer johnbillion
Plugin Icon 128x128 WP Crontrol
Version 1.8.2
Comparing to
See all releases

Code changes from version 1.8.1 to 1.8.2

css/wp-crontrol.css CHANGED
@@ -50,6 +50,10 @@ table.wp-list-table {
50
  border-color: #dc3232;
51
  }
52
 
 
 
 
 
53
  .status-crontrol-complete,
54
  .wp-list-table tr.crontrol-complete td.column-status {
55
  color: #080;
50
  border-color: #dc3232;
51
  }
52
 
53
+ #crontrol-hash-message {
54
+ display: none;
55
+ }
56
+
57
  .status-crontrol-complete,
58
  .wp-list-table tr.crontrol-complete td.column-status {
59
  color: #080;
js/wp-crontrol.js CHANGED
@@ -6,7 +6,31 @@
6
 
7
  const header = document.getElementById( 'crontrol-header' );
8
  const wpbody = document.getElementById( 'wpbody-content' );
 
9
 
10
  if ( header && wpbody ) {
11
  wpbody.prepend( header );
12
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  const header = document.getElementById( 'crontrol-header' );
8
  const wpbody = document.getElementById( 'wpbody-content' );
9
+ let hashtimer = null;
10
 
11
  if ( header && wpbody ) {
12
  wpbody.prepend( header );
13
  }
14
+
15
+ if ( window.wpCrontrol && window.wpCrontrol.eventsHash && window.wpCrontrol.eventsHashInterval ) {
16
+ hashtimer = setInterval( crontrolCheckHash, ( 1000 * window.wpCrontrol.eventsHashInterval ) );
17
+ }
18
+
19
+ function crontrolCheckHash() {
20
+ jQuery.ajax( {
21
+ url: window.ajaxurl,
22
+ type: 'post',
23
+ data: {
24
+ action: 'crontrol_checkhash',
25
+ },
26
+ dataType: 'json',
27
+ } ).done( function( response ) {
28
+ if ( response.success && response.data && response.data !== window.wpCrontrol.eventsHash ) {
29
+ jQuery( '#crontrol-hash-message' ).slideDown();
30
+
31
+ if ( hashtimer ) {
32
+ clearInterval( hashtimer );
33
+ }
34
+ }
35
+ } );
36
+ }
readme.md CHANGED
@@ -4,7 +4,7 @@ Contributors: johnbillion, scompt
4
  Tags: cron, wp-cron, crontrol, debug
5
  Requires at least: 4.1
6
  Tested up to: 5.4
7
- Stable tag: 1.8.1
8
  Requires PHP: 5.3
9
 
10
  WP Crontrol lets you view and control what's happening in the WP-Cron system.
@@ -76,6 +76,15 @@ The cron commands which were previously included in WP Crontrol are now part of
76
 
77
  ## Changelog ##
78
 
 
 
 
 
 
 
 
 
 
79
  ### 1.8.1 ###
80
 
81
  * Fix the bottom bulk action menu on the event listing screen.
4
  Tags: cron, wp-cron, crontrol, debug
5
  Requires at least: 4.1
6
  Tested up to: 5.4
7
+ Stable tag: 1.8.2
8
  Requires PHP: 5.3
9
 
10
  WP Crontrol lets you view and control what's happening in the WP-Cron system.
76
 
77
  ## Changelog ##
78
 
79
+ ### 1.8.2 ###
80
+
81
+ * Bypass the duplicate event check when manually running an event. This allows an event to manually run even if it's due within ten minutes or if it's overdue.
82
+ * Force only one event to fire when manually running a cron event.
83
+ * Introduce polling of the events list in order to show a warning when the event listing screen is out of date.
84
+ * Add a warning for cron schedules which are shorter than `WP_CRON_LOCK_TIMEOUT`.
85
+ * Add the Site Health check event to the list of persistent core hooks.
86
+
87
+
88
  ### 1.8.1 ###
89
 
90
  * Fix the bottom bulk action menu on the event listing screen.
src/event-list-table.php CHANGED
@@ -5,7 +5,7 @@
5
  * @package wp-crontrol
6
  */
7
 
8
- namespace Crontrol;
9
 
10
  use stdClass;
11
 
@@ -14,7 +14,7 @@ require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
14
  /**
15
  * Cron event list table class.
16
  */
17
- class Event_List_Table extends \WP_List_Table {
18
 
19
  /**
20
  * Array of cron event hooks that are persistently added by WordPress core.
@@ -53,11 +53,11 @@ class Event_List_Table extends \WP_List_Table {
53
  * Prepares the list table items and arguments.
54
  */
55
  public function prepare_items() {
56
- self::$persistent_core_hooks = get_persistent_core_hooks();
57
  self::$can_edit_files = current_user_can( 'edit_files' );
58
- self::$count_by_hook = Event\count_by_hook();
59
 
60
- $events = Event\get();
61
 
62
  if ( ! empty( $_GET['s'] ) ) {
63
  $s = sanitize_text_field( wp_unslash( $_GET['s'] ) );
@@ -93,7 +93,7 @@ class Event_List_Table extends \WP_List_Table {
93
  'crontrol_next' => sprintf(
94
  /* translators: %s: UTC offset */
95
  __( 'Next Run (%s)', 'wp-crontrol' ),
96
- get_utc_offset()
97
  ),
98
  'crontrol_actions' => __( 'Action', 'wp-crontrol' ),
99
  'crontrol_recurrence' => __( 'Recurrence', 'wp-crontrol' ),
@@ -135,17 +135,17 @@ class Event_List_Table extends \WP_List_Table {
135
  $classes[] = 'crontrol-error';
136
  }
137
 
138
- $schedule_name = ( $event->interval ? Event\get_schedule_name( $event ) : false );
139
 
140
  if ( is_wp_error( $schedule_name ) ) {
141
  $classes[] = 'crontrol-error';
142
  }
143
 
144
- if ( ! get_hook_callbacks( $event->hook ) ) {
145
  $classes[] = 'crontrol-warning';
146
  }
147
 
148
- if ( Event\is_late( $event ) ) {
149
  $classes[] = 'crontrol-warning';
150
  }
151
 
@@ -284,7 +284,7 @@ class Event_List_Table extends \WP_List_Table {
284
  */
285
  protected function column_crontrol_args( $event ) {
286
  if ( ! empty( $event->args ) ) {
287
- $args = json_output( $event->args );
288
  }
289
 
290
  if ( 'crontrol_cron_job' === $event->hook ) {
@@ -336,7 +336,7 @@ class Event_List_Table extends \WP_List_Table {
336
  * @return string The cell output.
337
  */
338
  protected function column_crontrol_actions( $event ) {
339
- $hook_callbacks = get_hook_callbacks( $event->hook );
340
 
341
  if ( 'crontrol_cron_job' === $event->hook ) {
342
  return '<em>' . esc_html__( 'WP Crontrol', 'wp-crontrol' ) . '</em>';
@@ -344,7 +344,7 @@ class Event_List_Table extends \WP_List_Table {
344
  $callbacks = array();
345
 
346
  foreach ( $hook_callbacks as $callback ) {
347
- $callbacks[] = output_callback( $callback );
348
  }
349
 
350
  return implode( '<br>', $callbacks ); // WPCS:: XSS ok.
@@ -373,14 +373,14 @@ class Event_List_Table extends \WP_List_Table {
373
  );
374
 
375
  $until = $event->time - time();
376
- $late = Event\is_late( $event );
377
 
378
  if ( $late ) {
379
  // Show a warning for events that are late.
380
  $ago = sprintf(
381
  /* translators: %s: Time period, for example "8 minutes" */
382
  __( '%s ago', 'wp-crontrol' ),
383
- interval( abs( $until ) )
384
  );
385
  return sprintf(
386
  '%s<br><span class="status-crontrol-warning"><span class="dashicons dashicons-warning" aria-hidden="true"></span> %s</span>',
@@ -392,7 +392,7 @@ class Event_List_Table extends \WP_List_Table {
392
  return sprintf(
393
  '%s<br>%s',
394
  $time,
395
- esc_html( interval( $until ) )
396
  );
397
  }
398
 
@@ -404,7 +404,7 @@ class Event_List_Table extends \WP_List_Table {
404
  */
405
  protected function column_crontrol_recurrence( $event ) {
406
  if ( $event->schedule ) {
407
- $schedule_name = Event\get_schedule_name( $event );
408
  if ( is_wp_error( $schedule_name ) ) {
409
  return sprintf(
410
  '<span class="status-crontrol-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span> %s</span>',
5
  * @package wp-crontrol
6
  */
7
 
8
+ namespace Crontrol\Event;
9
 
10
  use stdClass;
11
 
14
  /**
15
  * Cron event list table class.
16
  */
17
+ class Table extends \WP_List_Table {
18
 
19
  /**
20
  * Array of cron event hooks that are persistently added by WordPress core.
53
  * Prepares the list table items and arguments.
54
  */
55
  public function prepare_items() {
56
+ self::$persistent_core_hooks = \Crontrol\get_persistent_core_hooks();
57
  self::$can_edit_files = current_user_can( 'edit_files' );
58
+ self::$count_by_hook = count_by_hook();
59
 
60
+ $events = get();
61
 
62
  if ( ! empty( $_GET['s'] ) ) {
63
  $s = sanitize_text_field( wp_unslash( $_GET['s'] ) );
93
  'crontrol_next' => sprintf(
94
  /* translators: %s: UTC offset */
95
  __( 'Next Run (%s)', 'wp-crontrol' ),
96
+ \Crontrol\get_utc_offset()
97
  ),
98
  'crontrol_actions' => __( 'Action', 'wp-crontrol' ),
99
  'crontrol_recurrence' => __( 'Recurrence', 'wp-crontrol' ),
135
  $classes[] = 'crontrol-error';
136
  }
137
 
138
+ $schedule_name = ( $event->interval ? get_schedule_name( $event ) : false );
139
 
140
  if ( is_wp_error( $schedule_name ) ) {
141
  $classes[] = 'crontrol-error';
142
  }
143
 
144
+ if ( ! \Crontrol\get_hook_callbacks( $event->hook ) ) {
145
  $classes[] = 'crontrol-warning';
146
  }
147
 
148
+ if ( is_late( $event ) ) {
149
  $classes[] = 'crontrol-warning';
150
  }
151
 
284
  */
285
  protected function column_crontrol_args( $event ) {
286
  if ( ! empty( $event->args ) ) {
287
+ $args = \Crontrol\json_output( $event->args );
288
  }
289
 
290
  if ( 'crontrol_cron_job' === $event->hook ) {
336
  * @return string The cell output.
337
  */
338
  protected function column_crontrol_actions( $event ) {
339
+ $hook_callbacks = \Crontrol\get_hook_callbacks( $event->hook );
340
 
341
  if ( 'crontrol_cron_job' === $event->hook ) {
342
  return '<em>' . esc_html__( 'WP Crontrol', 'wp-crontrol' ) . '</em>';
344
  $callbacks = array();
345
 
346
  foreach ( $hook_callbacks as $callback ) {
347
+ $callbacks[] = \Crontrol\output_callback( $callback );
348
  }
349
 
350
  return implode( '<br>', $callbacks ); // WPCS:: XSS ok.
373
  );
374
 
375
  $until = $event->time - time();
376
+ $late = is_late( $event );
377
 
378
  if ( $late ) {
379
  // Show a warning for events that are late.
380
  $ago = sprintf(
381
  /* translators: %s: Time period, for example "8 minutes" */
382
  __( '%s ago', 'wp-crontrol' ),
383
+ \Crontrol\interval( abs( $until ) )
384
  );
385
  return sprintf(
386
  '%s<br><span class="status-crontrol-warning"><span class="dashicons dashicons-warning" aria-hidden="true"></span> %s</span>',
392
  return sprintf(
393
  '%s<br>%s',
394
  $time,
395
+ esc_html( \Crontrol\interval( $until ) )
396
  );
397
  }
398
 
404
  */
405
  protected function column_crontrol_recurrence( $event ) {
406
  if ( $event->schedule ) {
407
+ $schedule_name = get_schedule_name( $event );
408
  if ( is_wp_error( $schedule_name ) ) {
409
  return sprintf(
410
  '<span class="status-crontrol-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span> %s</span>',
src/event.php CHANGED
@@ -26,12 +26,17 @@ function run( $hookname, $sig ) {
26
  if ( isset( $cron[ $hookname ][ $sig ] ) ) {
27
  $args = $cron[ $hookname ][ $sig ]['args'];
28
  delete_transient( 'doing_cron' );
29
- $scheduled = wp_schedule_single_event( time() - 1, $hookname, $args ); // UTC
30
 
31
  if ( false === $scheduled ) {
32
  return $scheduled;
33
  }
34
 
 
 
 
 
 
35
  spawn_cron();
36
 
37
  sleep( 1 );
@@ -42,6 +47,34 @@ function run( $hookname, $sig ) {
42
  return false;
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  /**
46
  * Adds a new cron event.
47
  *
@@ -206,8 +239,27 @@ function get_schedule_name( stdClass $event ) {
206
  * @param stdClass $event The event.
207
  * @return bool Whether the event is late.
208
  */
209
- function is_late( $event ) {
210
  $until = $event->time - time();
211
 
212
  return ( $until < ( 0 - ( 10 * MINUTE_IN_SECONDS ) ) );
213
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  if ( isset( $cron[ $hookname ][ $sig ] ) ) {
27
  $args = $cron[ $hookname ][ $sig ]['args'];
28
  delete_transient( 'doing_cron' );
29
+ $scheduled = force_schedule_single_event( $hookname, $args ); // UTC
30
 
31
  if ( false === $scheduled ) {
32
  return $scheduled;
33
  }
34
 
35
+ add_filter( 'cron_request', function( array $cron_request_array ) {
36
+ $cron_request_array['url'] = add_query_arg( 'crontrol-single-event', 1, $cron_request_array['url'] );
37
+ return $cron_request_array;
38
+ } );
39
+
40
  spawn_cron();
41
 
42
  sleep( 1 );
47
  return false;
48
  }
49
 
50
+ /**
51
+ * Forcibly schedules a single event for the purpose of manually running it.
52
+ *
53
+ * This is used instead of `wp_schedule_single_event()` to avoid the duplicate check that's otherwise performed.
54
+ *
55
+ * @param string $hook Action hook to execute when the event is run.
56
+ * @param array $args Optional. Array containing each separate argument to pass to the hook's callback function.
57
+ * @return bool True if event successfully scheduled. False for failure.
58
+ */
59
+ function force_schedule_single_event( $hook, $args = array() ) {
60
+ $event = (object) array(
61
+ 'hook' => $hook,
62
+ 'timestamp' => 1,
63
+ 'schedule' => false,
64
+ 'args' => $args,
65
+ );
66
+ $crons = (array) _get_cron_array();
67
+ $key = md5( serialize( $event->args ) );
68
+
69
+ $crons[ $event->timestamp ][ $event->hook ][ $key ] = array(
70
+ 'schedule' => $event->schedule,
71
+ 'args' => $event->args,
72
+ );
73
+ uksort( $crons, 'strnatcasecmp' );
74
+
75
+ return _set_cron_array( $crons );
76
+ }
77
+
78
  /**
79
  * Adds a new cron event.
80
  *
239
  * @param stdClass $event The event.
240
  * @return bool Whether the event is late.
241
  */
242
+ function is_late( stdClass $event ) {
243
  $until = $event->time - time();
244
 
245
  return ( $until < ( 0 - ( 10 * MINUTE_IN_SECONDS ) ) );
246
  }
247
+
248
+ /**
249
+ * Initialises and returns the list table for events.
250
+ *
251
+ * @return Table The list table.
252
+ */
253
+ function get_list_table() {
254
+ static $table = null;
255
+
256
+ if ( ! $table ) {
257
+ require_once __DIR__ . '/event-list-table.php';
258
+
259
+ $table = new Table();
260
+ $table->prepare_items();
261
+
262
+ }
263
+
264
+ return $table;
265
+ }
src/schedule-list-table.php CHANGED
@@ -134,11 +134,25 @@ class Schedule_List_Table extends \WP_List_Table {
134
  * @return string The cell output.
135
  */
136
  protected function column_crontrol_interval( array $schedule ) {
137
- return sprintf(
138
  '%s (%s)',
139
  esc_html( $schedule['interval'] ),
140
  esc_html( interval( $schedule['interval'] ) )
141
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  }
143
 
144
  /**
134
  * @return string The cell output.
135
  */
136
  protected function column_crontrol_interval( array $schedule ) {
137
+ $interval = sprintf(
138
  '%s (%s)',
139
  esc_html( $schedule['interval'] ),
140
  esc_html( interval( $schedule['interval'] ) )
141
  );
142
+
143
+ if ( $schedule['interval'] < WP_CRON_LOCK_TIMEOUT ) {
144
+ $interval .= sprintf(
145
+ '<span class="status-crontrol-warning"><br><span class="dashicons dashicons-warning" aria-hidden="true"></span> %s</span>',
146
+ sprintf(
147
+ /* translators: 1: The name of the configuration constant, 2: The value of the configuration constant */
148
+ esc_html__( 'This interval is less than the %1$s constant which is set to %2$s. Events that use it may not run on time.', 'wp-crontrol' ),
149
+ '<code>WP_CRON_LOCK_TIMEOUT</code>',
150
+ intval( WP_CRON_LOCK_TIMEOUT )
151
+ )
152
+ );
153
+ }
154
+
155
+ return $interval;
156
  }
157
 
158
  /**
wp-crontrol.php CHANGED
@@ -5,7 +5,7 @@
5
  * Description: WP Crontrol lets you view and control what's happening in the WP-Cron system.
6
  * Author: John Blackbourn & crontributors
7
  * Author URI: https://github.com/johnbillion/wp-crontrol/graphs/contributors
8
- * Version: 1.8.1
9
  * Text Domain: wp-crontrol
10
  * Domain Path: /languages/
11
  * Requires PHP: 5.3.6
@@ -50,11 +50,13 @@ function init_hooks() {
50
  add_action( 'init', __NAMESPACE__ . '\action_init' );
51
  add_action( 'init', __NAMESPACE__ . '\action_handle_posts' );
52
  add_action( 'admin_menu', __NAMESPACE__ . '\action_admin_menu' );
 
53
  add_filter( "plugin_action_links_{$plugin_file}", __NAMESPACE__ . '\plugin_action_links', 10, 4 );
54
  add_filter( 'removable_query_args', __NAMESPACE__ . '\filter_removable_query_args' );
55
  add_filter( 'in_admin_header', __NAMESPACE__ . '\do_tabs' );
 
56
 
57
- add_action( 'load-tools_page_crontrol_admin_manage_page', __NAMESPACE__ . '\enqueue_code_editor' );
58
 
59
  add_filter( 'cron_schedules', __NAMESPACE__ . '\filter_cron_schedules' );
60
  add_action( 'crontrol_cron_job', __NAMESPACE__ . '\action_php_cron_event' );
@@ -518,6 +520,52 @@ function admin_options_page() {
518
  <?php
519
  }
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  /**
522
  * Gets the status of WP-Cron functionality on the site by performing a test spawn if necessary. Cached for one hour when all is well.
523
  *
@@ -568,7 +616,7 @@ function test_cron_spawn( $cache = true ) {
568
  $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
569
 
570
  $cron_request = apply_filters( 'cron_request', array(
571
- 'url' => site_url( 'wp-cron.php?doing_wp_cron=' . $doing_wp_cron ),
572
  'key' => $doing_wp_cron,
573
  'args' => array(
574
  'timeout' => 3,
@@ -999,12 +1047,8 @@ function admin_manage_page() {
999
  );
1000
  }
1001
 
1002
- require_once __DIR__ . '/src/event-list-table.php';
1003
-
1004
  $tabs = get_tab_states();
1005
- $table = new Event_List_Table();
1006
-
1007
- $table->prepare_items();
1008
 
1009
  switch ( true ) {
1010
  case $tabs['events']:
@@ -1324,9 +1368,20 @@ function interval( $since ) {
1324
  }
1325
 
1326
  /**
1327
- * Enqueues the editor UI that's used for the PHP cron event code editor.
1328
  */
1329
- function enqueue_code_editor() {
 
 
 
 
 
 
 
 
 
 
 
1330
  if ( ! function_exists( 'wp_enqueue_code_editor' ) ) {
1331
  return;
1332
  }
@@ -1368,7 +1423,14 @@ function enqueue_assets( $hook_suffix ) {
1368
  wp_enqueue_style( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'css/wp-crontrol.css', array(), $ver );
1369
 
1370
  $ver = filemtime( plugin_dir_path( __FILE__ ) . 'js/wp-crontrol.js' );
1371
- wp_enqueue_script( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'js/wp-crontrol.js', array(), $ver, true );
 
 
 
 
 
 
 
1372
  }
1373
 
1374
  /**
@@ -1397,6 +1459,7 @@ function get_persistent_core_hooks() {
1397
  'wp_privacy_delete_old_export_files',
1398
  'wp_scheduled_auto_draft_delete',
1399
  'wp_scheduled_delete',
 
1400
  'wp_update_plugins',
1401
  'wp_update_themes',
1402
  'wp_version_check',
@@ -1461,6 +1524,8 @@ function json_output( $input ) {
1461
  * Security: A user can only add or edit a PHP cron event if they have the `edit_files` capability. This means if a user
1462
  * cannot edit files on the site (eg. through the plugin or theme editor) then they cannot edit or add a PHP cron event.
1463
  *
 
 
1464
  * @param string $code The PHP code to evaluate.
1465
  */
1466
  function action_php_cron_event( $code ) {
5
  * Description: WP Crontrol lets you view and control what's happening in the WP-Cron system.
6
  * Author: John Blackbourn & crontributors
7
  * Author URI: https://github.com/johnbillion/wp-crontrol/graphs/contributors
8
+ * Version: 1.8.2
9
  * Text Domain: wp-crontrol
10
  * Domain Path: /languages/
11
  * Requires PHP: 5.3.6
50
  add_action( 'init', __NAMESPACE__ . '\action_init' );
51
  add_action( 'init', __NAMESPACE__ . '\action_handle_posts' );
52
  add_action( 'admin_menu', __NAMESPACE__ . '\action_admin_menu' );
53
+ add_action( 'wp_ajax_crontrol_checkhash', __NAMESPACE__ . '\ajax_check_events_hash' );
54
  add_filter( "plugin_action_links_{$plugin_file}", __NAMESPACE__ . '\plugin_action_links', 10, 4 );
55
  add_filter( 'removable_query_args', __NAMESPACE__ . '\filter_removable_query_args' );
56
  add_filter( 'in_admin_header', __NAMESPACE__ . '\do_tabs' );
57
+ add_filter( 'pre_unschedule_event', __NAMESPACE__ . '\maybe_clear_doing_cron' );
58
 
59
+ add_action( 'load-tools_page_crontrol_admin_manage_page', __NAMESPACE__ . '\setup_manage_page' );
60
 
61
  add_filter( 'cron_schedules', __NAMESPACE__ . '\filter_cron_schedules' );
62
  add_action( 'crontrol_cron_job', __NAMESPACE__ . '\action_php_cron_event' );
520
  <?php
521
  }
522
 
523
+ /**
524
+ * Clears the doing cron status when an event is unscheduled.
525
+ *
526
+ * What on earth does this function do, and why?
527
+ *
528
+ * Good question. The purpose of this function is to prevent other overdue cron events from firing when an event is run
529
+ * manually with the "Run Now" action. WP Crontrol works very hard to ensure that when cron event runs manually that it
530
+ * runs in the exact same way it would run as part of its schedule - via a properly spawned cron with a queued event in
531
+ * place. It does this by queueing an event at time `1` (1 second into 1st January 1970) and then immediately spawning
532
+ * cron (see the `Event\run()` function).
533
+ *
534
+ * The problem this causes is if other events are due then they will all run too, and this isn't desirable because if a
535
+ * site has a large number of stuck events due to a problem with the cron runner then it's not desirable for all those
536
+ * events to run when another is manually run. This happens because WordPress core will attempt to run all due events
537
+ * whenever cron is spawned.
538
+ *
539
+ * The code in this function prevents multiple events from running by changing the value of the `doing_cron` transient
540
+ * when an event gets unscheduled during a manual run, which prevents wp-cron.php from iterating more than one event.
541
+ *
542
+ * The `pre_unschedule_event` filter is used for this because it's just about the only hook available within this loop.
543
+ *
544
+ * Refs:
545
+ * - https://core.trac.wordpress.org/browser/trunk/src/wp-cron.php?rev=47198&marks=127,141#L122
546
+ *
547
+ * @param mixed $pre The pre-flight value of the event unschedule short-circuit. Not used.
548
+ * @return mixed Thee unaltered pre-flight value.
549
+ */
550
+ function maybe_clear_doing_cron( $pre ) {
551
+ if ( defined( 'DOING_CRON' ) && DOING_CRON && isset( $_GET['crontrol-single-event'] ) ) {
552
+ delete_transient( 'doing_cron' );
553
+ }
554
+
555
+ return $pre;
556
+ }
557
+
558
+ /**
559
+ * Ajax handler which outputs a hash of the current list of scheduled events.
560
+ */
561
+ function ajax_check_events_hash() {
562
+ if ( ! current_user_can( 'manage_options' ) ) {
563
+ wp_send_json_error( null, 403 );
564
+ }
565
+
566
+ wp_send_json_success( md5( json_encode( Event\get_list_table()->items ) ) );
567
+ }
568
+
569
  /**
570
  * Gets the status of WP-Cron functionality on the site by performing a test spawn if necessary. Cached for one hour when all is well.
571
  *
616
  $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
617
 
618
  $cron_request = apply_filters( 'cron_request', array(
619
+ 'url' => add_query_arg( 'doing_wp_cron', $doing_wp_cron, site_url( 'wp-cron.php' ) ),
620
  'key' => $doing_wp_cron,
621
  'args' => array(
622
  'timeout' => 3,
1047
  );
1048
  }
1049
 
 
 
1050
  $tabs = get_tab_states();
1051
+ $table = Event\get_list_table();
 
 
1052
 
1053
  switch ( true ) {
1054
  case $tabs['events']:
1368
  }
1369
 
1370
  /**
1371
+ * Sets up the Events listing screen.
1372
  */
1373
+ function setup_manage_page() {
1374
+ // Initialise the list table
1375
+ Event\get_list_table();
1376
+
1377
+ // Add the initially hidden admin notice about the out of date events list
1378
+ add_action( 'admin_notices', function() {
1379
+ printf(
1380
+ '<div id="crontrol-hash-message" class="notice notice-warning"><p>%s</p></div>',
1381
+ esc_html__( 'The scheduled cron events have changed since you first opened this page. Reload the page to see the up to date list.', 'wp-crontrol' )
1382
+ );
1383
+ } );
1384
+
1385
  if ( ! function_exists( 'wp_enqueue_code_editor' ) ) {
1386
  return;
1387
  }
1423
  wp_enqueue_style( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'css/wp-crontrol.css', array(), $ver );
1424
 
1425
  $ver = filemtime( plugin_dir_path( __FILE__ ) . 'js/wp-crontrol.js' );
1426
+ wp_enqueue_script( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'js/wp-crontrol.js', array( 'jquery' ), $ver, true );
1427
+
1428
+ if ( ! empty( $tab['events'] ) ) {
1429
+ wp_localize_script( 'wp-crontrol', 'wpCrontrol', array(
1430
+ 'eventsHash' => md5( json_encode( Event\get_list_table()->items ) ),
1431
+ 'eventsHashInterval' => 20,
1432
+ ) );
1433
+ }
1434
  }
1435
 
1436
  /**
1459
  'wp_privacy_delete_old_export_files',
1460
  'wp_scheduled_auto_draft_delete',
1461
  'wp_scheduled_delete',
1462
+ 'wp_site_health_scheduled_check',
1463
  'wp_update_plugins',
1464
  'wp_update_themes',
1465
  'wp_version_check',
1524
  * Security: A user can only add or edit a PHP cron event if they have the `edit_files` capability. This means if a user
1525
  * cannot edit files on the site (eg. through the plugin or theme editor) then they cannot edit or add a PHP cron event.
1526
  *
1527
+ * Therefore, the user access level required to execute arbitrary PHP code does not change with WP Crontrol activated.
1528
+ *
1529
  * @param string $code The PHP code to evaluate.
1530
  */
1531
  function action_php_cron_event( $code ) {