WP Crontrol - Version 1.11.0

Version Description

Download this release

Release Info

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

Code changes from version 1.10.0 to 1.11.0

.editorconfig ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = tab
9
+
10
+ [*.yml]
11
+ indent_style = space
12
+ indent_size = 2
13
+
14
+ [{*.neon,*.neon.dist}]
15
+ indent_style = space
16
+ indent_size = 4
17
+
18
+ [*.md]
19
+ trim_trailing_whitespace = false
phpstan.neon.dist ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ includes:
2
+ - vendor/szepeviktor/phpstan-wordpress/extension.neon
3
+ parameters:
4
+ level: 8
5
+ inferPrivatePropertyTypeFromConstructor: true
6
+ paths:
7
+ - wp-crontrol.php
8
+ - src/
9
+ bootstrapFiles:
10
+ - tests/stubs.php
11
+ ignoreErrors:
12
+ # Uses func_get_args()
13
+ - '#^Function apply_filters invoked with [34567] parameters, 2 required\.$#'
14
+ # External plugin
15
+ - '#QM_Util#'
readme.md CHANGED
@@ -4,7 +4,7 @@ Contributors: johnbillion, scompt
4
  Tags: cron, wp-cron, crontrol, debug
5
  Requires at least: 4.2
6
  Tested up to: 5.8
7
- Stable tag: 1.10.0
8
  Requires PHP: 5.3
9
  Donate link: https://github.com/sponsors/johnbillion
10
 
@@ -19,6 +19,7 @@ WP Crontrol enables you to view and control what's happening in the WP-Cron syst
19
  * Add new cron events.
20
  * Bulk delete cron events.
21
  * Add and remove custom cron schedules.
 
22
 
23
  WP Crontrol is aware of timezones, will alert you to events that have no actions or that have missed their schedule, and will show you a warning message if your cron system doesn't appear to be working (for example if your server can't connect to itself to fire scheduled cron events).
24
 
@@ -84,10 +85,18 @@ Please see the "Which users can manage PHP cron events?" FAQ for information abo
84
 
85
  [You can read all about problems with editing cron events here](https://github.com/johnbillion/wp-crontrol/wiki/Problems-adding-or-editing-WP-Cron-events).
86
 
 
 
 
 
87
  ### Can I see a historical log of all the cron events that ran on my site?
88
 
89
  Not yet, but I hope to add this functionality soon.
90
 
 
 
 
 
91
  ### What's the use of adding new cron schedules?
92
 
93
  Cron schedules are used by WordPress and plugins for scheduling events to be executed at regular intervals. Intervals must be provided by the WordPress core or a plugin in order to be used. As an example, many backup plugins provide support for periodic backups. In order to do a weekly backup, a weekly cron schedule must be entered into WP Crontrol first and then a backup plugin can take advantage of it as an interval.
@@ -116,6 +125,8 @@ The next step is to write your function. Here's a simple example:
116
 
117
  In the Tools → Cron Events admin panel, click on "Add New". In the form that appears, select "PHP Cron Event" and enter the schedule and next run time. The event schedule is how often your event will be executed. If you don't see a good interval, then add one in the Settings → Cron Schedules admin panel. In the "Hook code" area, enter the PHP code that should be run when your cron event is executed. You don't need to provide the PHP opening tag (`<?php`).
118
 
 
 
119
  ### Which users can manage cron events and schedules?
120
 
121
  Only users with the `manage_options` capability can manage cron events and schedules. By default, only Administrators have this capability.
@@ -132,6 +143,10 @@ Therefore, the user access level required to execute arbitrary PHP code does not
132
 
133
  The cron commands which were previously included in WP Crontrol are now part of WP-CLI (since 0.16), so this plugin no longer provides any WP-CLI commands. See `wp help cron` for more info.
134
 
 
 
 
 
135
  ## Screenshots
136
 
137
  1. Cron events can be modified, deleted, and executed<br>![](.wordpress-org/screenshot-1.png)
@@ -142,6 +157,14 @@ The cron commands which were previously included in WP Crontrol are now part of
142
 
143
  ## Changelog ##
144
 
 
 
 
 
 
 
 
 
145
  ### 1.10.0 ###
146
 
147
  * Support for more granular cron-related error messages in WordPress 5.7
4
  Tags: cron, wp-cron, crontrol, debug
5
  Requires at least: 4.2
6
  Tested up to: 5.8
7
+ Stable tag: 1.11.0
8
  Requires PHP: 5.3
9
  Donate link: https://github.com/sponsors/johnbillion
10
 
19
  * Add new cron events.
20
  * Bulk delete cron events.
21
  * Add and remove custom cron schedules.
22
+ * Export cron event lists as a CSV file.
23
 
24
  WP Crontrol is aware of timezones, will alert you to events that have no actions or that have missed their schedule, and will show you a warning message if your cron system doesn't appear to be working (for example if your server can't connect to itself to fire scheduled cron events).
25
 
85
 
