PluginProbe
WP Crontrol / 1.9.1
WP Crontrol v1.9.1
1.21.2 trunk 0.1 0.2 0.3 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.1 1.10.0 1.11.0 1.12.0 1.12.1 1.13.0 1.13.1 1.13.2 1.14.0 1.15.0 1.15.1 1.15.2 1.15.3 1.16.0 1.16.1 1.16.2 All 57 releases
wp-crontrol / wp-crontrol.php

wp-crontrol.php in WP Crontrol 1.9.1, at wp-crontrol.php

1,800 lines 58.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: WP Crontrol
4 * Plugin URI: https://wordpress.org/plugins/wp-crontrol/
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.9.1
9 * Text Domain: wp-crontrol
10 * Domain Path: /languages/
11 * Requires PHP: 5.3.6
12 * License: GPL v2 or later
13 *
14 * LICENSE
15 * This file is part of WP Crontrol.
16 *
17 * WP Crontrol is free software; you can redistribute it and/or
18 * modify it under the terms of the GNU General Public License
19 * as published by the Free Software Foundation; either version 2
20 * of the License, or (at your option) any later version.
21 *
22 * This program is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * @package wp-crontrol
28 * @author John Blackbourn <john@johnblackbourn.com> & Edward Dale <scompt@scompt.com>
29 * @copyright Copyright 2008 Edward Dale, 2012-2021 John Blackbourn
30 * @license http://www.gnu.org/licenses/gpl.txt GPL 2.0
31 * @link https://wordpress.org/plugins/wp-crontrol/
32 * @since 0.2
33 */
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 /**
45 * Hook onto all of the actions and filters needed by the plugin.
46 */
47 function init_hooks() {
48 $plugin_file = plugin_basename( __FILE__ );
49
50 add_action( 'init', __NAMESPACE__ . '\action_init' );
51 add_action( 'init', __NAMESPACE__ . '\action_handle_posts' );
52 add_action( 'admin_menu', __NAMESPACE__ . '\action_admin_menu' );
53 add_action( 'wp_ajax_crontrol_checkhash', __NAMESPACE__ . '\ajax_check_events_hash' );
54 add_filter( "plugin_action_links_{$plugin_file}", __NAMESPACE__ . '\plugin_action_links', 10, 4 );
55 add_filter( 'removable_query_args', __NAMESPACE__ . '\filter_removable_query_args' );
56 add_filter( 'in_admin_header', __NAMESPACE__ . '\do_tabs' );
57 add_filter( 'pre_unschedule_event', __NAMESPACE__ . '\maybe_clear_doing_cron' );
58 add_filter( 'plugin_row_meta', __NAMESPACE__ . '\filter_plugin_row_meta', 10, 4 );
59
60 add_action( 'load-tools_page_crontrol_admin_manage_page', __NAMESPACE__ . '\setup_manage_page' );
61
62 add_filter( 'cron_schedules', __NAMESPACE__ . '\filter_cron_schedules' );
63 add_action( 'crontrol_cron_job', __NAMESPACE__ . '\action_php_cron_event' );
64 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' );
65 add_action( 'crontrol/tab-header', __NAMESPACE__ . '\show_cron_status', 20 );
66 }
67
68 /**
69 * Filters the array of row meta for each plugin in the Plugins list table.
70 *
71 * @param string[] $plugin_meta An array of the plugin's metadata.
72 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
73 * @return string[] An array of the plugin's metadata.
74 */
75 function filter_plugin_row_meta( array $plugin_meta, $plugin_file ) {
76 if ( 'wp-crontrol/wp-crontrol.php' !== $plugin_file ) {
77 return $plugin_meta;
78 }
79
80 $plugin_meta[] = sprintf(
81 '<a href="%1$s"><span class="dashicons dashicons-star-filled" aria-hidden="true" style="font-size:14px;line-height:1.3"></span>%2$s</a>',
82 'https://github.com/sponsors/johnbillion',
83 esc_html_x( 'Sponsor', 'verb', 'wp-crontrol' )
84 );
85
86 return $plugin_meta;
87 }
88
89 /**
90 * Run using the 'init' action.
91 */
92 function action_init() {
93 load_plugin_textdomain( 'wp-crontrol', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
94 }
95
96 /**
97 * Handles any POSTs made by the plugin. Run using the 'init' action.
98 */
99 function action_handle_posts() {
100 if ( isset( $_POST['action'] ) && ( 'new_cron' === $_POST['action'] ) ) {
101 if ( ! current_user_can( 'manage_options' ) ) {
102 wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
103 }
104 check_admin_referer( 'new-cron' );
105 extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
106 if ( 'crontrol_cron_job' === $in_hookname && ! current_user_can( 'edit_files' ) ) {
107 wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
108 }
109 $in_args = json_decode( $in_args, true );
110
111 if ( empty( $in_args ) ) {
112 $in_args = array();
113 }
114
115 $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;
116
117 add_filter( 'schedule_event', function( $event ) {
118 if ( ! $event ) {
119 return $event;
120 }
121
122 /**
123 * Fires after a new cron event is added.
124 *
125 * @param object $event {
126 * An object containing the event's data.
127 *
128 * @type string $hook Action hook to execute when the event is run.
129 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
130 * @type string|false $schedule How often the event should subsequently recur.
131 * @type array $args Array containing each separate argument to pass to the hook's callback function.
132 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
133 * }
134 */
135 do_action( 'crontrol/added_new_event', $event );
136
137 return $event;
138 }, 99 );
139
140 $added = Event\add( $next_run_local, $in_schedule, $in_hookname, $in_args );
141
142 $redirect = array(
143 'page' => 'crontrol_admin_manage_page',
144 'crontrol_message' => '5',
145 'crontrol_name' => rawurlencode( $in_hookname ),
146 );
147
148 if ( false === $added ) {
149 $redirect['crontrol_message'] = '10';
150 }
151
152 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
153 exit;
154
155 } elseif ( isset( $_POST['action'] ) && ( 'new_php_cron' === $_POST['action'] ) ) {
156 if ( ! current_user_can( 'edit_files' ) ) {
157 wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
158 }
159 check_admin_referer( 'new-cron' );
160 extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
161 $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;
162 $args = array(
163 'code' => $in_hookcode,
164 'name' => $in_eventname,
165 );
166
167 add_filter( 'schedule_event', function( $event ) {
168 if ( ! $event ) {
169 return $event;
170 }
171
172 /**
173 * Fires after a new PHP cron event is added.
174 *
175 * @param object $event {
176 * An object containing the event's data.
177 *
178 * @type string $hook Action hook to execute when the event is run.
179 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
180 * @type string|false $schedule How often the event should subsequently recur.
181 * @type array $args Array containing each separate argument to pass to the hook's callback function.
182 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
183 * }
184 */
185 do_action( 'crontrol/added_new_php_event', $event );
186
187 return $event;
188 }, 99 );
189
190 $added = Event\add( $next_run_local, $in_schedule, 'crontrol_cron_job', $args );
191
192 $hookname = ( ! empty( $in_eventname ) ) ? $in_eventname : __( 'PHP Cron', 'wp-crontrol' );
193 $redirect = array(
194 'page' => 'crontrol_admin_manage_page',
195 'crontrol_message' => '5',
196 'crontrol_name' => rawurlencode( $hookname ),
197 );
198
199 if ( false === $added ) {
200 $redirect['crontrol_message'] = '10';
201 }
202
203 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
204 exit;
205
206 } elseif ( isset( $_POST['action'] ) && ( 'edit_cron' === $_POST['action'] ) ) {
207 if ( ! current_user_can( 'manage_options' ) ) {
208 wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
209 }
210
211 extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
212 check_admin_referer( "edit-cron_{$in_original_hookname}_{$in_original_sig}_{$in_original_next_run_utc}" );
213
214 if ( 'crontrol_cron_job' === $in_hookname && ! current_user_can( 'edit_files' ) ) {
215 wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
216 }
217
218 $in_args = json_decode( $in_args, true );
219
220 if ( empty( $in_args ) ) {
221 $in_args = array();
222 }
223
224 $original = Event\get_single( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
225 Event\delete( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
226
227 $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;
228
229 add_filter( 'schedule_event', function( $event ) use ( $original ) {
230 if ( ! $event || ! $original ) {
231 return $event;
232 }
233
234 /**
235 * Fires after a cron event is edited.
236 *
237 * @param object $event {
238 * An object containing the new event's data.
239 *
240 * @type string $hook Action hook to execute when the event is run.
241 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
242 * @type string|false $schedule How often the event should subsequently recur.
243 * @type array $args Array containing each separate argument to pass to the hook's callback function.
244 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
245 * }
246 * @param object $original {
247 * An object containing the original event's data.
248 *
249 * @type string $hook Action hook to execute when the event is run.
250 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
251 * @type string|false $schedule How often the event should subsequently recur.
252 * @type array $args Array containing each separate argument to pass to the hook's callback function.
253 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
254 * }
255 */
256 do_action( 'crontrol/edited_event', $event, $original );
257
258 return $event;
259 }, 99 );
260
261 $added = Event\add( $next_run_local, $in_schedule, $in_hookname, $in_args );
262
263 $redirect = array(
264 'page' => 'crontrol_admin_manage_page',
265 'crontrol_message' => '4',
266 'crontrol_name' => rawurlencode( $in_hookname ),
267 );
268
269 if ( false === $added ) {
270 $redirect['crontrol_message'] = '10';
271 }
272
273 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
274 exit;
275
276 } elseif ( isset( $_POST['action'] ) && ( 'edit_php_cron' === $_POST['action'] ) ) {
277 if ( ! current_user_can( 'edit_files' ) ) {
278 wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
279 }
280
281 extract( wp_unslash( $_POST ), EXTR_PREFIX_ALL, 'in' );
282 check_admin_referer( "edit-cron_{$in_original_hookname}_{$in_original_sig}_{$in_original_next_run_utc}" );
283 $args = array(
284 'code' => $in_hookcode,
285 'name' => $in_eventname,
286 );
287
288 $original = Event\get_single( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
289 Event\delete( $in_original_hookname, $in_original_sig, $in_original_next_run_utc );
290
291 $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;
292
293 add_filter( 'schedule_event', function( $event ) use ( $original ) {
294 if ( ! $event || ! $original ) {
295 return $event;
296 }
297
298 /**
299 * Fires after a PHP cron event is edited.
300 *
301 * @param object $event {
302 * An object containing the new event's data.
303 *
304 * @type string $hook Action hook to execute when the event is run.
305 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
306 * @type string|false $schedule How often the event should subsequently recur.
307 * @type array $args Array containing each separate argument to pass to the hook's callback function.
308 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
309 * }
310 * @param object $original {
311 * An object containing the original event's data.
312 *
313 * @type string $hook Action hook to execute when the event is run.
314 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
315 * @type string|false $schedule How often the event should subsequently recur.
316 * @type array $args Array containing each separate argument to pass to the hook's callback function.
317 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
318 * }
319 */
320 do_action( 'crontrol/edited_php_event', $event, $original );
321
322 return $event;
323 }, 99 );
324
325 $added = Event\add( $next_run_local, $in_schedule, 'crontrol_cron_job', $args );
326
327 $hookname = ( ! empty( $in_eventname ) ) ? $in_eventname : __( 'PHP Cron', 'wp-crontrol' );
328 $redirect = array(
329 'page' => 'crontrol_admin_manage_page',
330 'crontrol_message' => '4',
331 'crontrol_name' => rawurlencode( $hookname ),
332 );
333
334 if ( false === $added ) {
335 $redirect['crontrol_message'] = '10';
336 }
337
338 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
339 exit;
340
341 } elseif ( isset( $_POST['new_schedule'] ) ) {
342 if ( ! current_user_can( 'manage_options' ) ) {
343 wp_die( esc_html__( 'You are not allowed to add new cron schedules.', 'wp-crontrol' ), 401 );
344 }
345 check_admin_referer( 'new-sched' );
346 $name = wp_unslash( $_POST['internal_name'] );
347 $interval = wp_unslash( $_POST['interval'] );
348 $display = wp_unslash( $_POST['display_name'] );
349
350 // The user entered something that wasn't a number.
351 // Try to convert it with strtotime.
352 if ( ! is_numeric( $interval ) ) {
353 $now = time();
354 $future = strtotime( $interval, $now );
355 if ( false === $future || $now > $future ) {
356 $redirect = array(
357 'page' => 'crontrol_admin_options_page',
358 'crontrol_message' => '7',
359 'crontrol_name' => rawurlencode( $interval ),
360 );
361 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
362 exit;
363 }
364 $interval = $future - $now;
365 } elseif ( $interval <= 0 ) {
366 $redirect = array(
367 'page' => 'crontrol_admin_options_page',
368 'crontrol_message' => '7',
369 'crontrol_name' => rawurlencode( $interval ),
370 );
371 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
372 exit;
373 }
374
375 Schedule\add( $name, $interval, $display );
376 $redirect = array(
377 'page' => 'crontrol_admin_options_page',
378 'crontrol_message' => '3',
379 'crontrol_name' => rawurlencode( $name ),
380 );
381 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
382 exit;
383
384 } elseif ( isset( $_GET['action'] ) && 'delete-sched' === $_GET['action'] ) {
385 if ( ! current_user_can( 'manage_options' ) ) {
386 wp_die( esc_html__( 'You are not allowed to delete cron schedules.', 'wp-crontrol' ), 401 );
387 }
388 $schedule = wp_unslash( $_GET['id'] );
389 check_admin_referer( "delete-sched_{$schedule}" );
390 Schedule\delete( $schedule );
391 $redirect = array(
392 'page' => 'crontrol_admin_options_page',
393 'crontrol_message' => '2',
394 'crontrol_name' => rawurlencode( $schedule ),
395 );
396 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'options-general.php' ) ) );
397 exit;
398
399 } elseif ( ( isset( $_POST['action'] ) && 'delete_crons' === $_POST['action'] ) || ( isset( $_POST['action2'] ) && 'delete_crons' === $_POST['action2'] ) ) {
400 if ( ! current_user_can( 'manage_options' ) ) {
401 wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
402 }
403 check_admin_referer( 'bulk-crontrol-events' );
404
405 if ( empty( $_POST['delete'] ) ) {
406 return;
407 }
408
409 $delete = wp_unslash( $_POST['delete'] );
410 $deleted = 0;
411
412 foreach ( $delete as $next_run_utc => $events ) {
413 foreach ( $events as $hook => $sig ) {
414 if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
415 continue;
416 }
417
418 $event = Event\get_single( urldecode( $hook ), $sig, $next_run_utc );
419
420 if ( Event\delete( urldecode( $hook ), $sig, $next_run_utc ) ) {
421 $deleted++;
422
423 /** This action is documented in wp-crontrol.php */
424 do_action( 'crontrol/deleted_event', $event );
425 }
426 }
427 }
428
429 $redirect = array(
430 'page' => 'crontrol_admin_manage_page',
431 'crontrol_name' => $deleted,
432 'crontrol_message' => '9',
433 );
434 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
435 exit;
436
437 } elseif ( isset( $_GET['action'] ) && 'delete-cron' === $_GET['action'] ) {
438 if ( ! current_user_can( 'manage_options' ) ) {
439 wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
440 }
441 $hook = wp_unslash( $_GET['id'] );
442 $sig = wp_unslash( $_GET['sig'] );
443 $next_run_utc = intval( $_GET['next_run_utc'] );
444 check_admin_referer( "delete-cron_{$hook}_{$sig}_{$next_run_utc}" );
445
446 if ( 'crontrol_cron_job' === $hook && ! current_user_can( 'edit_files' ) ) {
447 wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
448 }
449
450 $event = Event\get_single( $hook, $sig, $next_run_utc );
451 $deleted = Event\delete( $hook, $sig, $next_run_utc );
452 $redirect = array(
453 'page' => 'crontrol_admin_manage_page',
454 'crontrol_message' => '6',
455 'crontrol_name' => rawurlencode( $hook ),
456 );
457
458 if ( false === $deleted ) {
459 $redirect['crontrol_message'] = '7';
460 } else {
461 /**
462 * Fires after a cron event is deleted.
463 *
464 * @param object $event {
465 * An object containing the event's data.
466 *
467 * @type string $hook Action hook to execute when the event is run.
468 * @type int $timestamp Unix timestamp (UTC) for when to next run the event.
469 * @type string|false $schedule How often the event should subsequently recur.
470 * @type array $args Array containing each separate argument to pass to the hook's callback function.
471 * @type int $interval The interval time in seconds for the schedule. Only present for recurring events.
472 * }
473 */
474 do_action( 'crontrol/deleted_event', $event );
475 }
476
477 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
478 exit;
479
480 } elseif ( isset( $_GET['action'] ) && 'delete-hook' === $_GET['action'] ) {
481 if ( ! current_user_can( 'manage_options' ) ) {
482 wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
483 }
484 $hook = wp_unslash( $_GET['id'] );
485 $deleted = false;
486 check_admin_referer( "delete-hook_{$hook}" );
487
488 if ( 'crontrol_cron_job' === $hook ) {
489 wp_die( esc_html__( 'You are not allowed to delete PHP cron events.', 'wp-crontrol' ), 401 );
490 }
491
492 if ( function_exists( 'wp_unschedule_hook' ) ) {
493 $deleted = wp_unschedule_hook( $hook );
494 }
495
496 if ( 0 === $deleted ) {
497 $redirect = array(
498 'page' => 'crontrol_admin_manage_page',
499 'crontrol_message' => '3',
500 'crontrol_name' => rawurlencode( $hook ),
501 );
502 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
503 exit;
504 } elseif ( $deleted ) {
505 /**
506 * Fires after all cron events with the given hook are deleted.
507 *
508 * @param string $hook The hook name.
509 * @param int $deleted The number of events that were deleted.
510 */
511 do_action( 'crontrol/deleted_all_with_hook', $hook, $deleted );
512
513 $redirect = array(
514 'page' => 'crontrol_admin_manage_page',
515 'crontrol_message' => '2',
516 'crontrol_name' => rawurlencode( $hook ),
517 );
518 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
519 exit;
520 } else {
521 $redirect = array(
522 'page' => 'crontrol_admin_manage_page',
523 'crontrol_message' => '7',
524 'crontrol_name' => rawurlencode( $hook ),
525 );
526 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
527 exit;
528 }
529 } elseif ( isset( $_GET['action'] ) && 'run-cron' === $_GET['action'] ) {
530 if ( ! current_user_can( 'manage_options' ) ) {
531 wp_die( esc_html__( 'You are not allowed to run cron events.', 'wp-crontrol' ), 401 );
532 }
533 $hook = wp_unslash( $_GET['id'] );
534 $sig = wp_unslash( $_GET['sig'] );
535 check_admin_referer( "run-cron_{$hook}_{$sig}" );
536
537 $ran = Event\run( $hook, $sig );
538
539 $redirect = array(
540 'page' => 'crontrol_admin_manage_page',
541 'crontrol_message' => '1',
542 'crontrol_name' => rawurlencode( $hook ),
543 );
544
545 if ( false === $ran ) {
546 $redirect['crontrol_message'] = '8';
547 }
548
549 wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
550 exit;
551 }
552 }
553
554 /**
555 * Adds options & management pages to the admin menu.
556 *
557 * Run using the 'admin_menu' action.
558 */
559 function action_admin_menu() {
560 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' );
561 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' );
562 }
563
564 /**
565 * Adds items to the plugin's action links on the Plugins listing screen.
566 *
567 * @param string[] $actions Array of action links.
568 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
569 * @param array $plugin_data An array of plugin data.
570 * @param string $context The plugin context.
571 * @return string[] Array of action links.
572 */
573 function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
574 $new = array(
575 'crontrol-events' => sprintf(
576 '<a href="%s">%s</a>',
577 esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page' ) ),
578 esc_html__( 'Events', 'wp-crontrol' )
579 ),
580 'crontrol-schedules' => sprintf(
581 '<a href="%s">%s</a>',
582 esc_url( admin_url( 'options-general.php?page=crontrol_admin_options_page' ) ),
583 esc_html__( 'Schedules', 'wp-crontrol' )
584 ),
585 );
586
587 return array_merge( $new, $actions );
588 }
589
590 /**
591 * Gives WordPress the plugin's set of cron schedules.
592 *
593 * Called by the `cron_schedules` filter.
594 *
595 * @param array[] $scheds Array of cron schedule arrays. Usually empty.
596 * @return array[] Array of modified cron schedule arrays.
597 */
598 function filter_cron_schedules( array $scheds ) {
599 $new_scheds = get_option( 'crontrol_schedules', array() );
600 return array_merge( $new_scheds, $scheds );
601 }
602
603 /**
604 * Displays the options page for the plugin.
605 */
606 function admin_options_page() {
607 $messages = array(
608 '2' => array(
609 /* translators: 1: The name of the cron schedule. */
610 __( 'Deleted the cron schedule %s.', 'wp-crontrol' ),
611 'success',
612 ),
613 '3' => array(
614 /* translators: 1: The name of the cron schedule. */
615 __( 'Added the cron schedule %s.', 'wp-crontrol' ),
616 'success',
617 ),
618 '7' => array(
619 /* translators: 1: The name of the cron schedule. */
620 __( 'Cron schedule not added because there was a problem parsing %s.', 'wp-crontrol' ),
621 'error',
622 ),
623 );
624 if ( isset( $_GET['crontrol_message'] ) && isset( $_GET['crontrol_name'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
625 $hook = wp_unslash( $_GET['crontrol_name'] );
626 $message = wp_unslash( $_GET['crontrol_message'] );
627
628 printf(
629 '<div id="crontrol-message" class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
630 esc_attr( $messages[ $message ][1] ),
631 sprintf(
632 esc_html( $messages[ $message ][0] ),
633 '<strong>' . esc_html( $hook ) . '</strong>'
634 )
635 );
636 }
637
638 require_once __DIR__ . '/src/schedule-list-table.php';
639
640 $table = new Schedule_List_Table();
641
642 $table->prepare_items();
643
644 ?>
645 <div class="wrap">
646
647 <h1><?php esc_html_e( 'Cron Schedules', 'wp-crontrol' ); ?></h1>
648
649 <?php $table->views(); ?>
650
651 <div id="col-container" class="wp-clearfix">
652 <div id="col-left">
653 <div class="col-wrap">
654 <div class="form-wrap">
655 <h2><?php esc_html_e( 'Add Cron Schedule', 'wp-crontrol' ); ?></h2>
656 <p><?php esc_html_e( 'Adding a new cron schedule will allow you to schedule events that re-occur at the given interval.', 'wp-crontrol' ); ?></p>
657 <form method="post" action="options-general.php?page=crontrol_admin_options_page">
658 <div class="form-field form-required">
659 <label for="cron_internal_name">
660 <?php esc_html_e( 'Internal Name', 'wp-crontrol' ); ?>
661 </label>
662 <input type="text" value="" id="cron_internal_name" name="internal_name" required/>
663 </div>
664 <div class="form-field form-required">
665 <label for="cron_interval">
666 <?php esc_html_e( 'Interval (seconds)', 'wp-crontrol' ); ?>
667 </label>
668 <input type="number" value="" id="cron_interval" name="interval" min="1" step="1" required/>
669 </div>
670 <div class="form-field form-required">
671 <label for="cron_display_name">
672 <?php esc_html_e( 'Display Name', 'wp-crontrol' ); ?>
673 </label>
674 <input type="text" value="" id="cron_display_name" name="display_name" required/>
675 </div>
676 <p class="submit">
677 <input id="schedadd-submit" type="submit" class="button button-primary" value="<?php esc_attr_e( 'Add Cron Schedule', 'wp-crontrol' ); ?>" name="new_schedule"/>
678 </p>
679 <?php wp_nonce_field( 'new-sched' ); ?>
680 </form>
681 </div>
682 </div>
683 </div>
684 <div id="col-right">
685 <div class="col-wrap">
686 <?php $table->display(); ?>
687 </div>
688 </div>
689 </div>
690 <?php
691 }
692
693 /**
694 * Clears the doing cron status when an event is unscheduled.
695 *
696 * What on earth does this function do, and why?
697 *
698 * Good question. The purpose of this function is to prevent other overdue cron events from firing when an event is run
699 * manually with the "Run Now" action. WP Crontrol works very hard to ensure that when cron event runs manually that it
700 * runs in the exact same way it would run as part of its schedule - via a properly spawned cron with a queued event in
701 * place. It does this by queueing an event at time `1` (1 second into 1st January 1970) and then immediately spawning
702 * cron (see the `Event\run()` function).
703 *
704 * The problem this causes is if other events are due then they will all run too, and this isn't desirable because if a
705 * site has a large number of stuck events due to a problem with the cron runner then it's not desirable for all those
706 * events to run when another is manually run. This happens because WordPress core will attempt to run all due events
707 * whenever cron is spawned.
708 *
709 * The code in this function prevents multiple events from running by changing the value of the `doing_cron` transient
710 * when an event gets unscheduled during a manual run, which prevents wp-cron.php from iterating more than one event.
711 *
712 * The `pre_unschedule_event` filter is used for this because it's just about the only hook available within this loop.
713 *
714 * Refs:
715 * - https://core.trac.wordpress.org/browser/trunk/src/wp-cron.php?rev=47198&marks=127,141#L122
716 *
717 * @param mixed $pre The pre-flight value of the event unschedule short-circuit. Not used.
718 * @return mixed Thee unaltered pre-flight value.
719 */
720 function maybe_clear_doing_cron( $pre ) {
721 if ( defined( 'DOING_CRON' ) && DOING_CRON && isset( $_GET['crontrol-single-event'] ) ) {
722 delete_transient( 'doing_cron' );
723 }
724
725 return $pre;
726 }
727
728 /**
729 * Ajax handler which outputs a hash of the current list of scheduled events.
730 */
731 function ajax_check_events_hash() {
732 if ( ! current_user_can( 'manage_options' ) ) {
733 wp_send_json_error( null, 403 );
734 }
735
736 wp_send_json_success( md5( json_encode( Event\get_list_table()->items ) ) );
737 }
738
739 /**
740 * Gets the status of WP-Cron functionality on the site by performing a test spawn if necessary. Cached for one hour when all is well.
741 *
742 * @param bool $cache Whether to use the cached result from previous calls.
743 * @return true|WP_Error Boolean true if the cron spawner is working as expected, or a `WP_Error` object if not.
744 */
745 function test_cron_spawn( $cache = true ) {
746 global $wp_version;
747
748 $cron_runner_plugins = array(
749 '\HM\Cavalcade\Plugin\Job' => 'Cavalcade',
750 '\Automattic\WP\Cron_Control' => 'Cron Control',
751 );
752
753 foreach ( $cron_runner_plugins as $class => $plugin ) {
754 if ( class_exists( $class ) ) {
755 return new WP_Error( 'crontrol_info', sprintf(
756 /* translators: 1: The name of the plugin that controls the running of cron events. */
757 __( 'WP-Cron spawning is being managed by the %s plugin.', 'wp-crontrol' ),
758 $plugin
759 ) );
760 }
761 }
762
763 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
764 return new WP_Error( 'crontrol_info', sprintf(
765 /* translators: 1: The name of the PHP constant that is set. */
766 __( 'The %s constant is set to true. WP-Cron spawning is disabled.', 'wp-crontrol' ),
767 'DISABLE_WP_CRON'
768 ) );
769 }
770
771 if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) {
772 return new WP_Error( 'crontrol_info', sprintf(
773 /* translators: 1: The name of the PHP constant that is set. */
774 __( 'The %s constant is set to true.', 'wp-crontrol' ),
775 'ALTERNATE_WP_CRON'
776 ) );
777 }
778
779 $cached_status = get_transient( 'crontrol-cron-test-ok' );
780
781 if ( $cache && $cached_status ) {
782 return true;
783 }
784
785 $sslverify = version_compare( $wp_version, 4.0, '<' );
786 $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
787
788 $cron_request = apply_filters( 'cron_request', array(
789 'url' => add_query_arg( 'doing_wp_cron', $doing_wp_cron, site_url( 'wp-cron.php' ) ),
790 'key' => $doing_wp_cron,
791 'args' => array(
792 'timeout' => 3,
793 'blocking' => true,
794 'sslverify' => apply_filters( 'https_local_ssl_verify', $sslverify ),
795 ),
796 ) );
797
798 $cron_request['args']['blocking'] = true;
799
800 $result = wp_remote_post( $cron_request['url'], $cron_request['args'] );
801
802 if ( is_wp_error( $result ) ) {
803 return $result;
804 } elseif ( wp_remote_retrieve_response_code( $result ) >= 300 ) {
805 return new WP_Error( 'unexpected_http_response_code', sprintf(
806 /* translators: 1: The HTTP response code. */
807 __( 'Unexpected HTTP response code: %s', 'wp-crontrol' ),
808 intval( wp_remote_retrieve_response_code( $result ) )
809 ) );
810 } else {
811 set_transient( 'crontrol-cron-test-ok', 1, 3600 );
812 return true;
813 }
814
815 }
816
817 /**
818 * Determines whether the given feature is enabled.
819 *
820 * The feature directly corresponds to one of WP Crontrol's tabs. Currently the only feature
821 * that's not enabled by default is "logs" which are provided by WP Crontrol Pro.
822 *
823 * @param string $feature The feature name.
824 * @return bool Whether the specified tab is active.
825 */
826 function is_feature_enabled( $feature ) {
827 $enabled = ( 'logs' !== $feature );
828 return apply_filters( "crontrol/enabled/{$feature}", $enabled );
829 }
830
831 /**
832 * Shows the status of WP-Cron functionality on the site. Only displays a message when there's a problem.
833 *
834 * @param string $tab The tab name.
835 */
836 function show_cron_status( $tab ) {
837 if ( ! is_feature_enabled( $tab ) ) {
838 return;
839 }
840
841 if ( 'UTC' !== date_default_timezone_get() ) {
842 ?>
843 <div id="crontrol-timezone-warning" class="notice notice-warning">
844 <?php
845 printf(
846 '<p>%1$s</p><p><a href="%2$s">%3$s</a></p>',
847 /* translators: %s: Help page URL. */
848 esc_html__( 'PHP default timezone is not set to UTC. This may cause issues with cron event timings.', 'wp-crontrol' ),
849 'https://github.com/johnbillion/wp-crontrol/wiki/PHP-default-timezone-is-not-set-to-UTC',
850 esc_html__( 'More information', 'wp-crontrol' )
851 );
852 ?>
853 </div>
854 <?php
855 }
856
857 $status = test_cron_spawn();
858
859 if ( is_wp_error( $status ) ) {
860 if ( 'crontrol_info' === $status->get_error_code() ) {
861 ?>
862 <div id="cron-status-notice" class="notice notice-info">
863 <p><?php echo esc_html( $status->get_error_message() ); ?></p>
864 </div>
865 <?php
866 } else {
867 ?>
868 <div id="cron-status-error" class="error">
869 <?php
870 printf(
871 '<p>%1$s</p><p><a href="%2$s">%3$s</a></p>',
872 sprintf(
873 /* translators: 1: Error message text. */
874 esc_html__( 'There was a problem spawning a call to the WP-Cron system on your site. This means WP-Cron events on your site may not work. The problem was: %s', 'wp-crontrol' ),
875 '</p><p><strong>' . esc_html( $status->get_error_message() ) . '</strong>'
876 ),
877 'https://github.com/johnbillion/wp-crontrol/wiki/Problems-with-spawning-a-call-to-the-WP-Cron-system',
878 esc_html__( 'More information', 'wp-crontrol' )
879 );
880 ?>
881 </div>
882 <?php
883 }
884 }
885 }
886
887 /**
888 * Get the display name for the site's timezone.
889 *
890 * @return string The name and UTC offset for the site's timezone.
891 */
892 function get_timezone_name() {
893 $timezone_string = get_option( 'timezone_string', '' );
894 $gmt_offset = get_option( 'gmt_offset', 0 );
895
896 if ( 'UTC' === $timezone_string || ( empty( $gmt_offset ) && empty( $timezone_string ) ) ) {
897 return 'UTC';
898 }
899
900 if ( '' === $timezone_string ) {
901 return get_utc_offset();
902 }
903
904 return sprintf(
905 '%s, %s',
906 str_replace( '_', ' ', $timezone_string ),
907 get_utc_offset()
908 );
909 }
910
911 /**
912 * Returns a display value for a UTC offset.
913 *
914 * Examples:
915 * - UTC
916 * - UTC+4
917 * - UTC-6
918 *
919 * @return string The UTC offset display value.
920 */
921 function get_utc_offset() {
922 $offset = get_option( 'gmt_offset', 0 );
923
924 if ( empty( $offset ) ) {
925 return 'UTC';
926 }
927
928 if ( 0 <= $offset ) {
929 $formatted_offset = '+' . (string) $offset;
930 } else {
931 $formatted_offset = (string) $offset;
932 }
933 $formatted_offset = str_replace(
934 array( '.25', '.5', '.75' ),
935 array( ':15', ':30', ':45' ),
936 $formatted_offset
937 );
938 return 'UTC' . $formatted_offset;
939 }
940
941 /**
942 * Shows the form used to add/edit cron events.
943 *
944 * @param bool $editing Whether the form is for the event editor.
945 * @return void
946 */
947 function show_cron_form( $editing ) {
948 $display_args = '';
949 $edit_id = null;
950 $existing = false;
951
952 if ( $editing && ! empty( $_GET['id'] ) ) {
953 $edit_id = wp_unslash( $_GET['id'] );
954
955 foreach ( Event\get() as $event ) {
956 if ( $edit_id === $event->hook && intval( $_GET['next_run_utc'] ) === $event->time && $event->sig === $_GET['sig'] ) {
957 $existing = array(
958 'hookname' => $event->hook,
959 'next_run' => $event->time, // UTC
960 'schedule' => ( $event->schedule ? $event->schedule : '_oneoff' ),
961 'sig' => $event->sig,
962 'args' => $event->args,
963 );
964 break;
965 }
966 }
967
968 if ( empty( $existing ) ) {
969 ?>
970 <div id="crontrol-event-not-found" class="notice notice-error">
971 <?php
972 printf(
973 '<p>%1$s</p>',
974 esc_html__( 'The event you are trying to edit does not exist.', 'wp-crontrol' )
975 );
976 ?>
977 </div>
978 <?php
979 return;
980 }
981 }
982
983 $is_editing_php = ( $existing && 'crontrol_cron_job' === $existing['hookname'] );
984
985 if ( $is_editing_php ) {
986 $helper_text = esc_html__( 'Cron events trigger actions in your code. Enter the schedule of the event, as well as the PHP code to execute when the action is triggered.', 'wp-crontrol' );
987 } else {
988 $helper_text = sprintf(
989 /* translators: %s: A file name */
990 esc_html__( 'Cron events trigger actions in your code. A cron event needs a corresponding action hook somewhere in code, e.g. the %1$s file in your theme.', 'wp-crontrol' ),
991 '<code>functions.php</code>'
992 );
993 }
994
995 if ( is_array( $existing ) ) {
996 $other_fields = wp_nonce_field( "edit-cron_{$existing['hookname']}_{$existing['sig']}_{$existing['next_run']}", '_wpnonce', true, false );
997 $other_fields .= sprintf( '<input name="original_hookname" type="hidden" value="%s" />',
998 esc_attr( $existing['hookname'] )
999 );
1000 $other_fields .= sprintf( '<input name="original_sig" type="hidden" value="%s" />',
1001 esc_attr( $existing['sig'] )
1002 );
1003 $other_fields .= sprintf( '<input name="original_next_run_utc" type="hidden" value="%s" />',
1004 esc_attr( $existing['next_run'] )
1005 );
1006 if ( ! empty( $existing['args'] ) ) {
1007 $display_args = wp_json_encode( $existing['args'] );
1008 }
1009 $action = $is_editing_php ? 'edit_php_cron' : 'edit_cron';
1010 $button = __( 'Update Event', 'wp-crontrol' );
1011 $next_run_gmt = gmdate( 'Y-m-d H:i:s', $existing['next_run'] );
1012 $next_run_date_local = get_date_from_gmt( $next_run_gmt, 'Y-m-d' );
1013 $next_run_time_local = get_date_from_gmt( $next_run_gmt, 'H:i:s' );
1014 } else {
1015 $other_fields = wp_nonce_field( 'new-cron', '_wpnonce', true, false );
1016 $existing = array(
1017 'hookname' => '',
1018 'args' => array(),
1019 'next_run' => 'now', // UTC
1020 'schedule' => false,
1021 );
1022
1023 $button = __( 'Add Event', 'wp-crontrol' );
1024 $next_run_date_local = '';
1025 $next_run_time_local = '';
1026 }
1027
1028 if ( $is_editing_php ) {
1029 if ( ! isset( $existing['args']['code'] ) ) {
1030 $existing['args']['code'] = '';
1031 }
1032 if ( ! isset( $existing['args']['name'] ) ) {
1033 $existing['args']['name'] = '';
1034 }
1035 }
1036
1037 $can_add_php = current_user_can( 'edit_files' ) && ! $editing;
1038 $allowed = ( ! $is_editing_php || current_user_can( 'edit_files' ) );
1039 ?>
1040 <div id="crontrol_form" class="wrap narrow">
1041 <?php
1042 if ( $allowed ) {
1043 if ( $editing ) {
1044 $heading = __( 'Edit Cron Event', 'wp-crontrol' );
1045 } else {
1046 $heading = __( 'Add Cron Event', 'wp-crontrol' );
1047 }
1048
1049 printf(
1050 '<h1>%s</h1>',
1051 esc_html( $heading )
1052 );
1053 printf(
1054 '<p>%s</p>',
1055 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1056 $helper_text
1057 );
1058 ?>
1059 <form method="post" action="<?php echo esc_url( admin_url( 'tools.php?page=crontrol_admin_manage_page' ) ); ?>" class="crontrol-edit-event crontrol-edit-event-<?php echo ( $is_editing_php ) ? 'php' : 'standard'; ?>">
1060 <?php
1061 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1062 echo $other_fields;
1063 ?>
1064 <table class="form-table"><tbody>
1065 <?php
1066 if ( $editing ) {
1067 printf(
1068 '<input type="hidden" name="action" value="%s"/>',
1069 esc_attr( $action )
1070 );
1071 } elseif ( $can_add_php ) {
1072 ?>
1073 <tr class="hide-if-no-js">
1074 <th valign="top" scope="row">
1075 <?php esc_html_e( 'Event Type', 'wp-crontrol' ); ?>
1076 </th>
1077 <td>
1078 <p><label><input type="radio" name="action" value="new_cron" checked>Standard cron event</label></p>
1079 <p><label><input type="radio" name="action" value="new_php_cron">PHP cron event</label></p>
1080 </td>
1081 </tr>
1082 <?php
1083 } else {
1084 ?>
1085 <input type="hidden" name="action" value="new_cron"/>
1086 <?php
1087 }
1088
1089 if ( $is_editing_php || $can_add_php ) {
1090 ?>
1091 <tr class="crontrol-event-php">
1092 <th valign="top" scope="row">
1093 <label for="hookcode">
1094 <?php esc_html_e( 'PHP Code', 'wp-crontrol' ); ?>
1095 </label>
1096 </th>
1097 <td>
1098 <p class="description">
1099 <?php
1100 printf(
1101 /* translators: The PHP tag name */
1102 esc_html__( 'The opening %s tag must not be included.', 'wp-crontrol' ),
1103 '<code>&lt;?php</code>'
1104 );
1105 ?>
1106 </p>
1107 <p><textarea class="large-text code" rows="10" cols="50" id="hookcode" name="hookcode"><?php echo esc_textarea( $editing ? $existing['args']['code'] : '' ); ?></textarea></p>
1108 </td>
1109 </tr>
1110 <tr class="crontrol-event-php">
1111 <th valign="top" scope="row">
1112 <label for="eventname">
1113 <?php esc_html_e( 'Event Name (optional)', 'wp-crontrol' ); ?>
1114 </label>
1115 </th>
1116 <td>
1117 <input type="text" class="regular-text" id="eventname" name="eventname" value="<?php echo esc_attr( $editing ? $existing['args']['name'] : '' ); ?>"/>
1118 </td>
1119 </tr>
1120 <?php
1121 }
1122
1123 if ( ! $is_editing_php ) {
1124 ?>
1125 <tr class="crontrol-event-standard">
1126 <th valign="top" scope="row">
1127 <label for="hookname">
1128 <?php esc_html_e( 'Hook Name', 'wp-crontrol' ); ?>
1129 </label>
1130 </th>
1131 <td>
1132 <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text" id="hookname" name="hookname" value="<?php echo esc_attr( $existing['hookname'] ); ?>" required />
1133 </td>
1134 </tr>
1135 <tr class="crontrol-event-standard">
1136 <th valign="top" scope="row">
1137 <label for="args">
1138 <?php esc_html_e( 'Arguments (optional)', 'wp-crontrol' ); ?>
1139 </label>
1140 </th>
1141 <td>
1142 <input type="text" autocorrect="off" autocapitalize="off" spellcheck="false" class="regular-text code" id="args" name="args" value="<?php echo esc_attr( $display_args ); ?>"/>
1143 <p class="description">
1144 <?php
1145 printf(
1146 /* translators: 1, 2, and 3: Example values for an input field. */
1147 esc_html__( 'Use a JSON encoded array, e.g. %1$s, %2$s, or %3$s', 'wp-crontrol' ),
1148 '<code>[25]</code>',
1149 '<code>["asdf"]</code>',
1150 '<code>["i","want",25,"cakes"]</code>'
1151 );
1152 ?>
1153 </p>
1154 </td>
1155 </tr>
1156 <?php
1157 }
1158 ?>
1159 <tr>
1160 <th valign="top" scope="row">
1161 <label for="next_run_date_local">
1162 <?php esc_html_e( 'Next Run', 'wp-crontrol' ); ?>
1163 </label>
1164 </th>
1165 <td>
1166 <ul>
1167 <li>
1168 <label>
1169 <input type="radio" name="next_run_date_local" value="now" checked>
1170 <?php esc_html_e( 'Now', 'wp-crontrol' ); ?>
1171 </label>
1172 </li>
1173 <li>
1174 <label>
1175 <input type="radio" name="next_run_date_local" value="+1 day">
1176 <?php esc_html_e( 'Tomorrow', 'wp-crontrol' ); ?>
1177 </label>
1178 </li>
1179 <li>
1180 <label>
1181 <input type="radio" name="next_run_date_local" value="custom" id="next_run_date_local_custom" <?php checked( $editing ); ?>>
1182 <?php
1183 printf(
1184 /* translators: %s: An input field for specifying a date and time */
1185 esc_html__( 'At: %s', 'wp-crontrol' ),
1186 sprintf(
1187 '<br>
1188 <input type="date" autocorrect="off" autocapitalize="off" spellcheck="false" name="next_run_date_local_custom_date" id="next_run_date_local_custom_date" value="%1$s" placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" />
1189 <input type="time" autocorrect="off" autocapitalize="off" spellcheck="false" name="next_run_date_local_custom_time" id="next_run_date_local_custom_time" value="%2$s" step="1" placeholder="hh:mm:ss" pattern="\d{2}:\d{2}:\d{2}" />',
1190 esc_attr( $next_run_date_local ),
1191 esc_attr( $next_run_time_local )
1192 )
1193 );
1194 ?>
1195 </label>
1196 </li>
1197 </ul>
1198
1199 <p class="description">
1200 <?php
1201 printf(
1202 /* translators: %s Timezone name. */
1203 esc_html__( 'Timezone: %s', 'wp-crontrol' ),
1204 esc_html( get_timezone_name() )
1205 );
1206 ?>
1207 </p>
1208 </td>
1209 </tr>
1210 <tr>
1211 <th valign="top" scope="row">
1212 <label for="schedule">
1213 <?php esc_html_e( 'Recurrence', 'wp-crontrol' ); ?>
1214 </label>
1215 </th>
1216 <td>
1217 <?php Schedule\dropdown( $existing['schedule'] ); ?>
1218 </td>
1219 </tr>
1220 </tbody></table>
1221 <p class="submit">
1222 <input type="submit" class="button button-primary" value="<?php echo esc_attr( $button ); ?>"/>
1223 </p>
1224 </form>
1225 <?php } else { ?>
1226 <div class="error inline">
1227 <p><?php esc_html_e( 'You cannot add, edit, or delete PHP cron events because your user account does not have the ability to edit files.', 'wp-crontrol' ); ?></p>
1228 </div>
1229 <?php } ?>
1230 </div>
1231 <?php
1232 }
1233
1234 /**
1235 * Displays the manage page for the plugin.
1236 */
1237 function admin_manage_page() {
1238 $messages = array(
1239 '1' => array(
1240 /* translators: 1: The name of the cron event. */
1241 __( 'Scheduled the cron event %s to run now.', 'wp-crontrol' ),
1242 'success',
1243 true,
1244 ),
1245 '2' => array(
1246 /* translators: 1: The name of the cron event. */
1247 __( 'Deleted all %s cron events.', 'wp-crontrol' ),
1248 'success',
1249 false,
1250 ),
1251 '3' => array(
1252 /* translators: 1: The name of the cron event. */
1253 __( 'There are no %s cron events to delete.', 'wp-crontrol' ),
1254 'info',
1255 false,
1256 ),
1257 '4' => array(
1258 /* translators: 1: The name of the cron event. */
1259 __( 'Saved the cron event %s.', 'wp-crontrol' ),
1260 'success',
1261 false,
1262 ),
1263 '5' => array(
1264 /* translators: 1: The name of the cron event. */
1265 __( 'Created the cron event %s.', 'wp-crontrol' ),
1266 'success',
1267 false,
1268 ),
1269 '6' => array(
1270 /* translators: 1: The name of the cron event. */
1271 __( 'Deleted the cron event %s.', 'wp-crontrol' ),
1272 'success',
1273 false,
1274 ),
1275 '7' => array(
1276 /* translators: 1: The name of the cron event. */
1277 __( 'Failed to the delete the cron event %s.', 'wp-crontrol' ),
1278 'error',
1279 false,
1280 ),
1281 '8' => array(
1282 /* translators: 1: The name of the cron event. */
1283 __( 'Failed to the execute the cron event %s.', 'wp-crontrol' ),
1284 'error',
1285 false,
1286 ),
1287 '9' => array(
1288 __( 'Deleted the selected cron events.', 'wp-crontrol' ),
1289 'success',
1290 false,
1291 ),
1292 '10' => array(
1293 /* translators: 1: The name of the cron event. */
1294 __( 'Failed to save the cron event %s.', 'wp-crontrol' ),
1295 'error',
1296 false,
1297 ),
1298 );
1299
1300 if ( isset( $_GET['crontrol_name'] ) && isset( $_GET['crontrol_message'] ) && isset( $messages[ $_GET['crontrol_message'] ] ) ) {
1301 $hook = wp_unslash( $_GET['crontrol_name'] );
1302 $message = wp_unslash( $_GET['crontrol_message'] );
1303 $link = '';
1304
1305 printf(
1306 '<div id="crontrol-message" class="notice notice-%1$s is-dismissible"><p>%2$s%3$s</p></div>',
1307 esc_attr( $messages[ $message ][1] ),
1308 sprintf(
1309 esc_html( $messages[ $message ][0] ),
1310 '<strong>' . esc_html( $hook ) . '</strong>'
1311 ),
1312 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1313 $link
1314 );
1315 }
1316
1317 $tabs = get_tab_states();
1318 $table = Event\get_list_table();
1319
1320 switch ( true ) {
1321 case $tabs['events']:
1322 ?>
1323 <div class="wrap">
1324 <h1 class="wp-heading-inline"><?php esc_html_e( 'Cron Events', 'wp-crontrol' ); ?></h1>
1325
1326 <?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>'; ?>
1327
1328 <hr class="wp-header-end">
1329
1330 <?php $table->views(); ?>
1331
1332 <form id="posts-filter" method="get" action="tools.php">
1333 <input type="hidden" name="page" value="crontrol_admin_manage_page" />
1334 <?php $table->search_box( __( 'Search Hook Names', 'wp-crontrol' ), 'cron-event' ); ?>
1335 </form>
1336
1337 <form method="post" action="tools.php?page=crontrol_admin_manage_page">
1338 <div class="table-responsive">
1339 <?php $table->display(); ?>
1340 </div>
1341 </form>
1342
1343 <p>
1344 <?php
1345 echo esc_html( sprintf(
1346 /* translators: 1: Date and time, 2: Timezone */
1347 __( 'Site time: %1$s (%2$s)', 'wp-crontrol' ),
1348 date_i18n( 'Y-m-d H:i:s' ),
1349 get_timezone_name()
1350 ) );
1351 ?>
1352 </p>
1353 </div>
1354 <?php
1355
1356 break;
1357
1358 case $tabs['add-event']:
1359 show_cron_form( false );
1360 break;
1361
1362 case $tabs['edit-event']:
1363 show_cron_form( true );
1364 break;
1365
1366 }
1367
1368 }
1369
1370 /**
1371 * Get the states of the various cron-related tabs.
1372 *
1373 * @return bool[] Array of states keyed by tab name.
1374 */
1375 function get_tab_states() {
1376 return array(
1377 'events' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_manage_page' === $_GET['page'] && empty( $_GET['action'] ) ),
1378 'schedules' => ( ! empty( $_GET['page'] ) && 'crontrol_admin_options_page' === $_GET['page'] ),
1379 'add-event' => ( ! empty( $_GET['action'] ) && 'new-cron' === $_GET['action'] ),
1380 'edit-event' => ( ! empty( $_GET['action'] ) && 'edit-cron' === $_GET['action'] ),
1381 );
1382 }
1383
1384 /**
1385 * Output the cron-related tabs if we're on a cron-related admin screen.
1386 */
1387 function do_tabs() {
1388 $tabs = get_tab_states();
1389 $tab = array_filter( $tabs );
1390
1391 if ( ! $tab ) {
1392 return;
1393 }
1394
1395 $tab = array_keys( $tab );
1396 $tab = reset( $tab );
1397 $links = array(
1398 'events' => array(
1399 'tools.php?page=crontrol_admin_manage_page',
1400 __( 'Cron Events', 'wp-crontrol' ),
1401 ),
1402 'schedules' => array(
1403 'options-general.php?page=crontrol_admin_options_page',
1404 __( 'Cron Schedules', 'wp-crontrol' ),
1405 ),
1406 );
1407
1408 ?>
1409 <div id="crontrol-header">
1410 <nav class="nav-tab-wrapper">
1411 <?php
1412 foreach ( $links as $id => $link ) {
1413 if ( $tabs[ $id ] ) {
1414 printf(
1415 '<a href="%s" class="nav-tab nav-tab-active">%s</a>',
1416 esc_url( $link[0] ),
1417 esc_html( $link[1] )
1418 );
1419 } else {
1420 printf(
1421 '<a href="%s" class="nav-tab">%s</a>',
1422 esc_url( $link[0] ),
1423 esc_html( $link[1] )
1424 );
1425 }
1426 }
1427
1428 if ( $tabs['add-event'] ) {
1429 printf(
1430 '<span class="nav-tab nav-tab-active">%s</span>',
1431 esc_html__( 'Add Cron Event', 'wp-crontrol' )
1432 );
1433 } elseif ( $tabs['edit-event'] ) {
1434 printf(
1435 '<span class="nav-tab nav-tab-active">%s</span>',
1436 esc_html__( 'Edit Cron Event', 'wp-crontrol' )
1437 );
1438 }
1439 ?>
1440 </nav>
1441 <?php
1442 do_action( 'crontrol/tab-header', $tab, $tabs );
1443 ?>
1444 </div>
1445 <?php
1446 }
1447
1448 /**
1449 * Returns an array of the callback functions that are attached to the given hook name.
1450 *
1451 * @param string $name The hook name.
1452 * @return array[] Array of callbacks attached to the hook.
1453 */
1454 function get_hook_callbacks( $name ) {
1455 global $wp_filter;
1456
1457 $actions = array();
1458
1459 if ( isset( $wp_filter[ $name ] ) ) {
1460 // See http://core.trac.wordpress.org/ticket/17817.
1461 $action = $wp_filter[ $name ];
1462
1463 foreach ( $action as $priority => $callbacks ) {
1464 foreach ( $callbacks as $callback ) {
1465 $callback = populate_callback( $callback );
1466
1467 $actions[] = array(
1468 'priority' => $priority,
1469 'callback' => $callback,
1470 );
1471 }
1472 }
1473 }
1474
1475 return $actions;
1476 }
1477
1478 /**
1479 * Populates the details of the given callback function.
1480 *
1481 * @param array $callback A callback entry.
1482 * @return array The updated callback entry.
1483 */
1484 function populate_callback( array $callback ) {
1485 // If Query Monitor is installed, use its rich callback analysis.
1486 if ( method_exists( '\QM_Util', 'populate_callback' ) ) {
1487 return \QM_Util::populate_callback( $callback );
1488 }
1489
1490 if ( is_string( $callback['function'] ) && ( false !== strpos( $callback['function'], '::' ) ) ) {
1491 $callback['function'] = explode( '::', $callback['function'] );
1492 }
1493
1494 if ( is_array( $callback['function'] ) ) {
1495 if ( is_object( $callback['function'][0] ) ) {
1496 $class = get_class( $callback['function'][0] );
1497 $access = '->';
1498 } else {
1499 $class = $callback['function'][0];
1500 $access = '::';
1501 }
1502
1503 $callback['name'] = $class . $access . $callback['function'][1] . '()';
1504 } elseif ( is_object( $callback['function'] ) ) {
1505 if ( is_a( $callback['function'], 'Closure' ) ) {
1506 $callback['name'] = 'Closure';
1507 } else {
1508 $class = get_class( $callback['function'] );
1509
1510 $callback['name'] = $class . '->__invoke()';
1511 }
1512 } else {
1513 $callback['name'] = $callback['function'] . '()';
1514 }
1515
1516 return $callback;
1517 }
1518
1519 /**
1520 * Returns a user-friendly representation of the callback function.
1521 *
1522 * @param array $callback The callback entry.
1523 * @return string The displayable version of the callback name.
1524 */
1525 function output_callback( array $callback ) {
1526 $qm = WP_PLUGIN_DIR . '/query-monitor/query-monitor.php';
1527 $html = plugin_dir_path( $qm ) . 'output/Html.php';
1528
1529 // If Query Monitor is installed, use its rich callback output.
1530 if ( class_exists( '\QueryMonitor' ) && file_exists( $html ) ) {
1531 require_once $html;
1532
1533 if ( class_exists( '\QM_Output_Html' ) ) {
1534 if ( ! empty( $callback['callback']['error'] ) ) {
1535 $return = '<code>' . $callback['callback']['name'] . '</code>';
1536 $return .= '<br><span class="status-crontrol-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span> ';
1537 $return .= esc_html( $callback['callback']['error']->get_error_message() );
1538 $return .= '</span>';
1539 return $return;
1540 }
1541
1542 return \QM_Output_Html::output_filename(
1543 $callback['callback']['name'],
1544 $callback['callback']['file'],
1545 $callback['callback']['line']
1546 );
1547 }
1548 }
1549
1550 return '<code>' . $callback['callback']['name'] . '</code>';
1551 }
1552
1553 /**
1554 * Pretty-prints the difference in two times.
1555 *
1556 * @param int $older_date Unix timestamp.
1557 * @param int $newer_date Unix timestamp.
1558 * @return string The pretty time_since value
1559 * @link http://binarybonsai.com/code/timesince.txt
1560 */
1561 function time_since( $older_date, $newer_date ) {
1562 return interval( $newer_date - $older_date );
1563 }
1564
1565 /**
1566 * Converts a period of time in seconds into a human-readable format representing the interval.
1567 *
1568 * Example:
1569 *
1570 * echo \Crontrol\interval( 90 );
1571 * // 1 minute 30 seconds
1572 *
1573 * @param int $since A period of time in seconds.
1574 * @return string An interval represented as a string.
1575 */
1576 function interval( $since ) {
1577 // Array of time period chunks.
1578 $chunks = array(
1579 /* translators: 1: The number of years in an interval of time. */
1580 array( 60 * 60 * 24 * 365, _n_noop( '%s year', '%s years', 'wp-crontrol' ) ),
1581 /* translators: 1: The number of months in an interval of time. */
1582 array( 60 * 60 * 24 * 30, _n_noop( '%s month', '%s months', 'wp-crontrol' ) ),
1583 /* translators: 1: The number of weeks in an interval of time. */
1584 array( 60 * 60 * 24 * 7, _n_noop( '%s week', '%s weeks', 'wp-crontrol' ) ),
1585 /* translators: 1: The number of days in an interval of time. */
1586 array( 60 * 60 * 24, _n_noop( '%s day', '%s days', 'wp-crontrol' ) ),
1587 /* translators: 1: The number of hours in an interval of time. */
1588 array( 60 * 60, _n_noop( '%s hour', '%s hours', 'wp-crontrol' ) ),
1589 /* translators: 1: The number of minutes in an interval of time. */
1590 array( 60, _n_noop( '%s minute', '%s minutes', 'wp-crontrol' ) ),
1591 /* translators: 1: The number of seconds in an interval of time. */
1592 array( 1, _n_noop( '%s second', '%s seconds', 'wp-crontrol' ) ),
1593 );
1594
1595 if ( $since <= 0 ) {
1596 return __( 'now', 'wp-crontrol' );
1597 }
1598
1599 /**
1600 * We only want to output two chunks of time here, eg:
1601 * x years, xx months
1602 * x days, xx hours
1603 * so there's only two bits of calculation below:
1604 */
1605 $j = count( $chunks );
1606
1607 // Step one: the first chunk.
1608 for ( $i = 0; $i < $j; $i++ ) {
1609 $seconds = $chunks[ $i ][0];
1610 $name = $chunks[ $i ][1];
1611
1612 // Finding the biggest chunk (if the chunk fits, break).
1613 $count = floor( $since / $seconds );
1614 if ( $count ) {
1615 break;
1616 }
1617 }
1618
1619 // Set output var.
1620 $output = sprintf( translate_nooped_plural( $name, $count, 'wp-crontrol' ), $count );
1621
1622 // Step two: the second chunk.
1623 if ( $i + 1 < $j ) {
1624 $seconds2 = $chunks[ $i + 1 ][0];
1625 $name2 = $chunks[ $i + 1 ][1];
1626 $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 );
1627 if ( $count2 ) {
1628 // Add to output var.
1629 $output .= ' ' . sprintf( translate_nooped_plural( $name2, $count2, 'wp-crontrol' ), $count2 );
1630 }
1631 }
1632
1633 return $output;
1634 }
1635
1636 /**
1637 * Sets up the Events listing screen.
1638 */
1639 function setup_manage_page() {
1640 // Initialise the list table
1641 Event\get_list_table();
1642
1643 // Add the initially hidden admin notice about the out of date events list
1644 add_action( 'admin_notices', function() {
1645 printf(
1646 '<div id="crontrol-hash-message" class="notice notice-warning"><p>%s</p></div>',
1647 esc_html__( 'The scheduled cron events have changed since you first opened this page. Reload the page to see the up to date list.', 'wp-crontrol' )
1648 );
1649 } );
1650 }
1651
1652 /**
1653 * Registers the stylesheet and JavaScript for the admin areas.
1654 *
1655 * @param string $hook_suffix The admin screen ID.
1656 */
1657 function enqueue_assets( $hook_suffix ) {
1658 $tab = get_tab_states();
1659
1660 if ( ! array_filter( $tab ) ) {
1661 return;
1662 }
1663
1664 $ver = filemtime( plugin_dir_path( __FILE__ ) . 'css/wp-crontrol.css' );
1665 wp_enqueue_style( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'css/wp-crontrol.css', array( 'dashicons' ), $ver );
1666
1667 $ver = filemtime( plugin_dir_path( __FILE__ ) . 'js/wp-crontrol.js' );
1668 wp_enqueue_script( 'wp-crontrol', plugin_dir_url( __FILE__ ) . 'js/wp-crontrol.js', array( 'jquery' ), $ver, true );
1669
1670 $vars = array();
1671
1672 if ( ! empty( $tab['events'] ) ) {
1673 $vars['eventsHash'] = md5( json_encode( Event\get_list_table()->items ) );
1674 $vars['eventsHashInterval'] = 20;
1675 }
1676
1677 if ( ! empty( $tab['add-event'] ) || ! empty( $tab['edit-event'] ) ) {
1678 if ( function_exists( 'wp_enqueue_code_editor' ) && current_user_can( 'edit_files' ) ) {
1679 $settings = wp_enqueue_code_editor( array(
1680 'type' => 'text/x-php',
1681 ) );
1682
1683 if ( false !== $settings ) {
1684 $vars['codeEditor'] = $settings;
1685 }
1686 }
1687 }
1688
1689 wp_localize_script( 'wp-crontrol', 'wpCrontrol', $vars );
1690 }
1691
1692 /**
1693 * Filters the list of query arguments which get removed from admin area URLs in WordPress.
1694 *
1695 * @param string[] $args List of removable query arguments.
1696 * @return string[] Updated list of removable query arguments.
1697 */
1698 function filter_removable_query_args( array $args ) {
1699 return array_merge( $args, array(
1700 'crontrol_message',
1701 'crontrol_name',
1702 ) );
1703 }
1704
1705 /**
1706 * Returns an array of cron event hooks that are persistently added by WordPress core.
1707 *
1708 * @return string[] Array of hook names.
1709 */
1710 function get_persistent_core_hooks() {
1711 return array(
1712 'delete_expired_transients',
1713 'recovery_mode_clean_expired_keys',
1714 'update_network_counts',
1715 'wp_privacy_delete_old_export_files',
1716 'wp_scheduled_auto_draft_delete',
1717 'wp_scheduled_delete',
1718 'wp_site_health_scheduled_check',
1719 'wp_update_plugins',
1720 'wp_update_themes',
1721 'wp_version_check',
1722 );
1723 }
1724
1725 /**
1726 * Returns an array of all cron event hooks that are added by WordPress core.
1727 *
1728 * @return string[] Array of hook names.
1729 */
1730 function get_all_core_hooks() {
1731 return array_merge(
1732 get_persistent_core_hooks(),
1733 array(
1734 'do_pings',
1735 'importer_scheduled_cleanup',
1736 'publish_future_post',
1737 'upgrader_scheduled_cleanup',
1738 'wp_maybe_auto_update',
1739 'wp_split_shared_term_batch',
1740 'wp_update_comment_type_batch',
1741 )
1742 );
1743 }
1744
1745 /**
1746 * Returns an array of cron schedules that are added by WordPress core.
1747 *
1748 * @return string[] Array of schedule names.
1749 */
1750 function get_core_schedules() {
1751 return array(
1752 'hourly',
1753 'twicedaily',
1754 'daily',
1755 'weekly',
1756 );
1757 }
1758
1759 /**
1760 * Encodes some input as JSON for output.
1761 *
1762 * @param mixed $input The input.
1763 * @return string The JSON-encoded output.
1764 */
1765 function json_output( $input ) {
1766 $json_options = 0;
1767
1768 if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
1769 // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_unescaped_slashesFound
1770 $json_options |= JSON_UNESCAPED_SLASHES;
1771 }
1772 if ( defined( 'JSON_PRETTY_PRINT' ) ) {
1773 $json_options |= JSON_PRETTY_PRINT;
1774 }
1775
1776 return wp_json_encode( $input, $json_options );
1777 }
1778
1779 /**
1780 * Evaluates the code in a PHP cron event using eval.
1781 *
1782 * Security: Only users with the `edit_files` capability can manage PHP cron events. This means if a user cannot edit
1783 * files on the site (eg. through the Plugin Editor or Theme Editor) then they cannot edit or add a PHP cron event. By
1784 * default, only Administrators have this capability, and with Multisite enabled only Super Admins have this capability.
1785 *
1786 * If file editing has been disabled via the `DISALLOW_FILE_MODS` or `DISALLOW_FILE_EDIT` configuration constants then
1787 * no user will have the `edit_files` capability, which means editing or adding a PHP cron event will not be permitted.
1788 *
1789 * Therefore, the user access level required to execute arbitrary PHP code does not change with WP Crontrol activated.
1790 *
1791 * @param string $code The PHP code to evaluate.
1792 */
1793 function action_php_cron_event( $code ) {
1794 // phpcs:ignore Squiz.PHP.Eval.Discouraged
1795 eval( $code );
1796 }
1797
1798 // Get this show on the road.
1799 init_hooks();
1800