Properly convert shorthand date queries in local time to UTC (#40146)
- Allow overriding of precision in `OrdersTableQuery::date_to_date_query_arg() - Shorthand date queries should either date-only or timestamp - Properly convert shorthand date queries from local to UTC for querying - Add utility function to convert local-time args to UTC - Simplify date args processing - Drop no longer necessary params from date_to_date_query_arg() - Dates in orders list table filter should be local time
This commit is contained in:
parent
1b37042d55
commit
fd6da30df2
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Properly convert local time date queries to UTC in the HPOS datastore.
|
|
@ -711,20 +711,15 @@ class ListTable extends WP_List_Table {
|
|||
global $wpdb;
|
||||
|
||||
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
|
||||
$utc_offset = wc_timezone_offset();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$order_dates = $wpdb->get_results(
|
||||
"
|
||||
SELECT DISTINCT YEAR( date_created_gmt ) AS year,
|
||||
MONTH( date_created_gmt ) AS month
|
||||
|
||||
FROM $orders_table
|
||||
|
||||
WHERE status NOT IN (
|
||||
'trash'
|
||||
)
|
||||
|
||||
ORDER BY year DESC, month DESC;
|
||||
SELECT DISTINCT YEAR( t.date_created_local ) AS year,
|
||||
MONTH( t.date_created_local ) AS month
|
||||
FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE status != 'trash' ) t
|
||||
ORDER BY year DESC, month DESC
|
||||
"
|
||||
);
|
||||
|
||||
|
|
|
@ -332,22 +332,22 @@ class OrdersTableQuery {
|
|||
* YYYY-MM-DD queries have 'day' precision for backwards compatibility.
|
||||
*
|
||||
* @param mixed $date The date. Can be a {@see \WC_DateTime}, a timestamp or a string.
|
||||
* @param string $timezone The timezone to use for the date.
|
||||
* @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'.
|
||||
*/
|
||||
private function date_to_date_query_arg( $date, $timezone ): array {
|
||||
private function date_to_date_query_arg( $date ): array {
|
||||
$result = array(
|
||||
'year' => '',
|
||||
'month' => '',
|
||||
'day' => '',
|
||||
);
|
||||
$precision = 'second';
|
||||
|
||||
if ( is_numeric( $date ) ) {
|
||||
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( $timezone ) );
|
||||
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( 'UTC' ) );
|
||||
$precision = 'second';
|
||||
} elseif ( ! is_a( $date, 'WC_DateTime' ) ) {
|
||||
// YYYY-MM-DD queries have 'day' precision for backwards compat.
|
||||
$date = wc_string_to_datetime( $date );
|
||||
// For backwards compat (see https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date)
|
||||
// only YYYY-MM-DD is considered for date values. Timestamps do support second precision.
|
||||
$date = wc_string_to_datetime( date( 'Y-m-d', strtotime( $date ) ) );
|
||||
$precision = 'day';
|
||||
}
|
||||
|
||||
|
@ -364,6 +364,80 @@ class OrdersTableQuery {
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns UTC-based date query arguments for a combination of local time dates and a date shorthand operator.
|
||||
*
|
||||
* @param array $dates_raw Array of dates (in local time) to use in combination with the operator.
|
||||
* @param string $operator One of the operators supported by date queries (<, <=, =, ..., >, >=).
|
||||
* @return array Partial date query arg with relevant dates now UTC-based.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
private function local_time_to_gmt_date_query( $dates_raw, $operator ) {
|
||||
$result = array();
|
||||
|
||||
// Convert YYYY-MM-DD to UTC timestamp. Per https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date only date is relevant (time is ignored).
|
||||
foreach ( $dates_raw as &$raw_date ) {
|
||||
$raw_date = is_numeric( $raw_date ) ? $raw_date : strtotime( get_gmt_from_date( date( 'Y-m-d', strtotime( $raw_date ) ) ) );
|
||||
}
|
||||
|
||||
$date1 = end( $dates_raw );
|
||||
|
||||
switch ( $operator ) {
|
||||
case '>':
|
||||
$result = array(
|
||||
'after' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
|
||||
'inclusive' => true,
|
||||
);
|
||||
break;
|
||||
case '>=':
|
||||
$result = array(
|
||||
'after' => $this->date_to_date_query_arg( $date1 ),
|
||||
'inclusive' => true,
|
||||
);
|
||||
break;
|
||||
case '=':
|
||||
$result = array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'after' => $this->date_to_date_query_arg( $date1 ),
|
||||
'inclusive' => true,
|
||||
),
|
||||
array(
|
||||
'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
|
||||
'inclusive' => false,
|
||||
)
|
||||
);
|
||||
break;
|
||||
case '<=':
|
||||
$result = array(
|
||||
'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
|
||||
'inclusive' => false,
|
||||
);
|
||||
break;
|
||||
case '<':
|
||||
$result = array(
|
||||
'before' => $this->date_to_date_query_arg( $date1 ),
|
||||
'inclusive' => false,
|
||||
);
|
||||
break;
|
||||
case '...':
|
||||
$result = array(
|
||||
'relation' => 'AND',
|
||||
$this->local_time_to_gmt_date_query( array( $dates_raw[1] ), '<=' ),
|
||||
$this->local_time_to_gmt_date_query( array( $dates_raw[0] ), '>=' ),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! $result ) {
|
||||
throw new \Exception( 'Please specify a valid date shorthand operator.' );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes date-related query args and merges the result into 'date_query'.
|
||||
*
|
||||
|
@ -392,27 +466,45 @@ class OrdersTableQuery {
|
|||
$date_keys = array_filter( $valid_date_keys, array( $this, 'arg_isset' ) );
|
||||
|
||||
foreach ( $date_keys as $date_key ) {
|
||||
$is_local = in_array( $date_key, $local_date_keys, true );
|
||||
$date_value = $this->args[ $date_key ];
|
||||
|
||||
$operator = '=';
|
||||
$dates_raw = array();
|
||||
$dates = array();
|
||||
$timezone = in_array( $date_key, $gmt_date_keys, true ) ? '+0000' : wc_timezone_string();
|
||||
|
||||
if ( is_string( $date_value ) && preg_match( self::REGEX_SHORTHAND_DATES, $date_value, $matches ) ) {
|
||||
$operator = in_array( $matches[2], $valid_operators, true ) ? $matches[2] : '';
|
||||
|
||||
if ( ! empty( $matches[1] ) ) {
|
||||
$dates[] = $this->date_to_date_query_arg( $matches[1], $timezone );
|
||||
$dates_raw[] = $matches[1];
|
||||
}
|
||||
|
||||
$dates[] = $this->date_to_date_query_arg( $matches[3], $timezone );
|
||||
$dates_raw[] = $matches[3];
|
||||
} else {
|
||||
$dates[] = $this->date_to_date_query_arg( $date_value, $timezone );
|
||||
$dates_raw[] = $date_value;
|
||||
}
|
||||
|
||||
if ( empty( $dates ) || ! $operator || ( '...' === $operator && count( $dates ) < 2 ) ) {
|
||||
if ( empty( $dates_raw ) || ! $operator || ( '...' === $operator && count( $dates_raw ) < 2 ) ) {
|
||||
throw new \Exception( 'Invalid date_query' );
|
||||
}
|
||||
|
||||
if ( $is_local ) {
|
||||
$date_key = $local_to_gmt_date_keys[ $date_key ];
|
||||
|
||||
if ( ! is_numeric( $dates_raw[0] ) && ( ! isset( $dates_raw[1] ) || ! is_numeric( $dates_raw[1] ) ) ) {
|
||||
// Only non-numeric args can be considered local time. Timestamps are assumed to be UTC per https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date.
|
||||
$date_queries[] = array_merge(
|
||||
array(
|
||||
'column' => $date_key,
|
||||
),
|
||||
$this->local_time_to_gmt_date_query( $dates_raw, $operator )
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$operator_to_keys = array();
|
||||
|
||||
if ( in_array( $operator, array( '>', '>=', '...' ), true ) ) {
|
||||
|
@ -423,7 +515,7 @@ class OrdersTableQuery {
|
|||
$operator_to_keys[] = 'before';
|
||||
}
|
||||
|
||||
$date_key = in_array( $date_key, $local_date_keys, true ) ? $local_to_gmt_date_keys[ $date_key ] : $date_key;
|
||||
$dates = array_map( array( $this, 'date_to_date_query_arg' ), $dates_raw );
|
||||
$date_queries[] = array_merge(
|
||||
array(
|
||||
'column' => $date_key,
|
||||
|
@ -515,7 +607,7 @@ class OrdersTableQuery {
|
|||
$op = isset( $query['after'] ) ? 'after' : 'before';
|
||||
$date_value_local = $query[ $op ];
|
||||
$date_value_gmt = wc_string_to_timestamp( get_gmt_from_date( wc_string_to_datetime( $date_value_local ) ) );
|
||||
$query[ $op ] = $this->date_to_date_query_arg( $date_value_gmt, 'UTC' );
|
||||
$query[ $op ] = $this->date_to_date_query_arg( $date_value_gmt );
|
||||
}
|
||||
|
||||
return $query;
|
||||
|
|
|
@ -2974,4 +2974,36 @@ class OrdersTableDataStoreTests extends HposTestCase {
|
|||
$this->assertTrue( in_array( 'test_value4', $post_meta, true ) );
|
||||
$this->assertFalse( in_array( 'test_value5', $post_meta, true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testDox Test that date queries correctly handle timezones.
|
||||
*/
|
||||
public function test_timezone_date_query_support() {
|
||||
$order = new WC_Order();
|
||||
$order->set_date_created( '2023-09-01T00:30:00' ); // This would be 2023-08-31T18:00:00 UTC given the current timezone.
|
||||
$this->sut->create( $order );
|
||||
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created_gmt' => '2023-09-01' ) );
|
||||
$this->assertEquals( 0, count( $query->orders ) ); // Should not return anything as the order was created on 2023-08-31 UTC.
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created_gmt' => '2023-08-31' ) );
|
||||
$this->assertEquals( 1, count( $query->orders ) );
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created_gmt' => '<=2023-09-01' ) );
|
||||
$this->assertEquals( 1, count( $query->orders ) );
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created' => '2023-08-31' ) );
|
||||
$this->assertEquals( 0, count( $query->orders ) );
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created' => '2023-09-01' ) );
|
||||
$this->assertEquals( 1, count( $query->orders ) );
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created' => '>2023-09-01' ) );
|
||||
$this->assertEquals( 0, count( $query->orders ) );
|
||||
|
||||
$query = new OrdersTableQuery( array( 'date_created' => '<2023-09-01' ) );
|
||||
$this->assertEquals( 0, count( $query->orders ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue