Make WordPress Core

Opened 8 weeks ago

Last modified 4 days ago

#63987 new defect (bug)

wp_get_scheduled_event() incorrectly returns false for cron events scheduled with a timestamp of 0.

Reported by: codekraft's profile codekraft Owned by:
Milestone: 7.0 Priority: normal
Severity: normal Version:
Component: Cron API Keywords: has-patch has-unit-tests has-test-info needs-testing 2nd-opinion
Focuses: Cc:

Description

When using the wp_get_scheduled_event() function to retrieve a scheduled cron event, it incorrectly returns false if the event's timestamp is exactly 0 (the Unix epoch).

wp_get_scheduled_event can be found in wp-includes/cron.php:787

The issue

The issue lies in this line:

<?php
if ( ! $timestamp ) {

In PHP, the integer 0 is evaluated as false in a boolean context. This causes the function to enter the "get next event" block, which then fails to find the event at timestamp 0 and ultimately returns false, despite the event existing in the cron array.

How to Reproduce
Schedule a cron event with a timestamp of 0. You can do this by manually adding it to the cron array or by using a function like this:

<?php
// Add an event with a timestamp of 0.
// This is for demonstration purposes. In a real scenario, this might happen due to a bug in a plugin.
$crons = _get_cron_array();
$crons[0]['my_test_hook']['unique_key'] = array(
    'schedule' => false,
    'args'     => array(),
);
_set_cron_array($crons);

Attempt to retrieve this specific event using wp_get_scheduled_event() with the timestamp 0:

<?php
$event = wp_get_scheduled_event( 'my_test_hook', array(), 0 );

Check the result.

<?php
if ( false === $event ) {
    echo "The function returned false. This is the bug.";
} else {
    echo "The function correctly found the event.";
}

Expected behavior: The function should return the event object, as the event with a timestamp of 0 exists.

Current behavior: The function returns false, indicating the event does not exist. So i cannot remove that event from the cron array using wp_unschedule_event

Suggested Fix

A possible fix is to change the if condition to be more explicit, for example:

<?php
if ( null === $timestamp ) {

This would only trigger the "get next event" logic when no timestamp is provided, correctly handling a timestamp of 0 as a valid value.

Change History (12)

#1 @johnbillion
8 weeks ago

  • Keywords needs-patch needs-unit-tests added
  • Version trunk deleted

Thanks for the report @codekraft !

This ticket was mentioned in PR #9914 on WordPress/wordpress-develop by @rishabhwp.


8 weeks ago
#2

  • Keywords has-patch has-unit-tests added; needs-patch needs-unit-tests removed

#3 @SergeyBiryukov
8 weeks ago

  • Milestone changed from Awaiting Review to 6.9

#4 @rollybueno
7 weeks ago

  • Keywords dev-feedback changes-requested added

Hey @rishabhwp,

I see that you have this code in wp_unschedule_event but not in wp_schedule_single_event?:

if ( ! is_numeric( $timestamp ) || $timestamp < 0 ) {

When in fact both function shares same validation?

#5 @rollybueno
7 weeks ago

  • Keywords has-test-info added

Reproduction Report

Description

This report validates whether the issue can be reproduced.

The attached test plugin (Ticket 63987) injects two cron events for the same hook my_test_hook:

  • One at timestamp 0 (bug case).
  • One at a non-zero future timestamp (control case).

When calling wp_get_scheduled_event() with timestamp 0, the function incorrectly returns false.

Environment

  • WordPress: 6.9-alpha-60093-src
  • PHP: 8.2.29
  • Server: nginx/1.29.1
  • Database: mysqli (Server: 8.4.6 / Client: mysqlnd 8.2.29)
  • Browser: Chrome 139.0.0.0
  • OS: Linux
  • Theme: Twenty Twenty-Five 1.3
  • MU Plugins: None activated
  • Plugins:
    • Test Reports 1.2.0
    • Ticket 63987 1.0

Steps to Reproduce

  1. Activate the Ticket 63987 plugin.
  2. Visit any front-end page with ?test-cron=1 appended to the URL.
  3. The plugin injects two events and dumps the results of wp_get_scheduled_event().

Actual Results

  • For timestamp 0:
bool(false)
  • For the future timestamp:
object(stdClass)#311 (4) {
  ["hook"]=>
  string(12) "my_test_hook"
  ["timestamp"]=>
  int(1758456619)
  ["schedule"]=>
  bool(false)
  ["args"]=>
  array(0) {
  }
}

✅ Error condition occurs (reproduced).

Expected Results

  • For timestamp 0, the event should be returned in the same format as the non-zero timestamp event. It should be:
  object(stdClass)[312]
  public 'hook' => string 'my_test_hook' (length=12)
  public 'timestamp' => int 0
  public 'schedule' => boolean false
  public 'args' => 
    array (size=0)

  • Returning false is incorrect since the event exists in the cron array.

Additional Notes

  • This confirms that the falsy check for $timestamp in wp_get_scheduled_event() causes timestamp 0 to be treated as "no timestamp."
  • Non-zero timestamps behave as expected.

Supplemental Artifacts

Inline test plugin code for reproduction:

<?php
/**
 * Plugin Name: Ticket 63987
 * Description: Demonstrates the bug in wp_get_scheduled_event() when timestamp is 0.
 * Version: 1.0
 * Author: Rolly Bueno
 */

add_action( 'init', 'test_cron_zero_timestamp' );

function test_cron_zero_timestamp() {
    if ( ! isset( $_GET['test-cron'] ) ) {
        return;
    }

    // 1. Inject a cron event at timestamp 0 (bug case).
    $crons = _get_cron_array();
    $args  = array();
    $key   = md5( serialize( $args ) );
    $crons[0]['my_test_hook'][ $key ] = array(
        'schedule' => false,
        'args'     => $args,
    );

    // 2. Inject a cron event at a non-zero timestamp (valid case).
    $future = time() + 3600; // 1 hour from now
    $crons[ $future ]['my_test_hook'][ $key ] = array(
        'schedule' => false,
        'args'     => $args,
    );

    // Save back into cron array.
    _set_cron_array( $crons );

    // 3. Try fetching both.
    $event_zero   = wp_get_scheduled_event( 'my_test_hook', array(), 0 );
    $event_future = wp_get_scheduled_event( 'my_test_hook', array(), $future );

    // 4. Output results.
    echo '<pre>';
    echo "Testing wp_get_scheduled_event()\n\n";

    echo "At timestamp 0:\n";
    var_dump( $event_zero );

    echo "\nAt timestamp {$future}:\n";
    var_dump( $event_future );

    echo '</pre>';

    exit;
}

Actual result:
https://i.imgur.com/3nnCxiy.png

(Correct result) If https://github.com/WordPress/WordPress/blob/master/wp-includes/cron.php#L787 updated into if ( null === $timestamp ) {:
https://i.imgur.com/kBZYGx9.png

Last edited 7 weeks ago by rollybueno (previous) (diff)

#6 @rollybueno
7 weeks ago

I left a review feedback on https://github.com/WordPress/wordpress-develop/pull/9914, which needs an update before putting this to needs-testing

#7 @rollybueno
7 weeks ago

  • Keywords needs-testing added; dev-feedback changes-requested removed

Changes set - adding needs-testing

#8 @mindctrl
5 weeks ago

  • Keywords 2nd-opinion added

It appears this behavior is intentional, since the helper functions for scheduling events return false and/or a WP_Error object when a timestamp of 0 is provided. (wp_schedule_event, wp_schedule_single_event, wp_reschedule_event, wp_unschedule_event). / Example

If using wp_schedule_event() or related, this bug doesn't really exist because the 0 timestamp is considered invalid and it won't be added to the cron array.

0 is a valid timestamp, but is there a use case for scheduling events at 0? What would be the purpose of scheduling at 0, instead of say now() or even 1?

In the original ticket, the example uses _set_cron_array(), which is marked as private in the phpdocs, like many/most functions in WP that begin with the _ character.

This ticket was mentioned in Slack in #core-test by jon_bossenger. View the logs.


12 days ago

This ticket was mentioned in Slack in #core-test by krupajnanda. View the logs.


5 days ago

#11 @krupajnanda
5 days ago

Given the remaining work and the limited time in the 6.9 cycle, moving this to 7.0 will give contributors enough time to properly test and validate the fix.

#12 @wildworks
4 days ago

  • Milestone changed from 6.9 to 7.0
Note: See TracTickets for help on using tickets.