diff --git src/wp-includes/cron.php src/wp-includes/cron.php index fbf5a629af..7a147433dd 100644 --- src/wp-includes/cron.php +++ src/wp-includes/cron.php @@ -26,7 +26,7 @@ * @param int $timestamp Unix timestamp (UTC) for when to next run the event. * @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 false|void False if the event did not get scheduled. + * @return bool Whether or not the requested event has been scheduled. */ function wp_schedule_single_event( $timestamp, $hook, $args = array() ) { // Make sure timestamp is a positive integer @@ -34,13 +34,6 @@ function wp_schedule_single_event( $timestamp, $hook, $args = array() ) { return false; } - // Don't schedule a duplicate if there's already an identical event due within 10 minutes of it - $next = wp_next_scheduled( $hook, $args ); - if ( $next && abs( $next - $timestamp ) <= 10 * MINUTE_IN_SECONDS ) { - return false; - } - - $crons = _get_cron_array(); $event = (object) array( 'hook' => $hook, 'timestamp' => $timestamp, @@ -48,6 +41,45 @@ function wp_schedule_single_event( $timestamp, $hook, $args = array() ) { 'args' => $args, ); + /** + * Filter to preflight or hijack scheduling an event. + * + * Passing a non-null value will short-circuit adding the event to the cron + * array, returning the passed value instead. + * + * Both single events and recurring events are passed through this filter; + * single events have `$event->schedule` as false, whereas recurring events + * have this set to a recurrence from {@see wp_get_schedules}. Recurring + * events also have the integer recurrence interval set as `$event->interval`. + * + * Before scheduling, it is recommended you check for an identical event + * within ten minutes and apply the {@see schedule_event} filter to check + * if another plugin has disallowed the event. + * + * Pass true if the event was successfully scheduled, false if not. + * + * @param null|bool $pre Value to return instead. Default null to continue adding the event. + * @param stdClass $event { + * An object containing an 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. + * } + */ + $pre = apply_filters( 'pre_schedule_event', null, $event ); + if ( null !== $pre ) { + return $pre; + } + + // Don't schedule a duplicate if there's already an identical event due within 10 minutes of it + $next = wp_next_scheduled( $hook, $args ); + if ( $next && abs( $next - $timestamp ) <= 10 * MINUTE_IN_SECONDS ) { + return false; + } + /** * Filters a single event before it is scheduled. * @@ -72,12 +104,13 @@ function wp_schedule_single_event( $timestamp, $hook, $args = array() ) { $key = md5( serialize( $event->args ) ); + $crons = _get_cron_array(); $crons[ $event->timestamp ][ $event->hook ][ $key ] = array( 'schedule' => $event->schedule, 'args' => $event->args, ); uksort( $crons, 'strnatcasecmp' ); - _set_cron_array( $crons ); + return _set_cron_array( $crons ); } /** @@ -105,7 +138,7 @@ function wp_schedule_single_event( $timestamp, $hook, $args = array() ) { * @param string $recurrence How often the event should subsequently recur. See wp_get_schedules() for accepted values. * @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 false|void False if the event did not get scheduled. + * @return bool Whether or not the requested event has been scheduled. */ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) { // Make sure timestamp is a positive integer @@ -113,7 +146,6 @@ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) { return false; } - $crons = _get_cron_array(); $schedules = wp_get_schedules(); if ( ! isset( $schedules[ $recurrence ] ) ) { @@ -127,6 +159,13 @@ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) { 'args' => $args, 'interval' => $schedules[ $recurrence ]['interval'], ); + + /** This filter is documented in wp-includes/cron.php */ + $pre = apply_filters( 'pre_schedule_event', null, $event ); + if ( null !== $pre ) { + return $pre; + } + /** This filter is documented in wp-includes/cron.php */ $event = apply_filters( 'schedule_event', $event ); @@ -137,13 +176,14 @@ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) { $key = md5( serialize( $event->args ) ); + $crons = _get_cron_array(); $crons[ $event->timestamp ][ $event->hook ][ $key ] = array( 'schedule' => $event->schedule, 'args' => $event->args, 'interval' => $event->interval, ); uksort( $crons, 'strnatcasecmp' ); - _set_cron_array( $crons ); + return _set_cron_array( $crons ); } /** @@ -155,7 +195,7 @@ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array() ) { * @param string $recurrence How often the event should subsequently recur. See wp_get_schedules() for accepted values. * @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 false|void False if the event did not get rescheduled. + * @return bool Whether or not the requested event has been rescheduled. */ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array() ) { // Make sure timestamp is a positive integer @@ -163,19 +203,54 @@ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array() ) return false; } - $crons = _get_cron_array(); $schedules = wp_get_schedules(); - $key = md5( serialize( $args ) ); $interval = 0; - // First we try to get it from the schedule + // First we try to get the interval from the schedule. if ( isset( $schedules[ $recurrence ] ) ) { $interval = $schedules[ $recurrence ]['interval']; } - // Now we try to get it from the saved interval in case the schedule disappears - if ( 0 == $interval ) { - $interval = $crons[ $timestamp ][ $hook ][ $key ]['interval']; + + // Now we try to get it from the saved interval in case the schedule disappears. + if ( 0 === $interval ) { + $scheduled_event = wp_get_scheduled_event( $hook, $args, $timestamp ); + if ( $scheduled_event && isset( $scheduled_event->interval ) ) { + $interval = $scheduled_event->interval; + } } + + $event = (object) array( + 'hook' => $hook, + 'timestamp' => $timestamp, + 'schedule' => $recurrence, + 'args' => $args, + 'interval' => $interval, + ); + + /** + * Filter to preflight or hijack rescheduling of events. + * + * Passing a non-null value will short-circuit the normal rescheduling + * process, returning the passed value instead. + * + * Pass true if the event was successfully scheduled, false if not. + * + * @param null|bool $pre Value to return instead. Default null to continue adding the event. + * @param stdClass $event { + * An object containing an 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. + * } + */ + $pre = apply_filters( 'pre_reschedule_event', null, $event ); + if ( null !== $pre ) { + return $pre; + } + // Now we assume something is wrong and fail to schedule if ( 0 == $interval ) { return false; @@ -189,7 +264,7 @@ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array() ) $timestamp = $now + ( $interval - ( ( $now - $timestamp ) % $interval ) ); } - wp_schedule_event( $timestamp, $recurrence, $hook, $args ); + return wp_schedule_event( $timestamp, $recurrence, $hook, $args ); } /** @@ -205,7 +280,7 @@ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array() ) * @param array $args Optional. Array containing each separate argument to pass to the hook's callback function. * Although not passed to a callback, these arguments are used to uniquely identify the * event, so they should be the same as those used when originally scheduling the event. - * @return false|void False if the event did not get unscheduled. + * @return bool Whether or not the requested event has been unscheduled. */ function wp_unschedule_event( $timestamp, $hook, $args = array() ) { // Make sure timestamp is a positive integer @@ -213,6 +288,24 @@ function wp_unschedule_event( $timestamp, $hook, $args = array() ) { return false; } + /** + * Filter to preflight or hijack unscheduling of events. + * + * Passing a non-null value will short-circuit the normal unscheduling + * process, returning the passed value instead. + * + * Pass true if the event was successfully unscheduled, false if not. + * + * @param null|bool $pre Value to return instead. Default null to continue unscheduling the event. + * @param int $timestamp Timestamp for when to run the event. + * @param string $hook Action hook, the execution of which will be unscheduled. + * @param array $args Arguments to pass to the hook's callback function. + */ + $pre = apply_filters( 'pre_unschedule_event', null, $timestamp, $hook, $args ); + if ( null !== $pre ) { + return $pre; + } + $crons = _get_cron_array(); $key = md5( serialize( $args ) ); unset( $crons[ $timestamp ][ $hook ][ $key ] ); @@ -222,7 +315,7 @@ function wp_unschedule_event( $timestamp, $hook, $args = array() ) { if ( empty( $crons[ $timestamp ] ) ) { unset( $crons[ $timestamp ] ); } - _set_cron_array( $crons ); + return _set_cron_array( $crons ); } /** @@ -232,6 +325,7 @@ function wp_unschedule_event( $timestamp, $hook, $args = array() ) { * * @param string $hook Action hook, the execution of which will be unscheduled. * @param array $args Optional. Arguments that were to be passed to the hook's callback function. + * @return array Boolean values, indicating the result of attempting to unschedule each indicated event, with timestamps as keys. */ function wp_clear_scheduled_hook( $hook, $args = array() ) { // Backward compatibility @@ -241,20 +335,40 @@ function wp_clear_scheduled_hook( $hook, $args = array() ) { $args = array_slice( func_get_args(), 1 ); } + /** + * Filter to preflight or hijack clearing a scheduled hook. + * + * Passing a non-null value will short-circuit the normal unscheduling + * process, returning the passed value instead. + * + * Pass an empty array if no events are unscheduled, an array of boolean + * values if events are unscheduled see {@see wp_clear_scheduled_hook}. + * + * @param null|array $pre Value to return instead. Default null to continue unscheduling the event. + * @param string $hook Action hook, the execution of which will be unscheduled. + * @param array $args Arguments to pass to the hook's callback function. + */ + $pre = apply_filters( 'pre_clear_scheduled_hook', null, $hook, $args ); + if ( null !== $pre ) { + return $pre; + } + // This logic duplicates wp_next_scheduled() // It's required due to a scenario where wp_unschedule_event() fails due to update_option() failing, // and, wp_next_scheduled() returns the same schedule in an infinite loop. $crons = _get_cron_array(); if ( empty( $crons ) ) { - return; + return array(); } - $key = md5( serialize( $args ) ); + $results = array(); + $key = md5( serialize( $args ) ); foreach ( $crons as $timestamp => $cron ) { if ( isset( $cron[ $hook ][ $key ] ) ) { - wp_unschedule_event( $timestamp, $hook, $args ); + $results[ $timestamp ] = wp_unschedule_event( $timestamp, $hook, $args ); } } + return $results; } /** @@ -265,8 +379,25 @@ function wp_clear_scheduled_hook( $hook, $args = array() ) { * @since 4.9.0 * * @param string $hook Action hook, the execution of which will be unscheduled. + * @return bool Whether or not the requested events have been unscheduled. */ function wp_unschedule_hook( $hook ) { + /** + * Filter to preflight or hijack clearing all events attached to the hook. + * + * Passing a non-null value will short-circuit the normal unscheduling + * process, returning the passed value instead. + * + * Pass true if events are cleared, false if not. + * + * @param null|array $pre Value to return instead. Default null to continue unscheduling the hook. + * @param string $hook Action hook, the execution of which will be unscheduled. + */ + $pre = apply_filters( 'pre_unschedule_hook', null, $hook ); + if ( null !== $pre ) { + return $pre; + } + $crons = _get_cron_array(); foreach ( $crons as $timestamp => $args ) { @@ -277,7 +408,72 @@ function wp_unschedule_hook( $hook ) { } } - _set_cron_array( $crons ); + return _set_cron_array( $crons ); +} + +/** + * Retrieve a scheduled event. + * + * Retrieve the full event object for a given event. + * + * @param string $hook Action hook of the event. + * @param array $args Optional. Array containing each separate argument to pass to the hook's callback function. + * Although not passed to a callback, these arguments are used to uniquely identify the + * event, so they should be the same as those used when originally scheduling the event. + * @param int|null $timestamp Optional. Unix timestamp (UTC) of the event. If not specified, the next scheduled event is returned. + * @return bool|object The event object. False if the event does not exist. + */ +function wp_get_scheduled_event( $hook, $args = array(), $timestamp = null ) { + if ( ! $timestamp ) { + // Get the next scheduled event. + $timestamp = wp_next_scheduled( $hook, $args ); + } + + /** + * Filter to preflight or hijack retrieving a scheduled event. + * + * Passing a non-null value will short-circuit the normal + * process, returning the passed value instead. + * + * Pass false if the event does not exist, otherwise an event object + * should be returned. + * + * @param null|bool $pre Value to return instead. Default null to continue retrieving the event. + * @param string $hook Action hook of the event. + * @param array $args Array containing each separate argument to pass to the hook's callback function. + * Although not passed to a callback, these arguments are used to uniquely identify the + * event. + * @param int $timestamp Unix timestamp (UTC) of the event. + */ + $pre = apply_filters( 'pre_get_scheduled_event', null, $hook, $args, $timestamp ); + if ( null !== $pre ) { + return $pre; + } + + $crons = _get_cron_array(); + $key = md5( serialize( $args ) ); + + if ( ! $timestamp || ! isset( $crons[ $timestamp ] ) ) { + // No such event. + return false; + } + + if ( ! isset( $crons[ $timestamp ][ $hook ] ) || ! isset( $crons[ $timestamp ][ $hook ][ $key ] ) ) { + return false; + } + + $event = (object) array( + 'hook' => $hook, + 'timestamp' => $timestamp, + 'schedule' => $crons[ $timestamp ][ $hook ][ $key ]['schedule'], + 'args' => $args, + ); + + if ( isset( $crons[ $timestamp ][ $hook ][ $key ]['interval'] ) ) { + $event->interval = $crons[ $timestamp ][ $hook ][ $key ]['interval']; + } + + return $event; } /** @@ -292,17 +488,44 @@ function wp_unschedule_hook( $hook ) { * @return false|int The Unix timestamp of the next time the event will occur. False if the event doesn't exist. */ function wp_next_scheduled( $hook, $args = array() ) { + /** + * Filter to preflight or hijack retrieving the next scheduled event timestamp. + * + * Passing a non-null value will short-circuit the normal retrieval + * process, returning the passed value instead. + * + * Pass the timestamp of the next event if it exists, false if not. + * + * @param null|bool $pre Value to return instead. Default null to continue unscheduling the event. + * @param string $hook Action hook of the event. + * @param array $args Arguments to pass to the hook's callback function. + */ + $pre = apply_filters( 'pre_next_scheduled', null, $hook, $args ); + if ( null !== $pre ) { + return $pre; + } + $crons = _get_cron_array(); $key = md5( serialize( $args ) ); - if ( empty( $crons ) ) { - return false; - } - foreach ( $crons as $timestamp => $cron ) { - if ( isset( $cron[ $hook ][ $key ] ) ) { - return $timestamp; + $next = false; + + if ( ! empty( $crons ) ) { + foreach ( $crons as $timestamp => $cron ) { + if ( isset( $cron[ $hook ][ $key ] ) ) { + $next = $timestamp; + break; + } } } - return false; + + /** + * Filter the next scheduled event timestamp. + * + * @param int|bool $next The UNIX timestamp when the scheduled event will next occur, or false if not found. + * @param string $hook Action hook to execute when cron is run. + * @param array $args Arguments to be passed to the callback function. Used for deduplicating events. + */ + return apply_filters( 'next_scheduled', $next, $hook, $args ); } /** @@ -311,6 +534,8 @@ function wp_next_scheduled( $hook, $args = array() ) { * @since 2.1.0 * * @param int $gmt_time Optional. Unix timestamp (UTC). Default 0 (current time is used). + * @return null|WP_Error|array Null when cron could not be spawned, because it is not needed to run. + * When cron runs, return the result of {@see wp_remote_post} */ function spawn_cron( $gmt_time = 0 ) { if ( ! $gmt_time ) { @@ -318,7 +543,7 @@ function spawn_cron( $gmt_time = 0 ) { } if ( defined( 'DOING_CRON' ) || isset( $_GET['doing_wp_cron'] ) ) { - return; + return null; } /* @@ -336,23 +561,23 @@ function spawn_cron( $gmt_time = 0 ) { // don't run if another process is currently running it or more than once every 60 sec. if ( $lock + WP_CRON_LOCK_TIMEOUT > $gmt_time ) { - return; + return null; } //sanity check $crons = _get_cron_array(); if ( ! is_array( $crons ) ) { - return; + return null; } $keys = array_keys( $crons ); if ( isset( $keys[0] ) && $keys[0] > $gmt_time ) { - return; + return null; } if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) { if ( 'GET' !== $_SERVER['REQUEST_METHOD'] || defined( 'DOING_AJAX' ) || defined( 'XMLRPC_REQUEST' ) ) { - return; + return null; } $doing_wp_cron = sprintf( '%.22F', $gmt_time ); @@ -368,7 +593,7 @@ function spawn_cron( $gmt_time = 0 ) { flush(); WP_DEBUG ? include_once( ABSPATH . 'wp-cron.php' ) : @include_once( ABSPATH . 'wp-cron.php' ); - return; + return null; } // Set the cron lock with the current unix timestamp, when the cron is being spawned. @@ -409,30 +634,34 @@ function spawn_cron( $gmt_time = 0 ) { ), $doing_wp_cron ); - wp_remote_post( $cron_request['url'], $cron_request['args'] ); + return wp_remote_post( $cron_request['url'], $cron_request['args'] ); } /** * Run scheduled callbacks or spawn cron for all scheduled events. * * @since 2.1.0 + * + * @return array Array of spawn_cron() results for any cron jobs that were run, with cron timestamps as keys. */ function wp_cron() { // Prevent infinite loops caused by lack of wp-cron.php if ( strpos( $_SERVER['REQUEST_URI'], '/wp-cron.php' ) !== false || ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) { - return; + return array(); } - if ( false === $crons = _get_cron_array() ) { - return; + $crons = _get_cron_array(); + if ( false === $crons ) { + return array(); } $gmt_time = microtime( true ); $keys = array_keys( $crons ); if ( isset( $keys[0] ) && $keys[0] > $gmt_time ) { - return; + return array(); } + $results = array(); $schedules = wp_get_schedules(); foreach ( $crons as $timestamp => $cronhooks ) { if ( $timestamp > $gmt_time ) { @@ -442,10 +671,12 @@ function wp_cron() { if ( isset( $schedules[ $hook ]['callback'] ) && ! call_user_func( $schedules[ $hook ]['callback'] ) ) { continue; } - spawn_cron( $gmt_time ); + $results[ $timestamp ] = spawn_cron( $gmt_time ); break 2; } } + + return $results; } /** @@ -514,17 +745,21 @@ function wp_get_schedules() { * @return string|false False, if no schedule. Schedule name on success. */ function wp_get_schedule( $hook, $args = array() ) { - $crons = _get_cron_array(); - $key = md5( serialize( $args ) ); - if ( empty( $crons ) ) { - return false; - } - foreach ( $crons as $timestamp => $cron ) { - if ( isset( $cron[ $hook ][ $key ] ) ) { - return $cron[ $hook ][ $key ]['schedule']; - } + $schedule = false; + $event = wp_get_scheduled_event( $hook, $args ); + + if ( $event ) { + $schedule = $event->schedule; } - return false; + + /** + * Filter the schedule for a hook. + * + * @param string|bool $schedule Schedule for the hook. False if not found. + * @param string $hook Action hook to execute when cron is run. + * @param array $args Optional. Arguments to pass to the hook's callback function. + */ + return apply_filters( 'get_schedule', $schedule, $hook, $args ); } // @@ -561,10 +796,11 @@ function _get_cron_array() { * @access private * * @param array $cron Cron info array from _get_cron_array(). + * @return bool Whether the update of the cron option succeeded (according to {@see update_option}) */ function _set_cron_array( $cron ) { $cron['version'] = 2; - update_option( 'cron', $cron ); + return update_option( 'cron', $cron ); } /**