From 23c3861eda801f31bde4efd0040d03aadd499ae4 Mon Sep 17 00:00:00 2001 From: Ricardo Moraleida Date: Mon, 16 Mar 2020 23:42:24 -0300 Subject: [PATCH 1/8] validate meta_value as a proper date before trying to manipulate it. In turn, only store date, datetime, time values if they are meaningful --- includes/classes/Indexable.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index 523d42a4c7..3c7516da66 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -420,22 +420,20 @@ public function prepare_meta_value_types( $meta_value ) { $meta_types['boolean'] = filter_var( $meta_value, FILTER_VALIDATE_BOOLEAN ); - if ( is_string( $meta_value ) ) { - $timestamp = strtotime( $meta_value ); - - $date = '1971-01-01'; - $datetime = '1971-01-01 00:00:01'; - $time = '00:00:01'; + try { + // if this is a recognizable date format + $new_date = new \DateTime( $meta_value, \wp_timezone() ); + $timestamp = $new_date->getTimestamp(); if ( false !== $timestamp ) { - $date = date_i18n( 'Y-m-d', $timestamp ); - $datetime = date_i18n( 'Y-m-d H:i:s', $timestamp ); - $time = date_i18n( 'H:i:s', $timestamp ); + $meta_types['date'] = $new_date->format( 'Y-m-d' ); + $meta_types['datetime'] = $new_date->format( 'Y-m-d H:i:s' ); + $meta_types['time'] = $new_date->format( 'H:i:s' ); } - $meta_types['date'] = $date; - $meta_types['datetime'] = $datetime; - $meta_types['time'] = $time; + } catch (\Exception $e ) { + // if $meta_value is not a recognizable date format, DateTime will throw an exception, + // just catch it and move on. } return $meta_types; From f95b69f83fe6a30a0f4a2b0c509ac8ff4fe04423 Mon Sep 17 00:00:00 2001 From: Ricardo Moraleida Date: Tue, 17 Mar 2020 01:05:36 -0300 Subject: [PATCH 2/8] add/check unit tests --- includes/classes/Indexable.php | 2 +- tests/php/indexables/TestPost.php | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index 3c7516da66..671f33c12e 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -421,7 +421,7 @@ public function prepare_meta_value_types( $meta_value ) { $meta_types['boolean'] = filter_var( $meta_value, FILTER_VALIDATE_BOOLEAN ); try { - // if this is a recognizable date format + // is this is a recognizable date format? $new_date = new \DateTime( $meta_value, \wp_timezone() ); $timestamp = $new_date->getTimestamp(); diff --git a/tests/php/indexables/TestPost.php b/tests/php/indexables/TestPost.php index 2e9f6db6a4..f0ee424cf3 100644 --- a/tests/php/indexables/TestPost.php +++ b/tests/php/indexables/TestPost.php @@ -3125,25 +3125,34 @@ public function filter_ep_prepare_meta_excluded_public_keys( $meta_keys ) { */ public function testMetaValueTypes() { - $intval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 13 ); - $floatval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 13.43 ); - $textval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 'some text' ); - $bool_false_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( false ); - $bool_true_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( true ); - $dateval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( '2015-01-01' ); + $intval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 13 ); + $floatval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 13.43 ); + $textval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 'some text' ); + $float_string = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( '20.000000' ); + $bool_false_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( false ); + $bool_true_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( true ); + $dateval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( '2015-01-01' ); + $recognizable_time = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 'third day of January 2020' ); + $relative_format = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( '+1 year' ); $this->assertTrue( is_array( $intval ) && 5 === count( $intval ) ); $this->assertTrue( is_array( $intval ) && array_key_exists( 'long', $intval ) && 13 === $intval['long'] ); - $this->assertTrue( is_array( $floatval ) && 5 === count( $floatval ) ); + $this->assertTrue( is_array( $floatval ) && 8 === count( $floatval ) ); $this->assertTrue( is_array( $floatval ) && array_key_exists( 'double', $floatval ) && 13.43 === $floatval['double'] ); - $this->assertTrue( is_array( $textval ) && 6 === count( $textval ) ); + $this->assertTrue( is_array( $textval ) && 3 === count( $textval ) ); $this->assertTrue( is_array( $textval ) && array_key_exists( 'raw', $textval ) && 'some text' === $textval['raw'] ); + $this->assertTrue( is_array( $float_string ) && 8 === count( $float_string ) ); + $this->assertTrue( is_array( $float_string ) && array_key_exists( 'raw', $float_string ) && '20.000000' === $float_string['raw'] ); $this->assertTrue( is_array( $bool_false_val ) && 3 === count( $bool_false_val ) ); $this->assertTrue( is_array( $bool_false_val ) && array_key_exists( 'boolean', $bool_false_val ) && false === $bool_false_val['boolean'] ); $this->assertTrue( is_array( $bool_true_val ) && 3 === count( $bool_true_val ) ); $this->assertTrue( is_array( $bool_true_val ) && array_key_exists( 'boolean', $bool_true_val ) && true === $bool_true_val['boolean'] ); $this->assertTrue( is_array( $dateval ) && 6 === count( $dateval ) ); $this->assertTrue( is_array( $dateval ) && array_key_exists( 'datetime', $dateval ) && '2015-01-01 00:00:00' === $dateval['datetime'] ); + $this->assertTrue( is_array( $recognizable_time ) && 6 === count( $recognizable_time ) ); + $this->assertTrue( is_array( $recognizable_time ) && array_key_exists( 'datetime', $recognizable_time ) && '2020-01-03 00:00:00' === $recognizable_time['datetime'] ); + $this->assertTrue( is_array( $relative_format ) && 6 === count( $relative_format ) ); + $this->assertTrue( is_array( $relative_format ) && array_key_exists( 'datetime', $relative_format ) && date( 'Y-m-d H:i:s', strtotime( '+1 year' ) ) === $relative_format['datetime'] ); } From b005bfa5848efb10678f43576c3bc78edfe20077 Mon Sep 17 00:00:00 2001 From: Ricardo Moraleida Date: Tue, 17 Mar 2020 23:40:01 -0300 Subject: [PATCH 3/8] harden checks to avoid year 0000 --- includes/classes/Indexable.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index 671f33c12e..6844900862 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -425,7 +425,10 @@ public function prepare_meta_value_types( $meta_value ) { $new_date = new \DateTime( $meta_value, \wp_timezone() ); $timestamp = $new_date->getTimestamp(); - if ( false !== $timestamp ) { + // PHP allows DateTime to build dates with the non-existing year 0000, and this causes + // issues when integrating into stricter systems. This is by design: + // https://bugs.php.net/bug.php?id=60288 + if ( false !== $timestamp && '0000' !== $new_date->format( 'Y' ) ) { $meta_types['date'] = $new_date->format( 'Y-m-d' ); $meta_types['datetime'] = $new_date->format( 'Y-m-d H:i:s' ); $meta_types['time'] = $new_date->format( 'H:i:s' ); From 028db7f17b2bfc9a7a1f1f92e393f4ca729742e9 Mon Sep 17 00:00:00 2001 From: Ricardo Moraleida Date: Wed, 18 Mar 2020 11:22:15 -0300 Subject: [PATCH 4/8] improve separation of concerns and unit tests --- includes/classes/Indexable.php | 20 ++++++++++++++++++++ tests/php/indexables/TestPost.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index 6844900862..acdd0bb28b 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -420,6 +420,25 @@ public function prepare_meta_value_types( $meta_value ) { $meta_types['boolean'] = filter_var( $meta_value, FILTER_VALIDATE_BOOLEAN ); + $meta_types = $this->prepare_date_meta_values( $meta_types, $meta_value ); + + return $meta_types; + } + + /** + * Checks if a meta_value is a valid date and prepare extra meta-data. + * + * @param array $meta_types Array of currently prepared data + * @param string $meta_value Meta value to prepare. + * + * @return array + */ + public function prepare_date_meta_values( $meta_types, $meta_value ) { + + if ( empty( $meta_value ) || true === $meta_value ) { + return $meta_types; + } + try { // is this is a recognizable date format? $new_date = new \DateTime( $meta_value, \wp_timezone() ); @@ -440,6 +459,7 @@ public function prepare_meta_value_types( $meta_value ) { } return $meta_types; + } /** diff --git a/tests/php/indexables/TestPost.php b/tests/php/indexables/TestPost.php index f0ee424cf3..edc0bc325c 100644 --- a/tests/php/indexables/TestPost.php +++ b/tests/php/indexables/TestPost.php @@ -3156,6 +3156,36 @@ public function testMetaValueTypes() { } + public function testMetaValueTypeDate() { + $meta_types = array(); + + // Invalid dates + $textval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, 'some text' ); + $k20_string = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, '20.000000' ); + $bool_false_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, false ); + $bool_true_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, true ); + + $this->assertEquals( $meta_types, $textval ); + $this->assertEquals( $meta_types, $k20_string ); + $this->assertEquals( $meta_types, $bool_false_val ); + $this->assertEquals( $meta_types, $bool_true_val ); + + // Valid dates + $intval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, time() ); + $floatval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, 13.43 ); + $float_string = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, '20.000001' ); + $dateval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, '2015-01-01' ); + $recognizable_time = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, 'third day of January 2020' ); + $relative_format = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, '+1 year' ); + + $this->assertTrue( isset( $intval['date'] ) && isset( $intval['datetime'] ) && isset( $intval['time'] ) ); + $this->assertTrue( isset( $floatval['date'] ) && isset( $floatval['datetime'] ) && isset( $floatval['time'] ) ); + $this->assertTrue( isset( $float_string['date'] ) && isset( $float_string['datetime'] ) && isset( $float_string['time'] ) ); + $this->assertTrue( isset( $dateval['date'] ) && isset( $dateval['datetime'] ) && isset( $dateval['time'] ) ); + $this->assertTrue( isset( $recognizable_time['date'] ) && isset( $recognizable_time['datetime'] ) && isset( $recognizable_time['time'] ) ); + $this->assertTrue( isset( $relative_format['date'] ) && isset( $relative_format['datetime'] ) && isset( $relative_format['time'] ) ); + } + /** * Test meta key query * From a43c03505c136ba524503e77bd1b04e863df53d6 Mon Sep 17 00:00:00 2001 From: Ricardo Moraleida Date: Wed, 1 Apr 2020 17:07:37 -0300 Subject: [PATCH 5/8] include default dates and string check back --- includes/classes/Indexable.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index acdd0bb28b..4107781af4 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -435,10 +435,14 @@ public function prepare_meta_value_types( $meta_value ) { */ public function prepare_date_meta_values( $meta_types, $meta_value ) { - if ( empty( $meta_value ) || true === $meta_value ) { + if ( empty( $meta_value ) || ! is_string( $meta_value ) ) { return $meta_types; } + $meta_types['date'] = '1971-01-01'; + $meta_types['datetime'] = '1971-01-01 00:00:01'; + $meta_types['time'] = '00:00:01'; + try { // is this is a recognizable date format? $new_date = new \DateTime( $meta_value, \wp_timezone() ); @@ -459,7 +463,6 @@ public function prepare_date_meta_values( $meta_types, $meta_value ) { } return $meta_types; - } /** From 96da23f1eb95175e886869a046a772885f34e8a0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 22 Mar 2021 10:19:01 -0300 Subject: [PATCH 6/8] Update default date to 1970 (start of UNIX epoch) --- includes/classes/Indexable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index 4107781af4..4240c0ed1b 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -439,8 +439,8 @@ public function prepare_date_meta_values( $meta_types, $meta_value ) { return $meta_types; } - $meta_types['date'] = '1971-01-01'; - $meta_types['datetime'] = '1971-01-01 00:00:01'; + $meta_types['date'] = '1970-01-01'; + $meta_types['datetime'] = '1970-01-01 00:00:01'; $meta_types['time'] = '00:00:01'; try { From 56fd5664fcae5a2651866eabe4ba56326a0af2c2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 22 Mar 2021 11:30:03 -0300 Subject: [PATCH 7/8] Update tests --- tests/php/indexables/TestPost.php | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/php/indexables/TestPost.php b/tests/php/indexables/TestPost.php index d3a0dc951f..8d3d046f74 100644 --- a/tests/php/indexables/TestPost.php +++ b/tests/php/indexables/TestPost.php @@ -3177,14 +3177,14 @@ public function testMetaValueTypes() { $bool_false_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( false ); $bool_true_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( true ); $dateval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( '2015-01-01' ); - $recognizable_time = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 'third day of January 2020' ); + $recognizable_time = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( 'third monday of January 2020' ); $relative_format = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta_value_types( '+1 year' ); $this->assertTrue( is_array( $intval ) && 5 === count( $intval ) ); $this->assertTrue( is_array( $intval ) && array_key_exists( 'long', $intval ) && 13 === $intval['long'] ); - $this->assertTrue( is_array( $floatval ) && 8 === count( $floatval ) ); + $this->assertTrue( is_array( $floatval ) && 5 === count( $floatval ) ); $this->assertTrue( is_array( $floatval ) && array_key_exists( 'double', $floatval ) && 13.43 === $floatval['double'] ); - $this->assertTrue( is_array( $textval ) && 3 === count( $textval ) ); + $this->assertTrue( is_array( $textval ) && 6 === count( $textval ) ); $this->assertTrue( is_array( $textval ) && array_key_exists( 'raw', $textval ) && 'some text' === $textval['raw'] ); $this->assertTrue( is_array( $float_string ) && 8 === count( $float_string ) ); $this->assertTrue( is_array( $float_string ) && array_key_exists( 'raw', $float_string ) && '20.000000' === $float_string['raw'] ); @@ -3195,7 +3195,7 @@ public function testMetaValueTypes() { $this->assertTrue( is_array( $dateval ) && 6 === count( $dateval ) ); $this->assertTrue( is_array( $dateval ) && array_key_exists( 'datetime', $dateval ) && '2015-01-01 00:00:00' === $dateval['datetime'] ); $this->assertTrue( is_array( $recognizable_time ) && 6 === count( $recognizable_time ) ); - $this->assertTrue( is_array( $recognizable_time ) && array_key_exists( 'datetime', $recognizable_time ) && '2020-01-03 00:00:00' === $recognizable_time['datetime'] ); + $this->assertTrue( is_array( $recognizable_time ) && array_key_exists( 'datetime', $recognizable_time ) && '2020-01-20 00:00:00' === $recognizable_time['datetime'] ); $this->assertTrue( is_array( $relative_format ) && 6 === count( $relative_format ) ); $this->assertTrue( is_array( $relative_format ) && array_key_exists( 'datetime', $relative_format ) && date( 'Y-m-d H:i:s', strtotime( '+1 year' ) ) === $relative_format['datetime'] ); @@ -3204,16 +3204,22 @@ public function testMetaValueTypes() { public function testMetaValueTypeDate() { $meta_types = array(); + $default_date_time = array( + 'date' => '1970-01-01', + 'datetime' => '1970-01-01 00:00:01', + 'time' => '00:00:01' + ); + // Invalid dates $textval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, 'some text' ); $k20_string = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, '20.000000' ); $bool_false_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, false ); $bool_true_val = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, true ); - $this->assertEquals( $meta_types, $textval ); - $this->assertEquals( $meta_types, $k20_string ); - $this->assertEquals( $meta_types, $bool_false_val ); - $this->assertEquals( $meta_types, $bool_true_val ); + $this->assertEquals( $default_date_time, $textval ); + $this->assertEquals( $default_date_time, $k20_string ); + $this->assertEmpty( $bool_false_val ); + $this->assertEmpty( $bool_true_val ); // Valid dates $intval = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, time() ); @@ -3223,8 +3229,8 @@ public function testMetaValueTypeDate() { $recognizable_time = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, 'third day of January 2020' ); $relative_format = ElasticPress\Indexables::factory()->get( 'post' )->prepare_date_meta_values( $meta_types, '+1 year' ); - $this->assertTrue( isset( $intval['date'] ) && isset( $intval['datetime'] ) && isset( $intval['time'] ) ); - $this->assertTrue( isset( $floatval['date'] ) && isset( $floatval['datetime'] ) && isset( $floatval['time'] ) ); + $this->assertFalse( isset( $intval['date'] ) || isset( $intval['datetime'] ) || isset( $intval['time'] ) ); + $this->assertFalse( isset( $floatval['date'] ) || isset( $floatval['datetime'] ) || isset( $floatval['time'] ) ); $this->assertTrue( isset( $float_string['date'] ) && isset( $float_string['datetime'] ) && isset( $float_string['time'] ) ); $this->assertTrue( isset( $dateval['date'] ) && isset( $dateval['datetime'] ) && isset( $dateval['time'] ) ); $this->assertTrue( isset( $recognizable_time['date'] ) && isset( $recognizable_time['datetime'] ) && isset( $recognizable_time['time'] ) ); From b7d875e520674fac3a51e0e7652def55ec53e550 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 22 Mar 2021 14:06:43 -0300 Subject: [PATCH 8/8] Fix coding standards --- includes/classes/Indexable.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php index 4cc968e5e6..4162d0b494 100644 --- a/includes/classes/Indexable.php +++ b/includes/classes/Indexable.php @@ -477,8 +477,7 @@ public function prepare_date_meta_values( $meta_types, $meta_value ) { $meta_types['datetime'] = $new_date->format( 'Y-m-d H:i:s' ); $meta_types['time'] = $new_date->format( 'H:i:s' ); } - - } catch (\Exception $e ) { + } catch ( \Exception $e ) { // if $meta_value is not a recognizable date format, DateTime will throw an exception, // just catch it and move on. }