From 4d310cae7f1a664bd86b580074e9edb98cf6b5ae Mon Sep 17 00:00:00 2001
From: jrfnl <github_nospam@adviesenzo.nl>
Date: Tue, 8 Dec 2015 15:01:10 +0100
Subject: [PATCH] Fix issue #34913 "Unscheduling cron jobs fails when original
 arguments were not an array."

---
 src/wp-includes/cron.php     | 76 ++++++++++++++++++++++-------------
 tests/phpunit/tests/cron.php | 95 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 144 insertions(+), 27 deletions(-)

diff --git a/src/wp-includes/cron.php b/src/wp-includes/cron.php
index 60492c5..8dbc2b1 100644
--- a/src/wp-includes/cron.php
+++ b/src/wp-includes/cron.php
@@ -47,6 +47,7 @@ function wp_schedule_single_event( $timestamp, $hook, $args = array()) {
 	if ( ! $event )
 		return false;
 
+	$event->args = _cron_cast_to_array_helper( $event->args );
 	$key = md5(serialize($event->args));
 
 	$crons[$event->timestamp][$event->hook][$key] = array( 'schedule' => $event->schedule, 'args' => $event->args );
@@ -94,6 +95,7 @@ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array()) {
 	if ( ! $event )
 		return false;
 
+	$event->args = _cron_cast_to_array_helper( $event->args );
 	$key = md5(serialize($event->args));
 
 	$crons[$event->timestamp][$event->hook][$key] = array( 'schedule' => $event->schedule, 'args' => $event->args, 'interval' => $event->interval );
@@ -120,7 +122,6 @@ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array() )
 
 	$crons = _get_cron_array();
 	$schedules = wp_get_schedules();
-	$key = md5( serialize( $args ) );
 	$interval = 0;
 
 	// First we try to get it from the schedule
@@ -129,6 +130,7 @@ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array() )
 	}
 	// Now we try to get it from the saved interval in case the schedule disappears
 	if ( 0 == $interval ) {
+		$key = md5( serialize( _cron_cast_to_array_helper( $args ) ) );
 		$interval = $crons[ $timestamp ][ $hook ][ $key ]['interval'];
 	}
 	// Now we assume something is wrong and fail to schedule
@@ -170,12 +172,14 @@ function wp_unschedule_event( $timestamp, $hook, $args = array() ) {
 	}
 
 	$crons = _get_cron_array();
-	$key = md5(serialize($args));
-	unset( $crons[$timestamp][$hook][$key] );
-	if ( empty($crons[$timestamp][$hook]) )
-		unset( $crons[$timestamp][$hook] );
-	if ( empty($crons[$timestamp]) )
-		unset( $crons[$timestamp] );
+	$key = md5( serialize( _cron_cast_to_array_helper( $args ) ) );
+	unset( $crons[ $timestamp ][ $hook ][ $key ] );
+	if ( empty( $crons[ $timestamp ][ $hook ] ) ) {
+		unset( $crons[ $timestamp ][ $hook ] );
+	}
+	if ( empty( $crons[ $timestamp ] ) ) {
+		unset( $crons[ $timestamp ] );
+	}
 	_set_cron_array( $crons );
 }
 