86
  [You can read all about problems with editing cron events here](https://github.com/johnbillion/wp-crontrol/wiki/Problems-adding-or-editing-WP-Cron-events).
87
 
88
+ ### Can I export a list of cron events?
89
+
90
+ Yes, a CSV file of the event list can be exported via the "Export" button on the cron event listing screen. This file can be opened in any spreadsheet application.
91
+
92
  ### Can I see a historical log of all the cron events that ran on my site?
93
 
94
  Not yet, but I hope to add this functionality soon.
95
 
96
+ ### Can I see a historical log of edits, additions, and deletions of cron events and schedules?
97
+
98
+ Yes. The excellent <a href="https://wordpress.org/plugins/simple-history/">Simple History plugin</a> has built-in support for logging actions performed via WP Crontrol.
99
+
100
  ### What's the use of adding new cron schedules?
101
 
102
  Cron schedules are used by WordPress and plugins for scheduling events to be executed at regular intervals. Intervals must be provided by the WordPress core or a plugin in order to be used. As an example, many backup plugins provide support for periodic backups. In order to do a weekly backup, a weekly cron schedule must be entered into WP Crontrol first and then a backup plugin can take advantage of it as an interval.
125
 
126
  In the Tools → Cron Events admin panel, click on "Add New". In the form that appears, select "PHP Cron Event" and enter the schedule and next run time. The event schedule is how often your event will be executed. If you don't see a good interval, then add one in the Settings → Cron Schedules admin panel. In the "Hook code" area, enter the PHP code that should be run when your cron event is executed. You don't need to provide the PHP opening tag (`<?php`).
127
 
128
+ Please see the "Which users can manage PHP cron events?" FAQ for information about which users can create PHP cron events.
129
+
130
  ### Which users can manage cron events and schedules?
131
 
132
  Only users with the `manage_options` capability can manage cron events and schedules. By default, only Administrators have this capability.
143
 
144
  The cron commands which were previously included in WP Crontrol are now part of WP-CLI (since 0.16), so this plugin no longer provides any WP-CLI commands. See `wp help cron` for more info.
145
 
146
+ ### Who took the photo in the plugin header image?
147
+
148
+ The photo was taken by <a href="https://www.flickr.com/photos/michaelpardo/21453119315">Michael Pardo</a> and is in the public domain.
149
+
150
  ## Screenshots
151
 
152
  1. Cron events can be modified, deleted, and executed<br>![](.wordpress-org/screenshot-1.png)
157
 
158
  ## Changelog ##
159
 
160
+ ### 1.11.0 ###
161
+
162
+ * Introduced an `Export` feature to the event listing screen for exporting the list of events as a CSV file.
163
+ * Added the timezone offset to the date displayed for events that are due to run after the next DST change, for extra clarity.
164
+ * Introduced the `crontrol/filter-types` and `crontrol/filtered-events` filters for adjusting the available event filters on the event listing screen.
165
+ * Lots of code quality improvements (thanks, PHPStan!).
166
+
167
+
168
  ### 1.10.0 ###
169
 
170
  * Support for more granular cron-related error messages in WordPress 5.7
src/event-list-table.php CHANGED
@@ -40,7 +40,7 @@ class Table extends \WP_List_Table {
40
  /**
41
  * Array of all cron events.
42
  *
43
- * @var object[] Array of event objects.
44
  */
45
  protected $all_events = array();
46
 
@@ -58,6 +58,8 @@ class Table extends \WP_List_Table {
58
 
59
  /**
60
  * Prepares the list table items and arguments.
 
 
61
  */
62
  public function prepare_items() {
63
  self::$persistent_core_hooks = \Crontrol\get_persistent_core_hooks();
@@ -77,7 +79,7 @@ class Table extends \WP_List_Table {
77
 
78
  if ( ! empty( $_GET['hooks_type'] ) ) {
79
  $hooks_type = sanitize_text_field( $_GET['hooks_type'] );
80
- $filtered = $this->get_filtered_events( $events );
81
 
82
  if ( isset( $filtered[ $hooks_type ] ) ) {
83
  $events = $filtered[ $hooks_type ];
@@ -114,10 +116,10 @@ class Table extends \WP_List_Table {
114
  /**
115
  * Returns events filtered by various parameters
116
  *
117
- * @param array $events The list of all events.
118
- * @return array Array of filtered events keyed by parameter.
119
  */
120
- protected function get_filtered_events( array $events ) {
121
  $all_core_hooks = \Crontrol\get_all_core_hooks();
122
  $filtered = array(
123
  'all' => $events,
@@ -136,6 +138,18 @@ class Table extends \WP_List_Table {
136
  return ( ! in_array( $event->hook, $all_core_hooks, true ) );
137
  } );
138
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  return $filtered;
140
  }
141
 
@@ -162,7 +176,7 @@ class Table extends \WP_List_Table {
162
  /**
163
  * Columns to make sortable.
164
  *
165
- * @return array
166
  */
167
  public function get_sortable_columns() {
168
  return array(
@@ -174,7 +188,7 @@ class Table extends \WP_List_Table {
174
  /**
175
  * Returns an array of CSS class names for the table.
176
  *
177
- * @return string[] Array of class names.
178
  */
179
  protected function get_table_classes() {
180
  return array( 'widefat', 'striped', $this->_args['plural'] );
@@ -186,21 +200,21 @@ class Table extends \WP_List_Table {
186
  *
187
  * @since 3.1.0
188
  *
189
- * @return array
190
  */
191
  protected function get_bulk_actions() {
192
  return array(
193
- 'delete_crons' => esc_html__( 'Delete', 'wp-crontrol' ),
194
  );
195
  }
196
 
197
  /**
198
  * Display the list of hook types.
199
  *
200
- * @return string[]
201
  */
202
  public function get_views() {
203
- $filtered = $this->get_filtered_events( $this->all_events );
204
 
205
  $views = array();
206
  $hooks_type = ( ! empty( $_GET['hooks_type'] ) ? $_GET['hooks_type'] : 'all' );
@@ -212,25 +226,70 @@ class Table extends \WP_List_Table {
212
  'custom' => __( 'Custom events', 'wp-crontrol' ),
213
  );
214
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  $url = admin_url( 'tools.php?page=crontrol_admin_manage_page' );
216
 
 
 
 
217
  foreach ( $types as $key => $type ) {
 
 
 
 
 
 
218
  $views[ $key ] = sprintf(
219
  '<a href="%1$s"%2$s>%3$s <span class="count">(%4$s)</span></a>',
220
- 'all' === $key ? $url : add_query_arg( 'hooks_type', $key, $url ),
221
  $hooks_type === $key ? ' class="current"' : '',
222
- $type,
223
- count( $filtered[ $key ] )
224
  );
225
  }
226
 
227
  return $views;
228
  }
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  /**
231
  * Generates content for a single row of the table.
232
  *
233
- * @param object $event The current event.
 
234
  */
235
  public function single_row( $event ) {
236
  $classes = array();
@@ -289,7 +348,7 @@ class Table extends \WP_List_Table {
289
  if ( ( 'crontrol_cron_job' !== $event->hook ) || self::$can_edit_files ) {
290
  $link = array(
291
  'page' => 'crontrol_admin_manage_page',
292
- 'action' => 'edit-cron',
293
  'id' => rawurlencode( $event->hook ),
294
  'sig' => rawurlencode( $event->sig ),
295
  'next_run_utc' => rawurlencode( $event->time ),
@@ -301,26 +360,26 @@ class Table extends \WP_List_Table {
301
 
302
  $link = array(
303
  'page' => 'crontrol_admin_manage_page',
304
- 'action' => 'run-cron',
305
  'id' => rawurlencode( $event->hook ),
306
  'sig' => rawurlencode( $event->sig ),
307
  'next_run_utc' => rawurlencode( $event->time ),
308
  );
309
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
310
- $link = wp_nonce_url( $link, "run-cron_{$event->hook}_{$event->sig}" );
311
 
312
  $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Run Now', 'wp-crontrol' ) . '</a>';
313
 
314
  if ( ! in_array( $event->hook, self::$persistent_core_hooks, true ) && ( ( 'crontrol_cron_job' !== $event->hook ) || self::$can_edit_files ) ) {
315
  $link = array(
316
  'page' => 'crontrol_admin_manage_page',
317
- 'action' => 'delete-cron',
318
  'id' => rawurlencode( $event->hook ),
319
  'sig' => rawurlencode( $event->sig ),
320
  'next_run_utc' => rawurlencode( $event->time ),
321
  );
322
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
323
- $link = wp_nonce_url( $link, "delete-cron_{$event->hook}_{$event->sig}_{$event->time}" );
324
 
325
  $links[] = "<span class='delete'><a href='" . esc_url( $link ) . "'>" . esc_html__( 'Delete', 'wp-crontrol' ) . '</a></span>';
326
  }
@@ -329,11 +388,11 @@ class Table extends \WP_List_Table {
329
  if ( self::$count_by_hook[ $event->hook ] > 1 ) {
330
  $link = array(
331
  'page' => 'crontrol_admin_manage_page',
332
- 'action' => 'delete-hook',
333
  'id' => rawurlencode( $event->hook ),
334
  );
335
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
336
- $link = wp_nonce_url( $link, "delete-hook_{$event->hook}" );
337
 
338
  $text = sprintf(
339
  /* translators: %s: Number of events with a given name */
@@ -408,10 +467,6 @@ class Table extends \WP_List_Table {
408
  * @return string The cell output.
409
  */
410
  protected function column_crontrol_args( $event ) {
411
- if ( ! empty( $event->args ) ) {
412
- $args = \Crontrol\json_output( $event->args );
413
- }
414
-
415
  if ( 'crontrol_cron_job' === $event->hook ) {
416
  $return = '<em>' . esc_html__( 'PHP Code', 'wp-crontrol' ) . '</em>';
417
 
@@ -448,7 +503,7 @@ class Table extends \WP_List_Table {
448
  } else {
449
  return sprintf(
450
  '<pre>%s</pre>',
451
- esc_html( $args )
452
  );
453
  }
454
  }
@@ -488,8 +543,16 @@ class Table extends \WP_List_Table {
488
  * @return string The cell output.
489
  */
490
  protected function column_crontrol_next( $event ) {
491
- $date_utc = gmdate( 'Y-m-d\TH:i:s+00:00', $event->time );
492
- $date_local = get_date_from_gmt( date( 'Y-m-d H:i:s', $event->time ), 'Y-m-d H:i:s' );
 
 
 
 
 
 
 
 
493
 
494
  $time = sprintf(
495
  '<time datetime="%1$s">%2$s</time>',
@@ -556,6 +619,8 @@ class Table extends \WP_List_Table {
556
 
557
  /**
558
  * Outputs a message when there are no items to show in the table.
 
 
559
  */
560
  public function no_items() {
561
  if ( empty( $_GET['s'] ) && empty( $_GET['hooks_type'] ) ) {
40
  /**
41
  * Array of all cron events.
42
  *
43
+ * @var stdClass[] Array of event objects.
44
  */
45
  protected $all_events = array();
46
 
58
 
59
  /**
60
  * Prepares the list table items and arguments.
61
+ *
62
+ * @return void
63
  */
64
  public function prepare_items() {
65
  self::$persistent_core_hooks = \Crontrol\get_persistent_core_hooks();
79
 
80
  if ( ! empty( $_GET['hooks_type'] ) ) {
81
  $hooks_type = sanitize_text_field( $_GET['hooks_type'] );
82
+ $filtered = self::get_filtered_events( $events );
83
 
84
  if ( isset( $filtered[ $hooks_type ] ) ) {
85
  $events = $filtered[ $hooks_type ];
116
  /**
117
  * Returns events filtered by various parameters
118
  *
119
+ * @param stdClass[] $events The list of all events.
120
+ * @return stdClass[][] Array of filtered events keyed by filter name.
121
  */
122
+ public static function get_filtered_events( array $events ) {
123
  $all_core_hooks = \Crontrol\get_all_core_hooks();
124
  $filtered = array(
125
  'all' => $events,
138
  return ( ! in_array( $event->hook, $all_core_hooks, true ) );
139
  } );
140
 
141
+ /**
142
+ * Filters the available filtered events on the cron event listing screen.
143
+ *
144
+ * See the corresponding `crontrol/filter-types` filter to adjust the list of filter types.
145
+ *
146
+ * @since 1.11.0
147
+ *
148
+ * @param array[] $filtered Array of filtered event arrays keyed by filter name.
149
+ * @param stdClass[] $events Array of all events.
150
+ */
151
+ $filtered = apply_filters( 'crontrol/filtered-events', $filtered, $events );
152
+
153
  return $filtered;
154
  }
155
 
176
  /**
177
  * Columns to make sortable.
178
  *
179
+ * @return array<string,array<int,(string|bool)>>
180
  */
181
  public function get_sortable_columns() {
182
  return array(
188
  /**
189
  * Returns an array of CSS class names for the table.
190
  *
191
+ * @return array<int,string> Array of class names.
192
  */
193
  protected function get_table_classes() {
194
  return array( 'widefat', 'striped', $this->_args['plural'] );
200
  *
201
  * @since 3.1.0
202
  *
203
+ * @return array<string,string>
204
  */
205
  protected function get_bulk_actions() {
206
  return array(
207
+ 'crontrol_delete_crons' => esc_html__( 'Delete', 'wp-crontrol' ),
208
  );
209
  }
210
 
211
  /**
212
  * Display the list of hook types.
213
  *
214
+ * @return array<string,string>
215
  */
216
  public function get_views() {
217
+ $filtered = self::get_filtered_events( $this->all_events );
218
 
219
  $views = array();
220
  $hooks_type = ( ! empty( $_GET['hooks_type'] ) ? $_GET['hooks_type'] : 'all' );
226
  'custom' => __( 'Custom events', 'wp-crontrol' ),
227
  );
228
 
229
+ /**
230
+ * Filters the filter types on the cron event listing screen.
231
+ *
232
+ * See the corresponding `crontrol/filtered-events` filter to adjust the filtered events.
233
+ *
234
+ * @since 1.11.0
235
+ *
236
+ * @param string[] $types Array of filter names keyed by filter name.
237
+ * @param string $hooks_type The current filter name.
238
+ */
239
+ $types = apply_filters( 'crontrol/filter-types', $types, $hooks_type );
240
+
241
  $url = admin_url( 'tools.php?page=crontrol_admin_manage_page' );
242
 
243
+ /**
244
+ * @var array<string,string> $types
245
+ */
246
  foreach ( $types as $key => $type ) {
247
+ if ( ! isset( $filtered[ $key ] ) ) {
248
+ continue;
249
+ }
250
+
251
+ $link = ( 'all' === $key ) ? $url : add_query_arg( 'hooks_type', $key, $url );
252
+
253
  $views[ $key ] = sprintf(
254
  '<a href="%1$s"%2$s>%3$s <span class="count">(%4$s)</span></a>',
255
+ esc_url( $link ),
256
  $hooks_type === $key ? ' class="current"' : '',
257
+ esc_html( $type ),
258
+ number_format_i18n( count( $filtered[ $key ] ) )
259
  );
260
  }
261
 
262
  return $views;
263
  }
264
 
265
+ /**
266
+ * Extra controls to be displayed between bulk actions and pagination.
267
+ *
268
+ * @param string $which One of 'top' or 'bottom' to indicate the position on the screen.
269
+ *
270
+ * @return void
271
+ */
272
+ protected function extra_tablenav( $which ) {
273
+ wp_nonce_field( 'crontrol-export-event-csv', 'crontrol_nonce' );
274
+ printf(
275
+ '<input type="hidden" name="hooks_type" value="%s"/>',
276
+ esc_attr( isset( $_GET['hooks_type'] ) ? sanitize_text_field( wp_unslash( $_GET['hooks_type'] ) ) : 'all' )
277
+ );
278
+ printf(
279
+ '<input type="hidden" name="s" value="%s"/>',
280
+ esc_attr( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' )
281
+ );
282
+ printf(
283
+ '<button class="button" type="submit" name="action" value="crontrol-export-event-csv">%s</button>',
284
+ esc_html__( 'Export', 'wp-crontrol' )
285
+ );
286
+ }
287
+
288
  /**
289
  * Generates content for a single row of the table.
290
  *
291
+ * @param stdClass $event The current event.
292
+ * @return void
293
  */
294
  public function single_row( $event ) {
295
  $classes = array();
348
  if ( ( 'crontrol_cron_job' !== $event->hook ) || self::$can_edit_files ) {
349
  $link = array(
350
  'page' => 'crontrol_admin_manage_page',
351
+ 'action' => 'crontrol-edit-cron',
352
  'id' => rawurlencode( $event->hook ),
353
  'sig' => rawurlencode( $event->sig ),
354
  'next_run_utc' => rawurlencode( $event->time ),
360
 
361
  $link = array(
362
  'page' => 'crontrol_admin_manage_page',
363
+ 'action' => 'crontrol-run-cron',
364
  'id' => rawurlencode( $event->hook ),
365
  'sig' => rawurlencode( $event->sig ),
366
  'next_run_utc' => rawurlencode( $event->time ),
367
  );
368
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
369
+ $link = wp_nonce_url( $link, "crontrol-run-cron_{$event->hook}_{$event->sig}" );
370
 
371
  $links[] = "<a href='" . esc_url( $link ) . "'>" . esc_html__( 'Run Now', 'wp-crontrol' ) . '</a>';
372
 
373
  if ( ! in_array( $event->hook, self::$persistent_core_hooks, true ) && ( ( 'crontrol_cron_job' !== $event->hook ) || self::$can_edit_files ) ) {
374
  $link = array(
375
  'page' => 'crontrol_admin_manage_page',
376
+ 'action' => 'crontrol-delete-cron',
377
  'id' => rawurlencode( $event->hook ),
378
  'sig' => rawurlencode( $event->sig ),
379
  'next_run_utc' => rawurlencode( $event->time ),
380
  );
381
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
382
+ $link = wp_nonce_url( $link, "crontrol-delete-cron_{$event->hook}_{$event->sig}_{$event->time}" );
383
 
384
  $links[] = "<span class='delete'><a href='" . esc_url( $link ) . "'>" . esc_html__( 'Delete', 'wp-crontrol' ) . '</a></span>';
385
  }
388
  if ( self::$count_by_hook[ $event->hook ] > 1 ) {
389
  $link = array(
390
  'page' => 'crontrol_admin_manage_page',
391
+ 'action' => 'crontrol-delete-hook',
392
  'id' => rawurlencode( $event->hook ),
393
  );
394
  $link = add_query_arg( $link, admin_url( 'tools.php' ) );
395
+ $link = wp_nonce_url( $link, "crontrol-delete-hook_{$event->hook}" );
396
 
397
  $text = sprintf(
398
  /* translators: %s: Number of events with a given name */
467
  * @return string The cell output.
468
  */
469
  protected function column_crontrol_args( $event ) {
 
 
 
 
470
  if ( 'crontrol_cron_job' === $event->hook ) {
471
  $return = '<em>' . esc_html__( 'PHP Code', 'wp-crontrol' ) . '</em>';
472
 
503
  } else {
504
  return sprintf(
505
  '<pre>%s</pre>',
506
+ esc_html( \Crontrol\json_output( $event->args ) )
507
  );
508
  }
509
  }
543
  * @return string The cell output.
544
  */
545
  protected function column_crontrol_next( $event ) {
546
+ $date_local_format = 'Y-m-d H:i:s';
547
+ $offset_site = get_date_from_gmt( 'now', 'P' );
548
+ $offset_event = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->time ), 'P' );
549
+
550
+ if ( $offset_site !== $offset_event ) {
551
+ $date_local_format .= ' P';
552
+ }
553
+
554
+ $date_utc = gmdate( 'c', $event->time );
555
+ $date_local = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->time ), $date_local_format );
556
 
557
  $time = sprintf(
558
  '<time datetime="%1$s">%2$s</time>',
619
 
620
  /**
621
  * Outputs a message when there are no items to show in the table.
622
+ *
623
+ * @return void
624
  */
625
  public function no_items() {
626
  if ( empty( $_GET['s'] ) && empty( $_GET['hooks_type'] ) ) {
src/event.php CHANGED
@@ -21,7 +21,8 @@ use WP_Error;
21
  * @return true|WP_Error True if the execution was successful, WP_Error if not.
22
  */
23
  function run( $hookname, $sig ) {
24
- $crons = _get_cron_array();
 
25
  foreach ( $crons as $time => $cron ) {
26
  if ( isset( $cron[ $hookname ][ $sig ] ) ) {
27
  $event = $cron[ $hookname ][ $sig ];
@@ -50,7 +51,7 @@ function run( $hookname, $sig ) {
50
  /**
51
  * Fires after a cron event is scheduled to run manually.
52
  *
53
- * @param object $event {
54
  * An object containing the event's data.
55
  *
56
  * @type string $hook Action hook to execute when the event is run.
@@ -81,8 +82,8 @@ function run( $hookname, $sig ) {
81
  *
82
  * This is used instead of `wp_schedule_single_event()` to avoid the duplicate check that's otherwise performed.
83
  *
84
- * @param string $hook Action hook to execute when the event is run.
85
- * @param array $args Optional. Array containing each separate argument to pass to the hook's callback function.
86
  * @return true|WP_Error True if event successfully scheduled. WP_Error on failure.
87
  */
88
  function force_schedule_single_event( $hook, $args = array() ) {
@@ -92,7 +93,7 @@ function force_schedule_single_event( $hook, $args = array() ) {
92
  'schedule' => false,
93
  'args' => $args,
94
  );
95
- $crons = (array) _get_cron_array();
96
  $key = md5( serialize( $event->args ) );
97
 
98
  $crons[ $event->timestamp ][ $event->hook ][ $key ] = array(
@@ -121,14 +122,18 @@ function force_schedule_single_event( $hook, $args = array() ) {
121
  /**
122
  * Adds a new cron event.
123
  *
124
- * @param string $next_run_local The time that the event should be run at, in the site's timezone.
125
- * @param string $schedule The recurrence of the cron event.
126
- * @param string $hook The name of the hook to execute.
127
- * @param array $args Arguments to add to the cron event.
128
  * @return true|WP_error True if the addition was successful, WP_Error otherwise.
129
  */
130
  function add( $next_run_local, $schedule, $hook, array $args ) {
131
- $next_run_local = strtotime( $next_run_local, current_time( 'timestamp' ) );
 
 
 
 
132
 
133
  if ( false === $next_run_local ) {
134
  return new WP_Error(
@@ -137,7 +142,7 @@ function add( $next_run_local, $schedule, $hook, array $args ) {
137
  );
138
  }
139
 
140
- $next_run_utc = get_gmt_from_date( gmdate( 'Y-m-d H:i:s', $next_run_local ), 'U' );
141
 
142
  if ( ! is_array( $args ) ) {
143
  $args = array();
@@ -157,7 +162,7 @@ function add( $next_run_local, $schedule, $hook, array $args ) {
157
  }
158
  }
159
 
160
- if ( '_oneoff' === $schedule ) {
161
  $result = wp_schedule_single_event( $next_run_utc, $hook, $args, true );
162
  } else {
163
  $result = wp_schedule_event( $next_run_utc, $schedule, $hook, $args, true );
@@ -235,10 +240,10 @@ function delete( $hook, $sig, $next_run_utc ) {
235
  /**
236
  * Returns a flattened array of cron events.
237
  *
238
- * @return object[] An array of cron event objects.
239
  */
240
  function get() {
241
- $crons = _get_cron_array();
242
  $events = array();
243
 
244
  if ( empty( $crons ) ) {
@@ -276,10 +281,11 @@ function get() {
276
  * @param string $hook The hook name of the event.
277
  * @param string $sig The event signature.
278
  * @param string $next_run_utc The UTC time that the event would be run at.
279
- * @return object|WP_Error A cron event object, or a WP_Error if it's not found.
280
  */
281
  function get_single( $hook, $sig, $next_run_utc ) {
282
- $crons = _get_cron_array();
 
283
  if ( isset( $crons[ $next_run_utc ][ $hook ][ $sig ] ) ) {
284
  $event = $crons[ $next_run_utc ][ $hook ][ $sig ];
285
 
@@ -304,10 +310,10 @@ function get_single( $hook, $sig, $next_run_utc ) {
304
  /**
305
  * Returns an array of the number of events for each hook.
306
  *
307
- * @return int[] Array of number of events for each hook, keyed by the hook name.
308
  */
309
  function count_by_hook() {
310
- $crons = _get_cron_array();
311
  $events = array();
312
 
313
  if ( empty( $crons ) ) {
@@ -402,32 +408,55 @@ function get_list_table() {
402
  * The comparison function returns an integer less than, equal to, or greater than zero if the first argument is
403
  * considered to be respectively less than, equal to, or greater than the second.
404
  *
405
- * @param object $a The first event to compare.
406
- * @param object $b The second event to compare.
407
  * @return int
408
  */
409
  function uasort_order_events( $a, $b ) {
410
  $orderby = ( ! empty( $_GET['orderby'] ) ) ? sanitize_text_field( $_GET['orderby'] ) : 'crontrol_next';
411
  $order = ( ! empty( $_GET['order'] ) ) ? sanitize_text_field( $_GET['order'] ) : 'desc';
 
412
 
413
  switch ( $orderby ) {
414
  case 'crontrol_hook':
415
  if ( 'desc' === $order ) {
416
- return strcmp( $a->hook, $b->hook );
417
  } else {
418
- return strcmp( $b->hook, $a->hook );
419
  }
420
  break;
421
  default:
422
  if ( $a->time === $b->time ) {
423
- return 0;
424
  } else {
425
  if ( 'desc' === $order ) {
426
- return ( $a->time > $b->time ) ? 1 : -1;
427
  } else {
428
- return ( $a->time < $b->time ) ? 1 : -1;
429
  }
430
  }
431
  break;
432
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
21
  * @return true|WP_Error True if the execution was successful, WP_Error if not.
22
  */
23
  function run( $hookname, $sig ) {
24
+ $crons = get_core_cron_array();
25
+
26
  foreach ( $crons as $time => $cron ) {
27
  if ( isset( $cron[ $hookname ][ $sig ] ) ) {
28
  $event = $cron[ $hookname ][ $sig ];
51
  /**
52
  * Fires after a cron event is scheduled to run manually.
53
  *
54
+ * @param stdClass $event {
55
  * An object containing the event's data.
56
  *
57
  * @type string $hook Action hook to execute when the event is run.
82
  *
83
  * This is used instead of `wp_schedule_single_event()` to avoid the duplicate check that's otherwise performed.
84
  *
85
+ * @param string $hook Action hook to execute when the event is run.
86
+ * @param mixed[] $args Optional. Array containing each separate argument to pass to the hook's callback function.
87
  * @return true|WP_Error True if event successfully scheduled. WP_Error on failure.
88
  */
89
  function force_schedule_single_event( $hook, $args = array() ) {
93
  'schedule' => false,
94
  'args' => $args,
95
  );
96
+ $crons = get_core_cron_array();
97
  $key = md5( serialize( $event->args ) );
98
 
99
  $crons[ $event->timestamp ][ $event->hook ][ $key ] = array(
122
  /**
123
  * Adds a new cron event.
124
  *
125
+ * @param string $next_run_local The time that the event should be run at, in the site's timezone.
126
+ * @param string $schedule The recurrence of the cron event.
127
+ * @param string $hook The name of the hook to execute.
128
+ * @param mixed[] $args Arguments to add to the cron event.
129
  * @return true|WP_error True if the addition was successful, WP_Error otherwise.
130
  */
131
  function add( $next_run_local, $schedule, $hook, array $args ) {
132
+ /**
133
+ * @var int
134
+ */
135
+ $current_time = current_time( 'timestamp' );
136
+ $next_run_local = strtotime( $next_run_local, $current_time );
137
 
138
  if ( false === $next_run_local ) {
139
  return new WP_Error(
142
  );
143
  }
144
 
145
+ $next_run_utc = (int) get_gmt_from_date( gmdate( 'Y-m-d H:i:s', $next_run_local ), 'U' );
146
 
147
  if ( ! is_array( $args ) ) {
148
  $args = array();
162
  }
163
  }
164
 
165
+ if ( '_oneoff' === $schedule || '' === $schedule ) {
166
  $result = wp_schedule_single_event( $next_run_utc, $hook, $args, true );
167
  } else {
168
  $result = wp_schedule_event( $next_run_utc, $schedule, $hook, $args, true );
240
  /**
241
  * Returns a flattened array of cron events.
242
  *
243
+ * @return stdClass[] An array of cron event objects.
244
  */
245
  function get() {
246
+ $crons = get_core_cron_array();
247
  $events = array();
248
 
249
  if ( empty( $crons ) ) {
281
  * @param string $hook The hook name of the event.
282
  * @param string $sig The event signature.
283
  * @param string $next_run_utc The UTC time that the event would be run at.
284
+ * @return stdClass|WP_Error A cron event object, or a WP_Error if it's not found.
285
  */
286
  function get_single( $hook, $sig, $next_run_utc ) {
287
+ $crons = get_core_cron_array();
288
+
289
  if ( isset( $crons[ $next_run_utc ][ $hook ][ $sig ] ) ) {
290
  $event = $crons[ $next_run_utc ][ $hook ][ $sig ];
291
 
310
  /**
311
  * Returns an array of the number of events for each hook.
312
  *
313
+ * @return array<string,int> Array of number of events for each hook, keyed by the hook name.
314
  */
315
  function count_by_hook() {
316
+ $crons = get_core_cron_array();
317
  $events = array();
318
 
319
  if ( empty( $crons ) ) {
408
  * The comparison function returns an integer less than, equal to, or greater than zero if the first argument is
409
  * considered to be respectively less than, equal to, or greater than the second.
410
  *
411
+ * @param stdClass $a The first event to compare.
412
+ * @param stdClass $b The second event to compare.
413
  * @return int
414
  */
415
  function uasort_order_events( $a, $b ) {
416
  $orderby = ( ! empty( $_GET['orderby'] ) ) ? sanitize_text_field( $_GET['orderby'] ) : 'crontrol_next';
417
  $order = ( ! empty( $_GET['order'] ) ) ? sanitize_text_field( $_GET['order'] ) : 'desc';
418
+ $compare = 0;
419
 
420
  switch ( $orderby ) {
421
  case 'crontrol_hook':
422
  if ( 'desc' === $order ) {
423
+ $compare = strcmp( $a->hook, $b->hook );
424
  } else {
425
+ $compare = strcmp( $b->hook, $a->hook );
426
  }
427
  break;
428
  default:
429
  if ( $a->time === $b->time ) {
430
+ $compare = 0;
431
  } else {
432
  if ( 'desc' === $order ) {
433
+ $compare = ( $a->time > $b->time ) ? 1 : -1;
434
  } else {
435
+ $compare = ( $a->time < $b->time ) ? 1 : -1;
436
  }
437
  }
438
  break;
439
  }
440
+
441
+ return $compare;
442
+ }
443
+
444
+ /**
445
+ * Fetches the list of cron events from WordPress core.
446
+ *
447
+ * @return array<int,array<string,array<string,array<string,mixed[]>>>>
448
+ * @phpstan-return array<int,array<string,array<string,array<string,array{
449
+ * args: mixed[],
450
+ * schedule: string|false,
451
+ * interval?: int,
452
+ * }>>>>
453
+ */
454
+ function get_core_cron_array() {
455
+ $crons = _get_cron_array();
456
+
457
+ if ( empty( $crons ) ) {
458
+ $crons = array();
459
+ }
460
+
461
+ return $crons;
462
  }
src/request.php ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Request handler.
4
+ *
5
+ * @package wp-crontrol
6
+ */
7
+
8
+ namespace Crontrol;
9
+
10
+ /**
11
+ * Class Request
12
+ */
13
+ class Request {
14
+
15
+ /**
16
+ * Description.
17
+ *
18
+ * @var string
19
+ */
20
+ public $args = '';
21
+
22
+ /**
23
+ * Description.
24
+ *
25
+ * @var string
26
+ */
27
+ public $next_run_date_local = '';
28
+
29
+ /**
30
+ * Description.
31
+ *
32
+ * @var string
33
+ */
34
+ public $next_run_date_local_custom_date = '';
35
+
36
+ /**
37
+ * Description.
38
+ *
39
+ * @var string
40
+ */
41
+ public $next_run_date_local_custom_time = '';
42
+
43
+ /**
44
+ * Description.
45
+ *
46
+ * @var string
47
+ */
48
+ public $schedule = '';
49
+
50
+ /**
51
+ * Description.
52
+ *
53
+ * @var string
54
+ */
55
+ public $hookname = '';
56
+
57
+ /**
58
+ * Description.
59
+ *
60
+ * @var string
61
+ */
62
+ public $hookcode = '';
63
+
64
+ /**
65
+ * Description.
66
+ *
67
+ * @var string
68
+ */
69
+ public $eventname = '';
70
+
71
+ /**
72
+ * Description.
73
+ *
74
+ * @var string
75
+ */
76
+ public $original_hookname = '';
77
+
78
+ /**
79
+ * Description.
80
+ *
81
+ * @var string
82
+ */
83
+ public $original_sig = '';
84
+
85
+ /**
86
+ * Description.
87
+ *
88
+ * @var string
89
+ */
90
+ public $original_next_run_utc = '';
91
+
92
+ /**
93
+ * Crontrol_Request constructor.
94
+ *
95
+ * @param array<string,mixed> $props Properties.
96
+ * @return Request This object.
97
+ */
98
+ public function init( array $props ) {
99
+ foreach ( $props as $name => $value ) {
100
+ if ( property_exists( $this, $name ) ) {
101
+ $this->$name = $value;
102
+ }
103
+ }
104
+
105
+ return $this;
106
+ }
107
+
108
+ }
src/schedule-list-table.php CHANGED
@@ -17,14 +17,14 @@ class Schedule_List_Table extends \WP_List_Table {
17
  /**
18
  * Array of cron event schedules that are added by WordPress core.
19
  *
20
- * @var string[] Array of schedule names.
21
  */
22
  protected static $core_schedules;
23
 
24
  /**
25
  * Array of cron event schedule names that are in use by events.
26
  *
27
- * @var string[] Array of schedule names.
28
  */
29
  protected static $used_schedules;
30
 
@@ -51,6 +51,8 @@ class Schedule_List_Table extends \WP_List_Table {
51
 
52
  /**
53
  * Prepares the list table items and arguments.
 
 
54
  */
55
  public function prepare_items() {
56
  $schedules = Schedule\get();
@@ -71,7 +73,7 @@ class Schedule_List_Table extends \WP_List_Table {
71
  /**
72
  * Returns an array of column names for the table.
73
  *
74
- * @return string[] Array of column names keyed by their ID.
75
  */
76
  public function get_columns() {
77
  return array(
@@ -94,9 +96,9 @@ class Schedule_List_Table extends \WP_List_Table {
94
  /**
95
  * Generates and displays row action links for the table.
96
  *
97
- * @param array $schedule The schedule for the current row.
98
- * @param string $column_name Current column name.
99
- * @param string $primary Primary column name.
100
  * @return string The row actions HTML.
101
  */
102
  protected function handle_row_actions( $schedule, $column_name, $primary ) {
@@ -116,10 +118,10 @@ class Schedule_List_Table extends \WP_List_Table {
116
  } else {
117
  $link = add_query_arg( array(
118
  'page' => 'crontrol_admin_options_page',
119
- 'action' => 'delete-sched',
120
  'id' => rawurlencode( $schedule['name'] ),
121
  ), admin_url( 'options-general.php' ) );
122
- $link = wp_nonce_url( $link, 'delete-sched_' . $schedule['name'] );
123
 
124
  $links[] = "<span class='delete'><a href='" . esc_url( $link ) . "'>" . esc_html__( 'Delete', 'wp-crontrol' ) . '</a></span>';
125
  }
@@ -130,7 +132,7 @@ class Schedule_List_Table extends \WP_List_Table {
130
  /**
131
  * Returns the output for the icon cell of a table row.
132
  *
133
- * @param array $schedule The schedule for the current row.
134
  * @return string The cell output.
135
  */
136
  protected function column_crontrol_icon( array $schedule ) {
@@ -148,7 +150,7 @@ class Schedule_List_Table extends \WP_List_Table {
148
  /**
149
  * Returns the output for the schdule name cell of a table row.
150
  *
151
- * @param array $schedule The schedule for the current row.
152
  * @return string The cell output.
153
  */
154
  protected function column_crontrol_name( array $schedule ) {
@@ -158,7 +160,7 @@ class Schedule_List_Table extends \WP_List_Table {
158
  /**
159
  * Returns the output for the interval cell of a table row.
160
  *
161
- * @param array $schedule The schedule for the current row.
162
  * @return string The cell output.
163
  */
164
  protected function column_crontrol_interval( array $schedule ) {
@@ -186,7 +188,7 @@ class Schedule_List_Table extends \WP_List_Table {
186
  /**
187
  * Returns the output for the display name cell of a table row.
188
  *
189
- * @param array $schedule The schedule for the current row.
190
  * @return string The cell output.
191
  */
192
  protected function column_crontrol_display( array $schedule ) {
@@ -195,6 +197,8 @@ class Schedule_List_Table extends \WP_List_Table {
195
 
196
  /**
197
  * Outputs a message when there are no items to show in the table.
 
 
198
  */
199
  public function no_items() {
200
  esc_html_e( 'There are no schedules.', 'wp-crontrol' );
17
  /**
18
  * Array of cron event schedules that are added by WordPress core.
19
  *
20
+ * @var array<int,string> Array of schedule names.
21
  */
22
  protected static $core_schedules;
23
 
24
  /**
25
  * Array of cron event schedule names that are in use by events.
26
  *
27
+ * @var array<int,string> Array of schedule names.
28
  */
29
  protected static $used_schedules;
30
 
51
 
52
  /**
53
  * Prepares the list table items and arguments.
54
+ *
55
+ * @return void
56
  */
57
  public function prepare_items() {
58
  $schedules = Schedule\get();
73
  /**
74
  * Returns an array of column names for the table.
75
  *
76
+ * @return array<string,string> Array of column names keyed by their ID.
77
  */
78
  public function get_columns() {
79
  return array(
96
  /**
97
  * Generates and displays row action links for the table.
98
  *
99
+ * @param mixed[] $schedule The schedule for the current row.
100
+ * @param string $column_name Current column name.
101
+ * @param string $primary Primary column name.
102
  * @return string The row actions HTML.
103
  */
104
  protected function handle_row_actions( $schedule, $column_name, $primary ) {
118
  } else {
119
  $link = add_query_arg( array(
120
  'page' => 'crontrol_admin_options_page',
121
+ 'action' => 'crontrol-delete-schedule',
122
  'id' => rawurlencode( $schedule['name'] ),
123
  ), admin_url( 'options-general.php' ) );
124
+ $link = wp_nonce_url( $link, 'crontrol-delete-schedule_' . $schedule['name'] );
125
 
126
  $links[] = "<span class='delete'><a href='" . esc_url( $link ) . "'>" . esc_html__( 'Delete', 'wp-crontrol' ) . '</a></span>';
127
  }
132
  /**
133
  * Returns the output for the icon cell of a table row.
134
  *
135
+ * @param mixed[] $schedule The schedule for the current row.
136
  * @return string The cell output.
137
  */
138
  protected function column_crontrol_icon( array $schedule ) {
150
  /**
151
  * Returns the output for the schdule name cell of a table row.
152
  *
153
+ * @param mixed[] $schedule The schedule for the current row.
154
  * @return string The cell output.
155
  */
156
  protected function column_crontrol_name( array $schedule ) {
160
  /**
161
  * Returns the output for the interval cell of a table row.
162
  *
163
+ * @param mixed[] $schedule The schedule for the current row.
164
  * @return string The cell output.
165
  */
166
  protected function column_crontrol_interval( array $schedule ) {
188
  /**
189
  * Returns the output for the display name cell of a table row.
190
  *
191
+ * @param mixed[] $schedule The schedule for the current row.
192
  * @return string The cell output.
193
  */
194
  protected function column_crontrol_display( array $schedule ) {
197
 
198
  /**
199
  * Outputs a message when there are no items to show in the table.
200
+ *
201
+ * @return void
202
  */
203
  public function no_items() {
204
  esc_html_e( 'There are no schedules.', 'wp-crontrol' );
src/schedule.php CHANGED
@@ -13,6 +13,7 @@ namespace Crontrol\Schedule;
13
  * @param string $name The internal name of the schedule.
14
  * @param int $interval The interval between executions of the new schedule.
15
  * @param string $display The display name of the schedule.
 
16
  */
17
  function add( $name, $interval, $display ) {
18
  $old_scheds = get_option( 'crontrol_schedules', array() );
@@ -37,6 +38,7 @@ function add( $name, $interval, $display ) {
37
  * Deletes a custom cron schedule.
38
  *
39
  * @param string $name The internal name of the schedule to delete.
 
40
  */
41
  function delete( $name ) {
42
  $scheds = get_option( 'crontrol_schedules', array() );
@@ -54,7 +56,13 @@ function delete( $name ) {
54
  /**
55
  * Gets a sorted (according to interval) list of the cron schedules
56
  *
57
- * @return array[] Array of cron schedule arrays.
 
 
 
 
 
 
58
  */
59
  function get() {
60
  $schedules = wp_get_schedules();
@@ -74,6 +82,7 @@ function get() {
74
  * Displays a dropdown filled with the possible schedules, including non-repeating.
75
  *
76
  * @param string|false $current The currently selected schedule, or false for none.
 
77
  */
78
  function dropdown( $current = false ) {
79
  $schedules = get();
13
  * @param string $name The internal name of the schedule.
14
  * @param int $interval The interval between executions of the new schedule.
15
  * @param string $display The display name of the schedule.
16
+ * @return void
17
  */
18
  function add( $name, $interval, $display ) {
19
  $old_scheds = get_option( 'crontrol_schedules', array() );
38
  * Deletes a custom cron schedule.
39
  *
40
  * @param string $name The internal name of the schedule to delete.
41
+ * @return void
42
  */
43
  function delete( $name ) {
44
  $scheds = get_option( 'crontrol_schedules', array() );
56
  /**
57
  * Gets a sorted (according to interval) list of the cron schedules
58
  *
59
+ * @return array<string,array<string,(int|string)>> Array of cron schedule arrays.
60
+ * @phpstan-return array<string,array{
61
+ * interval: int,
62
+ * display: string,
63
+ * name: string,
64
+ * is_too_frequent: bool,
65
+ * }>
66
  */
67
  function get() {
68
  $schedules = wp_get_schedules();
82
  * Displays a dropdown filled with the possible schedules, including non-repeating.
83
  *
84
  * @param string|false $current The currently selected schedule, or false for none.
85
+ * @return void
86
  */
87
  function dropdown( $current = false ) {
88
  $schedules = get();
wp-crontrol.php CHANGED
@@ -5,7 +5,7 @@
5
  * Description: WP Crontrol enables you to 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.10.0
9
  * Text Domain: wp-crontrol
10
  * Domain Path: /languages/
11
  * Requires PHP: 5.3.6
@@ -34,17 +34,24 @@
34
 
35
  namespace Crontrol;
36
 
 
 
37
  use WP_Error;
38
 
39
- defined( 'ABSPATH' ) || die();
 
 
40
 
41
  require_once __DIR__ . '/src/event.php';
 
42
  require_once __DIR__ . '/src/schedule.php';
43
 
44
  const TRANSIENT = 'crontrol-message-%d';
45
 
46
  /**
47
  * Hook onto all of the actions and filters needed by the plugin.
 
 
48
  */
49
  function init_hooks() {
50
  $plugin_file = plugin_basename( __FILE__ );
@@ -96,9 +103,9 @@ function get_message() {
96
  /**
97
  * Filters the array of row meta for each plugin in the Plugins list table.
98
  *
99
- * @param string[] $plugin_meta An array of the plugin's metadata.
100
- * @param string $plugin_file Path to the plugin file relative to the plugins directory.
101
- * @return string[] An array of the plugin's metadata.
102
  */
103
  function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
104
  if ( 'wp-crontrol/wp-crontrol.php' !== $plugin_file ) {
@@ -116,6 +123,8 @@ function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
116
 
117
  /**
118
  * Run using the 'init' action.
 
 
119
  */
120
  function action_init() {
121
  load_plugin_textdomain( 'wp-crontrol', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
@@ -123,24 +132,30 @@ function action_init() {
123
 
124
  /**
125
  * Handles any POSTs made by the plugin. Run using the 'init' action.
 
 
126
  */
127
  function action_handle_posts() {
128
- if ( isset( $_POST['action'] ) && ( 'new_cron' === $_POST['action'] ) ) {
 
 
129
  if ( ! current_user_can( 'manage_options' ) ) {
130
  wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
131
  }
132
- check_admin_referer( 'new-cron' );
133
- extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
134
- if ( 'crontrol_cron_job' === $in_hookname && ! current_user_can( 'edit_files' ) ) {
 
 
135
  wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
136
  }
137
- $in_args = json_decode( $in_args, true );
138
 
139
- if ( empty( $in_args ) ) {
140
- $in_args = array();
141
  }
142
 
143
- $next_run_local = ( 'custom' === $in_next_run_date_local ) ? $in_next_run_date_local_custom_date . ' ' . $in_next_run_date_local_custom_time : $in_next_run_date_local;
144
 
145
  add_filter( 'schedule_event', function( $event ) {
146
  if ( ! $event ) {
@@ -150,13 +165,13 @@ function action_handle_posts() {
150
  /**
151
  * Fires after a new cron event is added.
152
  *
153
- * @param object $event {
154
  * An object containing the event's data.
155
  *
156
  * @type string $hook Action hook to execute when the event is run.
157
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
158
  * @type string|false $schedule How often the event should subsequently recur.
159
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
160
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
161
  * }
162
  */
@@ -165,12 +180,12 @@ function action_handle_posts() {
165
  return $event;
166
  }, 99 );
167
 
168
- $added = Event\add( $next_run_local, $in_schedule, $in_hookname, $in_args );
169
 
170
  $redirect = array(
171
  'page' => 'crontrol_admin_manage_page',
172
  'crontrol_message' => '5',
173
- 'crontrol_name' => rawurlencode( $in_hookname ),
174
  );
175
 
176
  if ( is_wp_error( $added ) ) {
@@ -181,16 +196,18 @@ function action_handle_posts() {
181
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
182
  exit;
183
 
184
- } elseif ( isset( $_POST['action'] ) && ( 'new_php_cron' === $_POST['action'] ) ) {
185
  if ( ! current_user_can( 'edit_files' ) ) {
186
  wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
187
  }
188
- check_admin_referer( 'new-cron' );
189
- extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
190
- $next_run_local = ( 'custom' === $in_next_run_date_local ) ? $in_next_run_date_local_custom_date . ' ' . $in_next_run_date_local_custom_time : $in_next_run_date_local;
 
 
191
  $args = array(
192
- 'code' => $in_hookcode,
193
- 'name' => $in_eventname,
194
  );
195
 
196
  add_filter( 'schedule_event', function( $event ) {
@@ -201,13 +218,13 @@ function action_handle_posts() {
201
  /**
202
  * Fires after a new PHP cron event is added.
203
  *
204
- * @param object $event {
205
  * An object containing the event's data.
206
  *
207
  * @type string $hook Action hook to execute when the event is run.
208
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
209
  * @type string|false $schedule How often the event should subsequently recur.
210
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
211
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
212
  * }
213
  */
@@ -216,9 +233,9 @@ function action_handle_posts() {
216
  return $event;
217
  }, 99 );
218
 
219
- $added = Event\add( $next_run_local, $in_schedule, 'crontrol_cron_job', $args );
220
 
221
- $hookname = ( ! empty( $in_eventname ) ) ? $in_eventname : __( 'PHP Cron', 'wp-crontrol' );
222
  $redirect = array(
223
  'page' => 'crontrol_admin_manage_page',
224
  'crontrol_message' => '5',
@@ -233,31 +250,32 @@ function action_handle_posts() {
233
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
234
  exit;
235
 
236
- } elseif ( isset( $_POST['action'] ) && ( 'edit_cron' === $_POST['action'] ) ) {
237
  if ( ! current_user_can( 'manage_options' ) ) {
238
  wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
239
  }
240
 
241
- extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
242
- check_admin_referer( "edit-cron_{$in_original_hookname}_{$in_original_sig}_{$in_original_next_run_utc}" );
 
243
 
244
- if ( 'crontrol_cron_job' === $in_hookname && ! current_user_can( 'edit_files' ) ) {
245
  wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
246
  }
247
 
248
- $in_args = json_decode( $in_args, true );
249
 
250
- if ( empty( $in_args ) ) {
251
- $in_args = array();
252
  }
253
 
254
  $redirect = array(
255
  'page' => 'crontrol_admin_manage_page',
256
  'crontrol_message' => '4',
257
- 'crontrol_name' => rawurlencode( $in_hookname ),
258
  );
259
 
260
- $original = Event\get_single( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
261
 
262
  if ( is_wp_error( $original ) ) {
263
  set_message( $original->get_error_message() );
@@ -266,7 +284,7 @@ function action_handle_posts() {
266
  exit;
267
  }
268
 
269
- $deleted = Event\delete( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
270
 
271
  if ( is_wp_error( $deleted ) ) {
272
  set_message( $deleted->get_error_message() );
@@ -275,32 +293,37 @@ function action_handle_posts() {
275
  exit;
276
  }
277
 
278
- $next_run_local = ( 'custom' === $in_next_run_date_local ) ? $in_next_run_date_local_custom_date . ' ' . $in_next_run_date_local_custom_time : $in_next_run_date_local;
279
 
 
 
 
 
 
280
  add_filter( 'schedule_event', function( $event ) use ( $original ) {
281
- if ( ! $event || ! $original ) {
282
  return $event;
283
  }
284
 
285
  /**
286
  * Fires after a cron event is edited.
287
  *
288
- * @param object $event {
289
  * An object containing the new event's data.
290
  *
291
  * @type string $hook Action hook to execute when the event is run.
292
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
293
  * @type string|false $schedule How often the event should subsequently recur.
294
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
295
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
296
  * }
297
- * @param object $original {
298
  * An object containing the original event's data.
299
  *
300
  * @type string $hook Action hook to execute when the event is run.
301
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
302
  * @type string|false $schedule How often the event should subsequently recur.
303
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
304
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
305
  * }
306
  */
@@ -309,7 +332,7 @@ function action_handle_posts() {
309
  return $event;
310
  }, 99 );
311
 
312
- $added = Event\add( $next_run_local, $in_schedule, $in_hookname, $in_args );
313
 
314
  if ( is_wp_error( $added ) ) {
315
  set_message( $added->get_error_message() );
@@ -319,25 +342,26 @@ function action_handle_posts() {
319
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
320
  exit;
321
 
322
- } elseif ( isset( $_POST['action'] ) && ( 'edit_php_cron' === $_POST['action'] ) ) {
323
  if ( ! current_user_can( 'edit_files' ) ) {
324
  wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
325
  }
326
 
327
- extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
328
- check_admin_referer( "edit-cron_{$in_original_hookname}_{$in_original_sig}_{$in_original_next_run_utc}" );
 
329
  $args = array(
330
- 'code' => $in_hookcode,
331
- 'name' => $in_eventname,
332
  );
333
- $hookname = ( ! empty( $in_eventname ) ) ? $in_eventname : __( 'PHP Cron', 'wp-crontrol' );
334
  $redirect = array(
335
  'page' => 'crontrol_admin_manage_page',
336
  'crontrol_message' => '4',
337
  'crontrol_name' => rawurlencode( $hookname ),
338
  );
339
 
340
- $original = Event\get_single( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
341
 
342
  if ( is_wp_error( $original ) ) {
343
  set_message( $original->get_error_message() );
@@ -346,7 +370,7 @@ function action_handle_posts() {
346
  exit;
347
  }
348
 
349
- $deleted = Event\delete( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
350
 
351
  if ( is_wp_error( $deleted ) ) {
352
  set_message( $deleted->get_error_message() );
@@ -355,32 +379,37 @@ function action_handle_posts() {
355
  exit;
356
  }
357
 
358
- $next_run_local = ( 'custom' === $in_next_run_date_local ) ? $in_next_run_date_local_custom_date . ' ' . $in_next_run_date_local_custom_time : $in_next_run_date_local;
359
 
 
 
 
 
 
360
  add_filter( 'schedule_event', function( $event ) use ( $original ) {
361
- if ( ! $event || ! $original ) {
362
  return $event;
363
  }
364
 
365
  /**
366
  * Fires after a PHP cron event is edited.
367
  *
368
- * @param object $event {
369
  * An object containing the new event's data.
370
  *
371
  * @type string $hook Action hook to execute when the event is run.
372
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
373
  * @type string|false $schedule How often the event should subsequently recur.
374
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
375
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
376
  * }
377
- * @param object $original {
378
  * An object containing the original event's data.
379
  *
380
  * @type string $hook Action hook to execute when the event is run.
381
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
382
  * @type string|false $schedule How often the event should subsequently recur.
383
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
384
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
385
  * }
386
  */
@@ -389,7 +418,7 @@ function action_handle_posts() {
389
  return $event;
390
  }, 99 );
391
 
392
- $added = Event\add( $next_run_local, $in_schedule, 'crontrol_cron_job', $args );
393
 
394
  if ( is_wp_error( $added ) ) {
395
  set_message( $added->get_error_message() );
@@ -399,11 +428,11 @@ function action_handle_posts() {
399
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
400
  exit;
401
 
402
- } elseif ( isset( $_POST['new_schedule'] ) ) {
403
  if ( ! current_user_can( 'manage_options' ) ) {
404
  wp_die( esc_html__( 'You are not allowed to add new cron schedules.', 'wp-crontrol' ), 401 );
405
  }
406
- check_admin_referer( 'new-sched' );
407
  $name = wp_unslash( $_POST['internal_name'] );
408
  $interval = absint( $_POST['interval'] );
409
  $display = wp_unslash( $_POST['display_name'] );
@@ -417,12 +446,12 @@ function action_handle_posts() {
417
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
418
  exit;
419
 
420
- } elseif ( isset( $_GET['action'] ) && 'delete-sched' === $_GET['action'] ) {
421
  if ( ! current_user_can( 'manage_options' ) ) {
422
  wp_die( esc_html__( 'You are not allowed to delete cron schedules.', 'wp-crontrol' ), 401 );
423
  }
424
  $schedule = wp_unslash( $_GET['id'] );
425
- check_admin_referer( "delete-sched_{$schedule}" );
426
  Schedule\delete( $schedule );
427
  $redirect = array(
428
  'page' => 'crontrol_admin_options_page',
@@ -432,7 +461,7 @@ function action_handle_posts() {
432
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
433
  exit;
434
 
435
- } elseif ( ( isset( $_POST['action'] ) && 'delete_crons' === $_POST['action'] ) || ( isset( $_POST['action2'] ) && 'delete_crons' === $_POST['action2'] ) ) {
436
  if ( ! current_user_can( 'manage_options' ) ) {
437
  wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
438
  }
@@ -442,11 +471,14 @@ function action_handle_posts() {
442
  return;
443
  }
444
 
445
- $delete = wp_unslash( $_POST['delete'] );
 
 
 
446
  $deleted = 0;
447
 
448
  foreach ( $delete as $next_run_utc => $events ) {
449
- foreach ( $events as $hook => $sig ) {
450
  if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
451
  continue;
452
  }
@@ -471,14 +503,14 @@ function action_handle_posts() {
471
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
472
  exit;
473
 
474
- } elseif ( isset( $_GET['action'] ) && 'delete-cron' === $_GET['action'] ) {
475
  if ( ! current_user_can( 'manage_options' ) ) {
476
  wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
477
  }
478
  $hook = wp_unslash( $_GET['id'] );
479
  $sig = wp_unslash( $_GET['sig'] );
480
- $next_run_utc = intval( $_GET['next_run_utc'] );
481
- check_admin_referer( "delete-cron_{$hook}_{$sig}_{$next_run_utc}" );
482
 
483
  if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
484
  wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
@@ -508,13 +540,13 @@ function action_handle_posts() {
508
  /**
509
  * Fires after a cron event is deleted.
510
  *
511
- * @param object $event {
512
  * An object containing the event's data.
513
  *
514
  * @type string $hook Action hook to execute when the event is run.
515
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
516
  * @type string|false $schedule How often the event should subsequently recur.
517
- * @type array $args Array containing each separate argument to pass to the hook's callback function.
518
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
519
  * }
520
  */
@@ -524,13 +556,13 @@ function action_handle_posts() {
524
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
525
  exit;
526
 
527
- } elseif ( isset( $_GET['action'] ) && 'delete-hook' === $_GET['action'] ) {
528
  if ( ! current_user_can( 'manage_options' ) ) {
529
  wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
530
  }
531
  $hook = wp_unslash( $_GET['id'] );
532
  $deleted = false;
533
- check_admin_referer( "delete-hook_{$hook}" );
534
 
535
  if ( 'crontrol_cron_job' === $hook ) {
536
  wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
@@ -573,13 +605,13 @@ function action_handle_posts() {
573
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
574
  exit;
575
  }
576
- } elseif ( isset( $_GET['action'] ) && 'run-cron' === $_GET['action'] ) {
577
  if ( ! current_user_can( 'manage_options' ) ) {
578
  wp_die( esc_html__( 'You are not allowed to run cron events.', 'wp-crontrol' ), 401 );
579
  }
580
  $hook = wp_unslash( $_GET['id'] );
581
  $sig = wp_unslash( $_GET['sig'] );
582
- check_admin_referer( "run-cron_{$hook}_{$sig}" );
583
 
584
  $ran = Event\run( $hook, $sig );
585
 
@@ -590,11 +622,112 @@ function action_handle_posts() {
590
  );
591
 
592
  if ( is_wp_error( $ran ) ) {
593
- set_message( $ran->get_error_message() );
 
 
 
 
 
 
 
 
 
 
 
 
594
  $redirect['crontrol_message'] = 'error';
595
  }
596
 
597
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  exit;
599
  }
600
  }
@@ -603,21 +736,41 @@ function action_handle_posts() {
603
  * Adds options & management pages to the admin menu.
604
  *
605
  * Run using the 'admin_menu' action.
 
 
606
  */
607
  function action_admin_menu() {
608
- $schedules = add_options_page( esc_html__( 'Cron Schedules', 'wp-crontrol' ), esc_html__( 'Cron Schedules', 'wp-crontrol' ), 'manage_options', 'crontrol_admin_options_page', __NAMESPACE__ . '\admin_options_page' );
609
- $events = add_management_page( esc_html__( 'Cron Events', 'wp-crontrol' ), esc_html__( 'Cron Events', 'wp-crontrol' ), 'manage_options', 'crontrol_admin_manage_page', __NAMESPACE__ . '\admin_manage_page' );
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
  add_action( "load-{$schedules}", __NAMESPACE__ . '\admin_help_tab' );
612
  add_action( "load-{$events}", __NAMESPACE__ . '\admin_help_tab' );
613
  }
614
 
615
  /**
616
- * Adds a Help tab with links to help resources;
 
 
617
  */
618
  function admin_help_tab() {
619
  $screen = get_current_screen();
620
 
 
 
 
 
621
  $content = '<p>' . __( 'There are several places to get help with issues relating to WP-Cron:', 'wp-crontrol' ) . '</p>';
622
  $content .= '<ul>';
623
  $content .= '<li>';
@@ -654,11 +807,11 @@ function admin_help_tab() {
654
  /**
655
  * Adds items to the plugin's action links on the Plugins listing screen.
656
  *
657
- * @param string[] $actions Array of action links.
658
- * @param string $plugin_file Path to the plugin file relative to the plugins directory.
659
- * @param array $plugin_data An array of plugin data.
660
- * @param string $context The plugin context.
661
- * @return string[] Array of action links.
662
  */
663
  function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
664
  $new = array(
@@ -687,8 +840,8 @@ function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
687
  *
688
  * Called by the `cron_schedules` filter.
689
  *
690
- * @param array[] $scheds Array of cron schedule arrays. Usually empty.
691
- * @return array[] Array of modified cron schedule arrays.
692
  */
693
  function filter_cron_schedules( array $scheds ) {
694
  $new_scheds = get_option( 'crontrol_schedules', array() );
@@ -702,6 +855,8 @@ function filter_cron_schedules( array $scheds ) {
702
 
703
  /**
704
  * Displays the options page for the plugin.
 
 
705
  */
706
  function admin_options_page() {
707
  $messages = array(
@@ -771,9 +926,9 @@ function admin_options_page() {
771
  <input type="text" value="" id="cron_display_name" name="display_name" required/>
772
  </div>
773
  <p class="submit">
774
- <input id="schedadd-submit" type="submit" class="button button-primary" value="<?php esc_attr_e( 'Add Cron Schedule', 'wp-crontrol' ); ?>" name="new_schedule"/>
775
  </p>
776
- <?php wp_nonce_field( 'new-sched' ); ?>
777
  </form>
778
  </div>
779
  </div>
@@ -812,7 +967,7 @@ function admin_options_page() {
812
  * - https://core.trac.wordpress.org/browser/trunk/src/wp-cron.php?rev=47198&marks=127,141#L122
813
  *
814
  * @param mixed $pre The pre-flight value of the event unschedule short-circuit. Not used.
815
- * @return mixed Thee unaltered pre-flight value.
816
  */
817
  function maybe_clear_doing_cron( $pre ) {
818
  if ( defined( 'DOING_CRON' ) && DOING_CRON && isset( $_GET['crontrol-single-event'] ) ) {
@@ -824,13 +979,21 @@ function maybe_clear_doing_cron( $pre ) {
824
 
825
  /**
826
  * Ajax handler which outputs a hash of the current list of scheduled events.
 
 
827
  */
828
  function ajax_check_events_hash() {
829
  if ( ! current_user_can( 'manage_options' ) ) {
830
  wp_send_json_error( null, 403 );
831
  }
832
 
833
- wp_send_json_success( md5( json_encode( Event\get_list_table()->items ) ) );
 
 
 
 
 
 
834
  }
835
 
836
  /**
@@ -879,7 +1042,7 @@ function test_cron_spawn( $cache = true ) {
879
  return true;
880
  }
881
 
882
- $sslverify = version_compare( $wp_version, 4.0, '<' );
883
  $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
884
 
885
  $cron_request = apply_filters( 'cron_request', array(
@@ -911,30 +1074,13 @@ function test_cron_spawn( $cache = true ) {
911
 
912
  }
913
 
914
- /**
915
- * Determines whether the given feature is enabled.
916
- *
917
- * The feature directly corresponds to one of WP Crontrol's tabs. Currently the only feature
918
- * that's not enabled by default is "logs" which are provided by WP Crontrol Pro.
919
- *
920
- * @param string $feature The feature name.
921
- * @return bool Whether the specified tab is active.
922
- */
923
- function is_feature_enabled( $feature ) {
924
- $enabled = ( 'logs' !== $feature );
925
- return apply_filters( "crontrol/enabled/{$feature}", $enabled );
926
- }
927
-
928
  /**
929
  * Shows the status of WP-Cron functionality on the site. Only displays a message when there's a problem.
930
  *
931
  * @param string $tab The tab name.
 
932
  */
933
  function show_cron_status( $tab ) {
934
- if ( ! is_feature_enabled( $tab ) ) {
935
- return;
936
- }
937
-
938
  if ( 'UTC' !== date_default_timezone_get() ) {
939
  ?>
940
  <div id="crontrol-timezone-warning" class="notice notice-warning">
@@ -1090,7 +1236,7 @@ function show_cron_form( $editing ) {
1090
  }
1091
 
1092
  if ( is_array( $existing ) ) {
1093
- $other_fields = wp_nonce_field( "edit-cron_{$existing['hookname']}_{$existing['sig']}_{$existing['next_run']}", '_wpnonce', true, false );
1094
  $other_fields .= sprintf( '<input name="original_hookname" type="hidden" value="%s" />',
1095
  esc_attr( $existing['hookname'] )
1096
  );
@@ -1098,18 +1244,21 @@ function show_cron_form( $editing ) {
1098
  esc_attr( $existing['sig'] )
1099
  );
1100
  $other_fields .= sprintf( '<input name="original_next_run_utc" type="hidden" value="%s" />',
1101
- esc_attr( $existing['next_run'] )
1102
  );
1103
  if ( ! empty( $existing['args'] ) ) {
1104
  $display_args = wp_json_encode( $existing['args'] );
 
 
 
 
1105
  }
1106
- $action = $is_editing_php ? 'edit_php_cron' : 'edit_cron';
1107
  $button = __( 'Update Event', 'wp-crontrol' );
1108
  $next_run_gmt = gmdate( 'Y-m-d H:i:s', $existing['next_run'] );
1109
  $next_run_date_local = get_date_from_gmt( $next_run_gmt, 'Y-m-d' );
1110
  $next_run_time_local = get_date_from_gmt( $next_run_gmt, 'H:i:s' );
1111
  } else {
1112
- $other_fields = wp_nonce_field( 'new-cron', '_wpnonce', true, false );
1113
  $existing = array(
1114
  'hookname' => '',
1115
  'args' => array(),
@@ -1163,6 +1312,7 @@ function show_cron_form( $editing ) {
1163
  <table class="form-table"><tbody>
1164
  <?php
1165
  if ( $editing ) {
 
1166
  printf(
1167
  '<input type="hidden" name="action" value="%s"/>',
1168
  esc_attr( $action )
@@ -1174,14 +1324,14 @@ function show_cron_form( $editing ) {
1174
  <?php esc_html_e( 'Event Type', 'wp-crontrol' ); ?>
1175
  </th>
1176
  <td>
1177
- <p><label><input type="radio" name="action" value="new_cron" checked>Standard cron event</label></p>
1178
- <p><label><input type="radio" name="action" value="new_php_cron">PHP cron event</label></p>
1179
  </td>
1180
  </tr>
1181
  <?php
1182
  } else {
1183
  ?>
1184
- <input type="hidden" name="action" value="new_cron"/>
1185
  <?php
1186
  }
1187
 
@@ -1204,6 +1354,7 @@ function show_cron_form( $editing ) {
1204
  ?>
1205
  </p>
1206
  <p><textarea class="large-text code" rows="10" cols="50" id="hookcode" name="hookcode"><?php echo esc_textarea( $editing ? $existing['args']['code'] : '' ); ?></textarea></p>
 
1207
  </td>
1208
  </tr>
1209
  <tr class="crontrol-event-php">
@@ -1214,6 +1365,7 @@ function show_cron_form( $editing ) {
1214
  </th>
1215
  <td>
1216
  <input type="text" class="regular-text" id="eventname" name="eventname" value="<?php echo esc_attr( $editing ? $existing['args']['name'] : '' ); ?>"/>
 
1217
  </td>
1218
  </tr>
1219
  <?php
@@ -1229,6 +1381,7 @@ function show_cron_form( $editing ) {
1229
  </th>
1230
  <td>
1231
  <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text" id="hookname" name="hookname" value="<?php echo esc_attr( $existing['hookname'] ); ?>" required />
 
1232
  </td>
1233
  </tr>
1234
  <tr class="crontrol-event-standard">
@@ -1239,6 +1392,7 @@ function show_cron_form( $editing ) {
1239
  </th>
1240
  <td>
1241
  <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text code" id="args" name="args" value="<?php echo esc_attr( $display_args ); ?>"/>
 
1242
  <p class="description">
1243
  <?php
1244
  printf(
@@ -1295,6 +1449,8 @@ function show_cron_form( $editing ) {
1295
  </li>
1296
  </ul>
1297
 
 
 
1298
  <p class="description">
1299
  <?php
1300
  printf(
@@ -1314,6 +1470,7 @@ function show_cron_form( $editing ) {
1314
  </th>
1315
  <td>
1316
  <?php Schedule\dropdown( $existing['schedule'] ); ?>
 
1317
  </td>
1318
  </tr>
1319
  </tbody></table>
@@ -1332,6 +1489,8 @@ function show_cron_form( $editing ) {
1332
 
1333
  /**
1334
  * Displays the manage page for the plugin.
 
 
1335
  */
1336
  function admin_manage_page() {
1337
  $messages = array(
@@ -1384,7 +1543,10 @@ function admin_manage_page() {
1384
  __( 'Failed to save the cron event %s.', 'wp-crontrol' ),
1385
  'error',
1386
  ),
1387
- 'error' => array(),
 
 
 
1388
  );
1389
 
1390
  if ( isset( $_GET['crontrol_name'] ) && isset( $_GET['crontrol_message'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
@@ -1393,10 +1555,11 @@ function admin_manage_page() {
1393
  $link = '';
1394
 
1395
  if ( 'error' === $message ) {
1396
- $messages['error'] = array(
1397
- get_message(),
1398
- 'error',
1399
- );
 
1400
  }
1401
 
1402
  printf(
@@ -1422,7 +1585,7 @@ function admin_manage_page() {
1422
 
1423
  <h1 class="wp-heading-inline"><?php esc_html_e( 'Cron Events', 'wp-crontrol' ); ?></h1>
1424
 
1425
- <?php echo '<a href="' . esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page&action=new-cron' ) ) . '" class="page-title-action">' . esc_html__( 'Add New', 'wp-crontrol' ) . '</a>'; ?>
1426
 
1427
  <hr class="wp-header-end">
1428
 
@@ -1469,19 +1632,25 @@ function admin_manage_page() {
1469
  /**
1470
  * Get the states of the various cron-related tabs.
1471
  *
1472
- * @return bool[] Array of states keyed by tab name.
1473
  */
1474
  function get_tab_states() {
1475
- return array(
1476
  'events' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_manage_page' === $_GET['page'] && empty( $_GET['action'] ) ),
1477
  'schedules' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_options_page' === $_GET['page'] ),
1478
- 'add-event' => ( ! empty( $_GET['action'] ) && 'new-cron' === $_GET['action'] ),
1479
- 'edit-event' => ( ! empty( $_GET['action'] ) && 'edit-cron' === $_GET['action'] ),
1480
  );
 
 
 
 
1481
  }
1482
 
1483
  /**
1484
  * Output the cron-related tabs if we're on a cron-related admin screen.
 
 
1485
  */
1486
  function do_tabs() {
1487
  $tabs = get_tab_states();
@@ -1509,7 +1678,7 @@ function do_tabs() {
1509
  <nav class="nav-tab-wrapper">
1510
  <?php
1511
  foreach ( $links as $id => $link ) {
1512
- if ( $tabs[ $id ] ) {
1513
  printf(
1514
  '<a href="%s" class="nav-tab nav-tab-active">%s</a>',
1515
  esc_url( $link[0] ),
@@ -1548,7 +1717,11 @@ function do_tabs() {
1548
  * Returns an array of the callback functions that are attached to the given hook name.
1549
  *
1550
  * @param string $name The hook name.
1551
- * @return array[] Array of callbacks attached to the hook.
 
 
 
 
1552
  */
1553
  function get_hook_callbacks( $name ) {
1554
  global $wp_filter;
@@ -1559,6 +1732,9 @@ function get_hook_callbacks( $name ) {
1559
  // See http://core.trac.wordpress.org/ticket/17817.
1560
  $action = $wp_filter[ $name ];
1561
 
 
 
 
1562
  foreach ( $action as $priority => $callbacks ) {
1563
  foreach ( $callbacks as $callback ) {
1564
  $callback = populate_callback( $callback );
@@ -1577,8 +1753,12 @@ function get_hook_callbacks( $name ) {
1577
  /**
1578
  * Populates the details of the given callback function.
1579
  *
1580
- * @param array $callback A callback entry.
1581
- * @return array The updated callback entry.
 
 
 
 
1582
  */
1583
  function populate_callback( array $callback ) {
1584
  // If Query Monitor is installed, use its rich callback analysis.
@@ -1618,7 +1798,7 @@ function populate_callback( array $callback ) {
1618
  /**
1619
  * Returns a user-friendly representation of the callback function.
1620
  *
1621
- * @param array $callback The callback entry.
1622
  * @return string The displayable version of the callback name.
1623
  */
1624
  function output_callback( array $callback ) {
@@ -1669,7 +1849,7 @@ function time_since( $older_date, $newer_date ) {
1669
  * echo \Crontrol\interval( 90 );
1670
  * // 1 minute 30 seconds
1671
  *
1672
- * @param int $since A period of time in seconds.
1673
  * @return string An interval represented as a string.
1674
  */
1675
  function interval( $since ) {
@@ -1701,15 +1881,14 @@ function interval( $since ) {
1701
  * x days, xx hours
1702
  * so there's only two bits of calculation below:
1703
  */
1704
- $j = count( $chunks );
1705
 
1706
  // Step one: the first chunk.
1707
- for ( $i = 0; $i < $j; $i++ ) {
1708
  $seconds = $chunks[ $i ][0];
1709
  $name = $chunks[ $i ][1];
1710
 
1711
  // Finding the biggest chunk (if the chunk fits, break).
1712
- $count = floor( $since / $seconds );
1713
  if ( $count ) {
1714
  break;
1715
  }
@@ -1719,10 +1898,10 @@ function interval( $since ) {
1719
  $output = sprintf( translate_nooped_plural( $name, $count, 'wp-crontrol' ), $count );
1720
 
1721
  // Step two: the second chunk.
1722
- if ( $i + 1 < $j ) {
1723
  $seconds2 = $chunks[ $i + 1 ][0];
1724
  $name2 = $chunks[ $i + 1 ][1];
1725
- $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 );
1726
  if ( $count2 ) {
1727
  // Add to output var.
1728
  $output .= ' ' . sprintf( translate_nooped_plural( $name2, $count2, 'wp-crontrol' ), $count2 );
@@ -1734,6 +1913,8 @@ function interval( $since ) {
1734
 
1735
  /**
1736
  * Sets up the Events listing screen.
 
 
1737
  */
1738
  function setup_manage_page() {
1739
  // Initialise the list table
@@ -1752,6 +1933,7 @@ function setup_manage_page() {
1752
  * Registers the stylesheet and JavaScript for the admin areas.
1753
  *
1754
  * @param string $hook_suffix The admin screen ID.
 
1755
  */
1756
  function enqueue_assets( $hook_suffix ) {
1757
  $tab = get_tab_states();
@@ -1760,17 +1942,21 @@ function enqueue_assets( $hook_suffix ) {
1760
  return;
1761
  }
1762
 
1763
- $ver = filemtime( plugin_dir_path( __FILE__ ) . 'css/wp-crontrol.css' );
1764
  wp_enqueue_style( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'css/wp-crontrol.css', array( 'dashicons' ), $ver );
1765
 
1766
- $ver = filemtime( plugin_dir_path( __FILE__ ) . 'js/wp-crontrol.js' );
1767
  wp_enqueue_script( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'js/wp-crontrol.js', array( 'jquery', 'wp-a11y' ), $ver, true );
1768
 
1769
  $vars = array();
1770
 
1771
  if ( ! empty( $tab['events'] ) ) {
1772
- $vars['eventsHash'] = md5( json_encode( Event\get_list_table()->items ) );
1773
- $vars['eventsHashInterval'] = 20;
 
 
 
 
1774
  }
1775
 
1776
  if ( ! empty( $tab['add-event'] ) || ! empty( $tab['edit-event'] ) ) {
@@ -1791,8 +1977,8 @@ function enqueue_assets( $hook_suffix ) {
1791
  /**
1792
  * Filters the list of query arguments which get removed from admin area URLs in WordPress.
1793
  *
1794
- * @param string[] $args List of removable query arguments.
1795
- * @return string[] Updated list of removable query arguments.
1796
  */
1797
  function filter_removable_query_args( array $args ) {
1798
  return array_merge( $args, array(
@@ -1804,40 +1990,42 @@ function filter_removable_query_args( array $args ) {
1804
  /**
1805
  * Returns an array of cron event hooks that are persistently added by WordPress core.
1806
  *
1807
- * @return string[] Array of hook names.
1808
  */
1809
  function get_persistent_core_hooks() {
1810
  return array(
1811
- 'delete_expired_transients',
1812
- 'recovery_mode_clean_expired_keys',
1813
- 'update_network_counts',
1814
- 'wp_https_detection',
1815
- 'wp_privacy_delete_old_export_files',
1816
- 'wp_scheduled_auto_draft_delete',
1817
- 'wp_scheduled_delete',
1818
- 'wp_site_health_scheduled_check',
1819
- 'wp_update_plugins',
1820
- 'wp_update_themes',
1821
- 'wp_version_check',
1822
  );
1823
  }
1824
 
1825
  /**
1826
  * Returns an array of all cron event hooks that are added by WordPress core.
1827
  *
1828
- * @return string[] Array of hook names.
1829
  */
1830
  function get_all_core_hooks() {
1831
  return array_merge(
1832
  get_persistent_core_hooks(),
1833
  array(
1834
- 'do_pings',
1835
- 'importer_scheduled_cleanup',
1836
- 'publish_future_post',
1837
- 'upgrader_scheduled_cleanup',
1838
- 'wp_maybe_auto_update',
1839
- 'wp_split_shared_term_batch',
1840
- 'wp_update_comment_type_batch',
 
 
1841
  )
1842
  );
1843
  }
@@ -1845,7 +2033,7 @@ function get_all_core_hooks() {
1845
  /**
1846
  * Returns an array of cron schedules that are added by WordPress core.
1847
  *
1848
- * @return string[] Array of schedule names.
1849
  */
1850
  function get_core_schedules() {
1851
  return array(
@@ -1859,21 +2047,29 @@ function get_core_schedules() {
1859
  /**
1860
  * Encodes some input as JSON for output.
1861
  *
1862
- * @param mixed $input The input.
 
1863
  * @return string The JSON-encoded output.
1864
  */
1865
- function json_output( $input ) {
1866
  $json_options = 0;
1867
 
1868
  if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
1869
  // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
1870
  $json_options |= JSON_UNESCAPED_SLASHES;
1871
  }
1872
- if ( defined( 'JSON_PRETTY_PRINT' ) ) {
 
1873
  $json_options |= JSON_PRETTY_PRINT;
1874
  }
1875
 
1876
- return wp_json_encode( $input, $json_options );
 
 
 
 
 
 
1877
  }
1878
 
1879
  /**
@@ -1889,6 +2085,7 @@ function json_output( $input ) {
1889
  * Therefore, the user access level required to execute arbitrary PHP code does not change with WP Crontrol activated.
1890
  *
1891
  * @param string $code The PHP code to evaluate.
 
1892
  */
1893
  function action_php_cron_event( $code ) {
1894
  // phpcs:ignore Squiz.PHP.Eval.Discouraged
5
  * Description: WP Crontrol enables you to 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.11.0
9
  * Text Domain: wp-crontrol
10
  * Domain Path: /languages/
11
  * Requires PHP: 5.3.6
34
 
35
  namespace Crontrol;
36
 
37
+ use Crontrol\Event\Table;
38
+ use stdClass;
39
  use WP_Error;
40
 
41
+ if ( ! defined( 'ABSPATH' ) ) {
42
+ exit;
43
+ }
44
 
45
  require_once __DIR__ . '/src/event.php';
46
+ require_once __DIR__ . '/src/request.php';
47
  require_once __DIR__ . '/src/schedule.php';
48
 
49
  const TRANSIENT = 'crontrol-message-%d';
50
 
51
  /**
52
  * Hook onto all of the actions and filters needed by the plugin.
53
+ *
54
+ * @return void
55
  */
56
  function init_hooks() {
57
  $plugin_file = plugin_basename( __FILE__ );
103
  /**
104
  * Filters the array of row meta for each plugin in the Plugins list table.
105
  *
106
+ * @param array<int,string> $plugin_meta An array of the plugin row's meta data.
107
+ * @param string $plugin_file Path to the plugin file relative to the plugins directory.
108
+ * @return array<int,string> An array of the plugin row's meta data.
109
  */
110
  function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
111
  if ( 'wp-crontrol/wp-crontrol.php' !== $plugin_file ) {
123
 
124
  /**
125
  * Run using the 'init' action.
126
+ *
127
+ * @return void
128
  */
129
  function action_init() {
130
  load_plugin_textdomain( 'wp-crontrol', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
132
 
133
  /**
134
  * Handles any POSTs made by the plugin. Run using the 'init' action.
135
+ *
136
+ * @return void
137
  */
138
  function action_handle_posts() {
139
+ $request = new Request();
140
+
141
+ if ( isset( $_POST['action'] ) && ( 'crontrol_new_cron' === $_POST['action'] ) ) {
142
  if ( ! current_user_can( 'manage_options' ) ) {
143
  wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
144
  }
145
+ check_admin_referer( 'crontrol-new-cron' );
146
+
147
+ $cr = $request->init( wp_unslash( $_POST ) );
148
+
149
+ if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'edit_files' ) ) {
150
  wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
151
  }
152
+ $args = json_decode( $cr->args, true );
153
 
154
+ if ( empty( $args ) ) {
155
+ $args = array();
156
  }
157
 
158
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
159
 
160
  add_filter( 'schedule_event', function( $event ) {
161
  if ( ! $event ) {
165
  /**
166
  * Fires after a new cron event is added.
167
  *
168
+ * @param stdClass $event {
169
  * An object containing the event's data.
170
  *
171
  * @type string $hook Action hook to execute when the event is run.
172
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
173
  * @type string|false $schedule How often the event should subsequently recur.
174
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
175
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
176
  * }
177
  */
180
  return $event;
181
  }, 99 );
182
 
183
+ $added = Event\add( $next_run_local, $cr->schedule, $cr->hookname, $args );
184
 
185
  $redirect = array(
186
  'page' => 'crontrol_admin_manage_page',
187
  'crontrol_message' => '5',
188
+ 'crontrol_name' => rawurlencode( $cr->hookname ),
189
  );
190
 
191
  if ( is_wp_error( $added ) ) {
196
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
197
  exit;
198
 
199
+ } elseif ( isset( $_POST['action'] ) && ( 'crontrol_new_php_cron' === $_POST['action'] ) ) {
200
  if ( ! current_user_can( 'edit_files' ) ) {
201
  wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
202
  }
203
+ check_admin_referer( 'crontrol-new-cron' );
204
+
205
+ $cr = $request->init( wp_unslash( $_POST ) );
206
+
207
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
208
  $args = array(
209
+ 'code' => $cr->hookcode,
210
+ 'name' => $cr->eventname,
211
  );
212
 
213
  add_filter( 'schedule_event', function( $event ) {
218
  /**
219
  * Fires after a new PHP cron event is added.
220
  *
221
+ * @param stdClass $event {
222
  * An object containing the event's data.
223
  *
224
  * @type string $hook Action hook to execute when the event is run.
225
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
226
  * @type string|false $schedule How often the event should subsequently recur.
227
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
228
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
229
  * }
230
  */
233
  return $event;
234
  }, 99 );
235
 
236
+ $added = Event\add( $next_run_local, $cr->schedule, 'crontrol_cron_job', $args );
237
 
238
+ $hookname = ( ! empty( $cr->eventname ) ) ? $cr->eventname : __( 'PHP Cron', 'wp-crontrol' );
239
  $redirect = array(
240
  'page' => 'crontrol_admin_manage_page',
241
  'crontrol_message' => '5',
250
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
251
  exit;
252
 
253
+ } elseif ( isset( $_POST['action'] ) && ( 'crontrol_edit_cron' === $_POST['action'] ) ) {
254
  if ( ! current_user_can( 'manage_options' ) ) {
255
  wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
256
  }
257
 
258
+ $cr = $request->init( wp_unslash( $_POST ) );
259
+
260
+ check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );
261
 
262
+ if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'edit_files' ) ) {
263
  wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
264
  }
265
 
266
+ $args = json_decode( $cr->args, true );
267
 
268
+ if ( empty( $args ) ) {
269
+ $args = array();
270
  }
271
 
272
  $redirect = array(
273
  'page' => 'crontrol_admin_manage_page',
274
  'crontrol_message' => '4',
275
+ 'crontrol_name' => rawurlencode( $cr->hookname ),
276
  );
277
 
278
+ $original = Event\get_single( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
279
 
280
  if ( is_wp_error( $original ) ) {
281
  set_message( $original->get_error_message() );
284
  exit;
285
  }
286
 
287
+ $deleted = Event\delete( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
288
 
289
  if ( is_wp_error( $deleted ) ) {
290
  set_message( $deleted->get_error_message() );
293
  exit;
294
  }
295
 
296
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
297
 
298
+ /**
299
+ * Modifies an event before it is scheduled.
300
+ *
301
+ * @param stdClass|false $event An object containing the new event's data, or boolean false.
302
+ */
303
  add_filter( 'schedule_event', function( $event ) use ( $original ) {
304
+ if ( ! $event ) {
305
  return $event;
306
  }
307
 
308
  /**
309
  * Fires after a cron event is edited.
310
  *
311
+ * @param stdClass $event {
312
  * An object containing the new event's data.
313
  *
314
  * @type string $hook Action hook to execute when the event is run.
315
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
316
  * @type string|false $schedule How often the event should subsequently recur.
317
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
318
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
319
  * }
320
+ * @param stdClass $original {
321
  * An object containing the original event's data.
322
  *
323
  * @type string $hook Action hook to execute when the event is run.
324
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
325
  * @type string|false $schedule How often the event should subsequently recur.
326
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
327
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
328
  * }
329
  */
332
  return $event;
333
  }, 99 );
334
 
335
+ $added = Event\add( $next_run_local, $cr->schedule, $cr->hookname, $args );
336
 
337
  if ( is_wp_error( $added ) ) {
338
  set_message( $added->get_error_message() );
342
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
343
  exit;
344
 
345
+ } elseif ( isset( $_POST['action'] ) && ( 'crontrol_edit_php_cron' === $_POST['action'] ) ) {
346
  if ( ! current_user_can( 'edit_files' ) ) {
347
  wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
348
  }
349
 
350
+ $cr = $request->init( wp_unslash( $_POST ) );
351
+
352
+ check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );
353
  $args = array(
354
+ 'code' => $cr->hookcode,
355
+ 'name' => $cr->eventname,
356
  );
357
+ $hookname = ( ! empty( $cr->eventname ) ) ? $cr->eventname : __( 'PHP Cron', 'wp-crontrol' );
358
  $redirect = array(
359
  'page' => 'crontrol_admin_manage_page',
360
  'crontrol_message' => '4',
361
  'crontrol_name' => rawurlencode( $hookname ),
362
  );
363
 
364
+ $original = Event\get_single( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
365
 
366
  if ( is_wp_error( $original ) ) {
367
  set_message( $original->get_error_message() );
370
  exit;
371
  }
372
 
373
+ $deleted = Event\delete( $cr->original_hookname, $cr->original_sig, $cr->original_next_run_utc );
374
 
375
  if ( is_wp_error( $deleted ) ) {
376
  set_message( $deleted->get_error_message() );
379
  exit;
380
  }
381
 
382
+ $next_run_local = ( 'custom' === $cr->next_run_date_local ) ? $cr->next_run_date_local_custom_date . ' ' . $cr->next_run_date_local_custom_time : $cr->next_run_date_local;
383
 
384
+ /**
385
+ * Modifies an event before it is scheduled.
386
+ *
387
+ * @param stdClass|false $event An object containing the new event's data, or boolean false.
388
+ */
389
  add_filter( 'schedule_event', function( $event ) use ( $original ) {
390
+ if ( ! $event ) {
391
  return $event;
392
  }
393
 
394
  /**
395
  * Fires after a PHP cron event is edited.
396
  *
397
+ * @param stdClass $event {
398
  * An object containing the new event's data.
399
  *
400
  * @type string $hook Action hook to execute when the event is run.
401
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
402
  * @type string|false $schedule How often the event should subsequently recur.
403
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
404
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
405
  * }
406
+ * @param stdClass $original {
407
  * An object containing the original event's data.
408
  *
409
  * @type string $hook Action hook to execute when the event is run.
410
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
411
  * @type string|false $schedule How often the event should subsequently recur.
412
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
413
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
414
  * }
415
  */
418
  return $event;
419
  }, 99 );
420
 
421
+ $added = Event\add( $next_run_local, $cr->schedule, 'crontrol_cron_job', $args );
422
 
423
  if ( is_wp_error( $added ) ) {
424
  set_message( $added->get_error_message() );
428
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
429
  exit;
430
 
431
+ } elseif ( isset( $_POST['crontrol_new_schedule'] ) ) {
432
  if ( ! current_user_can( 'manage_options' ) ) {
433
  wp_die( esc_html__( 'You are not allowed to add new cron schedules.', 'wp-crontrol' ), 401 );
434
  }
435
+ check_admin_referer( 'crontrol-new-schedule' );
436
  $name = wp_unslash( $_POST['internal_name'] );
437
  $interval = absint( $_POST['interval'] );
438
  $display = wp_unslash( $_POST['display_name'] );
446
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
447
  exit;
448
 
449
+ } elseif ( isset( $_GET['action'] ) && 'crontrol-delete-schedule' === $_GET['action'] ) {
450
  if ( ! current_user_can( 'manage_options' ) ) {
451
  wp_die( esc_html__( 'You are not allowed to delete cron schedules.', 'wp-crontrol' ), 401 );
452
  }
453
  $schedule = wp_unslash( $_GET['id'] );
454
+ check_admin_referer( "crontrol-delete-schedule_{$schedule}" );
455
  Schedule\delete( $schedule );
456
  $redirect = array(
457
  'page' => 'crontrol_admin_options_page',
461
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
462
  exit;
463
 
464
+ } elseif ( ( isset( $_POST['action'] ) && 'crontrol_delete_crons' === $_POST['action'] ) || ( isset( $_POST['action2'] ) && 'crontrol_delete_crons' === $_POST['action2'] ) ) {
465
  if ( ! current_user_can( 'manage_options' ) ) {
466
  wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
467
  }
471
  return;
472
  }
473
 
474
+ /**
475
+ * @var array<string,array<string,string>>
476
+ */
477
+ $delete = (array) wp_unslash( $_POST['delete'] );
478
  $deleted = 0;
479
 
480
  foreach ( $delete as $next_run_utc => $events ) {
481
+ foreach ( (array) $events as $hook => $sig ) {
482
  if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
483
  continue;
484
  }
503
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
504
  exit;
505
 
506
+ } elseif ( isset( $_GET['action'] ) && 'crontrol-delete-cron' === $_GET['action'] ) {
507
  if ( ! current_user_can( 'manage_options' ) ) {
508
  wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
509
  }
510
  $hook = wp_unslash( $_GET['id'] );
511
  $sig = wp_unslash( $_GET['sig'] );
512
+ $next_run_utc = wp_unslash( $_GET['next_run_utc'] );
513
+ check_admin_referer( "crontrol-delete-cron_{$hook}_{$sig}_{$next_run_utc}" );
514
 
515
  if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
516
  wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
540
  /**
541
  * Fires after a cron event is deleted.
542
  *
543
+ * @param stdClass $event {
544
  * An object containing the event's data.
545
  *
546
  * @type string $hook Action hook to execute when the event is run.
547
  * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
548
  * @type string|false $schedule How often the event should subsequently recur.
549
+ * @type mixed[] $args Array containing each separate argument to pass to the hook's callback function.
550
  * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
551
  * }
552
  */
556
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
557
  exit;
558
 
559
+ } elseif ( isset( $_GET['action'] ) && 'crontrol-delete-hook' === $_GET['action'] ) {
560
  if ( ! current_user_can( 'manage_options' ) ) {
561
  wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
562
  }
563
  $hook = wp_unslash( $_GET['id'] );
564
  $deleted = false;
565
+ check_admin_referer( "crontrol-delete-hook_{$hook}" );
566
 
567
  if ( 'crontrol_cron_job' === $hook ) {
568
  wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
605
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
606
  exit;
607
  }
608
+ } elseif ( isset( $_GET['action'] ) && 'crontrol-run-cron' === $_GET['action'] ) {
609
  if ( ! current_user_can( 'manage_options' ) ) {
610
  wp_die( esc_html__( 'You are not allowed to run cron events.', 'wp-crontrol' ), 401 );
611
  }
612
  $hook = wp_unslash( $_GET['id'] );
613
  $sig = wp_unslash( $_GET['sig'] );
614
+ check_admin_referer( "crontrol-run-cron_{$hook}_{$sig}" );
615
 
616
  $ran = Event\run( $hook, $sig );
617
 
622
  );
623
 
624
  if ( is_wp_error( $ran ) ) {
625
+ $set = set_message( $ran->get_error_message() );
626
+
627
+ // If we can't store the error message in a transient, just display it.
628
+ if ( ! $set ) {
629
+ wp_die(
630
+ esc_html( $ran->get_error_message() ),
631
+ '',
632
+ array(
633
+ 'response' => 500,
634
+ 'back_link' => true,
635
+ )
636
+ );
637
+ }
638
  $redirect['crontrol_message'] = 'error';
639
  }
640
 
641
  wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
642
+ exit;
643
+ } elseif ( isset( $_POST['action'] ) && 'crontrol-export-event-csv' === $_POST['action'] ) {
644
+ check_admin_referer( 'crontrol-export-event-csv', 'crontrol_nonce' );
645
+
646
+ require_once __DIR__ . '/src/event-list-table.php';
647
+
648
+ $type = isset( $_POST['hooks_type'] ) ? $_POST['hooks_type'] : 'all';
649
+ $headers = array(
650
+ 'hook',
651
+ 'arguments',
652
+ 'next_run',
653
+ 'next_run_gmt',
654
+ 'action',
655
+ 'recurrence',
656
+ 'interval',
657
+ );
658
+ $filename = sprintf(
659
+ 'cron-events-%s-%s.csv',
660
+ $type,
661
+ gmdate( 'Y-m-d-H.i.s' )
662
+ );
663
+ $csv = fopen( 'php://output', 'w' );
664
+
665
+ if ( false === $csv ) {
666
+ wp_die( esc_html__( 'Could not save CSV file.', 'wp-crontrol' ) );
667
+ }
668
+
669
+ $events = Table::get_filtered_events( Event\get() );
670
+
671
+ header( 'Content-Type: text/csv; charset=utf-8' );
672
+ header(
673
+ sprintf(
674
+ 'Content-Disposition: attachment; filename="%s"',
675
+ esc_attr( $filename )
676
+ )
677
+ );
678
+
679
+ fputcsv( $csv, $headers );
680
+
681
+ if ( isset( $events[ $type ] ) ) {
682
+ foreach ( $events[ $type ] as $event ) {
683
+ $next_run_local = get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $event->time ), 'c' );
684
+ $next_run_utc = gmdate( 'c', $event->time );
685
+ $hook_callbacks = \Crontrol\get_hook_callbacks( $event->hook );
686
+
687
+ if ( 'crontrol_cron_job' === $event->hook ) {
688
+ $args = __( 'PHP Code', 'wp-crontrol' );
689
+ } elseif ( empty( $event->args ) ) {
690
+ $args = '';
691
+ } else {
692
+ $args = \Crontrol\json_output( $event->args, false );
693
+ }
694
+
695
+ if ( 'crontrol_cron_job' === $event->hook ) {
696
+ $action = __( 'WP Crontrol', 'wp-crontrol' );
697
+ } else {
698
+ $callbacks = array();
699
+
700
+ foreach ( $hook_callbacks as $callback ) {
701
+ $callbacks[] = $callback['callback']['name'];
702
+ }
703
+
704
+ $action = implode( ',', $callbacks );
705
+ }
706
+
707
+ if ( $event->schedule ) {
708
+ $recurrence = Event\get_schedule_name( $event );
709
+ if ( is_wp_error( $recurrence ) ) {
710
+ $recurrence = $recurrence->get_error_message();
711
+ }
712
+ } else {
713
+ $recurrence = __( 'Non-repeating', 'wp-crontrol' );
714
+ }
715
+
716
+ $row = array(
717
+ $event->hook,
718
+ $args,
719
+ $next_run_local,
720
+ $next_run_utc,
721
+ $action,
722
+ $recurrence,
723
+ (int) $event->interval,
724
+ );
725
+ fputcsv( $csv, $row );
726
+ }
727
+ }
728
+
729
+ fclose( $csv );
730
+
731
  exit;
732
  }
733
  }
736
  * Adds options & management pages to the admin menu.
737
  *
738
  * Run using the 'admin_menu' action.
739
+ *
740
+ * @return void
741
  */
742
  function action_admin_menu() {
743
+ $schedules = add_options_page(
744
+ esc_html__( 'Cron Schedules', 'wp-crontrol' ),
745
+ esc_html__( 'Cron Schedules', 'wp-crontrol' ),
746
+ 'manage_options',
747
+ 'crontrol_admin_options_page',
748
+ __NAMESPACE__ . '\admin_options_page'
749
+ );
750
+ $events = add_management_page(
751
+ esc_html__( 'Cron Events', 'wp-crontrol' ),
752
+ esc_html__( 'Cron Events', 'wp-crontrol' ),
753
+ 'manage_options',
754
+ 'crontrol_admin_manage_page',
755
+ __NAMESPACE__ . '\admin_manage_page'
756
+ );
757
 
758
  add_action( "load-{$schedules}", __NAMESPACE__ . '\admin_help_tab' );
759
  add_action( "load-{$events}", __NAMESPACE__ . '\admin_help_tab' );
760
  }
761
 
762
  /**
763
+ * Adds a Help tab with links to help resources.
764
+ *
765
+ * @return void
766
  */
767
  function admin_help_tab() {
768
  $screen = get_current_screen();
769
 
770
+ if ( ! $screen ) {
771
+ return;
772
+ }
773
+
774
  $content = '<p>' . __( 'There are several places to get help with issues relating to WP-Cron:', 'wp-crontrol' ) . '</p>';
775
  $content .= '<ul>';
776
  $content .= '<li>';
807
  /**
808
  * Adds items to the plugin's action links on the Plugins listing screen.
809
  *
810
+ * @param array<string,string> $actions Array of action links.
811
+ * @param string $plugin_file Path to the plugin file relative to the plugins directory.
812
+ * @param mixed[] $plugin_data An array of plugin data.
813
+ * @param string $context The plugin context.
814
+ * @return array<string,string> Array of action links.
815
  */
816
  function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
817
  $new = array(
840
  *
841
  * Called by the `cron_schedules` filter.
842
  *
843
+ * @param array<string,array<string,(int|string)>> $scheds Array of cron schedule arrays. Usually empty.
844
+ * @return array<string,array<string,(int|string)>> Array of modified cron schedule arrays.
845
  */
846
  function filter_cron_schedules( array $scheds ) {
847
  $new_scheds = get_option( 'crontrol_schedules', array() );
855
 
856
  /**
857
  * Displays the options page for the plugin.
858
+ *
859
+ * @return void
860
  */
861
  function admin_options_page() {
862
  $messages = array(
926
  <input type="text" value="" id="cron_display_name" name="display_name" required/>
927
  </div>
928
  <p class="submit">
929
+ <input id="schedadd-submit" type="submit" class="button button-primary" value="<?php esc_attr_e( 'Add Cron Schedule', 'wp-crontrol' ); ?>" name="crontrol_new_schedule"/>
930
  </p>
931
+ <?php wp_nonce_field( 'crontrol-new-schedule' ); ?>
932
  </form>
933
  </div>
934
  </div>
967
  * - https://core.trac.wordpress.org/browser/trunk/src/wp-cron.php?rev=47198&marks=127,141#L122
968
  *
969
  * @param mixed $pre The pre-flight value of the event unschedule short-circuit. Not used.
970
+ * @return mixed The unaltered pre-flight value.
971
  */
972
  function maybe_clear_doing_cron( $pre ) {
973
  if ( defined( 'DOING_CRON' ) && DOING_CRON && isset( $_GET['crontrol-single-event'] ) ) {
979
 
980
  /**
981
  * Ajax handler which outputs a hash of the current list of scheduled events.
982
+ *
983
+ * @return void
984
  */
985
  function ajax_check_events_hash() {
986
  if ( ! current_user_can( 'manage_options' ) ) {
987
  wp_send_json_error( null, 403 );
988
  }
989
 
990
+ $data = json_encode( Event\get() );
991
+
992
+ if ( false === $data ) {
993
+ wp_send_json_error( null, 500 );
994
+ }
995
+
996
+ wp_send_json_success( md5( $data ) );
997
  }
998
 
999
  /**
1042
  return true;
1043
  }
1044
 
1045
+ $sslverify = version_compare( $wp_version, '4.0', '<' );
1046
  $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
1047
 
1048
  $cron_request = apply_filters( 'cron_request', array(
1074
 
1075
  }
1076
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  /**
1078
  * Shows the status of WP-Cron functionality on the site. Only displays a message when there's a problem.
1079
  *
1080
  * @param string $tab The tab name.
1081
+ * @return void
1082
  */
1083
  function show_cron_status( $tab ) {
 
 
 
 
1084
  if ( 'UTC' !== date_default_timezone_get() ) {
1085
  ?>
1086
  <div id="crontrol-timezone-warning" class="notice notice-warning">
1236
  }
1237
 
1238
  if ( is_array( $existing ) ) {
1239
+ $other_fields = wp_nonce_field( "crontrol-edit-cron_{$existing['hookname']}_{$existing['sig']}_{$existing['next_run']}", '_wpnonce', true, false );
1240
  $other_fields .= sprintf( '<input name="original_hookname" type="hidden" value="%s" />',
1241
  esc_attr( $existing['hookname'] )
1242
  );
1244
  esc_attr( $existing['sig'] )
1245
  );
1246
  $other_fields .= sprintf( '<input name="original_next_run_utc" type="hidden" value="%s" />',
1247
+ esc_attr( (string) $existing['next_run'] )
1248
  );
1249
  if ( ! empty( $existing['args'] ) ) {
1250
  $display_args = wp_json_encode( $existing['args'] );
1251
+
1252
+ if ( false === $display_args ) {
1253
+ $display_args = '';
1254
+ }
1255
  }
 
1256
  $button = __( 'Update Event', 'wp-crontrol' );
1257
  $next_run_gmt = gmdate( 'Y-m-d H:i:s', $existing['next_run'] );
1258
  $next_run_date_local = get_date_from_gmt( $next_run_gmt, 'Y-m-d' );
1259
  $next_run_time_local = get_date_from_gmt( $next_run_gmt, 'H:i:s' );
1260
  } else {
1261
+ $other_fields = wp_nonce_field( 'crontrol-new-cron', '_wpnonce', true, false );
1262
  $existing = array(
1263
  'hookname' => '',
1264
  'args' => array(),
1312
  <table class="form-table"><tbody>
1313
  <?php
1314
  if ( $editing ) {
1315
+ $action = $is_editing_php ? 'crontrol_edit_php_cron' : 'crontrol_edit_cron';
1316
  printf(
1317
  '<input type="hidden" name="action" value="%s"/>',
1318
  esc_attr( $action )
1324
  <?php esc_html_e( 'Event Type', 'wp-crontrol' ); ?>
1325
  </th>
1326
  <td>
1327
+ <p><label><input type="radio" name="action" value="crontrol_new_cron" checked>Standard cron event</label></p>
1328
+ <p><label><input type="radio" name="action" value="crontrol_new_php_cron">PHP cron event</label></p>
1329
  </td>
1330
  </tr>
1331
  <?php
1332
  } else {
1333
  ?>
1334
+ <input type="hidden" name="action" value="crontrol_new_cron"/>
1335
  <?php
1336
  }
1337
 
1354
  ?>
1355
  </p>
1356
  <p><textarea class="large-text code" rows="10" cols="50" id="hookcode" name="hookcode"><?php echo esc_textarea( $editing ? $existing['args']['code'] : '' ); ?></textarea></p>
1357
+ <?php do_action( 'crontrol/manage/hookcode', $existing ); ?>
1358
  </td>
1359
  </tr>
1360
  <tr class="crontrol-event-php">
1365
  </th>
1366
  <td>
1367
  <input type="text" class="regular-text" id="eventname" name="eventname" value="<?php echo esc_attr( $editing ? $existing['args']['name'] : '' ); ?>"/>
1368
+ <?php do_action( 'crontrol/manage/eventname', $existing ); ?>
1369
  </td>
1370
  </tr>
1371
  <?php
1381
  </th>
1382
  <td>
1383
  <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text" id="hookname" name="hookname" value="<?php echo esc_attr( $existing['hookname'] ); ?>" required />
1384
+ <?php do_action( 'crontrol/manage/hookname', $existing ); ?>
1385
  </td>
1386
  </tr>
1387
  <tr class="crontrol-event-standard">
1392
  </th>
1393
  <td>
1394
  <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text code" id="args" name="args" value="<?php echo esc_attr( $display_args ); ?>"/>
1395
+ <?php do_action( 'crontrol/manage/args', $existing ); ?>
1396
  <p class="description">
1397
  <?php
1398
  printf(
1449
  </li>
1450
  </ul>
1451
 
1452
+ <?php do_action( 'crontrol/manage/next_run', $existing ); ?>
1453
+
1454
  <p class="description">
1455
  <?php
1456
  printf(
1470
  </th>
1471
  <td>
1472
  <?php Schedule\dropdown( $existing['schedule'] ); ?>
1473
+ <?php do_action( 'crontrol/manage/schedule', $existing ); ?>
1474
  </td>
1475
  </tr>
1476
  </tbody></table>
1489
 
1490
  /**
1491
  * Displays the manage page for the plugin.
1492
+ *
1493
+ * @return void
1494
  */
1495
  function admin_manage_page() {
1496
  $messages = array(
1543
  __( 'Failed to save the cron event %s.', 'wp-crontrol' ),
1544
  'error',
1545
  ),
1546
+ 'error' => array(
1547
+ __( 'An unknown error occurred.', 'wp-crontrol' ),
1548
+ 'error',
1549
+ ),
1550
  );
1551
 
1552
  if ( isset( $_GET['crontrol_name'] ) && isset( $_GET['crontrol_message'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
1555
  $link = '';
1556
 
1557
  if ( 'error' === $message ) {
1558
+ $error = get_message();
1559
+
1560
+ if ( $error ) {
1561
+ $messages['error'][0] = $error;
1562
+ }
1563
  }
1564
 
1565
  printf(
1585
 
1586
  <h1 class="wp-heading-inline"><?php esc_html_e( 'Cron Events', 'wp-crontrol' ); ?></h1>
1587
 
1588
+ <?php echo '<a href="' . esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page&action=crontrol-new-cron' ) ) . '" class="page-title-action">' . esc_html__( 'Add New', 'wp-crontrol' ) . '</a>'; ?>
1589
 
1590
  <hr class="wp-header-end">
1591
 
1632
  /**
1633
  * Get the states of the various cron-related tabs.
1634
  *
1635
+ * @return array<string,bool> Array of states keyed by tab name.
1636
  */
1637
  function get_tab_states() {
1638
+ $tabs = array(
1639
  'events' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_manage_page' === $_GET['page'] && empty( $_GET['action'] ) ),
1640
  'schedules' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_options_page' === $_GET['page'] ),
1641
+ 'add-event' => ( ! empty( $_GET['action'] ) && 'crontrol-new-cron' === $_GET['action'] ),
1642
+ 'edit-event' => ( ! empty( $_GET['action'] ) && 'crontrol-edit-cron' === $_GET['action'] ),
1643
  );
1644
+
1645
+ $tabs = apply_filters( 'crontrol/tabs', $tabs );
1646
+
1647
+ return $tabs;
1648
  }
1649
 
1650
  /**
1651
  * Output the cron-related tabs if we're on a cron-related admin screen.
1652
+ *
1653
+ * @return void
1654
  */
1655
  function do_tabs() {
1656
  $tabs = get_tab_states();
1678
  <nav class="nav-tab-wrapper">
1679
  <?php
1680
  foreach ( $links as $id => $link ) {
1681
+ if ( ! empty( $tabs[ $id ] ) ) {
1682
  printf(
1683
  '<a href="%s" class="nav-tab nav-tab-active">%s</a>',
1684
  esc_url( $link[0] ),
1717
  * Returns an array of the callback functions that are attached to the given hook name.
1718
  *
1719
  * @param string $name The hook name.
1720
+ * @return array<int,array<string,mixed>> Array of callbacks attached to the hook.
1721
+ * @phpstan-return array<int,array{
1722
+ * priority: int,
1723
+ * callback: array<string,mixed>,
1724
+ * }>
1725
  */
1726
  function get_hook_callbacks( $name ) {
1727
  global $wp_filter;
1732
  // See http://core.trac.wordpress.org/ticket/17817.
1733
  $action = $wp_filter[ $name ];
1734
 
1735
+ /**
1736
+ * @var int $priority
1737
+ */
1738
  foreach ( $action as $priority => $callbacks ) {
1739
  foreach ( $callbacks as $callback ) {
1740
  $callback = populate_callback( $callback );
1753
  /**
1754
  * Populates the details of the given callback function.
1755
  *
1756
+ * @param array<string,mixed> $callback A callback entry.
1757
+ * @phpstan-param array{
1758
+ * function: string|array<int,mixed>|object,
1759
+ * accepted_args: int,
1760
+ * } $callback
1761
+ * @return array<string,mixed> The updated callback entry.
1762
  */
1763
  function populate_callback( array $callback ) {
1764
  // If Query Monitor is installed, use its rich callback analysis.
1798
  /**
1799
  * Returns a user-friendly representation of the callback function.
1800
  *
1801
+ * @param mixed[] $callback The callback entry.
1802
  * @return string The displayable version of the callback name.
1803
  */
1804
  function output_callback( array $callback ) {
1849
  * echo \Crontrol\interval( 90 );
1850
  * // 1 minute 30 seconds
1851
  *
1852
+ * @param int|float $since A period of time in seconds.
1853
  * @return string An interval represented as a string.
1854
  */
1855
  function interval( $since ) {
1881
  * x days, xx hours
1882
  * so there's only two bits of calculation below:
1883
  */
 
1884
 
1885
  // Step one: the first chunk.
1886
+ foreach ( array_keys( $chunks ) as $i ) {
1887
  $seconds = $chunks[ $i ][0];
1888
  $name = $chunks[ $i ][1];
1889
 
1890
  // Finding the biggest chunk (if the chunk fits, break).
1891
+ $count = (int) floor( $since / $seconds );
1892
  if ( $count ) {
1893
  break;
1894
  }
1898
  $output = sprintf( translate_nooped_plural( $name, $count, 'wp-crontrol' ), $count );
1899
 
1900
  // Step two: the second chunk.
1901
+ if ( $i + 1 < count( $chunks ) ) {
1902
  $seconds2 = $chunks[ $i + 1 ][0];
1903
  $name2 = $chunks[ $i + 1 ][1];
1904
+ $count2 = (int) floor( ( $since - ( $seconds * $count ) ) / $seconds2 );
1905
  if ( $count2 ) {
1906
  // Add to output var.
1907
  $output .= ' ' . sprintf( translate_nooped_plural( $name2, $count2, 'wp-crontrol' ), $count2 );
1913
 
1914
  /**
1915
  * Sets up the Events listing screen.
1916
+ *
1917
+ * @return void
1918
  */
1919
  function setup_manage_page() {
1920
  // Initialise the list table
1933
  * Registers the stylesheet and JavaScript for the admin areas.
1934
  *
1935
  * @param string $hook_suffix The admin screen ID.
1936
+ * @return void
1937
  */
1938
  function enqueue_assets( $hook_suffix ) {
1939
  $tab = get_tab_states();
1942
  return;
1943
  }
1944
 
1945
+ $ver = (string) filemtime( plugin_dir_path( __FILE__ ) . 'css/wp-crontrol.css' );
1946
  wp_enqueue_style( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'css/wp-crontrol.css', array( 'dashicons' ), $ver );
1947
 
1948
+ $ver = (string) filemtime( plugin_dir_path( __FILE__ ) . 'js/wp-crontrol.js' );
1949
  wp_enqueue_script( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'js/wp-crontrol.js', array( 'jquery', 'wp-a11y' ), $ver, true );
1950
 
1951
  $vars = array();
1952
 
1953
  if ( ! empty( $tab['events'] ) ) {
1954
+ $data = json_encode( Event\get() );
1955
+
1956
+ if ( false !== $data ) {
1957
+ $vars['eventsHash'] = md5( $data );
1958
+ $vars['eventsHashInterval'] = 20;
1959
+ }
1960
  }
1961
 
1962
  if ( ! empty( $tab['add-event'] ) || ! empty( $tab['edit-event'] ) ) {
1977
  /**
1978
  * Filters the list of query arguments which get removed from admin area URLs in WordPress.
1979
  *
1980
+ * @param array<int,string> $args List of removable query arguments.
1981
+ * @return array<int,string> Updated list of removable query arguments.
1982
  */
1983
  function filter_removable_query_args( array $args ) {
1984
  return array_merge( $args, array(
1990
  /**
1991
  * Returns an array of cron event hooks that are persistently added by WordPress core.
1992
  *
1993
+ * @return array<int,string> Array of hook names.
1994
  */
1995
  function get_persistent_core_hooks() {
1996
  return array(
1997
+ 'wp_update_plugins', // 2.7.0
1998
+ 'wp_update_themes', // 2.7.0
1999
+ 'wp_version_check', // 2.7.0
2000
+ 'wp_scheduled_delete', // 2.9.0
2001
+ 'update_network_counts', // 3.1.0
2002
+ 'wp_scheduled_auto_draft_delete', // 3.4.0
2003
+ 'delete_expired_transients', // 4.9.0
2004
+ 'wp_privacy_delete_old_export_files', // 4.9.6
2005
+ 'recovery_mode_clean_expired_keys', // 5.2.0
2006
+ 'wp_site_health_scheduled_check', // 5.4.0
2007
+ 'wp_https_detection', // 5.7.0
2008
  );
2009
  }
2010
 
2011
  /**
2012
  * Returns an array of all cron event hooks that are added by WordPress core.
2013
  *
2014
+ * @return array<int,string> Array of hook names.
2015
  */
2016
  function get_all_core_hooks() {
2017
  return array_merge(
2018
  get_persistent_core_hooks(),
2019
  array(
2020
+ 'do_pings', // 2.1.0
2021
+ 'publish_future_post', // 2.1.0
2022
+ 'importer_scheduled_cleanup', // 2.5.0
2023
+ 'upgrader_scheduled_cleanup', // 3.2.2
2024
+ 'wp_maybe_auto_update', // 3.7.0
2025
+ 'wp_split_shared_term_batch', // 4.3.0
2026
+ 'wp_update_comment_type_batch', // 5.5.0
2027
+ 'delete_temp_updater_backups', // 5.9.0
2028
+ 'wp_delete_temp_updater_backups', // 5.9.0
2029
  )
2030
  );
2031
  }
2033
  /**
2034
  * Returns an array of cron schedules that are added by WordPress core.
2035
  *
2036
+ * @return array<int,string> Array of schedule names.
2037
  */
2038
  function get_core_schedules() {
2039
  return array(
2047
  /**
2048
  * Encodes some input as JSON for output.
2049
  *
2050
+ * @param mixed $input The input.
2051
+ * @param bool $pretty Whether to pretty print the output. Default true.
2052
  * @return string The JSON-encoded output.
2053
  */
2054
+ function json_output( $input, $pretty = true ) {
2055
  $json_options = 0;
2056
 
2057
  if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2058
  // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
2059
  $json_options |= JSON_UNESCAPED_SLASHES;
2060
  }
2061
+
2062
+ if ( $pretty && defined( 'JSON_PRETTY_PRINT' ) ) {
2063
  $json_options |= JSON_PRETTY_PRINT;
2064
  }
2065
 
2066
+ $output = wp_json_encode( $input, $json_options );
2067
+
2068
+ if ( false === $output ) {
2069
+ $output = '';
2070
+ }
2071
+
2072
+ return $output;
2073
  }
2074
 
2075
  /**
2085
  * Therefore, the user access level required to execute arbitrary PHP code does not change with WP Crontrol activated.
2086
  *
2087
  * @param string $code The PHP code to evaluate.
2088
+ * @return void
2089
  */
2090
  function action_php_cron_event( $code ) {
2091
  // phpcs:ignore Squiz.PHP.Eval.Discouraged