$cron ) { if ( isset( $cron[ $hookname ][ $sig ] ) ) { $event = $cron[ $hookname ][ $sig ]; $event['hook'] = $hookname; $event['timestamp'] = $time; $event = (object) $event; delete_transient( 'doing_cron' ); $scheduled = force_schedule_single_event( $hookname, $event->args ); // UTC if ( is_wp_error( $scheduled ) ) { return $scheduled; } add_filter( 'cron_request', function( array $cron_request_array ) { $cron_request_array['url'] = add_query_arg( 'crontrol-single-event', 1, $cron_request_array['url'] ); return $cron_request_array; } ); spawn_cron(); sleep( 1 ); /** * Fires after a cron event is scheduled to run manually. * * @param object $event { * An object containing the event's data. * * @type string $hook Action hook to execute when the event is run. * @type int $timestamp Unix timestamp (UTC) for when to next run the event. * @type string|false $schedule How often the event should subsequently recur. * @type array $args Array containing each separate argument to pass to the hook's callback function. * @type int $interval The interval time in seconds for the schedule. Only present for recurring events. * } */ do_action( 'crontrol/ran_event', $event ); return true; } } return new WP_Error( 'not_found', sprintf( /* translators: 1: The name of the cron event. */ __( 'The cron event %s could not be found.', 'wp-crontrol' ), $hookname ) ); } /** * Forcibly schedules a single event for the purpose of manually running it. * * This is used instead of `wp_schedule_single_event()` to avoid the duplicate check that's otherwise performed. * * @param string $hook Action hook to execute when the event is run. * @param array $args Optional. Array containing each separate argument to pass to the hook's callback function. * @return true|WP_Error True if event successfully scheduled. WP_Error on failure. */ function force_schedule_single_event( $hook, $args = array() ) { $event = (object) array( 'hook' => $hook, 'timestamp' => 1, 'schedule' => false, 'args' => $args, ); $crons = (array) _get_cron_array(); $key = md5( serialize( $event->args ) ); $crons[ $event->timestamp ][ $event->hook ][ $key ] = array( 'schedule' => $event->schedule, 'args' => $event->args, ); uksort( $crons, 'strnatcasecmp' ); $result = _set_cron_array( $crons ); // Not using the WP_Error from `_set_cron_array()` here so we can provide a more specific error message. if ( false === $result ) { return new WP_Error( 'could_not_add', sprintf( /* translators: 1: The name of the cron event. */ __( 'Failed to schedule the cron event %s.', 'wp-crontrol' ), $hook ) ); } return true; } /** * Adds a new cron event. * * @param string $next_run_local The time that the event should be run at, in the site's timezone. * @param string $schedule The recurrence of the cron event. * @param string $hook The name of the hook to execute. * @param array $args Arguments to add to the cron event. * @return true|WP_error True if the addition was successful, WP_Error otherwise. */ function add( $next_run_local, $schedule, $hook, array $args ) { $next_run_local = strtotime( $next_run_local, current_time( 'timestamp' ) ); if ( false === $next_run_local ) { return new WP_Error( 'invalid_timestamp', __( 'Invalid timestamp provided.', 'wp-crontrol' ) ); } $next_run_utc = get_gmt_from_date( gmdate( 'Y-m-d H:i:s', $next_run_local ), 'U' ); if ( ! is_array( $args ) ) { $args = array(); } if ( 'crontrol_cron_job' === $hook && ! empty( $args['code'] ) && class_exists( '\ParseError' ) ) { try { // phpcs:ignore Squiz.PHP.Eval.Discouraged eval( sprintf( 'return true; %s', $args['code'] ) ); // phpcs:ignore PHPCompatibility.Classes.NewClasses.parseerrorFound } catch ( \ParseError $e ) { $args['syntax_error_message'] = $e->getMessage(); $args['syntax_error_line'] = $e->getLine(); } } if ( '_oneoff' === $schedule ) { $result = wp_schedule_single_event( $next_run_utc, $hook, $args, true ); } else { $result = wp_schedule_event( $next_run_utc, $schedule, $hook, $args, true ); } /** * Possible return values of `wp_schedule_*()` as called above: * * - 5.7+ Success: true, Failure: WP_Error * - 5.1+ Success: true, Failure: false * - <5.1 Success: null, Failure: false */ if ( is_wp_error( $result ) ) { return $result; } if ( false === $result ) { return new WP_Error( 'could_not_add', sprintf( /* translators: 1: The name of the cron event. */ __( 'Failed to schedule the cron event %s.', 'wp-crontrol' ), $hook ) ); } return true; } /** * Deletes a cron event. * * @param string $hook The hook name of the event to delete. * @param string $sig The cron event signature. * @param string $next_run_utc The UTC time that the event would be run at. * @return true|WP_Error True if the deletion was successful, WP_Error otherwise. */ function delete( $hook, $sig, $next_run_utc ) { $event = get_single( $hook, $sig, $next_run_utc ); if ( is_wp_error( $event ) ) { return $event; } $unscheduled = wp_unschedule_event( $event->timestamp, $event->hook, $event->args, true ); /** * Possible return values of `wp_unschedule_*()` as called above: * * - 5.7+ Success: true, Failure: WP_Error * - 5.1+ Success: true, Failure: false * - <5.1 Success: null, Failure: false */ if ( is_wp_error( $unscheduled ) ) { return $unscheduled; } if ( false === $unscheduled ) { return new WP_Error( 'could_not_delete', sprintf( /* translators: 1: The name of the cron event. */ __( 'Failed to the delete the cron event %s.', 'wp-crontrol' ), $hook ) ); } return true; } /** * Returns a flattened array of cron events. * * @return object[] An array of cron event objects. */ function get() { $crons = _get_cron_array(); $events = array(); if ( empty( $crons ) ) { return array(); } foreach ( $crons as $time => $cron ) { foreach ( $cron as $hook => $dings ) { foreach ( $dings as $sig => $data ) { // This is a prime candidate for a Crontrol_Event class but I'm not bothering currently. $events[ "$hook-$sig-$time" ] = (object) array( 'hook' => $hook, 'time' => $time, // UTC 'sig' => $sig, 'args' => $data['args'], 'schedule' => $data['schedule'], 'interval' => isset( $data['interval'] ) ? $data['interval'] : null, ); } } } // Ensure events are always returned in date descending order. // External cron runners such as Cavalcade don't guarantee events are returned in order of time. uasort( $events, 'Crontrol\Event\uasort_order_events' ); return $events; } /** * Gets a single cron event. * * @param string $hook The hook name of the event. * @param string $sig The event signature. * @param string $next_run_utc The UTC time that the event would be run at. * @return object|WP_Error A cron event object, or a WP_Error if it's not found. */ function get_single( $hook, $sig, $next_run_utc ) { $crons = _get_cron_array(); if ( isset( $crons[ $next_run_utc ][ $hook ][ $sig ] ) ) { $event = $crons[ $next_run_utc ][ $hook ][ $sig ]; $event['hook'] = $hook; $event['timestamp'] = $next_run_utc; $event = (object) $event; return $event; } return new WP_Error( 'not_found', sprintf( /* translators: 1: The name of the cron event. */ __( 'The cron event %s could not be found.', 'wp-crontrol' ), $hook ) ); } /** * Returns an array of the number of events for each hook. * * @return int[] Array of number of events for each hook, keyed by the hook name. */ function count_by_hook() { $crons = _get_cron_array(); $events = array(); if ( empty( $crons ) ) { return array(); } foreach ( $crons as $time => $cron ) { foreach ( $cron as $hook => $dings ) { if ( ! isset( $events[ $hook ] ) ) { $events[ $hook ] = 0; } $events[ $hook ] += count( $dings ); } } return $events; } /** * Returns the schedule display name for a given event. * * @param stdClass $event A WP-Cron event. * @return string|WP_Error The interval display name, or a WP_Error object if no such schedule exists. */ function get_schedule_name( stdClass $event ) { $schedules = Schedule\get(); if ( isset( $schedules[ $event->schedule ] ) ) { return $schedules[ $event->schedule ]['display']; } return new WP_Error( 'unknown_schedule', sprintf( /* translators: %s: Schedule name */ __( 'Unknown (%s)', 'wp-crontrol' ), $event->schedule ) ); } /** * Determines whether the schedule for an event means it runs too frequently to be reliable. * * @param stdClass $event A WP-Cron event. * @return bool Whether the event scheduled is too frequent. */ function is_too_frequent( stdClass $event ) { $schedules = Schedule\get(); if ( ! isset( $schedules[ $event->schedule ] ) ) { return false; } return $schedules[ $event->schedule ]['is_too_frequent']; } /** * Determines whether an event is late. * * An event which has missed its schedule by more than 10 minutes is considered late. * * @param stdClass $event The event. * @return bool Whether the event is late. */ function is_late( stdClass $event ) { $until = $event->time - time(); return ( $until < ( 0 - ( 10 * MINUTE_IN_SECONDS ) ) ); } /** * Initialises and returns the list table for events. * * @return Table The list table. */ function get_list_table() { static $table = null; if ( ! $table ) { require_once __DIR__ . '/event-list-table.php'; $table = new Table(); $table->prepare_items(); } return $table; } /** * Order events function. * * The comparison function returns an integer less than, equal to, or greater than zero if the first argument is * considered to be respectively less than, equal to, or greater than the second. * * @param object $a The first event to compare. * @param object $b The second event to compare. * @return int */ function uasort_order_events( $a, $b ) { $orderby = ( ! empty( $_GET['orderby'] ) ) ? sanitize_text_field( $_GET['orderby'] ) : 'crontrol_next'; $order = ( ! empty( $_GET['order'] ) ) ? sanitize_text_field( $_GET['order'] ) : 'desc'; switch ( $orderby ) { case 'crontrol_hook': if ( 'desc' === $order ) { return strcmp( $a->hook, $b->hook ); } else { return strcmp( $b->hook, $a->hook ); } break; default: if ( $a->time === $b->time ) { return 0; } else { if ( 'desc' === $order ) { return ( $a->time > $b->time ) ? 1 : -1; } else { return ( $a->time < $b->time ) ? 1 : -1; } } break; } }