@@ -221,12 +225,15 @@ function wp_clear_scheduled_hook( $hook, $args = array() ) {
  */
 function wp_next_scheduled( $hook, $args = array() ) {
 	$crons = _get_cron_array();
-	$key = md5(serialize($args));
-	if ( empty($crons) )
+	if ( empty( $crons ) ) {
 		return false;
+	}
+
+	$key = md5( serialize( _cron_cast_to_array_helper( $args ) ) );
 	foreach ( $crons as $timestamp => $cron ) {
-		if ( isset( $cron[$hook][$key] ) )
+		if ( isset( $cron[ $hook ][ $key ] ) ) {
 			return $timestamp;
+		}
 	}
 	return false;
 }
@@ -412,12 +419,15 @@ function wp_get_schedules() {
  */
 function wp_get_schedule($hook, $args = array()) {
 	$crons = _get_cron_array();
-	$key = md5(serialize($args));
-	if ( empty($crons) )
+	if ( empty( $crons ) ) {
 		return false;
+	}
+
+	$key = md5( serialize( _cron_cast_to_array_helper( $args ) ) );
 	foreach ( $crons as $timestamp => $cron ) {
-		if ( isset( $cron[$hook][$key] ) )
-			return $cron[$hook][$key]['schedule'];
+		if ( isset( $cron[ $hook ][ $key ] ) ) {
+			return $cron[ $hook ][ $key ]['schedule'];
+		}
 	}
 	return false;
 }
@@ -435,14 +445,16 @@ function wp_get_schedule($hook, $args = array()) {
  * @return false|array CRON info array.
  */
 function _get_cron_array()  {
-	$cron = get_option('cron');
-	if ( ! is_array($cron) )
+	$cron = get_option( 'cron' );
+	if ( ! is_array( $cron ) ) {
 		return false;
+	}
 
-	if ( !isset($cron['version']) )
-		$cron = _upgrade_cron_array($cron);
+	if ( ! isset( $cron['version'] ) || 3 > $cron['version'] ) {
+		$cron = _upgrade_cron_array( $cron );
+	}
 
-	unset($cron['version']);
+	unset( $cron['version'] );
 
 	return $cron;
 }
@@ -456,14 +468,14 @@ function _get_cron_array()  {
  * @param array $cron Cron info array from {@link _get_cron_array()}.
  */
 function _set_cron_array($cron) {
-	$cron['version'] = 2;
+	$cron['version'] = 3;
 	update_option( 'cron', $cron );
 }
 
 /**
  * Upgrade a Cron info array.
  *
- * This function upgrades the Cron info array to version 2.
+ * This function upgrades the Cron info array to version 3.
  *
  * @since 2.1.0
  * @access private
@@ -472,19 +484,29 @@ function _set_cron_array($cron) {
  * @return array An upgraded Cron info array.
  */
 function _upgrade_cron_array($cron) {
-	if ( isset($cron['version']) && 2 == $cron['version'])
+	if ( isset( $cron['version'] ) && 3 === $cron['version'] ) {
 		return $cron;
+	}
 
 	$new_cron = array();
 
-	foreach ( (array) $cron as $timestamp => $hooks) {
-		foreach ( (array) $hooks as $hook => $args ) {
-			$key = md5(serialize($args['args']));
-			$new_cron[$timestamp][$hook][$key] = $args;
+	foreach ( (array) $cron as $timestamp => $hooks ) {
+		foreach ( (array) $hooks as $hook => $event ) {
+			foreach( (array) $event as $args ) {
+				$key = md5( serialize( _cron_cast_to_array_helper( $args['args'] ) ) );
+				$new_cron[ $timestamp ][ $hook ][ $key ] = $args;
+			}
 		}
 	}
 
-	$new_cron['version'] = 2;
+	$new_cron['version'] = 3;
 	update_option( 'cron', $new_cron );
 	return $new_cron;
 }
+
+function _cron_cast_to_array_helper( $args ) {
+	if ( is_object( $args ) ) {
+		return array( $args );
+	}
+	return (array) $args;
+}
\ No newline at end of file
diff --git a/tests/phpunit/tests/cron.php b/tests/phpunit/tests/cron.php
index b411a5b..560182c 100644
--- a/tests/phpunit/tests/cron.php
+++ b/tests/phpunit/tests/cron.php
@@ -189,6 +189,88 @@ class Tests_Cron extends WP_UnitTestCase {
 
 	}
 
+
+	/**
+	 * @ticket 34913
+	 *
+	 * @expectedDeprecated wp_clear_scheduled_hook
+	 */
+	function test_clear_schedule_non_array_args() {
+		$hook = rand_str();
+		$bool_arg = true;
+		$int_arg = mt_rand();
+		$float_arg = mt_rand() / mt_getrandmax();
+		$string_arg = rand_str();
+		$obj_arg = new cronTestClass();
+
+		// Schedule several events with non-array arguments.
+		wp_schedule_single_event( strtotime('+1 hour'), $hook, $bool_arg );
+		wp_schedule_single_event( strtotime('+2 hour'), $hook, $int_arg );
+		wp_schedule_single_event( strtotime('+3 hour'), $hook, $float_arg );
+		wp_schedule_single_event( strtotime('+4 hour'), $hook, $string_arg );
+		wp_schedule_single_event( strtotime('+5 hour'), $hook, $obj_arg );
+
+		// Make sure they're returned by wp_next_scheduled().
+		$this->assertTrue( wp_next_scheduled($hook, $bool_arg) > 0 );
+		$this->assertTrue( wp_next_scheduled($hook, $int_arg) > 0 );
+		$this->assertTrue( wp_next_scheduled($hook, $float_arg) > 0 );
+		$this->assertTrue( wp_next_scheduled($hook, $string_arg) > 0 );
+		$this->assertTrue( wp_next_scheduled($hook, $obj_arg) > 0 );
+
+		// Clear the schedule for the bool_arg event and make sure it's gone.
+		wp_clear_scheduled_hook($hook, $bool_arg );
+		$this->assertFalse( wp_next_scheduled($hook, $bool_arg) );
+
+		// Clear the schedule for the int_arg event and make sure it's gone.
+		wp_clear_scheduled_hook($hook, $int_arg );
+		$this->assertFalse( wp_next_scheduled($hook, $int_arg) );
+
+		// Clear the schedule for the float_arg event and make sure it's gone.
+		wp_clear_scheduled_hook($hook, $float_arg );
+		$this->assertFalse( wp_next_scheduled($hook, $float_arg) );
+
+		// Clear the schedule for the string_arg event and make sure it's gone.
+		wp_clear_scheduled_hook($hook, $string_arg );
+		$this->assertFalse( wp_next_scheduled($hook, $string_arg) );
+
+		// Clear the schedule for the object_arg event and make sure it's gone.
+		wp_clear_scheduled_hook($hook, $obj_arg );
+		$this->assertFalse( wp_next_scheduled($hook, $obj_arg) );
+
+	}
+
+	/**
+	 * @ticket 34913
+	 *
+	 * @internal Separate test for resources as this one can easily fail and we don't want to skip the
+	 * complete group of tests if it does.
+	 *
+	 * @expectedDeprecated wp_clear_scheduled_hook
+	 */
+	function test_clear_schedule_resource_arg() {
+		$hook = rand_str();
+		$arg = tmpfile();
+
+		if ( $arg === false ) {
+			$this->markTestSkipped( 'Could not create resource.' );
+			return;
+		}
+
+		// Schedule event with resource argument.
+		wp_schedule_single_event( strtotime('+1 hour'), $hook, $arg );
+
+		// Make sure they're returned by wp_next_scheduled().
+		$this->assertTrue( wp_next_scheduled($hook, $arg) > 0 );
+
+		// Clear the schedule for the event and make sure it's gone.
+		wp_clear_scheduled_hook($hook, $arg );
+		$this->assertFalse( wp_next_scheduled($hook, $arg) );
+		
+		fclose( $arg );
+
+	}
+
+
 	/**
 	 * @ticket 6966
 	 */
@@ -327,3 +409,16 @@ class WPTestCronRunning extends _WPEmptyBlog {
 	}
 }
 */
+
+/**
+ * Test class belonging to the `test_clear_schedule_non_array_arg()` test.
+ */
+class cronTestClass {
+	public $property_a = 'something';
+	public $property_b = 123;
+	private $property_c = false;
+
+	public function some_function() {
+		return true;
+	}
+}
-- 
1.9.4.msysgit.2

